mirror of
				https://github.com/dromara/RuoYi-Vue-Plus.git
				synced 2025-10-31 14:23:45 +08:00 
			
		
		
		
	Compare commits
	
		
			220 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | d2a45156a2 | ||
|  | 9df0a8de1c | ||
|  | 5ea8d8c950 | ||
|  | 3318109044 | ||
|  | aa1f89e253 | ||
|  | 35c77403d6 | ||
|  | 603fb7b92d | ||
|  | 6cf0c79433 | ||
|  | 3934e119d6 | ||
|  | 33a6a21fdf | ||
|  | 7800b1259f | ||
|  | 3623fc33d9 | ||
|  | f8612eb52e | ||
|  | 8d32b0311a | ||
|  | 60bcd2d6e9 | ||
|  | 5ccb511064 | ||
|  | 78baf6497a | ||
|  | 0719e53f01 | ||
|  | 5f2c4205a5 | ||
|  | 2fe4c96706 | ||
|  | 5c634940c2 | ||
|  | 6036f8750b | ||
|  | dbcd8f58eb | ||
|  | 8905e232e5 | ||
|  | 4f15158486 | ||
|  | d2413abd5c | ||
|  | f7ffadeaff | ||
|  | f9eec856e7 | ||
|  | 62562650fe | ||
|  | df171097c3 | ||
|  | 1977aabc9a | ||
|  | 483c4e6d0a | ||
|  | f616c6931c | ||
|  | 26e10293f5 | ||
|  | 60e578f763 | ||
|  | 5cd4d8ca11 | ||
|  | 41a6230b6e | ||
|  | effda4f6e8 | ||
|  | af4c38e439 | ||
|  | fafa8cd573 | ||
|  | 8909b8a7d4 | ||
|  | 8ae9bde731 | ||
|  | a703cb2ad1 | ||
|  | a918b880d6 | ||
|  | e795e315eb | ||
|  | 81869cfeb3 | ||
|  | fc6f61bc95 | ||
|  | d44e45ad3b | ||
|  | 00ed9ddd10 | ||
|  | 341fc144a1 | ||
|  | b6b1b2de18 | ||
|  | c19f2b9e4e | ||
|  | 3a11f18656 | ||
|  | 5a43212ccc | ||
|  | f4cfd1c913 | ||
|  | 26ce8f30c9 | ||
|  | 2258962770 | ||
|  | 655e84012c | ||
|  | f683ef00b8 | ||
|  | 424b2ea164 | ||
|  | 7bb4838132 | ||
|  | 20516758ea | ||
|  | 2d5f84ebc2 | ||
|  | 6bc28e41de | ||
|  | a4fb3fadaf | ||
|  | cfa67fcd8c | ||
|  | e5e8d305d2 | ||
|  | 9d0084409e | ||
|  | ee02f46dfd | ||
|  | 25de0b3530 | ||
|  | aa76859a05 | ||
|  | 71b70a59fe | ||
|  | 05c9528549 | ||
|  | 1feb2a3861 | ||
|  | 237e78e80c | ||
|  | ffc3dcaec9 | ||
|  | a94e474069 | ||
|  | 40a0e57870 | ||
|  | c01ed34602 | ||
|  | 26a99003d2 | ||
|  | 93c886d3ed | ||
|  | 9e1027690b | ||
|  | cc120c06fd | ||
|  | 3827da078a | ||
|  | 70d3505b94 | ||
|  | a39a69cac5 | ||
|  | 1dbce3ab7c | ||
|  | 9742b1b596 | ||
|  | d98d11ae2d | ||
|  | 6742dcb33e | ||
|  | 09a51478a5 | ||
|  | f02601ab2c | ||
|  | ac56ca0e81 | ||
|  | 0fcf77e2ed | ||
|  | 0f0a3a181e | ||
|  | e24e2c51e4 | ||
|  | 2b0dd82d3d | ||
|  | b97f711eb4 | ||
|  | 0250ca4eb8 | ||
|  | 23338995d7 | ||
|  | 84fd02e7d8 | ||
|  | ae5bec994d | ||
|  | 8f3a1b589e | ||
|  | ad6b3d4b3f | ||
|  | e2801037cf | ||
|  | 65061f17fe | ||
|  | d0f4d93615 | ||
|  | 5d69832423 | ||
|  | 0c1e39ea14 | ||
|  | a39bc870d1 | ||
|  | 7357912681 | ||
|  | 901992674e | ||
|  | 7ceb85ffa0 | ||
|  | 4672d7de4d | ||
|  | 87ab6e1744 | ||
|  | ae0a03728b | ||
|  | 6fc82a59f1 | ||
|  | 6c33fa48ec | ||
|  | 343d5d21d8 | ||
|  | 0ba909c52e | ||
|  | 808ce9c25a | ||
|  | 4351fc5239 | ||
|  | a545f7fc44 | ||
|  | acfcdf4d9a | ||
|  | 9683252783 | ||
|  | 49c00e162b | ||
|  | 076a0a44fa | ||
|  | 0d93589d99 | ||
|  | 54a8189e27 | ||
|  | 84f17011ad | ||
|  | f47bd39644 | ||
|  | 89f9617ccb | ||
|  | bf10a13088 | ||
|  | f2e0361fb6 | ||
|  | 554152635d | ||
|  | b379574637 | ||
|  | 6a556cc6ff | ||
|  | a6950275ad | ||
|  | 58b1bf5c33 | ||
|  | c85f693ca6 | ||
|  | 5f466fd0c4 | ||
|  | 127eaf936c | ||
|  | fcd8556076 | ||
|  | 0512781513 | ||
|  | 2472359adb | ||
|  | 29d4bb4e59 | ||
|  | cce95424ce | ||
|  | 8d7358e663 | ||
|  | 240f10ab45 | ||
|  | 48213bc9c9 | ||
|  | 3995d9699d | ||
|  | ecd4e3eaf0 | ||
|  | 2e3a42c669 | ||
|  | 82997fc6cd | ||
|  | 0dce571270 | ||
|  | 9375578925 | ||
|  | e19ccf5064 | ||
|  | 1cea7b72d7 | ||
|  | 5da9ddf5e3 | ||
|  | 93ee01c6b9 | ||
|  | acd30fda3c | ||
|  | 3f62a76cc8 | ||
|  | b0b4e573f6 | ||
|  | de61899eed | ||
|  | 3a9bdb36f1 | ||
|  | b815b8e574 | ||
|  | 45edee4e63 | ||
|  | 868bc492a2 | ||
|  | 90fef1bb17 | ||
|  | d79b48ea99 | ||
|  | f6993a1491 | ||
|  | 6b0b7382a6 | ||
|  | c41add355f | ||
|  | 74e3d232f5 | ||
|  | 03fca40c7d | ||
|  | b2ad257bd8 | ||
|  | 7e4f0d73f4 | ||
|  | 2ec802f17f | ||
|  | e0ce662c28 | ||
|  | 3d9ed1b92f | ||
|  | d4e6e70c43 | ||
|  | 2095a96e67 | ||
|  | 328b61b252 | ||
|  | 781463417c | ||
|  | 446a14b928 | ||
|  | 6c2518640b | ||
|  | d8d138092f | ||
|  | ec31b736c7 | ||
|  | e7467b2c5c | ||
|  | 8050e2f1b1 | ||
|  | 7de4559b4a | ||
|  | fc9c0d7657 | ||
|  | ab3037dc4f | ||
|  | 8281b838b9 | ||
|  | 2bc7171abd | ||
|  | 4f99487d24 | ||
|  | a7cddc8d40 | ||
|  | f3c4c02d73 | ||
|  | eb631360f4 | ||
|  | a62bf04428 | ||
|  | 7147f81b42 | ||
|  | c9098563ca | ||
|  | d4a8c25eab | ||
|  | 0ddba506bf | ||
|  | d02bea85cb | ||
|  | d27c58bfe8 | ||
|  | 34bb51f5c0 | ||
|  | 64c37aaec6 | ||
|  | 3de036adde | ||
|  | e0df8c15d8 | ||
|  | 176793e15b | ||
|  | 589ec1fdbc | ||
|  | b421c8d017 | ||
|  | e2200bac71 | ||
|  | f8950d1e20 | ||
|  | 9dfe9f610d | ||
|  | 17610e8721 | ||
|  | f29b787767 | ||
|  | 9775283a24 | ||
|  | debc73d7d4 | 
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -38,6 +38,7 @@ nbdist/ | ||||
| ###################################################################### | ||||
| # Others | ||||
| *.log | ||||
| *.log.gz | ||||
| *.xml.versionsBackup | ||||
| *.swp | ||||
|  | ||||
|   | ||||
| @@ -2,7 +2,7 @@ | ||||
|   <configuration default="false" name="ruoyi-monitor-admin" type="docker-deploy" factoryName="dockerfile" server-name="Docker"> | ||||
|     <deployment type="dockerfile"> | ||||
|       <settings> | ||||
|         <option name="imageTag" value="ruoyi/ruoyi-monitor-admin:5.4.1" /> | ||||
|         <option name="imageTag" value="ruoyi/ruoyi-monitor-admin:5.5.1" /> | ||||
|         <option name="buildOnly" value="true" /> | ||||
|         <option name="sourceFilePath" value="ruoyi-extend/ruoyi-monitor-admin/Dockerfile" /> | ||||
|       </settings> | ||||
|   | ||||
| @@ -2,7 +2,7 @@ | ||||
|   <configuration default="false" name="ruoyi-server" type="docker-deploy" factoryName="dockerfile" server-name="Docker"> | ||||
|     <deployment type="dockerfile"> | ||||
|       <settings> | ||||
|         <option name="imageTag" value="ruoyi/ruoyi-server:5.4.1" /> | ||||
|         <option name="imageTag" value="ruoyi/ruoyi-server:5.5.1" /> | ||||
|         <option name="buildOnly" value="true" /> | ||||
|         <option name="sourceFilePath" value="ruoyi-admin/Dockerfile" /> | ||||
|       </settings> | ||||
|   | ||||
| @@ -2,7 +2,7 @@ | ||||
|   <configuration default="false" name="ruoyi-snailjob-server" type="docker-deploy" factoryName="dockerfile" server-name="Docker"> | ||||
|     <deployment type="dockerfile"> | ||||
|       <settings> | ||||
|         <option name="imageTag" value="ruoyi/ruoyi-snailjob-server:5.4.1" /> | ||||
|         <option name="imageTag" value="ruoyi/ruoyi-snailjob-server:5.5.1" /> | ||||
|         <option name="buildOnly" value="true" /> | ||||
|         <option name="sourceFilePath" value="ruoyi-extend/ruoyi-snailjob-server/Dockerfile" /> | ||||
|       </settings> | ||||
|   | ||||
| @@ -10,7 +10,7 @@ | ||||
| [](https://gitee.com/dromara/RuoYi-Vue-Plus/blob/5.X/LICENSE) | ||||
| [](https://www.jetbrains.com/?from=RuoYi-Vue-Plus) | ||||
| <br> | ||||
| [](https://gitee.com/dromara/RuoYi-Vue-Plus) | ||||
| [](https://gitee.com/dromara/RuoYi-Vue-Plus) | ||||
| []() | ||||
| []() | ||||
| []() | ||||
| @@ -27,7 +27,7 @@ | ||||
| > 成员前端项目地址: 基于soybean [ruoyi-plus-soybean](https://gitee.com/xlsea/ruoyi-plus-soybean)<br> | ||||
| > 成员项目地址: 删除多租户与工作流 [RuoYi-Vue-Plus-Single](https://gitee.com/ColorDreams/RuoYi-Vue-Plus-Single)<br> | ||||
|  | ||||
| > 文档地址: [plus-doc](https://plus-doc.dromara.org) 文档在华为云上如果打不开大概率是DNS问题 可以尝试切换网络等方式(或者科学上网) | ||||
| > 文档地址: [plus-doc](https://plus-doc.dromara.org) 国内加速: [plus-doc.top](https://plus-doc.top) | ||||
|  | ||||
| ## 赞助商 | ||||
|  | ||||
| @@ -37,7 +37,10 @@ CCFlow 驰聘低代码-流程-表单 - https://gitee.com/opencc/RuoYi-JFlow <br> | ||||
| 引迈信息 软件开发平台 - https://www.jnpfsoft.com/index.html?from=plus-doc <br> | ||||
| <font color="red">**启山商城系统 多租户商城源码可免费商用可二次开发 - https://www.73app.cn/** </font><br> | ||||
| Mall4J 高质量Java商城系统 - https://www.mall4j.com/cn/?statId=11 <br> | ||||
| [如何成为赞助商 加群联系作者详谈](https://plus-doc.dromara.org/#/common/add_group) | ||||
| aizuda flowlong 工作流 - https://gitee.com/aizuda/flowlong <br> | ||||
| Ruoyi-Plus-Uniapp - https://ruoyi.plus <br> | ||||
|  | ||||
| [如何成为赞助商 加群联系作者详谈 每日PV2500-3000 IP1700-2500](https://plus-doc.dromara.org/#/common/add_group) | ||||
|  | ||||
| # 本框架与RuoYi的功能差异 | ||||
|  | ||||
|   | ||||
							
								
								
									
										64
									
								
								pom.xml
									
									
									
									
									
								
							
							
						
						
									
										64
									
								
								pom.xml
									
									
									
									
									
								
							| @@ -13,28 +13,28 @@ | ||||
|     <description>Dromara RuoYi-Vue-Plus多租户管理系统</description> | ||||
|  | ||||
|     <properties> | ||||
|         <revision>5.4.1</revision> | ||||
|         <spring-boot.version>3.4.7</spring-boot.version> | ||||
|         <revision>5.5.1</revision> | ||||
|         <spring-boot.version>3.5.7</spring-boot.version> | ||||
|         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> | ||||
|         <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> | ||||
|         <java.version>17</java.version> | ||||
|         <mybatis.version>3.5.16</mybatis.version> | ||||
|         <springdoc.version>2.8.8</springdoc.version> | ||||
|         <springdoc.version>2.8.13</springdoc.version> | ||||
|         <therapi-javadoc.version>0.15.0</therapi-javadoc.version> | ||||
|         <fastexcel.version>1.2.0</fastexcel.version> | ||||
|         <fastexcel.version>1.3.0</fastexcel.version> | ||||
|         <velocity.version>2.3</velocity.version> | ||||
|         <satoken.version>1.44.0</satoken.version> | ||||
|         <mybatis-plus.version>3.5.12</mybatis-plus.version> | ||||
|         <mybatis-plus.version>3.5.14</mybatis-plus.version> | ||||
|         <p6spy.version>3.9.1</p6spy.version> | ||||
|         <hutool.version>5.8.38</hutool.version> | ||||
|         <spring-boot-admin.version>3.4.7</spring-boot-admin.version> | ||||
|         <redisson.version>3.50.0</redisson.version> | ||||
|         <hutool.version>5.8.40</hutool.version> | ||||
|         <spring-boot-admin.version>3.5.5</spring-boot-admin.version> | ||||
|         <redisson.version>3.51.0</redisson.version> | ||||
|         <lock4j.version>2.2.7</lock4j.version> | ||||
|         <dynamic-ds.version>4.3.1</dynamic-ds.version> | ||||
|         <snailjob.version>1.5.0</snailjob.version> | ||||
|         <mapstruct-plus.version>1.4.8</mapstruct-plus.version> | ||||
|         <snailjob.version>1.8.0</snailjob.version> | ||||
|         <mapstruct-plus.version>1.5.0</mapstruct-plus.version> | ||||
|         <mapstruct-plus.lombok.version>0.2.0</mapstruct-plus.lombok.version> | ||||
|         <lombok.version>1.18.36</lombok.version> | ||||
|         <lombok.version>1.18.40</lombok.version> | ||||
|         <bouncycastle.version>1.80</bouncycastle.version> | ||||
|         <justauth.version>1.16.7</justauth.version> | ||||
|         <!-- 离线IP地址定位库 --> | ||||
| @@ -42,13 +42,13 @@ | ||||
|         <!-- OSS 配置 --> | ||||
|         <aws.sdk.version>2.28.22</aws.sdk.version> | ||||
|         <!-- SMS 配置 --> | ||||
|         <sms4j.version>3.3.4</sms4j.version> | ||||
|         <sms4j.version>3.3.5</sms4j.version> | ||||
|         <!-- 限制框架中的fastjson版本 --> | ||||
|         <fastjson.version>1.2.83</fastjson.version> | ||||
|         <!-- 面向运行时的D-ORM依赖 --> | ||||
|         <anyline.version>8.7.2-20250603</anyline.version> | ||||
|         <!-- 工作流配置 --> | ||||
|         <warm-flow.version>1.7.4</warm-flow.version> | ||||
|         <warm-flow.version>1.8.2</warm-flow.version> | ||||
|  | ||||
|         <!-- 插件版本 --> | ||||
|         <maven-jar-plugin.version>3.4.2</maven-jar-plugin.version> | ||||
| @@ -118,25 +118,6 @@ | ||||
|                 <scope>import</scope> | ||||
|             </dependency> | ||||
|  | ||||
|             <!-- Warm-Flow国产工作流引擎, 在线文档:http://warm-flow.cn/ --> | ||||
|             <dependency> | ||||
|                 <groupId>org.dromara.warm</groupId> | ||||
|                 <artifactId>warm-flow-mybatis-plus-sb3-starter</artifactId> | ||||
|                 <version>${warm-flow.version}</version> | ||||
|             </dependency> | ||||
|             <dependency> | ||||
|                 <groupId>org.dromara.warm</groupId> | ||||
|                 <artifactId>warm-flow-plugin-ui-sb-web</artifactId> | ||||
|                 <version>${warm-flow.version}</version> | ||||
|             </dependency> | ||||
|  | ||||
|             <!-- JustAuth 的依赖配置--> | ||||
|             <dependency> | ||||
|                 <groupId>me.zhyd.oauth</groupId> | ||||
|                 <artifactId>JustAuth</artifactId> | ||||
|                 <version>${justauth.version}</version> | ||||
|             </dependency> | ||||
|  | ||||
|             <!-- common 的依赖配置--> | ||||
|             <dependency> | ||||
|                 <groupId>org.dromara</groupId> | ||||
| @@ -313,6 +294,25 @@ | ||||
|                 <version>${mapstruct-plus.version}</version> | ||||
|             </dependency> | ||||
|  | ||||
|             <!-- Warm-Flow国产工作流引擎, 在线文档:http://warm-flow.cn/ --> | ||||
|             <dependency> | ||||
|                 <groupId>org.dromara.warm</groupId> | ||||
|                 <artifactId>warm-flow-mybatis-plus-sb3-starter</artifactId> | ||||
|                 <version>${warm-flow.version}</version> | ||||
|             </dependency> | ||||
|             <dependency> | ||||
|                 <groupId>org.dromara.warm</groupId> | ||||
|                 <artifactId>warm-flow-plugin-ui-sb-web</artifactId> | ||||
|                 <version>${warm-flow.version}</version> | ||||
|             </dependency> | ||||
|  | ||||
|             <!-- JustAuth 的依赖配置--> | ||||
|             <dependency> | ||||
|                 <groupId>me.zhyd.oauth</groupId> | ||||
|                 <artifactId>JustAuth</artifactId> | ||||
|                 <version>${justauth.version}</version> | ||||
|             </dependency> | ||||
|  | ||||
|             <!-- 离线IP地址定位库 ip2region --> | ||||
|             <dependency> | ||||
|                 <groupId>org.lionsoul</groupId> | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| # 贝尔实验室 Spring 官方推荐镜像 JDK下载地址 https://bell-sw.com/pages/downloads/ | ||||
| FROM bellsoft/liberica-openjdk-rocky:17.0.15-cds | ||||
| #FROM bellsoft/liberica-openjdk-rocky:21.0.7-cds | ||||
| FROM bellsoft/liberica-openjdk-rocky:17.0.16-cds | ||||
| #FROM bellsoft/liberica-openjdk-rocky:21.0.8-cds | ||||
| #FROM findepi/graalvm:java17-native | ||||
|  | ||||
| LABEL maintainer="Lion Li" | ||||
|   | ||||
| @@ -21,6 +21,8 @@ import org.dromara.common.core.domain.model.SocialLoginBody; | ||||
| import org.dromara.common.core.utils.*; | ||||
| import org.dromara.common.encrypt.annotation.ApiEncrypt; | ||||
| import org.dromara.common.json.utils.JsonUtils; | ||||
| import org.dromara.common.ratelimiter.annotation.RateLimiter; | ||||
| import org.dromara.common.ratelimiter.enums.LimitType; | ||||
| import org.dromara.common.satoken.utils.LoginHelper; | ||||
| import org.dromara.common.social.config.properties.SocialLoginConfigProperties; | ||||
| import org.dromara.common.social.config.properties.SocialProperties; | ||||
| @@ -198,6 +200,7 @@ public class AuthController { | ||||
|      * | ||||
|      * @return 租户列表 | ||||
|      */ | ||||
|     @RateLimiter(time = 60, count = 20, limitType = LimitType.IP) | ||||
|     @GetMapping("/tenant/list") | ||||
|     public R<LoginTenantVo> tenantList(HttpServletRequest request) throws Exception { | ||||
|         // 返回对象 | ||||
|   | ||||
| @@ -131,15 +131,18 @@ public class CaptchaController { | ||||
|         String verifyKey = GlobalConstants.CAPTCHA_CODE_KEY + uuid; | ||||
|         // 生成验证码 | ||||
|         CaptchaType captchaType = captchaProperties.getType(); | ||||
|         boolean isMath = CaptchaType.MATH == captchaType; | ||||
|         Integer length = isMath ? captchaProperties.getNumberLength() : captchaProperties.getCharLength(); | ||||
|         CodeGenerator codeGenerator = ReflectUtils.newInstance(captchaType.getClazz(), length); | ||||
|         CodeGenerator codeGenerator; | ||||
|         if (CaptchaType.MATH == captchaType) { | ||||
|             codeGenerator = ReflectUtils.newInstance(captchaType.getClazz(), captchaProperties.getNumberLength(), false); | ||||
|         } else { | ||||
|             codeGenerator = ReflectUtils.newInstance(captchaType.getClazz(), captchaProperties.getCharLength()); | ||||
|         } | ||||
|         AbstractCaptcha captcha = SpringUtils.getBean(captchaProperties.getCategory().getClazz()); | ||||
|         captcha.setGenerator(codeGenerator); | ||||
|         captcha.createCode(); | ||||
|         // 如果是数学验证码,使用SpEL表达式处理验证码结果 | ||||
|         String code = captcha.getCode(); | ||||
|         if (isMath) { | ||||
|         if (CaptchaType.MATH == captchaType) { | ||||
|             ExpressionParser parser = new SpelExpressionParser(); | ||||
|             Expression exp = parser.parseExpression(StringUtils.remove(code, "=")); | ||||
|             code = exp.getValue(String.class); | ||||
|   | ||||
| @@ -87,7 +87,7 @@ public class SysRegisterService { | ||||
|             recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire")); | ||||
|             throw new CaptchaExpireException(); | ||||
|         } | ||||
|         if (!code.equalsIgnoreCase(captcha)) { | ||||
|         if (!StringUtils.equalsIgnoreCase(code, captcha)) { | ||||
|             recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")); | ||||
|             throw new CaptchaException(); | ||||
|         } | ||||
|   | ||||
| @@ -102,7 +102,7 @@ public class PasswordAuthStrategy implements IAuthStrategy { | ||||
|             loginService.recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire")); | ||||
|             throw new CaptchaExpireException(); | ||||
|         } | ||||
|         if (!code.equalsIgnoreCase(captcha)) { | ||||
|         if (!StringUtils.equalsIgnoreCase(code, captcha)) { | ||||
|             loginService.recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")); | ||||
|             throw new CaptchaException(); | ||||
|         } | ||||
|   | ||||
| @@ -27,8 +27,6 @@ snail-job: | ||||
|   port: 2${server.port} | ||||
|   # 客户端ip指定 | ||||
|   host: | ||||
|   # RPC类型: netty, grpc | ||||
|   rpc-type: grpc | ||||
|  | ||||
| --- # 数据源配置 | ||||
| spring: | ||||
|   | ||||
| @@ -30,8 +30,6 @@ snail-job: | ||||
|   port: 2${server.port} | ||||
|   # 客户端ip指定 | ||||
|   host: | ||||
|   # RPC类型: netty, grpc | ||||
|   rpc-type: grpc | ||||
|  | ||||
| --- # 数据源配置 | ||||
| spring: | ||||
|   | ||||
| @@ -57,6 +57,13 @@ spring: | ||||
|     # 开启虚拟线程 仅jdk21可用 | ||||
|     virtual: | ||||
|       enabled: false | ||||
|   task: | ||||
|     execution: | ||||
|       # 从 springboot 3.5 开始 spring自带线程池 | ||||
|       # 不再需要 AsyncConfig与ThreadPoolConfig 可直接注入线程池使用 | ||||
|       thread-name-prefix: async- | ||||
|       # 由spring自己初始化线程池 | ||||
|       mode: force | ||||
|   # 资源信息 | ||||
|   messages: | ||||
|     # 国际化资源文件路径 | ||||
| @@ -127,6 +134,7 @@ tenant: | ||||
|     - sys_user_role | ||||
|     - sys_client | ||||
|     - sys_oss_config | ||||
|     - flow_spel | ||||
|  | ||||
| # MyBatisPlus配置 | ||||
| # https://baomidou.com/config/ | ||||
| @@ -189,13 +197,6 @@ springdoc: | ||||
|       name: Lion Li | ||||
|       email: crazylionli@163.com | ||||
|       url: https://gitee.com/dromara/RuoYi-Vue-Plus | ||||
|   components: | ||||
|     # 鉴权方式配置 | ||||
|     security-schemes: | ||||
|       apiKey: | ||||
|         type: APIKEY | ||||
|         in: HEADER | ||||
|         name: ${sa-token.token-name} | ||||
|   #这里定义了两个分组,可定义多个,也可以不定义 | ||||
|   group-configs: | ||||
|     - group: 1.演示模块 | ||||
| @@ -213,20 +214,10 @@ springdoc: | ||||
| xss: | ||||
|   # 过滤开关 | ||||
|   enabled: true | ||||
|   # 排除链接(多个用逗号分隔) | ||||
|   # 排除链接 | ||||
|   excludeUrls: | ||||
|     - /system/notice | ||||
|  | ||||
| # 全局线程池相关配置 | ||||
| # 如使用JDK21请直接使用虚拟线程 不要开启此配置 | ||||
| thread-pool: | ||||
|   # 是否开启线程池 | ||||
|   enabled: false | ||||
|   # 队列最大长度 | ||||
|   queueCapacity: 128 | ||||
|   # 线程池维护线程所允许的空闲时间 | ||||
|   keepAliveSeconds: 300 | ||||
|  | ||||
| --- # 分布式锁 lock4j 全局配置 | ||||
| lock4j: | ||||
|   # 获取分布式锁超时时间,默认为 3000 毫秒 | ||||
| @@ -266,13 +257,9 @@ warm-flow: | ||||
|   enabled: true | ||||
|   # 是否开启设计器ui | ||||
|   ui: true | ||||
|   # 是否显示流程图顶部文字 | ||||
|   top-text-show: true | ||||
|   # 是否渲染节点悬浮提示,默认true | ||||
|   node-tooltip: true | ||||
|   # 默认Authorization,如果有多个token,用逗号分隔 | ||||
|   token-name: ${sa-token.token-name},clientid | ||||
|   # 流程状态对应的三元色 | ||||
|   chart-status-color: | ||||
|     ## 未办理 | ||||
|     - 62,62,62 | ||||
|     ## 待办理 | ||||
|     - 255,205,23 | ||||
|     ## 已办理 | ||||
|     - 157,255,0 | ||||
|   | ||||
| @@ -17,6 +17,7 @@ user.username.length.valid=账户长度必须在{min}到{max}个字符之间 | ||||
| user.password.not.blank=用户密码不能为空 | ||||
| user.password.length.valid=用户密码长度必须在{min}到{max}个字符之间 | ||||
| user.password.not.valid=* 5-50个字符 | ||||
| user.password.format.valid=密码必须包含大写字母、小写字母、数字和特殊字符 | ||||
| user.email.not.valid=邮箱格式错误 | ||||
| user.email.not.blank=邮箱不能为空 | ||||
| user.phonenumber.not.blank=用户手机号不能为空 | ||||
|   | ||||
| @@ -17,6 +17,7 @@ user.username.length.valid=Account length must be between {min} and {max} charac | ||||
| user.password.not.blank=Password cannot be empty | ||||
| user.password.length.valid=Password length must be between {min} and {max} characters | ||||
| user.password.not.valid=* 5-50 characters | ||||
| user.password.format.valid=Password must contain uppercase, lowercase, digit, and special character | ||||
| user.email.not.valid=Mailbox format error | ||||
| user.email.not.blank=Mailbox cannot be blank | ||||
| user.phonenumber.not.blank=Phone number cannot be blank | ||||
|   | ||||
| @@ -17,6 +17,7 @@ user.username.length.valid=账户长度必须在{min}到{max}个字符之间 | ||||
| user.password.not.blank=用户密码不能为空 | ||||
| user.password.length.valid=用户密码长度必须在{min}到{max}个字符之间 | ||||
| user.password.not.valid=* 5-50个字符 | ||||
| user.password.format.valid=密码必须包含大写字母、小写字母、数字和特殊字符 | ||||
| user.email.not.valid=邮箱格式错误 | ||||
| user.email.not.blank=邮箱不能为空 | ||||
| user.phonenumber.not.blank=用户手机号不能为空 | ||||
|   | ||||
										
											Binary file not shown.
										
									
								
							| @@ -38,7 +38,7 @@ | ||||
|         <!-- 循环政策:基于时间创建日志文件 --> | ||||
|         <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> | ||||
|             <!-- 日志文件名格式 --> | ||||
|             <fileNamePattern>${log.path}/sys-info.%d{yyyy-MM-dd}.log</fileNamePattern> | ||||
|             <fileNamePattern>${log.path}/sys-info.%d{yyyy-MM-dd}.log.gz</fileNamePattern> | ||||
|             <!-- 日志最大的历史 60天 --> | ||||
|             <maxHistory>60</maxHistory> | ||||
|         </rollingPolicy> | ||||
| @@ -60,7 +60,7 @@ | ||||
|         <!-- 循环政策:基于时间创建日志文件 --> | ||||
|         <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> | ||||
|             <!-- 日志文件名格式 --> | ||||
|             <fileNamePattern>${log.path}/sys-error.%d{yyyy-MM-dd}.log</fileNamePattern> | ||||
|             <fileNamePattern>${log.path}/sys-error.%d{yyyy-MM-dd}.log.gz</fileNamePattern> | ||||
|             <!-- 日志最大的历史 60天 --> | ||||
|             <maxHistory>60</maxHistory> | ||||
|         </rollingPolicy> | ||||
|   | ||||
| @@ -14,7 +14,7 @@ | ||||
|     </description> | ||||
|  | ||||
|     <properties> | ||||
|         <revision>5.4.1</revision> | ||||
|         <revision>5.5.1</revision> | ||||
|     </properties> | ||||
|  | ||||
|     <dependencyManagement> | ||||
|   | ||||
| @@ -1,52 +0,0 @@ | ||||
| package org.dromara.common.core.config; | ||||
|  | ||||
| import cn.hutool.core.util.ArrayUtil; | ||||
| import org.dromara.common.core.exception.ServiceException; | ||||
| import org.dromara.common.core.utils.SpringUtils; | ||||
| import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; | ||||
| import org.springframework.boot.autoconfigure.AutoConfiguration; | ||||
| import org.springframework.core.task.VirtualThreadTaskExecutor; | ||||
| import org.springframework.scheduling.annotation.AsyncConfigurer; | ||||
|  | ||||
| import java.util.Arrays; | ||||
| import java.util.concurrent.Executor; | ||||
|  | ||||
| /** | ||||
|  * 异步配置 | ||||
|  * <p> | ||||
|  * 如果未使用虚拟线程则生效 | ||||
|  * | ||||
|  * @author Lion Li | ||||
|  */ | ||||
| @AutoConfiguration | ||||
| public class AsyncConfig implements AsyncConfigurer { | ||||
|  | ||||
|     /** | ||||
|      * 自定义 @Async 注解使用系统线程池 | ||||
|      */ | ||||
|     @Override | ||||
|     public Executor getAsyncExecutor() { | ||||
|         if(SpringUtils.isVirtual()) { | ||||
|             return new VirtualThreadTaskExecutor("async-"); | ||||
|         } | ||||
|         return SpringUtils.getBean("scheduledExecutorService"); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 异步执行异常处理 | ||||
|      */ | ||||
|     @Override | ||||
|     public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { | ||||
|         return (throwable, method, objects) -> { | ||||
|             throwable.printStackTrace(); | ||||
|             StringBuilder sb = new StringBuilder(); | ||||
|             sb.append("Exception message - ").append(throwable.getMessage()) | ||||
|                 .append(", Method name - ").append(method.getName()); | ||||
|             if (ArrayUtil.isNotEmpty(objects)) { | ||||
|                 sb.append(", Parameter value - ").append(Arrays.toString(objects)); | ||||
|             } | ||||
|             throw new ServiceException(sb.toString()); | ||||
|         }; | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -5,17 +5,12 @@ import lombok.extern.slf4j.Slf4j; | ||||
| import org.apache.commons.lang3.concurrent.BasicThreadFactory; | ||||
| import org.dromara.common.core.config.properties.ThreadPoolProperties; | ||||
| import org.dromara.common.core.utils.SpringUtils; | ||||
| import org.dromara.common.core.utils.Threads; | ||||
| import org.springframework.boot.autoconfigure.AutoConfiguration; | ||||
| import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; | ||||
| import org.springframework.boot.context.properties.EnableConfigurationProperties; | ||||
| import org.springframework.context.annotation.Bean; | ||||
| import org.springframework.core.task.VirtualThreadTaskExecutor; | ||||
| import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; | ||||
|  | ||||
| import java.util.concurrent.ScheduledExecutorService; | ||||
| import java.util.concurrent.ScheduledThreadPoolExecutor; | ||||
| import java.util.concurrent.ThreadPoolExecutor; | ||||
| import java.util.concurrent.*; | ||||
|  | ||||
| /** | ||||
|  * 线程池配置 | ||||
| @@ -34,18 +29,6 @@ public class ThreadPoolConfig { | ||||
|  | ||||
|     private ScheduledExecutorService scheduledExecutorService; | ||||
|  | ||||
|     @Bean(name = "threadPoolTaskExecutor") | ||||
|     @ConditionalOnProperty(prefix = "thread-pool", name = "enabled", havingValue = "true") | ||||
|     public ThreadPoolTaskExecutor threadPoolTaskExecutor(ThreadPoolProperties threadPoolProperties) { | ||||
|         ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); | ||||
|         executor.setCorePoolSize(core); | ||||
|         executor.setMaxPoolSize(core * 2); | ||||
|         executor.setQueueCapacity(threadPoolProperties.getQueueCapacity()); | ||||
|         executor.setKeepAliveSeconds(threadPoolProperties.getKeepAliveSeconds()); | ||||
|         executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); | ||||
|         return executor; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 执行周期性或定时任务 | ||||
|      */ | ||||
| @@ -64,7 +47,7 @@ public class ThreadPoolConfig { | ||||
|             @Override | ||||
|             protected void afterExecute(Runnable r, Throwable t) { | ||||
|                 super.afterExecute(r, t); | ||||
|                 Threads.printException(r, t); | ||||
|                 printException(r, t); | ||||
|             } | ||||
|         }; | ||||
|         this.scheduledExecutorService = scheduledThreadPoolExecutor; | ||||
| @@ -73,15 +56,57 @@ public class ThreadPoolConfig { | ||||
|  | ||||
|     /** | ||||
|      * 销毁事件 | ||||
|      * 停止线程池 | ||||
|      * 先使用shutdown, 停止接收新任务并尝试完成所有已存在任务. | ||||
|      * 如果超时, 则调用shutdownNow, 取消在workQueue中Pending的任务,并中断所有阻塞函数. | ||||
|      * 如果仍然超時,則強制退出. | ||||
|      * 另对在shutdown时线程本身被调用中断做了处理. | ||||
|      */ | ||||
|     @PreDestroy | ||||
|     public void destroy() { | ||||
|         try { | ||||
|             log.info("====关闭后台任务任务线程池===="); | ||||
|             Threads.shutdownAndAwaitTermination(scheduledExecutorService); | ||||
|             ScheduledExecutorService pool = scheduledExecutorService; | ||||
|             if (pool != null && !pool.isShutdown()) { | ||||
|                 pool.shutdown(); | ||||
|                 try { | ||||
|                     if (!pool.awaitTermination(120, TimeUnit.SECONDS)) { | ||||
|                         pool.shutdownNow(); | ||||
|                         if (!pool.awaitTermination(120, TimeUnit.SECONDS)) { | ||||
|                             log.info("Pool did not terminate"); | ||||
|                         } | ||||
|                     } | ||||
|                 } catch (InterruptedException ie) { | ||||
|                     pool.shutdownNow(); | ||||
|                     Thread.currentThread().interrupt(); | ||||
|                 } | ||||
|             } | ||||
|         } catch (Exception e) { | ||||
|             log.error(e.getMessage(), e); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 打印线程异常信息 | ||||
|      */ | ||||
|     public static void printException(Runnable r, Throwable t) { | ||||
|         if (t == null && r instanceof Future<?>) { | ||||
|             try { | ||||
|                 Future<?> future = (Future<?>) r; | ||||
|                 if (future.isDone()) { | ||||
|                     future.get(); | ||||
|                 } | ||||
|             } catch (CancellationException ce) { | ||||
|                 t = ce; | ||||
|             } catch (ExecutionException ee) { | ||||
|                 t = ee.getCause(); | ||||
|             } catch (InterruptedException ie) { | ||||
|                 Thread.currentThread().interrupt(); | ||||
|             } | ||||
|         } | ||||
|         if (t != null) { | ||||
|             log.error(t.getMessage(), t); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -72,5 +72,10 @@ public interface Constants { | ||||
|      */ | ||||
|     Long TOP_PARENT_ID = 0L; | ||||
|  | ||||
|     /** | ||||
|      * 加密头 | ||||
|      */ | ||||
|     String ENCRYPT_HEADER = "ENC_"; | ||||
|  | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -50,6 +50,11 @@ public class CompleteTaskDTO implements Serializable { | ||||
|      */ | ||||
|     private String notice; | ||||
|  | ||||
|     /** | ||||
|      * 办理人(可不填 用于覆盖当前节点办理人) | ||||
|      */ | ||||
|     private String handler; | ||||
|  | ||||
|     /** | ||||
|      * 流程变量 | ||||
|      */ | ||||
| @@ -62,7 +67,8 @@ public class CompleteTaskDTO implements Serializable { | ||||
|  | ||||
|     public Map<String, Object> getVariables() { | ||||
|         if (variables == null) { | ||||
|             return new HashMap<>(16); | ||||
|             variables = new HashMap<>(16); | ||||
|             return variables; | ||||
|         } | ||||
|         variables.entrySet().removeIf(entry -> Objects.isNull(entry.getValue())); | ||||
|         return variables; | ||||
|   | ||||
| @@ -0,0 +1,45 @@ | ||||
| package org.dromara.common.core.domain.dto; | ||||
|  | ||||
| import lombok.Data; | ||||
|  | ||||
| import java.io.Serial; | ||||
| import java.io.Serializable; | ||||
|  | ||||
| /** | ||||
|  * 流程实例业务扩展对象 | ||||
|  * | ||||
|  * @author may | ||||
|  * @date 2025-08-05 | ||||
|  */ | ||||
| @Data | ||||
| public class FlowInstanceBizExtDTO implements Serializable { | ||||
|  | ||||
|     @Serial | ||||
|     private static final long serialVersionUID = 1L; | ||||
|  | ||||
|     /** | ||||
|      * 主键 | ||||
|      */ | ||||
|     private Long id; | ||||
|  | ||||
|     /** | ||||
|      * 流程实例ID | ||||
|      */ | ||||
|     private Long instanceId; | ||||
|  | ||||
|     /** | ||||
|      * 业务ID | ||||
|      */ | ||||
|     private String businessId; | ||||
|  | ||||
|     /** | ||||
|      * 业务编码 | ||||
|      */ | ||||
|     private String businessCode; | ||||
|  | ||||
|     /** | ||||
|      * 业务标题 | ||||
|      */ | ||||
|     private String businessTitle; | ||||
|  | ||||
| } | ||||
| @@ -1,6 +1,7 @@ | ||||
| package org.dromara.common.core.domain.dto; | ||||
|  | ||||
|  | ||||
| import cn.hutool.core.util.ObjectUtil; | ||||
| import lombok.Data; | ||||
|  | ||||
| import java.io.Serial; | ||||
| @@ -30,11 +31,21 @@ public class StartProcessDTO implements Serializable { | ||||
|      */ | ||||
|     private String flowCode; | ||||
|  | ||||
|     /** | ||||
|      * 办理人(可不填 用于覆盖当前节点办理人) | ||||
|      */ | ||||
|     private String handler; | ||||
|  | ||||
|     /** | ||||
|      * 流程变量,前端会提交一个元素{'entity': {业务详情数据对象}} | ||||
|      */ | ||||
|     private Map<String, Object> variables; | ||||
|  | ||||
|     /** | ||||
|      * 流程业务扩展信息 | ||||
|      */ | ||||
|     private FlowInstanceBizExtDTO bizExt; | ||||
|  | ||||
|     public Map<String, Object> getVariables() { | ||||
|         if (variables == null) { | ||||
|             return new HashMap<>(16); | ||||
| @@ -42,4 +53,11 @@ public class StartProcessDTO implements Serializable { | ||||
|         variables.entrySet().removeIf(entry -> Objects.isNull(entry.getValue())); | ||||
|         return variables; | ||||
|     } | ||||
|  | ||||
|     public FlowInstanceBizExtDTO getBizExt() { | ||||
|         if (ObjectUtil.isNull(bizExt)) { | ||||
|             bizExt = new FlowInstanceBizExtDTO(); | ||||
|         } | ||||
|         return bizExt; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -52,17 +52,17 @@ public class TaskAssigneeDTO implements Serializable { | ||||
|      */ | ||||
|     public static <T> List<TaskHandler> convertToHandlerList( | ||||
|         List<T> sourceList, | ||||
|         Function<T, Long> storageId, | ||||
|         Function<T, String> storageId, | ||||
|         Function<T, String> handlerCode, | ||||
|         Function<T, String> handlerName, | ||||
|         Function<T, Long> groupName, | ||||
|         Function<T, String> groupName, | ||||
|         Function<T, Date> createTimeMapper) { | ||||
|         return sourceList.stream() | ||||
|             .map(item -> new TaskHandler( | ||||
|                 String.valueOf(storageId.apply(item)), | ||||
|                 storageId.apply(item), | ||||
|                 handlerCode.apply(item), | ||||
|                 handlerName.apply(item), | ||||
|                 groupName != null ? String.valueOf(groupName.apply(item)) : null, | ||||
|                 groupName.apply(item), | ||||
|                 createTimeMapper.apply(item) | ||||
|             )).collect(Collectors.toList()); | ||||
|     } | ||||
|   | ||||
| @@ -27,6 +27,11 @@ public class ProcessEvent implements Serializable { | ||||
|      */ | ||||
|     private String flowCode; | ||||
|  | ||||
|     /** | ||||
|      * 实例id | ||||
|      */ | ||||
|     private Long instanceId; | ||||
|  | ||||
|     /** | ||||
|      * 业务id | ||||
|      */ | ||||
|   | ||||
| @@ -4,6 +4,7 @@ import lombok.Data; | ||||
|  | ||||
| import java.io.Serial; | ||||
| import java.io.Serializable; | ||||
| import java.util.Map; | ||||
|  | ||||
| /** | ||||
|  * 流程任务监听 | ||||
| @@ -46,6 +47,11 @@ public class ProcessTaskEvent implements Serializable { | ||||
|      */ | ||||
|     private Long taskId; | ||||
|  | ||||
|     /** | ||||
|      * 实例id | ||||
|      */ | ||||
|     private Long instanceId; | ||||
|  | ||||
|     /** | ||||
|      * 业务id | ||||
|      */ | ||||
| @@ -56,4 +62,9 @@ public class ProcessTaskEvent implements Serializable { | ||||
|      */ | ||||
|     private String status; | ||||
|  | ||||
|     /** | ||||
|      * 办理参数 | ||||
|      */ | ||||
|     private Map<String, Object> params; | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -26,6 +26,7 @@ public class PasswordLoginBody extends LoginBody { | ||||
|      */ | ||||
|     @NotBlank(message = "{user.password.not.blank}") | ||||
|     @Length(min = 5, max = 30, message = "{user.password.length.valid}") | ||||
| //    @Pattern(regexp = RegexConstants.PASSWORD, message = "{user.password.format.valid}") | ||||
|     private String password; | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -26,8 +26,12 @@ public class RegisterBody extends LoginBody { | ||||
|      */ | ||||
|     @NotBlank(message = "{user.password.not.blank}") | ||||
|     @Length(min = 5, max = 30, message = "{user.password.length.valid}") | ||||
| //    @Pattern(regexp = RegexConstants.PASSWORD, message = "{user.password.format.valid}") | ||||
|     private String password; | ||||
|  | ||||
|     /** | ||||
|      * 用户类型 | ||||
|      */ | ||||
|     private String userType; | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -1,11 +1,15 @@ | ||||
| package org.dromara.common.core.exception; | ||||
|  | ||||
| import lombok.*; | ||||
| import cn.hutool.core.text.StrFormatter; | ||||
| import lombok.AllArgsConstructor; | ||||
| import lombok.Data; | ||||
| import lombok.EqualsAndHashCode; | ||||
| import lombok.NoArgsConstructor; | ||||
|  | ||||
| import java.io.Serial; | ||||
|  | ||||
| /** | ||||
|  * 业务异常 | ||||
|  * 业务异常(支持占位符 {} ) | ||||
|  * | ||||
|  * @author ruoyi | ||||
|  */ | ||||
| @@ -42,6 +46,10 @@ public final class ServiceException extends RuntimeException { | ||||
|         this.code = code; | ||||
|     } | ||||
|  | ||||
|     public ServiceException(String message, Object... args) { | ||||
|         this.message = StrFormatter.format(message, args); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public String getMessage() { | ||||
|         return message; | ||||
|   | ||||
| @@ -3,6 +3,7 @@ package org.dromara.common.core.service; | ||||
| import org.dromara.common.core.domain.dto.DeptDTO; | ||||
|  | ||||
| import java.util.List; | ||||
| import java.util.Map; | ||||
|  | ||||
| /** | ||||
|  * 通用 部门服务 | ||||
| @@ -34,4 +35,12 @@ public interface DeptService { | ||||
|      */ | ||||
|     List<DeptDTO> selectDeptsByList(); | ||||
|  | ||||
|     /** | ||||
|      * 根据部门 ID 列表查询部门名称映射关系 | ||||
|      * | ||||
|      * @param deptIds 部门 ID 列表 | ||||
|      * @return Map,其中 key 为部门 ID,value 为对应的部门名称 | ||||
|      */ | ||||
|     Map<Long, String> selectDeptNamesByIds(List<Long> deptIds); | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -1,5 +1,8 @@ | ||||
| package org.dromara.common.core.service; | ||||
|  | ||||
| import java.util.List; | ||||
| import java.util.Map; | ||||
|  | ||||
| /** | ||||
|  * 通用 岗位服务 | ||||
|  * | ||||
| @@ -7,4 +10,12 @@ package org.dromara.common.core.service; | ||||
|  */ | ||||
| public interface PostService { | ||||
|  | ||||
|     /** | ||||
|      * 根据岗位 ID 列表查询岗位名称映射关系 | ||||
|      * | ||||
|      * @param postIds 岗位 ID 列表 | ||||
|      * @return Map,其中 key 为岗位 ID,value 为对应的岗位名称 | ||||
|      */ | ||||
|     Map<Long, String> selectPostNamesByIds(List<Long> postIds); | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -1,5 +1,8 @@ | ||||
| package org.dromara.common.core.service; | ||||
|  | ||||
| import java.util.List; | ||||
| import java.util.Map; | ||||
|  | ||||
| /** | ||||
|  * 通用 角色服务 | ||||
|  * | ||||
| @@ -7,4 +10,12 @@ package org.dromara.common.core.service; | ||||
|  */ | ||||
| public interface RoleService { | ||||
|  | ||||
|     /** | ||||
|      * 根据角色 ID 列表查询角色名称映射关系 | ||||
|      * | ||||
|      * @param roleIds 角色 ID 列表 | ||||
|      * @return Map,其中 key 为角色 ID,value 为对应的角色名称 | ||||
|      */ | ||||
|     Map<Long, String> selectRoleNamesByIds(List<Long> roleIds); | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -100,28 +100,4 @@ public interface UserService { | ||||
|      */ | ||||
|     Map<Long, String> selectUserNamesByIds(List<Long> userIds); | ||||
|  | ||||
|     /** | ||||
|      * 根据角色 ID 列表查询角色名称映射关系 | ||||
|      * | ||||
|      * @param roleIds 角色 ID 列表 | ||||
|      * @return Map,其中 key 为角色 ID,value 为对应的角色名称 | ||||
|      */ | ||||
|     Map<Long, String> selectRoleNamesByIds(List<Long> roleIds); | ||||
|  | ||||
|     /** | ||||
|      * 根据部门 ID 列表查询部门名称映射关系 | ||||
|      * | ||||
|      * @param deptIds 部门 ID 列表 | ||||
|      * @return Map,其中 key 为部门 ID,value 为对应的部门名称 | ||||
|      */ | ||||
|     Map<Long, String> selectDeptNamesByIds(List<Long> deptIds); | ||||
|  | ||||
|     /** | ||||
|      * 根据岗位 ID 列表查询岗位名称映射关系 | ||||
|      * | ||||
|      * @param postIds 岗位 ID 列表 | ||||
|      * @return Map,其中 key 为岗位 ID,value 为对应的岗位名称 | ||||
|      */ | ||||
|     Map<Long, String> selectPostNamesByIds(List<Long> postIds); | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -82,6 +82,7 @@ public interface WorkflowService { | ||||
|      * completeTask.getVariables().put("ignore", true); | ||||
|      * | ||||
|      * @param completeTask 参数 | ||||
|      * @return 结果 | ||||
|      */ | ||||
|     boolean completeTask(CompleteTaskDTO completeTask); | ||||
|  | ||||
| @@ -90,6 +91,15 @@ public interface WorkflowService { | ||||
|      * | ||||
|      * @param taskId  任务ID | ||||
|      * @param message 办理意见 | ||||
|      * @return 结果 | ||||
|      */ | ||||
|     boolean completeTask(Long taskId, String message); | ||||
|  | ||||
|     /** | ||||
|      * 启动流程并办理第一个任务 | ||||
|      * | ||||
|      * @param startProcess 参数 | ||||
|      * @return 结果 | ||||
|      */ | ||||
|     boolean startCompleteTask(StartProcessDTO startProcess); | ||||
| } | ||||
|   | ||||
| @@ -293,7 +293,7 @@ public class DateUtils extends org.apache.commons.lang3.time.DateUtils { | ||||
|  | ||||
|         // 校验时间跨度不超过最大限制 | ||||
|         if (diff > maxValue) { | ||||
|             throw new ServiceException("最大时间跨度为 " + maxValue + " " + unit.toString().toLowerCase()); | ||||
|             throw new ServiceException("最大时间跨度为 {} {}", maxValue, unit.toString().toLowerCase()); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -115,7 +115,7 @@ public class ServletUtils extends JakartaServletUtil { | ||||
|     public static Map<String, String> getParamMap(ServletRequest request) { | ||||
|         Map<String, String> params = new HashMap<>(); | ||||
|         for (Map.Entry<String, String[]> entry : getParams(request).entrySet()) { | ||||
|             params.put(entry.getKey(), StringUtils.join(entry.getValue(), StringUtils.SEPARATOR)); | ||||
|             params.put(entry.getKey(), StringUtils.joinComma(entry.getValue())); | ||||
|         } | ||||
|         return params; | ||||
|     } | ||||
|   | ||||
| @@ -7,7 +7,6 @@ import lombok.NoArgsConstructor; | ||||
|  | ||||
| import java.util.*; | ||||
| import java.util.function.BiFunction; | ||||
| import java.util.function.Consumer; | ||||
| import java.util.function.Function; | ||||
| import java.util.function.Predicate; | ||||
| import java.util.stream.Collectors; | ||||
| @@ -31,8 +30,10 @@ public class StreamUtils { | ||||
|         if (CollUtil.isEmpty(collection)) { | ||||
|             return CollUtil.newArrayList(); | ||||
|         } | ||||
|         // 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题 | ||||
|         return collection.stream().filter(function).collect(Collectors.toList()); | ||||
|         return collection.stream() | ||||
|             .filter(function) | ||||
|             // 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题 | ||||
|             .collect(Collectors.toList()); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -40,13 +41,26 @@ public class StreamUtils { | ||||
|      * | ||||
|      * @param collection 需要查询的集合 | ||||
|      * @param function   过滤方法 | ||||
|      * @return 找到符合条件的第一个元素,没有则返回null | ||||
|      * @return 找到符合条件的第一个元素,没有则返回 Optional.empty() | ||||
|      */ | ||||
|     public static <E> E findFirst(Collection<E> collection, Predicate<E> function) { | ||||
|     public static <E> Optional<E> findFirst(Collection<E> collection, Predicate<E> function) { | ||||
|         if (CollUtil.isEmpty(collection)) { | ||||
|             return null; | ||||
|             return Optional.empty(); | ||||
|         } | ||||
|         return collection.stream().filter(function).findFirst().orElse(null); | ||||
|         return collection.stream() | ||||
|             .filter(function) | ||||
|             .findFirst(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 找到流中满足条件的第一个元素值 | ||||
|      * | ||||
|      * @param collection 需要查询的集合 | ||||
|      * @param function   过滤方法 | ||||
|      * @return 找到符合条件的第一个元素,没有则返回 null | ||||
|      */ | ||||
|     public static <E> E findFirstValue(Collection<E> collection, Predicate<E> function) { | ||||
|         return findFirst(collection,function).orElse(null); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -54,13 +68,26 @@ public class StreamUtils { | ||||
|      * | ||||
|      * @param collection 需要查询的集合 | ||||
|      * @param function   过滤方法 | ||||
|      * @return 找到符合条件的任意一个元素,没有则返回null | ||||
|      * @return 找到符合条件的任意一个元素,没有则返回 Optional.empty() | ||||
|      */ | ||||
|     public static <E> Optional<E> findAny(Collection<E> collection, Predicate<E> function) { | ||||
|         if (CollUtil.isEmpty(collection)) { | ||||
|             return Optional.empty(); | ||||
|         } | ||||
|         return collection.stream().filter(function).findAny(); | ||||
|         return collection.stream() | ||||
|             .filter(function) | ||||
|             .findAny(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 找到流中任意一个满足条件的元素值 | ||||
|      * | ||||
|      * @param collection 需要查询的集合 | ||||
|      * @param function   过滤方法 | ||||
|      * @return 找到符合条件的任意一个元素,没有则返回null | ||||
|      */ | ||||
|     public static <E> E findAnyValue(Collection<E> collection, Predicate<E> function) { | ||||
|         return findAny(collection,function).orElse(null); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -86,7 +113,10 @@ public class StreamUtils { | ||||
|         if (CollUtil.isEmpty(collection)) { | ||||
|             return StringUtils.EMPTY; | ||||
|         } | ||||
|         return collection.stream().map(function).filter(Objects::nonNull).collect(Collectors.joining(delimiter)); | ||||
|         return collection.stream() | ||||
|             .map(function) | ||||
|             .filter(Objects::nonNull) | ||||
|             .collect(Collectors.joining(delimiter)); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -100,8 +130,11 @@ public class StreamUtils { | ||||
|         if (CollUtil.isEmpty(collection)) { | ||||
|             return CollUtil.newArrayList(); | ||||
|         } | ||||
|         // 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题 | ||||
|         return collection.stream().filter(Objects::nonNull).sorted(comparing).collect(Collectors.toList()); | ||||
|         return collection.stream() | ||||
|             .filter(Objects::nonNull) | ||||
|             .sorted(comparing) | ||||
|             // 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题 | ||||
|             .collect(Collectors.toList()); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -118,7 +151,9 @@ public class StreamUtils { | ||||
|         if (CollUtil.isEmpty(collection)) { | ||||
|             return MapUtil.newHashMap(); | ||||
|         } | ||||
|         return collection.stream().filter(Objects::nonNull).collect(Collectors.toMap(key, Function.identity(), (l, r) -> l)); | ||||
|         return collection.stream() | ||||
|             .filter(Objects::nonNull) | ||||
|             .collect(Collectors.toMap(key, Function.identity(), (l, r) -> l)); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -137,7 +172,25 @@ public class StreamUtils { | ||||
|         if (CollUtil.isEmpty(collection)) { | ||||
|             return MapUtil.newHashMap(); | ||||
|         } | ||||
|         return collection.stream().filter(Objects::nonNull).collect(Collectors.toMap(key, value, (l, r) -> l)); | ||||
|         return collection.stream() | ||||
|             .filter(Objects::nonNull) | ||||
|             .collect(Collectors.toMap(key, value, (l, r) -> l)); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取 map 中的数据作为新 Map 的 value ,key 不变 | ||||
|      * @param map 需要处理的map | ||||
|      * @param take 取值函数 | ||||
|      * @param <K> map中的key类型 | ||||
|      * @param <E> map中的value类型 | ||||
|      * @param <V> 新map中的value类型 | ||||
|      * @return 新的map | ||||
|      */ | ||||
|     public static <K, E, V> Map<K, V> toMap(Map<K, E> map, BiFunction<K, E, V> take) { | ||||
|         if (CollUtil.isEmpty(map)) { | ||||
|             return MapUtil.newHashMap(); | ||||
|         } | ||||
|         return toMap(map.entrySet(), Map.Entry::getKey, entry -> take.apply(entry.getKey(), entry.getValue())); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -154,8 +207,8 @@ public class StreamUtils { | ||||
|         if (CollUtil.isEmpty(collection)) { | ||||
|             return MapUtil.newHashMap(); | ||||
|         } | ||||
|         return collection | ||||
|             .stream().filter(Objects::nonNull) | ||||
|         return collection.stream() | ||||
|             .filter(Objects::nonNull) | ||||
|             .collect(Collectors.groupingBy(key, LinkedHashMap::new, Collectors.toList())); | ||||
|     } | ||||
|  | ||||
| @@ -175,8 +228,8 @@ public class StreamUtils { | ||||
|         if (CollUtil.isEmpty(collection)) { | ||||
|             return MapUtil.newHashMap(); | ||||
|         } | ||||
|         return collection | ||||
|             .stream().filter(Objects::nonNull) | ||||
|         return collection.stream() | ||||
|             .filter(Objects::nonNull) | ||||
|             .collect(Collectors.groupingBy(key1, LinkedHashMap::new, Collectors.groupingBy(key2, LinkedHashMap::new, Collectors.toList()))); | ||||
|     } | ||||
|  | ||||
| @@ -193,11 +246,11 @@ public class StreamUtils { | ||||
|      * @return 分类后的map | ||||
|      */ | ||||
|     public static <E, T, U> Map<T, Map<U, E>> group2Map(Collection<E> collection, Function<E, T> key1, Function<E, U> key2) { | ||||
|         if (CollUtil.isEmpty(collection) || key1 == null || key2 == null) { | ||||
|         if (CollUtil.isEmpty(collection)) { | ||||
|             return MapUtil.newHashMap(); | ||||
|         } | ||||
|         return collection | ||||
|             .stream().filter(Objects::nonNull) | ||||
|         return collection.stream() | ||||
|             .filter(Objects::nonNull) | ||||
|             .collect(Collectors.groupingBy(key1, LinkedHashMap::new, Collectors.toMap(key2, Function.identity(), (l, r) -> l))); | ||||
|     } | ||||
|  | ||||
| @@ -215,8 +268,7 @@ public class StreamUtils { | ||||
|         if (CollUtil.isEmpty(collection)) { | ||||
|             return CollUtil.newArrayList(); | ||||
|         } | ||||
|         return collection | ||||
|             .stream() | ||||
|         return collection.stream() | ||||
|             .map(function) | ||||
|             .filter(Objects::nonNull) | ||||
|             // 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题 | ||||
| @@ -234,11 +286,10 @@ public class StreamUtils { | ||||
|      * @return 转化后的Set | ||||
|      */ | ||||
|     public static <E, T> Set<T> toSet(Collection<E> collection, Function<E, T> function) { | ||||
|         if (CollUtil.isEmpty(collection) || function == null) { | ||||
|         if (CollUtil.isEmpty(collection)) { | ||||
|             return CollUtil.newHashSet(); | ||||
|         } | ||||
|         return collection | ||||
|             .stream() | ||||
|         return collection.stream() | ||||
|             .map(function) | ||||
|             .filter(Objects::nonNull) | ||||
|             .collect(Collectors.toSet()); | ||||
| @@ -258,26 +309,20 @@ public class StreamUtils { | ||||
|      * @return 合并后的map | ||||
|      */ | ||||
|     public static <K, X, Y, V> Map<K, V> merge(Map<K, X> map1, Map<K, Y> map2, BiFunction<X, Y, V> merge) { | ||||
|         if (MapUtil.isEmpty(map1) && MapUtil.isEmpty(map2)) { | ||||
|         if (CollUtil.isEmpty(map1) && CollUtil.isEmpty(map2)) { | ||||
|             // 如果两个 map 都为空,则直接返回空的 map | ||||
|             return MapUtil.newHashMap(); | ||||
|         } else if (MapUtil.isEmpty(map1)) { | ||||
|             map1 = MapUtil.newHashMap(); | ||||
|         } else if (MapUtil.isEmpty(map2)) { | ||||
|             map2 = MapUtil.newHashMap(); | ||||
|         } else if (CollUtil.isEmpty(map1)) { | ||||
|             // 如果 map1 为空,则直接处理返回 map2 | ||||
|             return toMap(map2.entrySet(), Map.Entry::getKey, entry -> merge.apply(null, entry.getValue())); | ||||
|         } else if (CollUtil.isEmpty(map2)) { | ||||
|             // 如果 map2 为空,则直接处理返回 map1 | ||||
|             return toMap(map1.entrySet(), Map.Entry::getKey, entry -> merge.apply(entry.getValue(), null)); | ||||
|         } | ||||
|         Set<K> key = new HashSet<>(); | ||||
|         key.addAll(map1.keySet()); | ||||
|         key.addAll(map2.keySet()); | ||||
|         Map<K, V> map = new HashMap<>(); | ||||
|         for (K t : key) { | ||||
|             X x = map1.get(t); | ||||
|             Y y = map2.get(t); | ||||
|             V z = merge.apply(x, y); | ||||
|             if (z != null) { | ||||
|                 map.put(t, z); | ||||
|             } | ||||
|         } | ||||
|         return map; | ||||
|         Set<K> keySet = new HashSet<>(); | ||||
|         keySet.addAll(map1.keySet()); | ||||
|         keySet.addAll(map2.keySet()); | ||||
|         return toMap(keySet, key -> key, key -> merge.apply(map1.get(key), map2.get(key))); | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -260,13 +260,13 @@ public class StringUtils extends org.apache.commons.lang3.StringUtils { | ||||
|         if (s != null) { | ||||
|             final int len = s.length(); | ||||
|             if (s.length() <= size) { | ||||
|                 sb.append(String.valueOf(c).repeat(size - len)); | ||||
|                 sb.append(Convert.toStr(c).repeat(size - len)); | ||||
|                 sb.append(s); | ||||
|             } else { | ||||
|                 return s.substring(len - size, len); | ||||
|             } | ||||
|         } else { | ||||
|             sb.append(String.valueOf(c).repeat(Math.max(0, size))); | ||||
|             sb.append(Convert.toStr(c).repeat(Math.max(0, size))); | ||||
|         } | ||||
|         return sb.toString(); | ||||
|     } | ||||
| @@ -361,5 +361,24 @@ public class StringUtils extends org.apache.commons.lang3.StringUtils { | ||||
|             return input; | ||||
|         } | ||||
|     } | ||||
|     /** | ||||
|      * 将可迭代对象中的元素使用逗号拼接成字符串 | ||||
|      * | ||||
|      * @param iterable 可迭代对象,如 List、Set 等 | ||||
|      * @return 拼接后的字符串 | ||||
|      */ | ||||
|     public static String joinComma(Iterable<?> iterable) { | ||||
|         return StringUtils.join(iterable, SEPARATOR); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 将数组中的元素使用逗号拼接成字符串 | ||||
|      * | ||||
|      * @param array 任意类型的数组 | ||||
|      * @return 拼接后的字符串 | ||||
|      */ | ||||
|     public static String joinComma(Object[] array) { | ||||
|         return StringUtils.join(array, SEPARATOR); | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -1,63 +0,0 @@ | ||||
| package org.dromara.common.core.utils; | ||||
|  | ||||
| import lombok.AccessLevel; | ||||
| import lombok.NoArgsConstructor; | ||||
| import lombok.extern.slf4j.Slf4j; | ||||
|  | ||||
| import java.util.concurrent.*; | ||||
|  | ||||
| /** | ||||
|  * 线程相关工具类. | ||||
|  * | ||||
|  * @author ruoyi | ||||
|  */ | ||||
| @Slf4j | ||||
| @NoArgsConstructor(access = AccessLevel.PRIVATE) | ||||
| public class Threads { | ||||
|     /** | ||||
|      * 停止线程池 | ||||
|      * 先使用shutdown, 停止接收新任务并尝试完成所有已存在任务. | ||||
|      * 如果超时, 则调用shutdownNow, 取消在workQueue中Pending的任务,并中断所有阻塞函数. | ||||
|      * 如果仍然超時,則強制退出. | ||||
|      * 另对在shutdown时线程本身被调用中断做了处理. | ||||
|      */ | ||||
|     public static void shutdownAndAwaitTermination(ExecutorService pool) { | ||||
|         if (pool != null && !pool.isShutdown()) { | ||||
|             pool.shutdown(); | ||||
|             try { | ||||
|                 if (!pool.awaitTermination(120, TimeUnit.SECONDS)) { | ||||
|                     pool.shutdownNow(); | ||||
|                     if (!pool.awaitTermination(120, TimeUnit.SECONDS)) { | ||||
|                         log.info("Pool did not terminate"); | ||||
|                     } | ||||
|                 } | ||||
|             } catch (InterruptedException ie) { | ||||
|                 pool.shutdownNow(); | ||||
|                 Thread.currentThread().interrupt(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 打印线程异常信息 | ||||
|      */ | ||||
|     public static void printException(Runnable r, Throwable t) { | ||||
|         if (t == null && r instanceof Future<?>) { | ||||
|             try { | ||||
|                 Future<?> future = (Future<?>) r; | ||||
|                 if (future.isDone()) { | ||||
|                     future.get(); | ||||
|                 } | ||||
|             } catch (CancellationException ce) { | ||||
|                 t = ce; | ||||
|             } catch (ExecutionException ee) { | ||||
|                 t = ee.getCause(); | ||||
|             } catch (InterruptedException ie) { | ||||
|                 Thread.currentThread().interrupt(); | ||||
|             } | ||||
|         } | ||||
|         if (t != null) { | ||||
|             log.error(t.getMessage(), t); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,5 +1,4 @@ | ||||
| org.dromara.common.core.config.ApplicationConfig | ||||
| org.dromara.common.core.config.AsyncConfig | ||||
| org.dromara.common.core.config.ThreadPoolConfig | ||||
| org.dromara.common.core.config.ValidatorConfig | ||||
| org.dromara.common.core.utils.SpringUtils | ||||
|   | ||||
| @@ -54,14 +54,15 @@ public class SpringDocConfig { | ||||
|         openApi.externalDocs(properties.getExternalDocs()); | ||||
|         openApi.tags(properties.getTags()); | ||||
|         openApi.paths(properties.getPaths()); | ||||
|         openApi.components(properties.getComponents()); | ||||
|         Set<String> keySet = properties.getComponents().getSecuritySchemes().keySet(); | ||||
|         List<SecurityRequirement> list = new ArrayList<>(); | ||||
|         SecurityRequirement securityRequirement = new SecurityRequirement(); | ||||
|         keySet.forEach(securityRequirement::addList); | ||||
|         list.add(securityRequirement); | ||||
|         openApi.security(list); | ||||
|  | ||||
|         if (properties.getComponents() != null) { | ||||
|             openApi.components(properties.getComponents()); | ||||
|             Set<String> keySet = properties.getComponents().getSecuritySchemes().keySet(); | ||||
|             List<SecurityRequirement> list = new ArrayList<>(); | ||||
|             SecurityRequirement securityRequirement = new SecurityRequirement(); | ||||
|             keySet.forEach(securityRequirement::addList); | ||||
|             list.add(securityRequirement); | ||||
|             openApi.security(list); | ||||
|         } | ||||
|         return openApi; | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -6,6 +6,7 @@ import org.dromara.common.encrypt.properties.ApiDecryptProperties; | ||||
| import org.springframework.boot.autoconfigure.AutoConfiguration; | ||||
| import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; | ||||
| import org.springframework.boot.context.properties.EnableConfigurationProperties; | ||||
| import org.springframework.boot.web.servlet.FilterRegistration; | ||||
| import org.springframework.boot.web.servlet.FilterRegistrationBean; | ||||
| import org.springframework.context.annotation.Bean; | ||||
|  | ||||
| @@ -20,13 +21,14 @@ import org.springframework.context.annotation.Bean; | ||||
| public class ApiDecryptAutoConfiguration { | ||||
|  | ||||
|     @Bean | ||||
|     public FilterRegistrationBean<CryptoFilter> cryptoFilterRegistration(ApiDecryptProperties properties) { | ||||
|         FilterRegistrationBean<CryptoFilter> registration = new FilterRegistrationBean<>(); | ||||
|         registration.setDispatcherTypes(DispatcherType.REQUEST); | ||||
|         registration.setFilter(new CryptoFilter(properties)); | ||||
|         registration.addUrlPatterns("/*"); | ||||
|         registration.setName("cryptoFilter"); | ||||
|         registration.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE); | ||||
|         return registration; | ||||
|     @FilterRegistration( | ||||
|         name = "cryptoFilter", | ||||
|         urlPatterns = "/*", | ||||
|         order = FilterRegistrationBean.HIGHEST_PRECEDENCE, | ||||
|         dispatcherTypes = DispatcherType.REQUEST | ||||
|     ) | ||||
|     public CryptoFilter cryptoFilter(ApiDecryptProperties properties) { | ||||
|         return new CryptoFilter(properties); | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -5,6 +5,7 @@ import cn.hutool.core.util.ReflectUtil; | ||||
| import lombok.NoArgsConstructor; | ||||
| import lombok.extern.slf4j.Slf4j; | ||||
| import org.apache.ibatis.io.Resources; | ||||
| import org.dromara.common.core.constant.Constants; | ||||
| import org.dromara.common.core.utils.ObjectUtils; | ||||
| import org.dromara.common.core.utils.StringUtils; | ||||
| import org.dromara.common.encrypt.annotation.EncryptField; | ||||
| @@ -92,8 +93,12 @@ public class EncryptorManager { | ||||
|      * @param encryptContext 加密相关的配置信息 | ||||
|      */ | ||||
|     public String encrypt(String value, EncryptContext encryptContext) { | ||||
|         if (StringUtils.startsWith(value, Constants.ENCRYPT_HEADER)) { | ||||
|             return value; | ||||
|         } | ||||
|         IEncryptor encryptor = this.registAndGetEncryptor(encryptContext); | ||||
|         return encryptor.encrypt(value, encryptContext.getEncode()); | ||||
|         String encrypt = encryptor.encrypt(value, encryptContext.getEncode()); | ||||
|         return Constants.ENCRYPT_HEADER + encrypt; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -103,8 +108,12 @@ public class EncryptorManager { | ||||
|      * @param encryptContext 加密相关的配置信息 | ||||
|      */ | ||||
|     public String decrypt(String value, EncryptContext encryptContext) { | ||||
|         if (!StringUtils.startsWith(value, Constants.ENCRYPT_HEADER)) { | ||||
|             return value; | ||||
|         } | ||||
|         IEncryptor encryptor = this.registAndGetEncryptor(encryptContext); | ||||
|         return encryptor.decrypt(value); | ||||
|         String str = StringUtils.removeStart(value, Constants.ENCRYPT_HEADER); | ||||
|         return encryptor.decrypt(str); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|   | ||||
| @@ -28,7 +28,7 @@ public class ExcelBigNumberConvert implements Converter<Long> { | ||||
|  | ||||
|     @Override | ||||
|     public CellDataTypeEnum supportExcelTypeKey() { | ||||
|         return CellDataTypeEnum.STRING; | ||||
|         return null; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|   | ||||
| @@ -0,0 +1,200 @@ | ||||
| package org.dromara.common.excel.core; | ||||
|  | ||||
| import cn.hutool.core.collection.CollUtil; | ||||
| import cn.hutool.core.util.ReflectUtil; | ||||
| import cn.hutool.core.util.StrUtil; | ||||
| import cn.idev.excel.annotation.ExcelIgnore; | ||||
| import cn.idev.excel.annotation.ExcelIgnoreUnannotated; | ||||
| import cn.idev.excel.annotation.ExcelProperty; | ||||
| import lombok.SneakyThrows; | ||||
| import org.apache.poi.ss.util.CellRangeAddress; | ||||
| import org.dromara.common.core.utils.reflect.ReflectUtils; | ||||
| import org.dromara.common.excel.annotation.CellMerge; | ||||
|  | ||||
| import java.lang.reflect.Field; | ||||
| import java.util.*; | ||||
|  | ||||
| /** | ||||
|  * 单元格合并处理器 | ||||
|  * | ||||
|  * @author Lion Li | ||||
|  */ | ||||
| public class CellMergeHandler { | ||||
|  | ||||
|     private final boolean hasTitle; | ||||
|     private int rowIndex; | ||||
|  | ||||
|     private CellMergeHandler(final boolean hasTitle) { | ||||
|         this.hasTitle = hasTitle; | ||||
|         // 行合并开始下标 | ||||
|         this.rowIndex = hasTitle ? 1 : 0; | ||||
|     } | ||||
|  | ||||
|     @SneakyThrows | ||||
|     public List<CellRangeAddress> handle(List<?> rows) { | ||||
|         // 如果入参为空集合则返回空集 | ||||
|         if (CollUtil.isEmpty(rows)) { | ||||
|             return Collections.emptyList(); | ||||
|         } | ||||
|  | ||||
|         // 获取有合并注解的字段 | ||||
|         Map<Field, FieldColumnIndex> mergeFields = getFieldColumnIndexMap(rows.get(0).getClass()); | ||||
|         // 如果没有需要合并的字段则返回空集 | ||||
|         if (CollUtil.isEmpty(mergeFields)) { | ||||
|             return Collections.emptyList(); | ||||
|         } | ||||
|  | ||||
|         // 结果集 | ||||
|         List<CellRangeAddress> result = new ArrayList<>(); | ||||
|  | ||||
|         // 生成两两合并单元格 | ||||
|         Map<Field, RepeatCell> rowRepeatCellMap = new HashMap<>(); | ||||
|         for (Map.Entry<Field, FieldColumnIndex> item : mergeFields.entrySet()) { | ||||
|             Field field = item.getKey(); | ||||
|             FieldColumnIndex itemValue = item.getValue(); | ||||
|             int colNum = itemValue.colIndex(); | ||||
|             CellMerge cellMerge = itemValue.cellMerge(); | ||||
|  | ||||
|             for (int i = 0; i < rows.size(); i++) { | ||||
|                 // 当前行数据 | ||||
|                 Object currentRowObj = rows.get(i); | ||||
|                 // 当前行数据字段值 | ||||
|                 Object currentRowObjFieldVal = ReflectUtils.invokeGetter(currentRowObj, field.getName()); | ||||
|  | ||||
|                 // 空值跳过不处理 | ||||
|                 if (currentRowObjFieldVal == null || "".equals(currentRowObjFieldVal)) { | ||||
|                     continue; | ||||
|                 } | ||||
|  | ||||
|                 // 单元格合并Map是否存在数据,如果不存在则添加当前行的字段值 | ||||
|                 if (!rowRepeatCellMap.containsKey(field)) { | ||||
|                     rowRepeatCellMap.put(field, RepeatCell.of(currentRowObjFieldVal, i)); | ||||
|                     continue; | ||||
|                 } | ||||
|  | ||||
|                 // 获取 单元格合并Map 中字段值 | ||||
|                 RepeatCell repeatCell = rowRepeatCellMap.get(field); | ||||
|                 Object cellValue = repeatCell.value(); | ||||
|                 int current = repeatCell.current(); | ||||
|  | ||||
|                 // 检查是否满足合并条件 | ||||
|                 // currentRowObj 当前行数据 | ||||
|                 // rows.get(i - 1) 上一行数据 注:由于 if (!rowRepeatCellMap.containsKey(field)) 条件的存在,所以该 i 必不可能小于1 | ||||
|                 // cellMerge 当前行字段合并注解 | ||||
|                 boolean merge = isMerge(currentRowObj, rows.get(i - 1), cellMerge); | ||||
|  | ||||
|                 // 是否添加到结果集 | ||||
|                 boolean isAddResult = false; | ||||
|                 // 最新行 | ||||
|                 int lastRow = i + rowIndex - 1; | ||||
|  | ||||
|                 // 如果当前行字段值和缓存中的字段值不相等,或不满足合并条件,则替换 | ||||
|                 if (!currentRowObjFieldVal.equals(cellValue) || !merge) { | ||||
|                     rowRepeatCellMap.put(field, RepeatCell.of(currentRowObjFieldVal, i)); | ||||
|                     isAddResult = true; | ||||
|                 } | ||||
|  | ||||
|                 // 如果最后一行不能合并,检查之前的数据是否需要合并;如果最后一行可以合并,则直接合并到最后 | ||||
|                 if (i == rows.size() - 1) { | ||||
|                     isAddResult = true; | ||||
|                     if (i > current) { | ||||
|                         lastRow = i + rowIndex; | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 if (isAddResult && i > current) { | ||||
|                     result.add(new CellRangeAddress(current + rowIndex, lastRow, colNum, colNum)); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         return result; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取带有合并注解的字段列索引和合并注解信息Map集 | ||||
|      */ | ||||
|     private Map<Field, FieldColumnIndex> getFieldColumnIndexMap(Class<?> clazz) { | ||||
|         boolean annotationPresent = clazz.isAnnotationPresent(ExcelIgnoreUnannotated.class); | ||||
|         Field[] fields = ReflectUtils.getFields(clazz, field -> { | ||||
|             if ("serialVersionUID".equals(field.getName())) { | ||||
|                 return false; | ||||
|             } | ||||
|             if (field.isAnnotationPresent(ExcelIgnore.class)) { | ||||
|                 return false; | ||||
|             } | ||||
|             return !annotationPresent || field.isAnnotationPresent(ExcelProperty.class); | ||||
|         }); | ||||
|  | ||||
|         // 有注解的字段 | ||||
|         Map<Field, FieldColumnIndex> mergeFields = new HashMap<>(); | ||||
|         for (int i = 0; i < fields.length; i++) { | ||||
|             Field field = fields[i]; | ||||
|             if (!field.isAnnotationPresent(CellMerge.class)) { | ||||
|                 continue; | ||||
|             } | ||||
|             CellMerge cm = field.getAnnotation(CellMerge.class); | ||||
|             int index = cm.index() == -1 ? i : cm.index(); | ||||
|             mergeFields.put(field, FieldColumnIndex.of(index, cm)); | ||||
|  | ||||
|             if (hasTitle) { | ||||
|                 ExcelProperty property = field.getAnnotation(ExcelProperty.class); | ||||
|                 rowIndex = Math.max(rowIndex, property.value().length); | ||||
|             } | ||||
|         } | ||||
|         return mergeFields; | ||||
|     } | ||||
|  | ||||
|     private boolean isMerge(Object currentRow, Object preRow, CellMerge cellMerge) { | ||||
|         final String[] mergeBy = cellMerge.mergeBy(); | ||||
|         if (StrUtil.isAllNotBlank(mergeBy)) { | ||||
|             //比对当前行和上一行的各个属性值一一比对 如果全为真 则为真 | ||||
|             for (String fieldName : mergeBy) { | ||||
|                 final Object valCurrent = ReflectUtil.getFieldValue(currentRow, fieldName); | ||||
|                 final Object valPre = ReflectUtil.getFieldValue(preRow, fieldName); | ||||
|                 if (!Objects.equals(valPre, valCurrent)) { | ||||
|                     //依赖字段如有任一不等值,则标记为不可合并 | ||||
|                     return false; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 单元格合并 | ||||
|      */ | ||||
|     record RepeatCell(Object value, int current) { | ||||
|         static RepeatCell of(Object value, int current) { | ||||
|             return new RepeatCell(value, current); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 字段列索引和合并注解信息 | ||||
|      */ | ||||
|     record FieldColumnIndex(int colIndex, CellMerge cellMerge) { | ||||
|         static FieldColumnIndex of(int colIndex, CellMerge cellMerge) { | ||||
|             return new FieldColumnIndex(colIndex, cellMerge); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 创建一个单元格合并处理器实例 | ||||
|      * | ||||
|      * @param hasTitle 是否合并标题 | ||||
|      * @return 单元格合并处理器 | ||||
|      */ | ||||
|     public static CellMergeHandler of(final boolean hasTitle) { | ||||
|         return new CellMergeHandler(hasTitle); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 创建一个单元格合并处理器实例(默认不合并标题) | ||||
|      * | ||||
|      * @return 单元格合并处理器 | ||||
|      */ | ||||
|     public static CellMergeHandler of() { | ||||
|         return new CellMergeHandler(false); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -1,24 +1,15 @@ | ||||
| package org.dromara.common.excel.core; | ||||
|  | ||||
| import cn.hutool.core.collection.CollUtil; | ||||
| import cn.hutool.core.util.ReflectUtil; | ||||
| import cn.hutool.core.util.StrUtil; | ||||
| import cn.idev.excel.annotation.ExcelProperty; | ||||
| import cn.idev.excel.metadata.Head; | ||||
| import cn.idev.excel.write.handler.WorkbookWriteHandler; | ||||
| import cn.idev.excel.write.handler.context.WorkbookWriteHandlerContext; | ||||
| import cn.idev.excel.write.merge.AbstractMergeStrategy; | ||||
| import lombok.AllArgsConstructor; | ||||
| import lombok.Data; | ||||
| import lombok.SneakyThrows; | ||||
| import lombok.extern.slf4j.Slf4j; | ||||
| import org.apache.poi.ss.usermodel.Cell; | ||||
| import org.apache.poi.ss.usermodel.Sheet; | ||||
| import org.apache.poi.ss.util.CellRangeAddress; | ||||
| import org.dromara.common.core.utils.reflect.ReflectUtils; | ||||
| import org.dromara.common.excel.annotation.CellMerge; | ||||
|  | ||||
| import java.lang.reflect.Field; | ||||
| import java.util.*; | ||||
|  | ||||
| /** | ||||
| @@ -30,134 +21,39 @@ import java.util.*; | ||||
| public class CellMergeStrategy extends AbstractMergeStrategy implements WorkbookWriteHandler { | ||||
|  | ||||
|     private final List<CellRangeAddress> cellList; | ||||
|     private final boolean hasTitle; | ||||
|     private int rowIndex; | ||||
|  | ||||
|     public CellMergeStrategy(List<CellRangeAddress> cellList) { | ||||
|         this.cellList = cellList; | ||||
|     } | ||||
|  | ||||
|     public CellMergeStrategy(List<?> list, boolean hasTitle) { | ||||
|         this.hasTitle = hasTitle; | ||||
|         // 行合并开始下标 | ||||
|         this.rowIndex = hasTitle ? 1 : 0; | ||||
|         this.cellList = handle(list, hasTitle); | ||||
|         this.cellList = CellMergeHandler.of(hasTitle).handle(list); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected void merge(Sheet sheet, Cell cell, Head head, Integer relativeRowIndex) { | ||||
|         if (CollUtil.isEmpty(cellList)){ | ||||
|             return; | ||||
|         } | ||||
|         //单元格写入了,遍历合并区域,如果该Cell在区域内,但非首行,则清空 | ||||
|         final int rowIndex = cell.getRowIndex(); | ||||
|         if (CollUtil.isNotEmpty(cellList)){ | ||||
|             for (CellRangeAddress cellAddresses : cellList) { | ||||
|                 final int firstRow = cellAddresses.getFirstRow(); | ||||
|                 if (cellAddresses.isInRange(cell) && rowIndex != firstRow){ | ||||
|                     cell.setBlank(); | ||||
|                 } | ||||
|         for (CellRangeAddress cellAddresses : cellList) { | ||||
|             final int firstRow = cellAddresses.getFirstRow(); | ||||
|             if (cellAddresses.isInRange(cell) && rowIndex != firstRow){ | ||||
|                 cell.setBlank(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void afterWorkbookDispose(final WorkbookWriteHandlerContext context) { | ||||
|         if (CollUtil.isEmpty(cellList)){ | ||||
|             return; | ||||
|         } | ||||
|         //当前表格写完后,统一写入 | ||||
|         if (CollUtil.isNotEmpty(cellList)){ | ||||
|             for (CellRangeAddress item : cellList) { | ||||
|                 context.getWriteContext().writeSheetHolder().getSheet().addMergedRegion(item); | ||||
|             } | ||||
|         for (CellRangeAddress item : cellList) { | ||||
|             context.getWriteContext().writeSheetHolder().getSheet().addMergedRegion(item); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @SneakyThrows | ||||
|     private List<CellRangeAddress> handle(List<?> list, boolean hasTitle) { | ||||
|         List<CellRangeAddress> cellList = new ArrayList<>(); | ||||
|         if (CollUtil.isEmpty(list)) { | ||||
|             return cellList; | ||||
|         } | ||||
|         Field[] fields = ReflectUtils.getFields(list.get(0).getClass(), field -> !"serialVersionUID".equals(field.getName())); | ||||
|  | ||||
|         // 有注解的字段 | ||||
|         List<Field> mergeFields = new ArrayList<>(); | ||||
|         List<Integer> mergeFieldsIndex = new ArrayList<>(); | ||||
|         for (int i = 0; i < fields.length; i++) { | ||||
|             Field field = fields[i]; | ||||
|             if (field.isAnnotationPresent(CellMerge.class)) { | ||||
|                 CellMerge cm = field.getAnnotation(CellMerge.class); | ||||
|                 mergeFields.add(field); | ||||
|                 mergeFieldsIndex.add(cm.index() == -1 ? i : cm.index()); | ||||
|                 if (hasTitle) { | ||||
|                     ExcelProperty property = field.getAnnotation(ExcelProperty.class); | ||||
|                     rowIndex = Math.max(rowIndex, property.value().length); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         Map<Field, RepeatCell> map = new HashMap<>(); | ||||
|         // 生成两两合并单元格 | ||||
|         for (int i = 0; i < list.size(); i++) { | ||||
|             for (int j = 0; j < mergeFields.size(); j++) { | ||||
|                 Field field = mergeFields.get(j); | ||||
|                 Object val = ReflectUtils.invokeGetter(list.get(i), field.getName()); | ||||
|  | ||||
|                 int colNum = mergeFieldsIndex.get(j); | ||||
|                 if (!map.containsKey(field)) { | ||||
|                     map.put(field, new RepeatCell(val, i)); | ||||
|                 } else { | ||||
|                     RepeatCell repeatCell = map.get(field); | ||||
|                     Object cellValue = repeatCell.getValue(); | ||||
|                     if (cellValue == null || "".equals(cellValue)) { | ||||
|                         // 空值跳过不合并 | ||||
|                         continue; | ||||
|                     } | ||||
|  | ||||
|                     if (!cellValue.equals(val)) { | ||||
|                         if ((i - repeatCell.getCurrent() > 1)) { | ||||
|                             cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex - 1, colNum, colNum)); | ||||
|                         } | ||||
|                         map.put(field, new RepeatCell(val, i)); | ||||
|                     } else if (i == list.size() - 1) { | ||||
|                         if (!isMerge(list, i, field)) { | ||||
|                             // 如果最后一行不能合并,检查之前的数据是否需要合并 | ||||
|                             if (i - repeatCell.getCurrent() > 1) { | ||||
|                                 cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex - 1, colNum, colNum)); | ||||
|                             } | ||||
|                         } else if (i > repeatCell.getCurrent()) { | ||||
|                             // 如果最后一行可以合并,则直接合并到最后 | ||||
|                             cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex, colNum, colNum)); | ||||
|                         } | ||||
|                     } else if (!isMerge(list, i, field)) { | ||||
|                         if ((i - repeatCell.getCurrent() > 1)) { | ||||
|                             cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex - 1, colNum, colNum)); | ||||
|                         } | ||||
|                         map.put(field, new RepeatCell(val, i)); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         return cellList; | ||||
|     } | ||||
|  | ||||
|     private boolean isMerge(List<?> list, int i, Field field) { | ||||
|         boolean isMerge = true; | ||||
|         CellMerge cm = field.getAnnotation(CellMerge.class); | ||||
|         final String[] mergeBy = cm.mergeBy(); | ||||
|         if (StrUtil.isAllNotBlank(mergeBy)) { | ||||
|             //比对当前list(i)和list(i - 1)的各个属性值一一比对 如果全为真 则为真 | ||||
|             for (String fieldName : mergeBy) { | ||||
|                 final Object valCurrent = ReflectUtil.getFieldValue(list.get(i), fieldName); | ||||
|                 final Object valPre = ReflectUtil.getFieldValue(list.get(i - 1), fieldName); | ||||
|                 if (!Objects.equals(valPre, valCurrent)) { | ||||
|                     //依赖字段如有任一不等值,则标记为不可合并 | ||||
|                     isMerge = false; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         return isMerge; | ||||
|     } | ||||
|  | ||||
|     @Data | ||||
|     @AllArgsConstructor | ||||
|     static class RepeatCell { | ||||
|  | ||||
|         private Object value; | ||||
|  | ||||
|         private int current; | ||||
|  | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| package org.dromara.common.excel.core; | ||||
|  | ||||
| import cn.hutool.core.convert.Convert; | ||||
| import cn.hutool.core.util.StrUtil; | ||||
| import lombok.AllArgsConstructor; | ||||
| import lombok.Data; | ||||
| @@ -65,7 +66,7 @@ public class DropDownOptions { | ||||
|         StringBuilder stringBuffer = new StringBuilder(); | ||||
|         String regex = "^[\\S\\d\\u4e00-\\u9fa5]+$"; | ||||
|         for (int i = 0; i < vars.length; i++) { | ||||
|             String var = StrUtil.trimToEmpty(String.valueOf(vars[i])); | ||||
|             String var = StrUtil.trimToEmpty(Convert.toStr(vars[i])); | ||||
|             if (!var.matches(regex)) { | ||||
|                 throw new ServiceException("选项数据不符合规则,仅允许使用中英文字符以及数字"); | ||||
|             } | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| package org.dromara.common.excel.core; | ||||
|  | ||||
| import cn.hutool.core.collection.CollUtil; | ||||
| import cn.hutool.core.convert.Convert; | ||||
| import cn.hutool.core.util.ArrayUtil; | ||||
| import cn.hutool.core.util.EnumUtil; | ||||
| import cn.hutool.core.util.ObjectUtil; | ||||
| @@ -103,7 +104,7 @@ public class ExcelDownHandler implements SheetWriteHandler { | ||||
|                 if (StringUtils.isNotBlank(dictType)) { | ||||
|                     // 如果传递了字典名,则依据字典建立下拉 | ||||
|                     Collection<String> values = Optional.ofNullable(dictService.getAllDictByDictType(dictType)) | ||||
|                         .orElseThrow(() -> new ServiceException(String.format("字典 %s 不存在", dictType))) | ||||
|                         .orElseThrow(() -> new ServiceException("字典 {} 不存在", dictType)) | ||||
|                         .values(); | ||||
|                     options = new ArrayList<>(values); | ||||
|                 } else if (StringUtils.isNotBlank(converterExp)) { | ||||
| @@ -115,7 +116,7 @@ public class ExcelDownHandler implements SheetWriteHandler { | ||||
|                 // 否则如果指定了@ExcelEnumFormat,则使用枚举的逻辑 | ||||
|                 ExcelEnumFormat format = field.getDeclaredAnnotation(ExcelEnumFormat.class); | ||||
|                 List<Object> values = EnumUtil.getFieldValues(format.enumClass(), format.textField()); | ||||
|                 options = StreamUtils.toList(values, String::valueOf); | ||||
|                 options = StreamUtils.toList(values, Convert::toStr); | ||||
|             } | ||||
|             if (ObjectUtil.isNotEmpty(options)) { | ||||
|                 // 仅当下拉可选项不为空时执行 | ||||
|   | ||||
| @@ -27,6 +27,7 @@ import java.io.UnsupportedEncodingException; | ||||
| import java.util.Collection; | ||||
| import java.util.List; | ||||
| import java.util.Map; | ||||
| import java.util.function.Consumer; | ||||
|  | ||||
| /** | ||||
|  * Excel相关处理 | ||||
| @@ -203,6 +204,44 @@ public class ExcelUtil { | ||||
|         builder.doWrite(list); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 导出excel | ||||
|      * | ||||
|      * @param headType 带Excel注解的类型 | ||||
|      * @param os       输出流 | ||||
|      * @param options  Excel下拉可选项 | ||||
|      * @param consumer 导出助手消费函数 | ||||
|      */ | ||||
|     public static <T> void exportExcel(Class<T> headType, OutputStream os, List<DropDownOptions> options, Consumer<ExcelWriterWrapper<T>> consumer) { | ||||
|         try (ExcelWriter writer = FastExcel.write(os, headType) | ||||
|             .autoCloseStream(false) | ||||
|             // 自动适配 | ||||
|             .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) | ||||
|             // 大数值自动转换 防止失真 | ||||
|             .registerConverter(new ExcelBigNumberConvert()) | ||||
|             // 批注必填项处理 | ||||
|             .registerWriteHandler(new DataWriteHandler(headType)) | ||||
|             // 添加下拉框操作 | ||||
|             .registerWriteHandler(new ExcelDownHandler(options)) | ||||
|             .build()) { | ||||
|             // 执行消费函数 | ||||
|             consumer.accept(ExcelWriterWrapper.of(writer)); | ||||
|         } catch (Exception e) { | ||||
|             throw new RuntimeException(e); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 导出excel | ||||
|      * | ||||
|      * @param headType 带Excel注解的类型 | ||||
|      * @param os       输出流 | ||||
|      * @param consumer 导出助手消费函数 | ||||
|      */ | ||||
|     public static <T> void exportExcel(Class<T> headType, OutputStream os, Consumer<ExcelWriterWrapper<T>> consumer) { | ||||
|         exportExcel(headType, os, null, consumer); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 单表多数据模板导出 模板格式为 {.属性} | ||||
|      * | ||||
|   | ||||
| @@ -0,0 +1,127 @@ | ||||
| package org.dromara.common.excel.utils; | ||||
|  | ||||
| import cn.idev.excel.ExcelWriter; | ||||
| import cn.idev.excel.FastExcel; | ||||
| import cn.idev.excel.context.WriteContext; | ||||
| import cn.idev.excel.write.builder.ExcelWriterSheetBuilder; | ||||
| import cn.idev.excel.write.builder.ExcelWriterTableBuilder; | ||||
| import cn.idev.excel.write.metadata.WriteSheet; | ||||
| import cn.idev.excel.write.metadata.WriteTable; | ||||
| import cn.idev.excel.write.metadata.fill.FillConfig; | ||||
|  | ||||
| import java.util.Collection; | ||||
| import java.util.function.Supplier; | ||||
|  | ||||
| /** | ||||
|  * ExcelWriterWrapper Excel写出包装器 | ||||
|  * <br> | ||||
|  * 提供了一组与 ExcelWriter 一一对应的写出方法,避免直接提供 ExcelWriter 而导致的一些不可控问题(比如提前关闭了IO流等) | ||||
|  * | ||||
|  * @author 秋辞未寒 | ||||
|  * @see ExcelWriter | ||||
|  */ | ||||
| public record ExcelWriterWrapper<T>(ExcelWriter excelWriter) { | ||||
|  | ||||
|     public void write(Collection<T> data, WriteSheet writeSheet) { | ||||
|         excelWriter.write(data, writeSheet); | ||||
|     } | ||||
|  | ||||
|     public void write(Supplier<Collection<T>> supplier, WriteSheet writeSheet) { | ||||
|         excelWriter.write(supplier.get(), writeSheet); | ||||
|     } | ||||
|  | ||||
|     public void write(Collection<T> data, WriteSheet writeSheet, WriteTable writeTable) { | ||||
|         excelWriter.write(data, writeSheet, writeTable); | ||||
|     } | ||||
|  | ||||
|     public void write(Supplier<Collection<T>> supplier, WriteSheet writeSheet, WriteTable writeTable) { | ||||
|         excelWriter.write(supplier.get(), writeSheet, writeTable); | ||||
|     } | ||||
|  | ||||
|     public void fill(Object data, WriteSheet writeSheet) { | ||||
|         excelWriter.fill(data, writeSheet); | ||||
|     } | ||||
|  | ||||
|     public void fill(Object data, FillConfig fillConfig, WriteSheet writeSheet) { | ||||
|         excelWriter.fill(data, fillConfig, writeSheet); | ||||
|     } | ||||
|  | ||||
|     public void fill(Supplier<Object> supplier, WriteSheet writeSheet) { | ||||
|         excelWriter.fill(supplier, writeSheet); | ||||
|     } | ||||
|  | ||||
|     public void fill(Supplier<Object> supplier, FillConfig fillConfig, WriteSheet writeSheet) { | ||||
|         excelWriter.fill(supplier, fillConfig, writeSheet); | ||||
|     } | ||||
|  | ||||
|     public WriteContext writeContext() { | ||||
|         return excelWriter.writeContext(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 创建一个 ExcelWriterWrapper | ||||
|      * | ||||
|      * @param excelWriter ExcelWriter | ||||
|      * @return ExcelWriterWrapper | ||||
|      */ | ||||
|     public static  <T> ExcelWriterWrapper<T> of(ExcelWriter excelWriter) { | ||||
|         return new ExcelWriterWrapper<>(excelWriter); | ||||
|     } | ||||
|  | ||||
|     // -------------------------------- sheet start | ||||
|  | ||||
|     public static WriteSheet buildSheet(Integer sheetNo, String sheetName) { | ||||
|         return sheetBuilder(sheetNo, sheetName).build(); | ||||
|     } | ||||
|  | ||||
|     public static WriteSheet buildSheet(Integer sheetNo) { | ||||
|         return sheetBuilder(sheetNo).build(); | ||||
|     } | ||||
|  | ||||
|     public static WriteSheet buildSheet(String sheetName) { | ||||
|         return sheetBuilder(sheetName).build(); | ||||
|     } | ||||
|  | ||||
|     public static WriteSheet buildSheet() { | ||||
|         return sheetBuilder().build(); | ||||
|     } | ||||
|  | ||||
|     public static ExcelWriterSheetBuilder sheetBuilder(Integer sheetNo, String sheetName) { | ||||
|         return FastExcel.writerSheet(sheetNo, sheetName); | ||||
|     } | ||||
|  | ||||
|     public static ExcelWriterSheetBuilder sheetBuilder(Integer sheetNo) { | ||||
|         return FastExcel.writerSheet(sheetNo); | ||||
|     } | ||||
|  | ||||
|     public static ExcelWriterSheetBuilder sheetBuilder(String sheetName) { | ||||
|         return FastExcel.writerSheet(sheetName); | ||||
|     } | ||||
|  | ||||
|     public static ExcelWriterSheetBuilder sheetBuilder() { | ||||
|         return FastExcel.writerSheet(); | ||||
|     } | ||||
|  | ||||
|     // -------------------------------- sheet end | ||||
|  | ||||
|     // -------------------------------- table start | ||||
|  | ||||
|     public static WriteTable buildTable(Integer tableNo) { | ||||
|         return tableBuilder(tableNo).build(); | ||||
|     } | ||||
|  | ||||
|     public static WriteTable buildTable() { | ||||
|         return tableBuilder().build(); | ||||
|     } | ||||
|  | ||||
|     public static ExcelWriterTableBuilder tableBuilder(Integer tableNo) { | ||||
|         return FastExcel.writerTable(tableNo); | ||||
|     } | ||||
|  | ||||
|     public static ExcelWriterTableBuilder tableBuilder() { | ||||
|         return FastExcel.writerTable(); | ||||
|     } | ||||
|  | ||||
|     // -------------------------------- table end | ||||
|  | ||||
| } | ||||
| @@ -1,5 +1,6 @@ | ||||
| package org.dromara.common.json.config; | ||||
|  | ||||
| import com.fasterxml.jackson.databind.Module; | ||||
| import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; | ||||
| import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; | ||||
| import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; | ||||
| @@ -28,20 +29,24 @@ import java.util.TimeZone; | ||||
| @AutoConfiguration(before = JacksonAutoConfiguration.class) | ||||
| public class JacksonConfig { | ||||
|  | ||||
|     @Bean | ||||
|     public Module registerJavaTimeModule() { | ||||
|         // 全局配置序列化返回 JSON 处理 | ||||
|         JavaTimeModule javaTimeModule = new JavaTimeModule(); | ||||
|         javaTimeModule.addSerializer(Long.class, BigNumberSerializer.INSTANCE); | ||||
|         javaTimeModule.addSerializer(Long.TYPE, BigNumberSerializer.INSTANCE); | ||||
|         javaTimeModule.addSerializer(BigInteger.class, BigNumberSerializer.INSTANCE); | ||||
|         javaTimeModule.addSerializer(BigDecimal.class, ToStringSerializer.instance); | ||||
|         DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); | ||||
|         javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(formatter)); | ||||
|         javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(formatter)); | ||||
|         javaTimeModule.addDeserializer(Date.class, new CustomDateDeserializer()); | ||||
|         return javaTimeModule; | ||||
|     } | ||||
|  | ||||
|     @Bean | ||||
|     public Jackson2ObjectMapperBuilderCustomizer customizer() { | ||||
|         return builder -> { | ||||
|             // 全局配置序列化返回 JSON 处理 | ||||
|             JavaTimeModule javaTimeModule = new JavaTimeModule(); | ||||
|             javaTimeModule.addSerializer(Long.class, BigNumberSerializer.INSTANCE); | ||||
|             javaTimeModule.addSerializer(Long.TYPE, BigNumberSerializer.INSTANCE); | ||||
|             javaTimeModule.addSerializer(BigInteger.class, BigNumberSerializer.INSTANCE); | ||||
|             javaTimeModule.addSerializer(BigDecimal.class, ToStringSerializer.instance); | ||||
|             DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); | ||||
|             javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(formatter)); | ||||
|             javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(formatter)); | ||||
|             javaTimeModule.addDeserializer(Date.class, new CustomDateDeserializer()); | ||||
|             builder.modules(javaTimeModule); | ||||
|             builder.timeZone(TimeZone.getDefault()); | ||||
|             log.info("初始化 jackson 配置"); | ||||
|         }; | ||||
|   | ||||
| @@ -1,9 +1,11 @@ | ||||
| package org.dromara.common.json.handler; | ||||
|  | ||||
| import cn.hutool.core.date.DateTime; | ||||
| import cn.hutool.core.date.DateUtil; | ||||
| import com.fasterxml.jackson.core.JsonParser; | ||||
| import com.fasterxml.jackson.databind.DeserializationContext; | ||||
| import com.fasterxml.jackson.databind.JsonDeserializer; | ||||
| import org.dromara.common.core.utils.ObjectUtils; | ||||
|  | ||||
| import java.io.IOException; | ||||
| import java.util.Date; | ||||
| @@ -25,7 +27,11 @@ public class CustomDateDeserializer extends JsonDeserializer<Date> { | ||||
|      */ | ||||
|     @Override | ||||
|     public Date deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { | ||||
|         return DateUtil.parse(p.getText()); | ||||
|         DateTime parse = DateUtil.parse(p.getText()); | ||||
|         if (ObjectUtils.isNull(parse)) { | ||||
|             return null; | ||||
|         } | ||||
|         return parse.toJdkDate(); | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -5,6 +5,7 @@ import cn.hutool.core.util.ArrayUtil; | ||||
| import cn.hutool.core.util.ObjectUtil; | ||||
| import com.fasterxml.jackson.core.JsonProcessingException; | ||||
| import com.fasterxml.jackson.core.type.TypeReference; | ||||
| import com.fasterxml.jackson.databind.JsonNode; | ||||
| import com.fasterxml.jackson.databind.ObjectMapper; | ||||
| import com.fasterxml.jackson.databind.exc.MismatchedInputException; | ||||
| import lombok.AccessLevel; | ||||
| @@ -167,4 +168,58 @@ public class JsonUtils { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 判断字符串是否为合法 JSON(对象或数组) | ||||
|      * | ||||
|      * @param str 待校验字符串 | ||||
|      * @return true = 合法 JSON,false = 非法或空 | ||||
|      */ | ||||
|     public static boolean isJson(String str) { | ||||
|         if (StringUtils.isBlank(str)) { | ||||
|             return false; | ||||
|         } | ||||
|         try { | ||||
|             OBJECT_MAPPER.readTree(str); | ||||
|             return true; | ||||
|         } catch (Exception e) { | ||||
|             return false; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 判断字符串是否为 JSON 对象({}) | ||||
|      * | ||||
|      * @param str 待校验字符串 | ||||
|      * @return true = JSON 对象 | ||||
|      */ | ||||
|     public static boolean isJsonObject(String str) { | ||||
|         if (StringUtils.isBlank(str)) { | ||||
|             return false; | ||||
|         } | ||||
|         try { | ||||
|             JsonNode node = OBJECT_MAPPER.readTree(str); | ||||
|             return node.isObject(); | ||||
|         } catch (Exception e) { | ||||
|             return false; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 判断字符串是否为 JSON 数组([]) | ||||
|      * | ||||
|      * @param str 待校验字符串 | ||||
|      * @return true = JSON 数组 | ||||
|      */ | ||||
|     public static boolean isJsonArray(String str) { | ||||
|         if (StringUtils.isBlank(str)) { | ||||
|             return false; | ||||
|         } | ||||
|         try { | ||||
|             JsonNode node = OBJECT_MAPPER.readTree(str); | ||||
|             return node.isArray(); | ||||
|         } catch (Exception e) { | ||||
|             return false; | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,33 @@ | ||||
| package org.dromara.common.json.validate; | ||||
|  | ||||
| import jakarta.validation.Constraint; | ||||
| import jakarta.validation.Payload; | ||||
|  | ||||
| import java.lang.annotation.*; | ||||
|  | ||||
| /** | ||||
|  * JSON 格式校验注解 | ||||
|  * | ||||
|  * @author AprilWind | ||||
|  */ | ||||
| @Documented | ||||
| @Target({ElementType.METHOD, ElementType.FIELD}) | ||||
| @Retention(RetentionPolicy.RUNTIME) | ||||
| @Constraint(validatedBy = JsonPatternValidator.class) | ||||
| public @interface JsonPattern { | ||||
|  | ||||
|     /** | ||||
|      * 限制 JSON 类型,默认为 {@link JsonType#ANY},即对象或数组都允许 | ||||
|      */ | ||||
|     JsonType type() default JsonType.ANY; | ||||
|  | ||||
|     /** | ||||
|      * 校验失败时的提示消息 | ||||
|      */ | ||||
|     String message() default "不是有效的 JSON 格式"; | ||||
|  | ||||
|     Class<?>[] groups() default {}; | ||||
|  | ||||
|     Class<? extends Payload>[] payload() default {}; | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,51 @@ | ||||
| package org.dromara.common.json.validate; | ||||
|  | ||||
| import jakarta.validation.ConstraintValidator; | ||||
| import jakarta.validation.ConstraintValidatorContext; | ||||
| import org.dromara.common.core.utils.StringUtils; | ||||
| import org.dromara.common.json.utils.JsonUtils; | ||||
|  | ||||
| /** | ||||
|  * JSON 格式校验器 | ||||
|  * | ||||
|  * @author AprilWind | ||||
|  */ | ||||
| public class JsonPatternValidator implements ConstraintValidator<JsonPattern, String> { | ||||
|  | ||||
|     /** | ||||
|      * 注解中指定的 JSON 类型枚举 | ||||
|      */ | ||||
|     private JsonType jsonType; | ||||
|  | ||||
|     /** | ||||
|      * 初始化校验器,从注解中提取 JSON 类型 | ||||
|      * | ||||
|      * @param annotation 注解实例 | ||||
|      */ | ||||
|     @Override | ||||
|     public void initialize(JsonPattern annotation) { | ||||
|         this.jsonType = annotation.type(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 校验字符串是否为合法 JSON | ||||
|      * | ||||
|      * @param value   待校验字符串 | ||||
|      * @param context 校验上下文,可用于自定义错误信息 | ||||
|      * @return true = 合法 JSON 或为空,false = 非法 JSON | ||||
|      */ | ||||
|     @Override | ||||
|     public boolean isValid(String value, ConstraintValidatorContext context) { | ||||
|         if (StringUtils.isBlank(value)) { | ||||
|             // 交给 @NotBlank 或 @NotNull 控制是否允许为空 | ||||
|             return true; | ||||
|         } | ||||
|         // 根据 JSON 类型进行不同的校验 | ||||
|         return switch (jsonType) { | ||||
|             case ANY -> JsonUtils.isJson(value); | ||||
|             case OBJECT -> JsonUtils.isJsonObject(value); | ||||
|             case ARRAY -> JsonUtils.isJsonArray(value); | ||||
|         }; | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,30 @@ | ||||
| package org.dromara.common.json.validate; | ||||
|  | ||||
| import lombok.AllArgsConstructor; | ||||
| import lombok.Getter; | ||||
|  | ||||
| /** | ||||
|  * JSON 类型枚举 | ||||
|  * | ||||
|  * @author AprilWind | ||||
|  */ | ||||
| @Getter | ||||
| @AllArgsConstructor | ||||
| public enum JsonType { | ||||
|  | ||||
|     /** | ||||
|      * JSON 对象,例如 {"a":1} | ||||
|      */ | ||||
|     OBJECT, | ||||
|  | ||||
|     /** | ||||
|      * JSON 数组,例如 [1,2,3] | ||||
|      */ | ||||
|     ARRAY, | ||||
|  | ||||
|     /** | ||||
|      * 任意 JSON 类型,对象或数组都可以 | ||||
|      */ | ||||
|     ANY | ||||
|  | ||||
| } | ||||
| @@ -27,9 +27,7 @@ import org.springframework.http.HttpMethod; | ||||
| import org.springframework.validation.BindingResult; | ||||
| import org.springframework.web.multipart.MultipartFile; | ||||
|  | ||||
| import java.util.Collection; | ||||
| import java.util.Map; | ||||
| import java.util.StringJoiner; | ||||
| import java.util.*; | ||||
|  | ||||
| /** | ||||
|  * 操作日志记录处理 | ||||
| @@ -176,14 +174,28 @@ public class LogAspect { | ||||
|         if (ArrayUtil.isEmpty(paramsArray)) { | ||||
|             return params.toString(); | ||||
|         } | ||||
|         String[] exclude = ArrayUtil.addAll(excludeParamNames, EXCLUDE_PROPERTIES); | ||||
|         for (Object o : paramsArray) { | ||||
|             if (ObjectUtil.isNotNull(o) && !isFilterObject(o)) { | ||||
|                 String str = JsonUtils.toJsonString(o); | ||||
|                 Dict dict = JsonUtils.parseMap(str); | ||||
|                 if (MapUtil.isNotEmpty(dict)) { | ||||
|                     MapUtil.removeAny(dict, EXCLUDE_PROPERTIES); | ||||
|                     MapUtil.removeAny(dict, excludeParamNames); | ||||
|                     str = JsonUtils.toJsonString(dict); | ||||
|                 String str = ""; | ||||
|                 if (o instanceof List<?> list) { | ||||
|                     List<Dict> list1 = new ArrayList<>(); | ||||
|                     for (Object obj : list) { | ||||
|                         String str1 = JsonUtils.toJsonString(obj); | ||||
|                         Dict dict = JsonUtils.parseMap(str1); | ||||
|                         if (MapUtil.isNotEmpty(dict)) { | ||||
|                             MapUtil.removeAny(dict, exclude); | ||||
|                             list1.add(dict); | ||||
|                         } | ||||
|                     } | ||||
|                     str = JsonUtils.toJsonString(list1); | ||||
|                 } else { | ||||
|                     str = JsonUtils.toJsonString(o); | ||||
|                     Dict dict = JsonUtils.parseMap(str); | ||||
|                     if (MapUtil.isNotEmpty(dict)) { | ||||
|                         MapUtil.removeAny(dict, exclude); | ||||
|                         str = JsonUtils.toJsonString(dict); | ||||
|                     } | ||||
|                 } | ||||
|                 params.add(str); | ||||
|             } | ||||
|   | ||||
| @@ -0,0 +1,54 @@ | ||||
| package org.dromara.common.mybatis.aspect; | ||||
|  | ||||
| import lombok.extern.slf4j.Slf4j; | ||||
| import org.aopalliance.intercept.MethodInterceptor; | ||||
| import org.aopalliance.intercept.MethodInvocation; | ||||
| import org.dromara.common.mybatis.annotation.DataPermission; | ||||
| import org.dromara.common.mybatis.helper.DataPermissionHelper; | ||||
|  | ||||
| import java.lang.reflect.Method; | ||||
| import java.lang.reflect.Proxy; | ||||
|  | ||||
| /** | ||||
|  * 数据权限注解Advice | ||||
|  * | ||||
|  * @author 秋辞未寒 | ||||
|  */ | ||||
| @Slf4j | ||||
| public class DataPermissionAdvice implements MethodInterceptor { | ||||
|  | ||||
|     @Override | ||||
|     public Object invoke(MethodInvocation invocation) throws Throwable { | ||||
|         Object target = invocation.getThis(); | ||||
|         Method method = invocation.getMethod(); | ||||
|         Object[] args = invocation.getArguments(); | ||||
|         // 设置权限注解 | ||||
|         DataPermissionHelper.setPermission(getDataPermissionAnnotation(target, method, args)); | ||||
|         try { | ||||
|             // 执行代理方法 | ||||
|             return invocation.proceed(); | ||||
|         } finally { | ||||
|             // 清除权限注解 | ||||
|             DataPermissionHelper.removePermission(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取数据权限注解 | ||||
|      */ | ||||
|     private DataPermission getDataPermissionAnnotation(Object target, Method method,Object[] args){ | ||||
|         DataPermission dataPermission = method.getAnnotation(DataPermission.class); | ||||
|         // 优先获取方法上的注解 | ||||
|         if (dataPermission != null) { | ||||
|             return dataPermission; | ||||
|         } | ||||
|         // 方法上没有注解,则获取类上的注解 | ||||
|         Class<?> targetClass = target.getClass(); | ||||
|         // 如果是 JDK 动态代理,则获取真实的Class实例 | ||||
|         if (Proxy.isProxyClass(targetClass)) { | ||||
|             targetClass = targetClass.getInterfaces()[0]; | ||||
|         } | ||||
|         dataPermission = targetClass.getAnnotation(DataPermission.class); | ||||
|         return dataPermission; | ||||
|     } | ||||
| } | ||||
| @@ -1,50 +0,0 @@ | ||||
| package org.dromara.common.mybatis.aspect; | ||||
|  | ||||
| import lombok.extern.slf4j.Slf4j; | ||||
| import org.aspectj.lang.JoinPoint; | ||||
| import org.aspectj.lang.annotation.AfterReturning; | ||||
| import org.aspectj.lang.annotation.AfterThrowing; | ||||
| import org.aspectj.lang.annotation.Aspect; | ||||
| import org.aspectj.lang.annotation.Before; | ||||
| import org.dromara.common.mybatis.annotation.DataPermission; | ||||
| import org.dromara.common.mybatis.helper.DataPermissionHelper; | ||||
|  | ||||
| /** | ||||
|  * 数据权限处理 | ||||
|  * | ||||
|  * @author Lion Li | ||||
|  */ | ||||
| @Slf4j | ||||
| @Aspect | ||||
| public class DataPermissionAspect { | ||||
|  | ||||
|     /** | ||||
|      * 处理请求前执行 | ||||
|      */ | ||||
|     @Before(value = "@annotation(dataPermission)") | ||||
|     public void doBefore(JoinPoint joinPoint, DataPermission dataPermission) { | ||||
|         DataPermissionHelper.setPermission(dataPermission); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 处理完请求后执行 | ||||
|      * | ||||
|      * @param joinPoint 切点 | ||||
|      */ | ||||
|     @AfterReturning(pointcut = "@annotation(dataPermission)") | ||||
|     public void doAfterReturning(JoinPoint joinPoint, DataPermission dataPermission) { | ||||
|         DataPermissionHelper.removePermission(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 拦截异常操作 | ||||
|      * | ||||
|      * @param joinPoint 切点 | ||||
|      * @param e         异常 | ||||
|      */ | ||||
|     @AfterThrowing(value = "@annotation(dataPermission)", throwing = "e") | ||||
|     public void doAfterThrowing(JoinPoint joinPoint, DataPermission dataPermission, Exception e) { | ||||
|         DataPermissionHelper.removePermission(); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,39 @@ | ||||
| package org.dromara.common.mybatis.aspect; | ||||
|  | ||||
| import lombok.extern.slf4j.Slf4j; | ||||
| import org.dromara.common.mybatis.annotation.DataPermission; | ||||
| import org.springframework.aop.support.StaticMethodMatcherPointcut; | ||||
|  | ||||
| import java.lang.reflect.Method; | ||||
| import java.lang.reflect.Proxy; | ||||
|  | ||||
| /** | ||||
|  * 数据权限匹配切点 | ||||
|  * | ||||
|  * @author 秋辞未寒 | ||||
|  */ | ||||
| @Slf4j | ||||
| @SuppressWarnings("all") | ||||
| public class DataPermissionPointcut extends StaticMethodMatcherPointcut { | ||||
|  | ||||
|     @Override | ||||
|     public boolean matches(Method method, Class<?> targetClass) { | ||||
|         // 优先匹配方法 | ||||
|         // 数据权限注解不对继承生效,所以检查当前方法是否有注解即可,不再往上匹配父类或接口 | ||||
|         if (method.isAnnotationPresent(DataPermission.class)) { | ||||
|             return true; | ||||
|         } | ||||
|  | ||||
|         // MyBatis 的 Mapper 就是通过 JDK 动态代理实现的,所以这里需要检查是否匹配 JDK 的动态代理 | ||||
|         Class<?> targetClassRef = targetClass; | ||||
|         if (Proxy.isProxyClass(targetClassRef)) { | ||||
|             // 数据权限注解不对继承生效,但由于 SpringIOC 容器拿到的实际上是 MyBatis 代理过后的 Mapper,而 targetClass.isAnnotationPresent 实际匹配的是 Proxy 类的注解,不会查找代理类。 | ||||
|             // 所以这里不能用 targetClass.isAnnotationPresent,只能用 AnnotatedElementUtils.hasAnnotation 或 targetClass.getInterfaces()[0].isAnnotationPresent 去做匹配,以检查被代理的 MapperClass 是否具有注解 | ||||
|             // 原理:JDK 动态代理本质上就是对接口进行实现然后对具体的接口实现做代理,所以直接通过接口可以拿到实际的 MapperClass | ||||
|             targetClassRef = targetClass.getInterfaces()[0]; | ||||
|  | ||||
|         } | ||||
|         return targetClassRef.isAnnotationPresent(DataPermission.class); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,33 @@ | ||||
| package org.dromara.common.mybatis.aspect; | ||||
|  | ||||
| import org.aopalliance.aop.Advice; | ||||
| import org.springframework.aop.Pointcut; | ||||
| import org.springframework.aop.support.AbstractPointcutAdvisor; | ||||
|  | ||||
| /** | ||||
|  * 数据权限注解切面定义 | ||||
|  * | ||||
|  * @author 秋辞未寒 | ||||
|  */ | ||||
| @SuppressWarnings("all") | ||||
| public class DataPermissionPointcutAdvisor extends AbstractPointcutAdvisor { | ||||
|  | ||||
|     private final Advice advice; | ||||
|     private final Pointcut pointcut; | ||||
|  | ||||
|     public DataPermissionPointcutAdvisor() { | ||||
|         this.advice = new DataPermissionAdvice(); | ||||
|         this.pointcut =  new DataPermissionPointcut(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public Pointcut getPointcut() { | ||||
|         return this.pointcut; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public Advice getAdvice() { | ||||
|         return this.advice; | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -11,15 +11,17 @@ import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerIntercept | ||||
| import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor; | ||||
| import org.dromara.common.core.factory.YmlPropertySourceFactory; | ||||
| import org.dromara.common.core.utils.SpringUtils; | ||||
| import org.dromara.common.mybatis.aspect.DataPermissionAspect; | ||||
| import org.dromara.common.mybatis.aspect.DataPermissionPointcutAdvisor; | ||||
| import org.dromara.common.mybatis.handler.InjectionMetaObjectHandler; | ||||
| import org.dromara.common.mybatis.handler.MybatisExceptionHandler; | ||||
| import org.dromara.common.mybatis.handler.PlusPostInitTableInfoHandler; | ||||
| import org.dromara.common.mybatis.interceptor.PlusDataPermissionInterceptor; | ||||
| import org.mybatis.spring.annotation.MapperScan; | ||||
| import org.springframework.beans.BeansException; | ||||
| import org.springframework.beans.factory.config.BeanDefinition; | ||||
| import org.springframework.context.annotation.Bean; | ||||
| import org.springframework.context.annotation.PropertySource; | ||||
| import org.springframework.context.annotation.Role; | ||||
| import org.springframework.transaction.annotation.EnableTransactionManagement; | ||||
|  | ||||
| /** | ||||
| @@ -27,6 +29,7 @@ import org.springframework.transaction.annotation.EnableTransactionManagement; | ||||
|  * | ||||
|  * @author Lion Li | ||||
|  */ | ||||
| @Role(BeanDefinition.ROLE_INFRASTRUCTURE) | ||||
| @EnableTransactionManagement(proxyTargetClass = true) | ||||
| @MapperScan("${mybatis-plus.mapperPackage}") | ||||
| @PropertySource(value = "classpath:common-mybatis.yml", factory = YmlPropertySourceFactory.class) | ||||
| @@ -54,15 +57,16 @@ public class MybatisPlusConfig { | ||||
|      * 数据权限拦截器 | ||||
|      */ | ||||
|     public PlusDataPermissionInterceptor dataPermissionInterceptor() { | ||||
|         return new PlusDataPermissionInterceptor(SpringUtils.getProperty("mybatis-plus.mapperPackage")); | ||||
|         return new PlusDataPermissionInterceptor(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 数据权限切面处理器 | ||||
|      */ | ||||
|     @Bean | ||||
|     public DataPermissionAspect dataPermissionAspect() { | ||||
|         return new DataPermissionAspect(); | ||||
|     @Role(BeanDefinition.ROLE_INFRASTRUCTURE) | ||||
|     public DataPermissionPointcutAdvisor dataPermissionPointcutAdvisor() { | ||||
|         return new DataPermissionPointcutAdvisor(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|   | ||||
| @@ -42,17 +42,46 @@ public enum DataBaseType { | ||||
|      * 根据数据库产品名称查找对应的数据库类型 | ||||
|      * | ||||
|      * @param databaseProductName 数据库产品名称 | ||||
|      * @return 对应的数据库类型枚举值,如果未找到则返回 null | ||||
|      * @return 对应的数据库类型枚举值 | ||||
|      */ | ||||
|     public static DataBaseType find(String databaseProductName) { | ||||
|         if (StringUtils.isBlank(databaseProductName)) { | ||||
|             return null; | ||||
|             return MY_SQL; | ||||
|         } | ||||
|         for (DataBaseType type : values()) { | ||||
|             if (type.getType().equals(databaseProductName)) { | ||||
|                 return type; | ||||
|             } | ||||
|         } | ||||
|         return null; | ||||
|         return MY_SQL; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 判断是否为 MySQL 类型 | ||||
|      */ | ||||
|     public boolean isMySql() { | ||||
|         return this == MY_SQL; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 判断是否为 Oracle 类型 | ||||
|      */ | ||||
|     public boolean isOracle() { | ||||
|         return this == ORACLE; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 判断是否为 PostgreSQL 类型 | ||||
|      */ | ||||
|     public boolean isPostgreSql() { | ||||
|         return this == POSTGRE_SQL; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 判断是否为 SQL Server 类型 | ||||
|      */ | ||||
|     public boolean isSqlServer() { | ||||
|         return this == SQL_SERVER; | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -1,10 +1,11 @@ | ||||
| package org.dromara.common.mybatis.handler; | ||||
|  | ||||
| import cn.dev33.satoken.exception.NotLoginException; | ||||
| import cn.hutool.http.HttpStatus; | ||||
| import com.baomidou.dynamic.datasource.exception.CannotFindDataSourceException; | ||||
| import jakarta.servlet.http.HttpServletRequest; | ||||
| import lombok.extern.slf4j.Slf4j; | ||||
| import org.dromara.common.core.domain.R; | ||||
| import org.dromara.common.core.utils.StringUtils; | ||||
| import org.mybatis.spring.MyBatisSystemException; | ||||
| import org.springframework.dao.DuplicateKeyException; | ||||
| import org.springframework.web.bind.annotation.ExceptionHandler; | ||||
| @@ -35,13 +36,54 @@ public class MybatisExceptionHandler { | ||||
|     @ExceptionHandler(MyBatisSystemException.class) | ||||
|     public R<Void> handleCannotFindDataSourceException(MyBatisSystemException e, HttpServletRequest request) { | ||||
|         String requestURI = request.getRequestURI(); | ||||
|         String message = e.getMessage(); | ||||
|         if (StringUtils.contains(message, "CannotFindDataSourceException")) { | ||||
|         Throwable root = getRootCause(e); | ||||
|         if (root instanceof NotLoginException) { | ||||
|             log.error("请求地址'{}',认证失败'{}',无法访问系统资源", requestURI, root.getMessage()); | ||||
|             return R.fail(HttpStatus.HTTP_UNAUTHORIZED, "认证失败,无法访问系统资源"); | ||||
|         } | ||||
|         if (root instanceof CannotFindDataSourceException) { | ||||
|             log.error("请求地址'{}', 未找到数据源", requestURI); | ||||
|             return R.fail(HttpStatus.HTTP_INTERNAL_ERROR, "未找到数据源,请联系管理员确认"); | ||||
|         } | ||||
|         log.error("请求地址'{}', Mybatis系统异常", requestURI, e); | ||||
|         return R.fail(HttpStatus.HTTP_INTERNAL_ERROR, message); | ||||
|         return R.fail(HttpStatus.HTTP_INTERNAL_ERROR, e.getMessage()); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取异常的根因(递归查找) | ||||
|      * | ||||
|      * @param e 当前异常 | ||||
|      * @return 根因异常(最底层的 cause) | ||||
|      * <p> | ||||
|      * 逻辑说明: | ||||
|      * 1. 如果 e 没有 cause,说明 e 本身就是根因,直接返回 | ||||
|      * 2. 如果 e 的 cause 和自身相同(防止循环引用),也返回 e | ||||
|      * 3. 否则递归调用,继续向下寻找最底层的 cause | ||||
|      */ | ||||
|     public static Throwable getRootCause(Throwable e) { | ||||
|         Throwable cause = e.getCause(); | ||||
|         if (cause == null || cause == e) { | ||||
|             return e; | ||||
|         } | ||||
|         return getRootCause(cause); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 在异常链中查找指定类型的异常 | ||||
|      * | ||||
|      * @param e     当前异常 | ||||
|      * @param clazz 目标异常类 | ||||
|      * @return 找到的指定类型异常,如果没有找到返回 null | ||||
|      */ | ||||
|     public static Throwable findCause(Throwable e, Class<? extends Throwable> clazz) { | ||||
|         Throwable t = e; | ||||
|         while (t != null && t != t.getCause()) { | ||||
|             if (clazz.isInstance(t)) { | ||||
|                 return t; | ||||
|             } | ||||
|             t = t.getCause(); | ||||
|         } | ||||
|         return null; | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
| package org.dromara.common.mybatis.handler; | ||||
|  | ||||
| import cn.hutool.core.annotation.AnnotationUtil; | ||||
| import cn.hutool.core.collection.CollUtil; | ||||
| import cn.hutool.core.util.ObjectUtil; | ||||
| import lombok.AllArgsConstructor; | ||||
| @@ -10,7 +9,6 @@ import net.sf.jsqlparser.expression.Expression; | ||||
| import net.sf.jsqlparser.expression.operators.conditional.AndExpression; | ||||
| import net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList; | ||||
| import net.sf.jsqlparser.parser.CCJSqlParserUtil; | ||||
| import org.apache.ibatis.io.Resources; | ||||
| import org.dromara.common.core.domain.dto.RoleDTO; | ||||
| import org.dromara.common.core.domain.model.LoginUser; | ||||
| import org.dromara.common.core.exception.ServiceException; | ||||
| @@ -22,22 +20,13 @@ import org.dromara.common.mybatis.annotation.DataPermission; | ||||
| import org.dromara.common.mybatis.enums.DataScopeType; | ||||
| import org.dromara.common.mybatis.helper.DataPermissionHelper; | ||||
| import org.dromara.common.satoken.utils.LoginHelper; | ||||
| import org.springframework.context.ConfigurableApplicationContext; | ||||
| import org.springframework.context.expression.BeanFactoryResolver; | ||||
| import org.springframework.core.io.Resource; | ||||
| import org.springframework.core.io.support.PathMatchingResourcePatternResolver; | ||||
| import org.springframework.core.io.support.ResourcePatternResolver; | ||||
| import org.springframework.core.type.ClassMetadata; | ||||
| import org.springframework.core.type.classreading.CachingMetadataReaderFactory; | ||||
| import org.springframework.expression.*; | ||||
| import org.springframework.expression.common.TemplateParserContext; | ||||
| import org.springframework.expression.spel.standard.SpelExpressionParser; | ||||
| import org.springframework.expression.spel.support.StandardEvaluationContext; | ||||
| import org.springframework.util.ClassUtils; | ||||
|  | ||||
| import java.lang.reflect.Method; | ||||
| import java.util.*; | ||||
| import java.util.concurrent.ConcurrentHashMap; | ||||
| import java.util.function.Function; | ||||
|  | ||||
| /** | ||||
| @@ -49,11 +38,6 @@ import java.util.function.Function; | ||||
| @Slf4j | ||||
| public class PlusDataPermissionHandler { | ||||
|  | ||||
|     /** | ||||
|      * 类名称与注解的映射关系缓存(由于aop无法拦截mybatis接口类上的注解 只能通过启动预扫描的方式进行) | ||||
|      */ | ||||
|     private final Map<String, DataPermission> dataPermissionCacheMap = new ConcurrentHashMap<>(); | ||||
|  | ||||
|     /** | ||||
|      * spel 解析器 | ||||
|      */ | ||||
| @@ -64,27 +48,17 @@ public class PlusDataPermissionHandler { | ||||
|      */ | ||||
|     private final BeanResolver beanResolver = new BeanFactoryResolver(SpringUtils.getBeanFactory()); | ||||
|  | ||||
|     /** | ||||
|      * 构造方法,扫描指定包下的 Mapper 类并初始化缓存 | ||||
|      * | ||||
|      * @param mapperPackage Mapper 类所在的包路径 | ||||
|      */ | ||||
|     public PlusDataPermissionHandler(String mapperPackage) { | ||||
|         scanMapperClasses(mapperPackage); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取数据过滤条件的 SQL 片段 | ||||
|      * | ||||
|      * @param where             原始的查询条件表达式 | ||||
|      * @param mappedStatementId Mapper 方法的 ID | ||||
|      * @param isSelect          是否为查询语句 | ||||
|      * @return 数据过滤条件的 SQL 片段 | ||||
|      */ | ||||
|     public Expression getSqlSegment(Expression where, String mappedStatementId, boolean isSelect) { | ||||
|     public Expression getSqlSegment(Expression where, boolean isSelect) { | ||||
|         try { | ||||
|             // 获取数据权限配置 | ||||
|             DataPermission dataPermission = getDataPermission(mappedStatementId); | ||||
|             DataPermission dataPermission = getDataPermission(); | ||||
|             // 获取当前登录用户信息 | ||||
|             LoginUser currentUser = DataPermissionHelper.getVariable("user"); | ||||
|             if (ObjectUtil.isNull(currentUser)) { | ||||
| @@ -206,92 +180,22 @@ public class PlusDataPermissionHandler { | ||||
|         return StringUtils.EMPTY; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 扫描指定包下的 Mapper 类,并查找其中带有特定注解的方法或类 | ||||
|      * | ||||
|      * @param mapperPackage Mapper 类所在的包路径 | ||||
|      */ | ||||
|     private void scanMapperClasses(String mapperPackage) { | ||||
|         // 创建资源解析器和元数据读取工厂 | ||||
|         PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); | ||||
|         CachingMetadataReaderFactory factory = new CachingMetadataReaderFactory(); | ||||
|         // 将 Mapper 包路径按分隔符拆分为数组 | ||||
|         String[] packagePatternArray = StringUtils.splitPreserveAllTokens(mapperPackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS); | ||||
|         String classpath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX; | ||||
|         try { | ||||
|             for (String packagePattern : packagePatternArray) { | ||||
|                 // 将包路径转换为资源路径 | ||||
|                 String path = ClassUtils.convertClassNameToResourcePath(packagePattern); | ||||
|                 // 获取指定路径下的所有 .class 文件资源 | ||||
|                 Resource[] resources = resolver.getResources(classpath + path + "/*.class"); | ||||
|                 for (Resource resource : resources) { | ||||
|                     // 获取资源的类元数据 | ||||
|                     ClassMetadata classMetadata = factory.getMetadataReader(resource).getClassMetadata(); | ||||
|                     // 获取资源对应的类对象 | ||||
|                     Class<?> clazz = Resources.classForName(classMetadata.getClassName()); | ||||
|                     // 查找类中的特定注解 | ||||
|                     findAnnotation(clazz); | ||||
|                 } | ||||
|             } | ||||
|         } catch (Exception e) { | ||||
|             log.error("初始化数据安全缓存时出错:{}", e.getMessage()); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 在指定的类中查找特定的注解 DataPermission,并将带有这个注解的方法或类存储到 dataPermissionCacheMap 中 | ||||
|      * | ||||
|      * @param clazz 要查找的类 | ||||
|      */ | ||||
|     private void findAnnotation(Class<?> clazz) { | ||||
|         DataPermission dataPermission; | ||||
|         for (Method method : clazz.getMethods()) { | ||||
|             if (method.isDefault() || method.isVarArgs()) { | ||||
|                 continue; | ||||
|             } | ||||
|             String mappedStatementId = clazz.getName() + "." + method.getName(); | ||||
|             if (AnnotationUtil.hasAnnotation(method, DataPermission.class)) { | ||||
|                 dataPermission = AnnotationUtil.getAnnotation(method, DataPermission.class); | ||||
|                 dataPermissionCacheMap.put(mappedStatementId, dataPermission); | ||||
|             } | ||||
|         } | ||||
|         if (AnnotationUtil.hasAnnotation(clazz, DataPermission.class)) { | ||||
|             dataPermission = AnnotationUtil.getAnnotation(clazz, DataPermission.class); | ||||
|             dataPermissionCacheMap.put(clazz.getName(), dataPermission); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 根据映射语句 ID 或类名获取对应的 DataPermission 注解对象 | ||||
|      * | ||||
|      * @param mapperId 映射语句 ID | ||||
|      * @return DataPermission 注解对象,如果不存在则返回 null | ||||
|      */ | ||||
|     public DataPermission getDataPermission(String mapperId) { | ||||
|         // 检查上下文中是否包含映射语句 ID 对应的 DataPermission 注解对象 | ||||
|         if (DataPermissionHelper.getPermission() != null) { | ||||
|             return DataPermissionHelper.getPermission(); | ||||
|         } | ||||
|         // 检查缓存中是否包含映射语句 ID 对应的 DataPermission 注解对象 | ||||
|         if (dataPermissionCacheMap.containsKey(mapperId)) { | ||||
|             return dataPermissionCacheMap.get(mapperId); | ||||
|         } | ||||
|         // 如果缓存中不包含映射语句 ID 对应的 DataPermission 注解对象,则尝试使用类名作为键查找 | ||||
|         String clazzName = mapperId.substring(0, mapperId.lastIndexOf(".")); | ||||
|         if (dataPermissionCacheMap.containsKey(clazzName)) { | ||||
|             return dataPermissionCacheMap.get(clazzName); | ||||
|         } | ||||
|         return null; | ||||
|     public DataPermission getDataPermission() { | ||||
|         return DataPermissionHelper.getPermission(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 检查给定的映射语句 ID 是否有效,即是否能够找到对应的 DataPermission 注解对象 | ||||
|      * | ||||
|      * @param mapperId 映射语句 ID | ||||
|      * @return 如果找到对应的 DataPermission 注解对象,则返回 false;否则返回 true | ||||
|      */ | ||||
|     public boolean invalid(String mapperId) { | ||||
|         return getDataPermission(mapperId) == null; | ||||
|     public boolean invalid() { | ||||
|         return getDataPermission() == null; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|   | ||||
| @@ -26,7 +26,14 @@ public class DataBaseHelper { | ||||
|     private static final DynamicRoutingDataSource DS = SpringUtils.getBean(DynamicRoutingDataSource.class); | ||||
|  | ||||
|     /** | ||||
|      * 获取当前数据库类型 | ||||
|      * 获取当前数据源对应的数据库类型 | ||||
|      * <p> | ||||
|      * 通过 DynamicRoutingDataSource 获取当前线程绑定的数据源, | ||||
|      * 然后从数据源获取数据库连接,利用连接的元数据获取数据库产品名称, | ||||
|      * 最后调用 DataBaseType.find 方法将数据库名称转换为对应的枚举类型 | ||||
|      * | ||||
|      * @return 当前数据库对应的 DataBaseType 枚举,找不到时默认返回 MY_SQL | ||||
|      * @throws ServiceException 当获取数据库连接或元数据出现异常时抛出业务异常 | ||||
|      */ | ||||
|     public static DataBaseType getDataBaseType() { | ||||
|         DataSource dataSource = DS.determineDataSource(); | ||||
| @@ -39,37 +46,31 @@ public class DataBaseHelper { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public static boolean isMySql() { | ||||
|         return DataBaseType.MY_SQL == getDataBaseType(); | ||||
|     } | ||||
|  | ||||
|     public static boolean isOracle() { | ||||
|         return DataBaseType.ORACLE == getDataBaseType(); | ||||
|     } | ||||
|  | ||||
|     public static boolean isPostgerSql() { | ||||
|         return DataBaseType.POSTGRE_SQL == getDataBaseType(); | ||||
|     } | ||||
|  | ||||
|     public static boolean isSqlServer() { | ||||
|         return DataBaseType.SQL_SERVER == getDataBaseType(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 根据当前数据库类型,生成兼容的 FIND_IN_SET 语句片段 | ||||
|      * <p> | ||||
|      * 用于判断指定值是否存在于逗号分隔的字符串列中,SQL写法根据不同数据库方言自动切换: | ||||
|      * - Oracle 使用 instr 函数 | ||||
|      * - PostgreSQL 使用 strpos 函数 | ||||
|      * - SQL Server 使用 charindex 函数 | ||||
|      * - 其他默认使用 MySQL 的 find_in_set 函数 | ||||
|      * | ||||
|      * @param var1 要查找的值(支持任意类型,内部会转换成字符串) | ||||
|      * @param var2 存储逗号分隔值的数据库列名 | ||||
|      * @return 适用于当前数据库的 SQL 条件字符串,通常用于 where 或 apply 中拼接 | ||||
|      */ | ||||
|     public static String findInSet(Object var1, String var2) { | ||||
|         DataBaseType dataBasyType = getDataBaseType(); | ||||
|         String var = Convert.toStr(var1); | ||||
|         if (dataBasyType == DataBaseType.SQL_SERVER) { | ||||
|             // charindex(',100,' , ',0,100,101,') <> 0 | ||||
|             return "charindex(',%s,' , ','+%s+',') <> 0".formatted(var, var2); | ||||
|         } else if (dataBasyType == DataBaseType.POSTGRE_SQL) { | ||||
|             // (select strpos(',0,100,101,' , ',100,')) <> 0 | ||||
|             return "(select strpos(','||%s||',' , ',%s,')) <> 0".formatted(var2, var); | ||||
|         } else if (dataBasyType == DataBaseType.ORACLE) { | ||||
|         return switch (getDataBaseType()) { | ||||
|             // instr(',0,100,101,' , ',100,') <> 0 | ||||
|             return "instr(','||%s||',' , ',%s,') <> 0".formatted(var2, var); | ||||
|         } | ||||
|         // find_in_set(100 , '0,100,101') | ||||
|         return "find_in_set('%s' , %s) <> 0".formatted(var, var2); | ||||
|             case ORACLE -> "instr(','||%s||',' , ',%s,') <> 0".formatted(var2, var); | ||||
|             // (select strpos(',0,100,101,' , ',100,')) <> 0 | ||||
|             case POSTGRE_SQL -> "(select strpos(','||%s||',' , ',%s,')) <> 0".formatted(var2, var); | ||||
|             // charindex(',100,' , ',0,100,101,') <> 0 | ||||
|             case SQL_SERVER -> "charindex(',%s,' , ','+%s+',') <> 0".formatted(var, var2); | ||||
|             // find_in_set(100 , '0,100,101') | ||||
|             default -> "find_in_set('%s' , %s) <> 0".formatted(var, var2); | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|   | ||||
| @@ -35,16 +35,7 @@ import java.util.List; | ||||
| @Slf4j | ||||
| public class PlusDataPermissionInterceptor extends BaseMultiTableInnerInterceptor implements InnerInterceptor { | ||||
|  | ||||
|     private final PlusDataPermissionHandler dataPermissionHandler; | ||||
|  | ||||
|     /** | ||||
|      * 构造函数,初始化 PlusDataPermissionHandler 实例 | ||||
|      * | ||||
|      * @param mapperPackage 扫描的映射器包 | ||||
|      */ | ||||
|     public PlusDataPermissionInterceptor(String mapperPackage) { | ||||
|         this.dataPermissionHandler = new PlusDataPermissionHandler(mapperPackage); | ||||
|     } | ||||
|     private final PlusDataPermissionHandler dataPermissionHandler = new PlusDataPermissionHandler(); | ||||
|  | ||||
|     /** | ||||
|      * 在执行查询之前,检查并处理数据权限相关逻辑 | ||||
| @@ -64,7 +55,7 @@ public class PlusDataPermissionInterceptor extends BaseMultiTableInnerIntercepto | ||||
|             return; | ||||
|         } | ||||
|         // 检查是否缺少有效的数据权限注解 | ||||
|         if (dataPermissionHandler.invalid(ms.getId())) { | ||||
|         if (dataPermissionHandler.invalid()) { | ||||
|             return; | ||||
|         } | ||||
|         // 解析 sql 分配对应方法 | ||||
| @@ -92,7 +83,7 @@ public class PlusDataPermissionInterceptor extends BaseMultiTableInnerIntercepto | ||||
|                 return; | ||||
|             } | ||||
|             // 检查是否缺少有效的数据权限注解 | ||||
|             if (dataPermissionHandler.invalid(ms.getId())) { | ||||
|             if (dataPermissionHandler.invalid()) { | ||||
|                 return; | ||||
|             } | ||||
|             PluginUtils.MPBoundSql mpBs = mpSh.mPBoundSql(); | ||||
| @@ -128,7 +119,7 @@ public class PlusDataPermissionInterceptor extends BaseMultiTableInnerIntercepto | ||||
|      */ | ||||
|     @Override | ||||
|     protected void processUpdate(Update update, int index, String sql, Object obj) { | ||||
|         Expression sqlSegment = dataPermissionHandler.getSqlSegment(update.getWhere(), (String) obj, false); | ||||
|         Expression sqlSegment = dataPermissionHandler.getSqlSegment(update.getWhere(), false); | ||||
|         if (null != sqlSegment) { | ||||
|             update.setWhere(sqlSegment); | ||||
|         } | ||||
| @@ -144,7 +135,7 @@ public class PlusDataPermissionInterceptor extends BaseMultiTableInnerIntercepto | ||||
|      */ | ||||
|     @Override | ||||
|     protected void processDelete(Delete delete, int index, String sql, Object obj) { | ||||
|         Expression sqlSegment = dataPermissionHandler.getSqlSegment(delete.getWhere(), (String) obj, false); | ||||
|         Expression sqlSegment = dataPermissionHandler.getSqlSegment(delete.getWhere(), false); | ||||
|         if (null != sqlSegment) { | ||||
|             delete.setWhere(sqlSegment); | ||||
|         } | ||||
| @@ -157,7 +148,7 @@ public class PlusDataPermissionInterceptor extends BaseMultiTableInnerIntercepto | ||||
|      * @param mappedStatementId 映射语句的 ID | ||||
|      */ | ||||
|     protected void setWhere(PlainSelect plainSelect, String mappedStatementId) { | ||||
|         Expression sqlSegment = dataPermissionHandler.getSqlSegment(plainSelect.getWhere(), mappedStatementId, true); | ||||
|         Expression sqlSegment = dataPermissionHandler.getSqlSegment(plainSelect.getWhere(), true); | ||||
|         if (null != sqlSegment) { | ||||
|             plainSelect.setWhere(sqlSegment); | ||||
|         } | ||||
|   | ||||
| @@ -2,6 +2,7 @@ package org.dromara.common.oss.core; | ||||
|  | ||||
| import cn.hutool.core.io.IoUtil; | ||||
| import cn.hutool.core.util.IdUtil; | ||||
| import lombok.extern.slf4j.Slf4j; | ||||
| import org.dromara.common.core.constant.Constants; | ||||
| import org.dromara.common.core.utils.DateUtils; | ||||
| import org.dromara.common.core.utils.StringUtils; | ||||
| @@ -13,9 +14,7 @@ import org.dromara.common.oss.exception.OssException; | ||||
| import org.dromara.common.oss.properties.OssProperties; | ||||
| import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; | ||||
| import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; | ||||
| import software.amazon.awssdk.core.ResponseInputStream; | ||||
| import software.amazon.awssdk.core.async.AsyncResponseTransformer; | ||||
| import software.amazon.awssdk.core.async.BlockingInputStreamAsyncRequestBody; | ||||
| import software.amazon.awssdk.core.async.*; | ||||
| import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient; | ||||
| import software.amazon.awssdk.regions.Region; | ||||
| import software.amazon.awssdk.services.s3.S3AsyncClient; | ||||
| @@ -29,9 +28,12 @@ import software.amazon.awssdk.transfer.s3.progress.LoggingTransferListener; | ||||
| import java.io.*; | ||||
| import java.net.URI; | ||||
| import java.net.URL; | ||||
| import java.nio.channels.Channels; | ||||
| import java.nio.channels.WritableByteChannel; | ||||
| import java.nio.file.Files; | ||||
| import java.nio.file.Path; | ||||
| import java.time.Duration; | ||||
| import java.util.Optional; | ||||
| import java.util.function.Consumer; | ||||
|  | ||||
| /** | ||||
| @@ -40,6 +42,7 @@ import java.util.function.Consumer; | ||||
|  * | ||||
|  * @author AprilWind | ||||
|  */ | ||||
| @Slf4j | ||||
| public class OssClient { | ||||
|  | ||||
|     /** | ||||
| @@ -177,12 +180,12 @@ public class OssClient { | ||||
|             // 创建异步请求体(length如果为空会报错) | ||||
|             BlockingInputStreamAsyncRequestBody body = BlockingInputStreamAsyncRequestBody.builder() | ||||
|                 .contentLength(length) | ||||
|                 .subscribeTimeout(Duration.ofSeconds(30)) | ||||
|                 .subscribeTimeout(Duration.ofSeconds(120)) | ||||
|                 .build(); | ||||
|  | ||||
|             // 使用 transferManager 进行上传 | ||||
|             Upload upload = transferManager.upload( | ||||
|                 x -> x.requestBody(body) | ||||
|                 x -> x.requestBody(body).addTransferListener(LoggingTransferListener.create()) | ||||
|                     .putObjectRequest( | ||||
|                         y -> y.bucket(properties.getBucketName()) | ||||
|                             .key(key) | ||||
| @@ -237,30 +240,61 @@ public class OssClient { | ||||
|      * @param key 文件在 Amazon S3 中的对象键 | ||||
|      * @param out 输出流 | ||||
|      * @param consumer 自定义处理逻辑 | ||||
|      * @return 输出流中写入的字节数(长度) | ||||
|      * @throws OssException 如果下载失败,抛出自定义异常 | ||||
|      */ | ||||
|     public void download(String key, OutputStream out, Consumer<Long> consumer) { | ||||
|         try { | ||||
|             this.download(key, consumer).writeTo(out); | ||||
|         } catch (Exception e) { | ||||
|             throw new OssException("文件下载失败,错误信息:[" + e.getMessage() + "]"); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 下载文件从 Amazon S3 到 输出流 | ||||
|      * | ||||
|      * @param key 文件在 Amazon S3 中的对象键 | ||||
|      * @param contentLengthConsumer 文件大小消费者函数 | ||||
|      * @return 写出订阅器 | ||||
|      * @throws OssException 如果下载失败,抛出自定义异常 | ||||
|      */ | ||||
|     public WriteOutSubscriber<OutputStream> download(String key, Consumer<Long> contentLengthConsumer) { | ||||
|         try { | ||||
|             // 构建下载请求 | ||||
|             DownloadRequest<ResponseInputStream<GetObjectResponse>> downloadRequest = DownloadRequest.builder() | ||||
|             DownloadRequest<ResponsePublisher<GetObjectResponse>> publisherDownloadRequest = DownloadRequest.builder() | ||||
|                 // 文件对象 | ||||
|                 .getObjectRequest(y -> y.bucket(properties.getBucketName()) | ||||
|                     .key(key) | ||||
|                     .build()) | ||||
|                 .addTransferListener(LoggingTransferListener.create()) | ||||
|                 // 使用订阅转换器 | ||||
|                 .responseTransformer(AsyncResponseTransformer.toBlockingInputStream()) | ||||
|                 // 使用发布订阅转换器 | ||||
|                 .responseTransformer(AsyncResponseTransformer.toPublisher()) | ||||
|                 .build(); | ||||
|  | ||||
|             // 使用 S3TransferManager 下载文件 | ||||
|             Download<ResponseInputStream<GetObjectResponse>> responseFuture = transferManager.download(downloadRequest); | ||||
|             // 输出到流中 | ||||
|             try (ResponseInputStream<GetObjectResponse> responseStream = responseFuture.completionFuture().join().result()) { // auto-closeable stream | ||||
|                 if (consumer != null) { | ||||
|                     consumer.accept(responseStream.response().contentLength()); | ||||
|             Download<ResponsePublisher<GetObjectResponse>> publisherDownload = transferManager.download(publisherDownloadRequest); | ||||
|             // 获取下载发布订阅转换器 | ||||
|             ResponsePublisher<GetObjectResponse> publisher = publisherDownload.completionFuture().join().result(); | ||||
|             // 执行文件大小消费者函数 | ||||
|             Optional.ofNullable(contentLengthConsumer) | ||||
|                 .ifPresent(lengthConsumer -> lengthConsumer.accept(publisher.response().contentLength())); | ||||
|  | ||||
|             // 构建写出订阅器对象 | ||||
|             return out -> { | ||||
|                 // 创建可写入的字节通道 | ||||
|                 try(WritableByteChannel channel = Channels.newChannel(out)){ | ||||
|                     // 订阅数据 | ||||
|                     publisher.subscribe(byteBuffer -> { | ||||
|                         while (byteBuffer.hasRemaining()) { | ||||
|                             try { | ||||
|                                 channel.write(byteBuffer); | ||||
|                             } catch (IOException e) { | ||||
|                                 throw new RuntimeException(e); | ||||
|                             } | ||||
|                         } | ||||
|                     }).join(); | ||||
|                 } | ||||
|                 responseStream.transferTo(out); // 阻塞调用线程 blocks the calling thread | ||||
|             } | ||||
|             }; | ||||
|         } catch (Exception e) { | ||||
|             throw new OssException("文件下载失败,错误信息:[" + e.getMessage() + "]"); | ||||
|         } | ||||
|   | ||||
| @@ -0,0 +1,15 @@ | ||||
| package org.dromara.common.oss.core; | ||||
|  | ||||
| import java.io.IOException; | ||||
|  | ||||
| /** | ||||
|  * 写出订阅器 | ||||
|  * | ||||
|  * @author 秋辞未寒 | ||||
|  */ | ||||
| @FunctionalInterface | ||||
| public interface WriteOutSubscriber<T> { | ||||
|  | ||||
|     void writeTo(T out) throws IOException; | ||||
|  | ||||
| } | ||||
| @@ -129,9 +129,9 @@ public class RedisUtils { | ||||
|             } catch (Exception e) { | ||||
|                 long timeToLive = bucket.remainTimeToLive(); | ||||
|                 if (timeToLive == -1) { | ||||
|                     setCacheObject(key, value); | ||||
|                     bucket.set(value); | ||||
|                 } else { | ||||
|                     setCacheObject(key, value, Duration.ofMillis(timeToLive)); | ||||
|                     bucket.set(value, Duration.ofMillis(timeToLive)); | ||||
|                 } | ||||
|             } | ||||
|         } else { | ||||
| @@ -147,11 +147,8 @@ public class RedisUtils { | ||||
|      * @param duration 时间 | ||||
|      */ | ||||
|     public static <T> void setCacheObject(final String key, final T value, final Duration duration) { | ||||
|         RBatch batch = CLIENT.createBatch(); | ||||
|         RBucketAsync<T> bucket = batch.getBucket(key); | ||||
|         bucket.setAsync(value); | ||||
|         bucket.expireAsync(duration); | ||||
|         batch.execute(); | ||||
|         RBucket<T> bucket = CLIENT.getBucket(key); | ||||
|         bucket.set(value, duration); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| package org.dromara.common.redis.utils; | ||||
|  | ||||
| import cn.hutool.core.convert.Convert; | ||||
| import cn.hutool.core.date.DatePattern; | ||||
| import cn.hutool.core.date.DateUtil; | ||||
| import lombok.AccessLevel; | ||||
| import lombok.NoArgsConstructor; | ||||
| import org.dromara.common.core.utils.SpringUtils; | ||||
| @@ -10,6 +10,10 @@ import org.redisson.api.RIdGenerator; | ||||
| import org.redisson.api.RedissonClient; | ||||
|  | ||||
| import java.time.Duration; | ||||
| import java.time.LocalDate; | ||||
| import java.time.LocalDateTime; | ||||
| import java.time.format.DateTimeFormatter; | ||||
| import java.time.temporal.TemporalAccessor; | ||||
|  | ||||
| /** | ||||
|  * 发号器工具类 | ||||
| @@ -23,12 +27,12 @@ public class SequenceUtils { | ||||
|     /** | ||||
|      * 默认初始值 | ||||
|      */ | ||||
|     public static final Long DEFAULT_INIT_VALUE = 1L; | ||||
|     public static final long DEFAULT_INIT_VALUE = 1L; | ||||
|  | ||||
|     /** | ||||
|      * 默认步长 | ||||
|      */ | ||||
|     public static final Long DEFAULT_STEP_VALUE = 1L; | ||||
|     public static final long DEFAULT_STEP_VALUE = 1L; | ||||
|  | ||||
|     /** | ||||
|      * 默认过期时间-天 | ||||
| @@ -40,6 +44,11 @@ public class SequenceUtils { | ||||
|      */ | ||||
|     public static final Duration DEFAULT_EXPIRE_TIME_MINUTE = Duration.ofMinutes(1); | ||||
|  | ||||
|     /** | ||||
|      * 默认最小ID容量位数 - 6位数(即至少可以生成的ID为999999个) | ||||
|      */ | ||||
|     public static final int DEFAULT_MIN_ID_CAPACITY_BITS = 6; | ||||
|  | ||||
|     /** | ||||
|      * 获取Redisson客户端实例 | ||||
|      */ | ||||
| @@ -54,14 +63,11 @@ public class SequenceUtils { | ||||
|      * @param stepValue  ID步长 | ||||
|      * @return ID生成器 | ||||
|      */ | ||||
|     private static RIdGenerator getIdGenerator(String key, Duration expireTime, Long initValue, Long stepValue) { | ||||
|         if (initValue == null || initValue <= 0) { | ||||
|             initValue = DEFAULT_INIT_VALUE; | ||||
|         } | ||||
|         if (stepValue == null || stepValue <= 0) { | ||||
|             stepValue = DEFAULT_STEP_VALUE; | ||||
|         } | ||||
|     public static RIdGenerator getIdGenerator(String key, Duration expireTime, long initValue, long stepValue) { | ||||
|         RIdGenerator idGenerator = REDISSON_CLIENT.getIdGenerator(key); | ||||
|         // 初始值和步长不能小于等于0 | ||||
|         initValue = initValue <= 0 ? DEFAULT_INIT_VALUE : initValue; | ||||
|         stepValue = stepValue <= 0 ? DEFAULT_STEP_VALUE : stepValue; | ||||
|         // 设置初始值和步长 | ||||
|         idGenerator.tryInit(initValue, stepValue); | ||||
|         // 设置过期时间 | ||||
| @@ -69,6 +75,17 @@ public class SequenceUtils { | ||||
|         return idGenerator; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取ID生成器 | ||||
|      * | ||||
|      * @param key        业务key | ||||
|      * @param expireTime 过期时间 | ||||
|      * @return ID生成器 | ||||
|      */ | ||||
|     public static RIdGenerator getIdGenerator(String key, Duration expireTime) { | ||||
|         return getIdGenerator(key, expireTime, DEFAULT_INIT_VALUE, DEFAULT_STEP_VALUE); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取指定业务key的唯一id | ||||
|      * | ||||
| @@ -78,10 +95,21 @@ public class SequenceUtils { | ||||
|      * @param stepValue  ID步长 | ||||
|      * @return 唯一id | ||||
|      */ | ||||
|     public static long nextId(String key, Duration expireTime, Long initValue, Long stepValue) { | ||||
|     public static long getNextId(String key, Duration expireTime, long initValue, long stepValue) { | ||||
|         return getIdGenerator(key, expireTime, initValue, stepValue).nextId(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取指定业务key的唯一id (ID初始值=1,ID步长=1) | ||||
|      * | ||||
|      * @param key        业务key | ||||
|      * @param expireTime 过期时间 | ||||
|      * @return 唯一id | ||||
|      */ | ||||
|     public static long getNextId(String key, Duration expireTime) { | ||||
|         return getIdGenerator(key, expireTime).nextId(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取指定业务key的唯一id字符串 | ||||
|      * | ||||
| @@ -91,19 +119,8 @@ public class SequenceUtils { | ||||
|      * @param stepValue  ID步长 | ||||
|      * @return 唯一id | ||||
|      */ | ||||
|     public static String nextIdStr(String key, Duration expireTime, Long initValue, Long stepValue) { | ||||
|         return String.valueOf(nextId(key, expireTime, initValue, stepValue)); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取指定业务key的唯一id (ID初始值=1,ID步长=1) | ||||
|      * | ||||
|      * @param key        业务key | ||||
|      * @param expireTime 过期时间 | ||||
|      * @return 唯一id | ||||
|      */ | ||||
|     public static long nextId(String key, Duration expireTime) { | ||||
|         return getIdGenerator(key, expireTime, DEFAULT_INIT_VALUE, DEFAULT_STEP_VALUE).nextId(); | ||||
|     public static String getNextIdString(String key, Duration expireTime, long initValue, long stepValue) { | ||||
|         return Convert.toStr(getNextId(key, expireTime, initValue, stepValue)); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -113,8 +130,8 @@ public class SequenceUtils { | ||||
|      * @param expireTime 过期时间 | ||||
|      * @return 唯一id | ||||
|      */ | ||||
|     public static String nextIdStr(String key, Duration expireTime) { | ||||
|         return String.valueOf(nextId(key, expireTime)); | ||||
|     public static String getNextIdString(String key, Duration expireTime) { | ||||
|         return Convert.toStr(getNextId(key, expireTime)); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -125,56 +142,210 @@ public class SequenceUtils { | ||||
|      * @param width      位数,不足左补0 | ||||
|      * @return 补零后的唯一id字符串 | ||||
|      */ | ||||
|     public static String nextPaddedIdStr(String key, Duration expireTime, Integer width) { | ||||
|         return StringUtils.leftPad(nextIdStr(key, expireTime), width, '0'); | ||||
|     public static String getPaddedNextIdString(String key, Duration expireTime, Integer width) { | ||||
|         return StringUtils.leftPad(getNextIdString(key, expireTime), width, '0'); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取 yyyyMMdd 开头的唯一id | ||||
|      * 获取 yyyyMMdd 格式的唯一id | ||||
|      * | ||||
|      * @return 唯一id | ||||
|      * @deprecated 请使用 {@link #getDateId(String)} 或 {@link #getDateId(String, boolean)}、{@link #getDateId(String, boolean, int)},确保不同业务的ID连续性 | ||||
|      */ | ||||
|     public static String nextIdDate() { | ||||
|         return nextIdDate(""); | ||||
|     @Deprecated | ||||
|     public static String getDateId() { | ||||
|         return getDateId(""); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取 prefix + yyyyMMdd 开头的唯一id | ||||
|      * 获取 prefix + yyyyMMdd 格式的唯一id | ||||
|      * | ||||
|      * @param prefix 业务前缀 | ||||
|      * @return 唯一id | ||||
|      */ | ||||
|     public static String nextIdDate(String prefix) { | ||||
|         // 前缀+日期 构建 prefixKey | ||||
|         String prefixKey = StringUtils.format("{}{}", StringUtils.blankToDefault(prefix, ""), DateUtil.format(DateUtil.date(), DatePattern.PURE_DATE_FORMATTER)); | ||||
|         // 获取下一个id | ||||
|         long nextId = getIdGenerator(prefixKey, DEFAULT_EXPIRE_TIME_DAY, DEFAULT_INIT_VALUE, DEFAULT_STEP_VALUE).nextId(); | ||||
|         // 返回完整id | ||||
|         return StringUtils.format("{}{}", prefixKey, nextId); | ||||
|     public static String getDateId(String prefix) { | ||||
|         return getDateId(prefix, true); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取 yyyyMMddHHmmss 开头的唯一id | ||||
|      * 获取 prefix + yyyyMMdd 格式的唯一id | ||||
|      * | ||||
|      * @param prefix       业务前缀 | ||||
|      * @param isWithPrefix id是否携带业务前缀 | ||||
|      * @return 唯一id | ||||
|      */ | ||||
|     public static String nextIdDateTime() { | ||||
|         return nextIdDateTime(""); | ||||
|     public static String getDateId(String prefix, boolean isWithPrefix) { | ||||
|         return getDateId(prefix, isWithPrefix, -1); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取 prefix + yyyyMMddHHmmss 开头的唯一id | ||||
|      * 获取 prefix + yyyyMMdd 格式的唯一id (启用ID补位,补位长度 = {@link #DEFAULT_MIN_ID_CAPACITY_BITS})}) | ||||
|      * | ||||
|      * @param prefix       业务前缀 | ||||
|      * @param isWithPrefix id是否携带业务前缀 | ||||
|      * @return 唯一id | ||||
|      */ | ||||
|     public static String getPaddedDateId(String prefix, boolean isWithPrefix) { | ||||
|         return getDateId(prefix, isWithPrefix, DEFAULT_MIN_ID_CAPACITY_BITS); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取 prefix + yyyyMMdd 格式的唯一id | ||||
|      * | ||||
|      * @param prefix            业务前缀 | ||||
|      * @param isWithPrefix      id是否携带业务前缀 | ||||
|      * @param minIdCapacityBits 最小ID容量位数,小于该位数的ID,左补0(小于等于0表示不启用补位) | ||||
|      * @return 唯一id | ||||
|      */ | ||||
|     public static String getDateId(String prefix, boolean isWithPrefix, int minIdCapacityBits) { | ||||
|         return getDateId(prefix, isWithPrefix, minIdCapacityBits, LocalDate.now()); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取 prefix + yyyyMMdd 格式的唯一id | ||||
|      * | ||||
|      * @param prefix            业务前缀 | ||||
|      * @param isWithPrefix      id是否携带业务前缀 | ||||
|      * @param minIdCapacityBits 最小ID容量位数,小于该位数的ID,左补0(小于等于0表示不启用补位) | ||||
|      * @param time              时间 | ||||
|      * @return 唯一id | ||||
|      */ | ||||
|     public static String getDateId(String prefix, boolean isWithPrefix, int minIdCapacityBits, LocalDate time) { | ||||
|         return getDateId(prefix, isWithPrefix, minIdCapacityBits, time, DEFAULT_INIT_VALUE, DEFAULT_STEP_VALUE); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取 prefix + yyyyMMdd 格式的唯一id | ||||
|      * | ||||
|      * @param prefix            业务前缀 | ||||
|      * @param isWithPrefix      id是否携带业务前缀 | ||||
|      * @param minIdCapacityBits 最小ID容量位数,小于该位数的ID,左补0(小于等于0表示不启用补位) | ||||
|      * @param time              时间 | ||||
|      * @param initValue         ID初始值 | ||||
|      * @param stepValue         ID步长 | ||||
|      * @return 唯一id | ||||
|      */ | ||||
|     public static String getDateId(String prefix, boolean isWithPrefix, int minIdCapacityBits, LocalDate time, long initValue, long stepValue) { | ||||
|         return getDatePatternId(prefix, isWithPrefix, minIdCapacityBits, time, DatePattern.PURE_DATE_FORMATTER, DEFAULT_EXPIRE_TIME_DAY, initValue, stepValue); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取 yyyyMMddHHmmss 格式的唯一id | ||||
|      * | ||||
|      * @return 唯一id | ||||
|      * @deprecated 请使用 {@link #getDateTimeId(String)} 或 {@link #getDateTimeId(String, boolean)}、{@link #getDateTimeId(String, boolean, int)},确保不同业务的ID连续性 | ||||
|      */ | ||||
|     @Deprecated | ||||
|     public static String getDateTimeId() { | ||||
|         return getDateTimeId("", false); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取 prefix + yyyyMMddHHmmss 格式的唯一id | ||||
|      * | ||||
|      * @param prefix 业务前缀 | ||||
|      * @return 唯一id | ||||
|      */ | ||||
|     public static String nextIdDateTime(String prefix) { | ||||
|         // 前缀+日期时间 构建 prefixKey | ||||
|         String prefixKey = StringUtils.format("{}{}", StringUtils.blankToDefault(prefix, ""), DateUtil.format(DateUtil.date(), DatePattern.PURE_DATETIME_FORMATTER)); | ||||
|         // 获取下一个id | ||||
|         long nextId = getIdGenerator(prefixKey, DEFAULT_EXPIRE_TIME_MINUTE, DEFAULT_INIT_VALUE, DEFAULT_STEP_VALUE).nextId(); | ||||
|         // 返回完整id | ||||
|         return StringUtils.format("{}{}", prefixKey, nextId); | ||||
|     public static String getDateTimeId(String prefix) { | ||||
|         return getDateTimeId(prefix, true); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取 prefix + yyyyMMddHHmmss 格式的唯一id | ||||
|      * | ||||
|      * @param prefix       业务前缀 | ||||
|      * @param isWithPrefix id是否携带业务前缀 | ||||
|      * @return 唯一id | ||||
|      */ | ||||
|     public static String getDateTimeId(String prefix, boolean isWithPrefix) { | ||||
|         return getDateTimeId(prefix, isWithPrefix, -1); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取 prefix + yyyyMMddHHmmss 格式的唯一id (启用ID补位,补位长度 = {@link #DEFAULT_MIN_ID_CAPACITY_BITS})}) | ||||
|      * | ||||
|      * @param prefix       业务前缀 | ||||
|      * @param isWithPrefix id是否携带业务前缀 | ||||
|      * @return 唯一id | ||||
|      */ | ||||
|     public static String getPaddedDateTimeId(String prefix, boolean isWithPrefix) { | ||||
|         return getDateTimeId(prefix, isWithPrefix, DEFAULT_MIN_ID_CAPACITY_BITS); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取 prefix + yyyyMMddHHmmss 格式的唯一id | ||||
|      * | ||||
|      * @param prefix            业务前缀 | ||||
|      * @param isWithPrefix      id是否携带业务前缀 | ||||
|      * @param minIdCapacityBits 最小ID容量位数,小于该位数的ID,左补0(小于等于0表示不启用补位) | ||||
|      * @return 唯一id | ||||
|      */ | ||||
|     public static String getDateTimeId(String prefix, boolean isWithPrefix, int minIdCapacityBits) { | ||||
|         return getDateTimeId(prefix, isWithPrefix, minIdCapacityBits, LocalDateTime.now()); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取 prefix + yyyyMMddHHmmss 格式的唯一id | ||||
|      * | ||||
|      * @param prefix            业务前缀 | ||||
|      * @param isWithPrefix      id是否携带业务前缀 | ||||
|      * @param minIdCapacityBits 最小ID容量位数,小于该位数的ID,左补0(小于等于0表示不启用补位) | ||||
|      * @param time              时间 | ||||
|      * @return 唯一id | ||||
|      */ | ||||
|     public static String getDateTimeId(String prefix, boolean isWithPrefix, int minIdCapacityBits, LocalDateTime time) { | ||||
|         return getDateTimeId(prefix, isWithPrefix, minIdCapacityBits, time, DEFAULT_INIT_VALUE, DEFAULT_STEP_VALUE); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取 prefix + yyyyMMddHHmmss 格式的唯一id | ||||
|      * | ||||
|      * @param prefix            业务前缀 | ||||
|      * @param isWithPrefix      id是否携带业务前缀 | ||||
|      * @param minIdCapacityBits 最小ID容量位数,小于该位数的ID,左补0(小于等于0表示不启用补位) | ||||
|      * @param initValue         ID初始值 | ||||
|      * @param stepValue         ID步长 | ||||
|      * @return 唯一id | ||||
|      */ | ||||
|     public static String getDateTimeId(String prefix, boolean isWithPrefix, int minIdCapacityBits, LocalDateTime time, long initValue, long stepValue) { | ||||
|         return getDatePatternId(prefix, isWithPrefix, minIdCapacityBits, time, DatePattern.PURE_DATETIME_FORMATTER, DEFAULT_EXPIRE_TIME_MINUTE, initValue, stepValue); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取指定业务key的指定时间格式的ID | ||||
|      * | ||||
|      * @param prefix            业务前缀 | ||||
|      * @param isWithPrefix      id是否携带业务前缀 | ||||
|      * @param minIdCapacityBits 最小ID容量位数,小于该位数的ID,左补0(小于等于0表示不启用补位) | ||||
|      * @param temporalAccessor  时间访问器 | ||||
|      * @param timeFormatter     时间格式 | ||||
|      * @param expireTime        过期时间 | ||||
|      * @param initValue         ID初始值 | ||||
|      * @param stepValue         ID步长 | ||||
|      * @return 唯一id | ||||
|      */ | ||||
|     private static String getDatePatternId(String prefix, boolean isWithPrefix, int minIdCapacityBits, TemporalAccessor temporalAccessor, DateTimeFormatter timeFormatter, Duration expireTime, long initValue, long stepValue) { | ||||
|         // 时间前缀 | ||||
|         String timePrefix = timeFormatter.format(temporalAccessor); | ||||
|         // 业务前缀 + 时间前缀 构建 prefixKey | ||||
|         String prefixKey = StringUtils.format("{}{}", StringUtils.blankToDefault(prefix, ""), timePrefix); | ||||
|  | ||||
|         // 获取id,例 -> 1 | ||||
|         String nextId = getNextIdString(prefixKey, expireTime, initValue, stepValue); | ||||
|  | ||||
|         // minIdCapacityBits 大于0,且 nextId 的长度小于 minIdCapacityBits,则左补0 | ||||
|         if (minIdCapacityBits > 0 && nextId.length() < minIdCapacityBits) { | ||||
|             nextId = StringUtils.leftPad(nextId, minIdCapacityBits, '0'); | ||||
|         } | ||||
|  | ||||
|         // 是否携带业务前缀 | ||||
|         if (isWithPrefix) { | ||||
|             // 例 -> P202507031 | ||||
|             // 其中 P 为业务前缀,202507031 为 yyyyMMdd 格式时间, 1 为nextId | ||||
|             return StringUtils.format("{}{}", prefixKey, nextId); | ||||
|         } | ||||
|         // 例 -> 202507031 | ||||
|         // 其中 202507031 为 yyyyMMdd 格式时间, 1 为nextId | ||||
|         return StringUtils.format("{}{}", timePrefix, nextId); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -53,11 +53,7 @@ public class PlusSaTokenDao implements SaTokenDaoBySessionFollowObject { | ||||
|         if (timeout == NEVER_EXPIRE) { | ||||
|             RedisUtils.setCacheObject(key, value); | ||||
|         } else { | ||||
|             if (RedisUtils.hasKey(key)) { | ||||
|                 RedisUtils.setCacheObject(key, value, true); | ||||
|             } else { | ||||
|                 RedisUtils.setCacheObject(key, value, Duration.ofSeconds(timeout)); | ||||
|             } | ||||
|             RedisUtils.setCacheObject(key, value, Duration.ofSeconds(timeout)); | ||||
|         } | ||||
|         CAFFEINE.invalidate(key); | ||||
|     } | ||||
| @@ -78,7 +74,9 @@ public class PlusSaTokenDao implements SaTokenDaoBySessionFollowObject { | ||||
|      */ | ||||
|     @Override | ||||
|     public void delete(String key) { | ||||
|         RedisUtils.deleteObject(key); | ||||
|         if (RedisUtils.deleteObject(key)) { | ||||
|             CAFFEINE.invalidate(key); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -134,11 +132,7 @@ public class PlusSaTokenDao implements SaTokenDaoBySessionFollowObject { | ||||
|         if (timeout == NEVER_EXPIRE) { | ||||
|             RedisUtils.setCacheObject(key, object); | ||||
|         } else { | ||||
|             if (RedisUtils.hasKey(key)) { | ||||
|                 RedisUtils.setCacheObject(key, object, true); | ||||
|             } else { | ||||
|                 RedisUtils.setCacheObject(key, object, Duration.ofSeconds(timeout)); | ||||
|             } | ||||
|             RedisUtils.setCacheObject(key, object, Duration.ofSeconds(timeout)); | ||||
|         } | ||||
|         CAFFEINE.invalidate(key); | ||||
|     } | ||||
| @@ -159,7 +153,9 @@ public class PlusSaTokenDao implements SaTokenDaoBySessionFollowObject { | ||||
|      */ | ||||
|     @Override | ||||
|     public void deleteObject(String key) { | ||||
|         RedisUtils.deleteObject(key); | ||||
|         if (RedisUtils.deleteObject(key)) { | ||||
|             CAFFEINE.invalidate(key); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| package org.dromara.common.satoken.core.service; | ||||
|  | ||||
| import cn.dev33.satoken.stp.StpInterface; | ||||
| import cn.hutool.core.collection.CollUtil; | ||||
| import cn.hutool.core.util.ObjectUtil; | ||||
| import org.dromara.common.core.domain.model.LoginUser; | ||||
| import org.dromara.common.core.enums.UserType; | ||||
| @@ -39,8 +40,12 @@ public class SaPermissionImpl implements StpInterface { | ||||
|         if (userType == UserType.APP_USER) { | ||||
|             // 其他端 自行根据业务编写 | ||||
|         } | ||||
|         // SYS_USER 默认返回权限 | ||||
|         return new ArrayList<>(loginUser.getMenuPermission()); | ||||
|         if (CollUtil.isNotEmpty(loginUser.getMenuPermission())) { | ||||
|             // SYS_USER 默认返回权限 | ||||
|             return new ArrayList<>(loginUser.getMenuPermission()); | ||||
|         } else { | ||||
|             return new ArrayList<>(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -62,8 +67,12 @@ public class SaPermissionImpl implements StpInterface { | ||||
|         if (userType == UserType.APP_USER) { | ||||
|             // 其他端 自行根据业务编写 | ||||
|         } | ||||
|         // SYS_USER 默认返回权限 | ||||
|         return new ArrayList<>(loginUser.getRolePermission()); | ||||
|         if (CollUtil.isNotEmpty(loginUser.getRolePermission())) { | ||||
|             // SYS_USER 默认返回权限 | ||||
|             return new ArrayList<>(loginUser.getRolePermission()); | ||||
|         } else { | ||||
|             return new ArrayList<>(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private PermissionService getPermissionService() { | ||||
|   | ||||
| @@ -207,7 +207,8 @@ public class LoginHelper { | ||||
|      */ | ||||
|     public static boolean isLogin() { | ||||
|         try { | ||||
|             return getLoginUser() != null; | ||||
|             StpUtil.checkLogin(); | ||||
|             return true; | ||||
|         } catch (Exception e) { | ||||
|             return false; | ||||
|         } | ||||
|   | ||||
| @@ -7,7 +7,9 @@ import cn.dev33.satoken.interceptor.SaInterceptor; | ||||
| import cn.dev33.satoken.router.SaRouter; | ||||
| import cn.dev33.satoken.stp.StpUtil; | ||||
| import cn.dev33.satoken.util.SaResult; | ||||
| import cn.dev33.satoken.util.SaTokenConsts; | ||||
| import jakarta.servlet.http.HttpServletRequest; | ||||
| import jakarta.servlet.http.HttpServletResponse; | ||||
| import lombok.RequiredArgsConstructor; | ||||
| import lombok.extern.slf4j.Slf4j; | ||||
| import org.dromara.common.core.constant.HttpStatus; | ||||
| @@ -55,6 +57,8 @@ public class SecurityConfig implements WebMvcConfigurer { | ||||
|                     // 对未排除的路径进行检查 | ||||
|                     .check(() -> { | ||||
|                         HttpServletRequest request = ServletUtils.getRequest(); | ||||
|                         HttpServletResponse response = ServletUtils.getResponse(); | ||||
|                         response.setContentType(SaTokenConsts.CONTENT_TYPE_APPLICATION_JSON); | ||||
|                         // 检查是否登录 是否有token | ||||
|                         StpUtil.checkLogin(); | ||||
|  | ||||
| @@ -94,7 +98,11 @@ public class SecurityConfig implements WebMvcConfigurer { | ||||
|             .setAuth(obj -> { | ||||
|                 SaHttpBasicUtil.check(username + ":" + password); | ||||
|             }) | ||||
|             .setError(e -> SaResult.error(e.getMessage()).setCode(HttpStatus.UNAUTHORIZED)); | ||||
|             .setError(e -> { | ||||
|                 HttpServletResponse response = ServletUtils.getResponse(); | ||||
|                 response.setContentType(SaTokenConsts.CONTENT_TYPE_APPLICATION_JSON); | ||||
|                 return SaResult.error(e.getMessage()).setCode(HttpStatus.UNAUTHORIZED); | ||||
|             }); | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| package org.dromara.common.sensitive.core; | ||||
|  | ||||
| import cn.hutool.core.convert.Convert; | ||||
| import cn.hutool.core.util.DesensitizedUtil; | ||||
| import lombok.AllArgsConstructor; | ||||
|  | ||||
| @@ -52,7 +53,7 @@ public enum SensitiveStrategy { | ||||
|     /** | ||||
|      * 用户ID | ||||
|      */ | ||||
|     USER_ID(s -> String.valueOf(DesensitizedUtil.userId())), | ||||
|     USER_ID(s -> Convert.toStr(DesensitizedUtil.userId())), | ||||
|  | ||||
|     /** | ||||
|      * 密码 | ||||
|   | ||||
| @@ -0,0 +1,109 @@ | ||||
| package me.zhyd.oauth.request; | ||||
|  | ||||
| import com.alibaba.fastjson.JSONObject; | ||||
| import com.xkcoding.http.support.HttpHeader; | ||||
| import me.zhyd.oauth.cache.AuthStateCache; | ||||
| import me.zhyd.oauth.config.AuthConfig; | ||||
| import me.zhyd.oauth.config.AuthDefaultSource; | ||||
| import me.zhyd.oauth.enums.scope.AuthDingTalkScope; | ||||
| import me.zhyd.oauth.exception.AuthException; | ||||
| import me.zhyd.oauth.model.AuthCallback; | ||||
| import me.zhyd.oauth.model.AuthToken; | ||||
| import me.zhyd.oauth.model.AuthUser; | ||||
| import me.zhyd.oauth.utils.AuthScopeUtils; | ||||
| import me.zhyd.oauth.utils.GlobalAuthUtils; | ||||
| import me.zhyd.oauth.utils.HttpUtils; | ||||
| import me.zhyd.oauth.utils.UrlBuilder; | ||||
|  | ||||
| import java.util.HashMap; | ||||
| import java.util.Map; | ||||
|  | ||||
| /** | ||||
|  * 新版钉钉二维码登录 | ||||
|  * | ||||
|  * @author yadong.zhang (yadong.zhang0415(a)gmail.com) | ||||
|  * @since 1.16.7 | ||||
|  */ | ||||
| public class AuthDingTalkV2Request extends AuthDefaultRequest { | ||||
|  | ||||
|     public AuthDingTalkV2Request(AuthConfig config) { | ||||
|         super(config, AuthDefaultSource.DINGTALK_V2); | ||||
|     } | ||||
|  | ||||
|     public AuthDingTalkV2Request(AuthConfig config, AuthStateCache authStateCache) { | ||||
|         super(config, AuthDefaultSource.DINGTALK_V2, authStateCache); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public String authorize(String state) { | ||||
|         return UrlBuilder.fromBaseUrl(source.authorize()) | ||||
|             .queryParam("response_type", "code") | ||||
|             .queryParam("client_id", config.getClientId()) | ||||
|             .queryParam("scope", this.getScopes(",", true, AuthScopeUtils.getDefaultScopes(AuthDingTalkScope.values()))) | ||||
|             .queryParam("redirect_uri", GlobalAuthUtils.urlEncode(config.getRedirectUri())) | ||||
|             .queryParam("prompt", "consent") | ||||
|             .queryParam("org_type", config.getDingTalkOrgType()) | ||||
|             .queryParam("corpId", config.getDingTalkCorpId()) | ||||
|             .queryParam("exclusiveLogin", config.isDingTalkExclusiveLogin()) | ||||
|             .queryParam("exclusiveCorpId", config.getDingTalkExclusiveCorpId()) | ||||
|             .queryParam("state", getRealState(state)) | ||||
|             .build(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public AuthToken getAccessToken(AuthCallback authCallback) { | ||||
|         Map<String, String> params = new HashMap<>(); | ||||
|         params.put("grantType", "authorization_code"); | ||||
|         params.put("clientId", config.getClientId()); | ||||
|         params.put("clientSecret", config.getClientSecret()); | ||||
|         params.put("code", authCallback.getCode()); | ||||
|         String response = new HttpUtils(config.getHttpConfig()).post(this.source.accessToken(), JSONObject.toJSONString(params)).getBody(); | ||||
|         JSONObject accessTokenObject = JSONObject.parseObject(response); | ||||
|         if (!accessTokenObject.containsKey("accessToken")) { | ||||
|             throw new AuthException(JSONObject.toJSONString(response), source); | ||||
|         } | ||||
|         return AuthToken.builder() | ||||
|             .accessToken(accessTokenObject.getString("accessToken")) | ||||
|             .refreshToken(accessTokenObject.getString("refreshToken")) | ||||
|             .expireIn(accessTokenObject.getIntValue("expireIn")) | ||||
|             .corpId(accessTokenObject.getString("corpId")) | ||||
|             .build(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public AuthUser getUserInfo(AuthToken authToken) { | ||||
|         HttpHeader header = new HttpHeader(); | ||||
|         header.add("x-acs-dingtalk-access-token", authToken.getAccessToken()); | ||||
|  | ||||
|         String response = new HttpUtils(config.getHttpConfig()).get(this.source.userInfo(), null, header, false).getBody(); | ||||
|         JSONObject object = JSONObject.parseObject(response); | ||||
|  | ||||
|         authToken.setOpenId(object.getString("openId")); | ||||
|         authToken.setUnionId(object.getString("unionId")); | ||||
|         return AuthUser.builder() | ||||
|             .rawUserInfo(object) | ||||
|             .uuid(object.getString("unionId")) | ||||
|             .username(object.getString("nick")) | ||||
|             .nickname(object.getString("nick")) | ||||
|             .avatar(object.getString("avatarUrl")) | ||||
|             .snapshotUser(object.getBooleanValue("visitor")) | ||||
|             .token(authToken) | ||||
|             .source(source.toString()) | ||||
|             .build(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 返回获取accessToken的url | ||||
|      * | ||||
|      * @param code 授权码 | ||||
|      * @return 返回获取accessToken的url | ||||
|      */ | ||||
|     protected String accessTokenUrl(String code) { | ||||
|         return UrlBuilder.fromBaseUrl(source.accessToken()) | ||||
|             .queryParam("code", code) | ||||
|             .queryParam("clientId", config.getClientId()) | ||||
|             .queryParam("clientSecret", config.getClientSecret()) | ||||
|             .queryParam("grantType", "authorization_code") | ||||
|             .build(); | ||||
|     } | ||||
| } | ||||
| @@ -6,7 +6,6 @@ import lombok.RequiredArgsConstructor; | ||||
| import org.dromara.common.core.domain.R; | ||||
| import org.dromara.common.satoken.utils.LoginHelper; | ||||
| import org.dromara.common.sse.core.SseEmitterManager; | ||||
| import org.dromara.common.sse.dto.SseMessageDto; | ||||
| import org.springframework.beans.factory.DisposableBean; | ||||
| import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; | ||||
| import org.springframework.http.MediaType; | ||||
| @@ -14,8 +13,6 @@ import org.springframework.web.bind.annotation.GetMapping; | ||||
| import org.springframework.web.bind.annotation.RestController; | ||||
| import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; | ||||
|  | ||||
| import java.util.List; | ||||
|  | ||||
| /** | ||||
|  * SSE 控制器 | ||||
|  * | ||||
| @@ -33,7 +30,9 @@ public class SseController implements DisposableBean { | ||||
|      */ | ||||
|     @GetMapping(value = "${sse.path}", produces = MediaType.TEXT_EVENT_STREAM_VALUE) | ||||
|     public SseEmitter connect() { | ||||
|         StpUtil.checkLogin(); | ||||
|         if (!StpUtil.isLogin()) { | ||||
|             return null; | ||||
|         } | ||||
|         String tokenValue = StpUtil.getTokenValue(); | ||||
|         Long userId = LoginHelper.getUserId(); | ||||
|         return sseEmitterManager.connect(userId, tokenValue); | ||||
| @@ -51,31 +50,32 @@ public class SseController implements DisposableBean { | ||||
|         return R.ok(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 向特定用户发送消息 | ||||
|      * | ||||
|      * @param userId 目标用户的 ID | ||||
|      * @param msg    要发送的消息内容 | ||||
|      */ | ||||
|     @GetMapping(value = "${sse.path}/send") | ||||
|     public R<Void> send(Long userId, String msg) { | ||||
|         SseMessageDto dto = new SseMessageDto(); | ||||
|         dto.setUserIds(List.of(userId)); | ||||
|         dto.setMessage(msg); | ||||
|         sseEmitterManager.publishMessage(dto); | ||||
|         return R.ok(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 向所有用户发送消息 | ||||
|      * | ||||
|      * @param msg 要发送的消息内容 | ||||
|      */ | ||||
|     @GetMapping(value = "${sse.path}/sendAll") | ||||
|     public R<Void> send(String msg) { | ||||
|         sseEmitterManager.publishAll(msg); | ||||
|         return R.ok(); | ||||
|     } | ||||
|     // 以下为demo仅供参考 禁止使用 请在业务逻辑中使用工具发送而不是用接口发送 | ||||
| //    /** | ||||
| //     * 向特定用户发送消息 | ||||
| //     * | ||||
| //     * @param userId 目标用户的 ID | ||||
| //     * @param msg    要发送的消息内容 | ||||
| //     */ | ||||
| //    @GetMapping(value = "${sse.path}/send") | ||||
| //    public R<Void> send(Long userId, String msg) { | ||||
| //        SseMessageDto dto = new SseMessageDto(); | ||||
| //        dto.setUserIds(List.of(userId)); | ||||
| //        dto.setMessage(msg); | ||||
| //        sseEmitterManager.publishMessage(dto); | ||||
| //        return R.ok(); | ||||
| //    } | ||||
| // | ||||
| //    /** | ||||
| //     * 向所有用户发送消息 | ||||
| //     * | ||||
| //     * @param msg 要发送的消息内容 | ||||
| //     */ | ||||
| //    @GetMapping(value = "${sse.path}/sendAll") | ||||
| //    public R<Void> send(String msg) { | ||||
| //        sseEmitterManager.publishAll(msg); | ||||
| //        return R.ok(); | ||||
| //    } | ||||
|  | ||||
|     /** | ||||
|      * 清理资源。此方法目前不执行任何操作,但避免因未实现而导致错误 | ||||
|   | ||||
| @@ -1,14 +1,21 @@ | ||||
| package org.dromara.common.sse.core; | ||||
|  | ||||
| import cn.hutool.core.collection.CollUtil; | ||||
| import cn.hutool.core.map.MapUtil; | ||||
| import lombok.extern.slf4j.Slf4j; | ||||
| import org.dromara.common.core.utils.SpringUtils; | ||||
| import org.dromara.common.redis.utils.RedisUtils; | ||||
| import org.dromara.common.sse.dto.SseMessageDto; | ||||
| import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; | ||||
|  | ||||
| import java.io.IOException; | ||||
| import java.util.ArrayList; | ||||
| import java.util.Iterator; | ||||
| import java.util.List; | ||||
| import java.util.Map; | ||||
| import java.util.concurrent.ConcurrentHashMap; | ||||
| import java.util.concurrent.ScheduledExecutorService; | ||||
| import java.util.concurrent.TimeUnit; | ||||
| import java.util.function.Consumer; | ||||
|  | ||||
| /** | ||||
| @@ -26,6 +33,12 @@ public class SseEmitterManager { | ||||
|  | ||||
|     private final static Map<Long, Map<String, SseEmitter>> USER_TOKEN_EMITTERS = new ConcurrentHashMap<>(); | ||||
|  | ||||
|     public SseEmitterManager() { | ||||
|         // 定时执行 SSE 心跳检测 | ||||
|         SpringUtils.getBean(ScheduledExecutorService.class) | ||||
|             .scheduleWithFixedDelay(this::sseMonitor, 60L, 60L, TimeUnit.SECONDS); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 建立与指定用户的 SSE 连接 | ||||
|      * | ||||
| @@ -38,6 +51,12 @@ public class SseEmitterManager { | ||||
|         // 每个用户可以有多个 SSE 连接,通过 token 进行区分 | ||||
|         Map<String, SseEmitter> emitters = USER_TOKEN_EMITTERS.computeIfAbsent(userId, k -> new ConcurrentHashMap<>()); | ||||
|  | ||||
|         // 关闭已存在的SseEmitter,防止超过最大连接数 | ||||
|         SseEmitter oldEmitter = emitters.remove(token); | ||||
|         if (oldEmitter != null) { | ||||
|             oldEmitter.complete(); | ||||
|         } | ||||
|  | ||||
|         // 创建一个新的 SseEmitter 实例,超时时间设置为一天 避免连接之后直接关闭浏览器导致连接停滞 | ||||
|         SseEmitter emitter = new SseEmitter(86400000L); | ||||
|  | ||||
| @@ -97,6 +116,44 @@ public class SseEmitterManager { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * SSE 心跳检测,关闭无效连接 | ||||
|      */ | ||||
|     public void sseMonitor() { | ||||
|         final SseEmitter.SseEventBuilder heartbeat = SseEmitter.event().comment("heartbeat"); | ||||
|         // 记录需要移除的用户ID | ||||
|         List<Long> toRemoveUsers = new ArrayList<>(); | ||||
|  | ||||
|         USER_TOKEN_EMITTERS.forEach((userId, emitterMap) -> { | ||||
|             if (CollUtil.isEmpty(emitterMap)) { | ||||
|                 toRemoveUsers.add(userId); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             emitterMap.entrySet().removeIf(entry -> { | ||||
|                 try { | ||||
|                     entry.getValue().send(heartbeat); | ||||
|                     return false; | ||||
|                 } catch (Exception ex) { | ||||
|                     try { | ||||
|                         entry.getValue().complete(); | ||||
|                     } catch (Exception ignore) { | ||||
|                         // 忽略重复关闭异常 | ||||
|                     } | ||||
|                     return true; // 发送失败 → 移除该连接 | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|             // 移除空连接用户 | ||||
|             if (emitterMap.isEmpty()) { | ||||
|                 toRemoveUsers.add(userId); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         // 循环结束后统一清理空用户,避免并发修改异常 | ||||
|         toRemoveUsers.forEach(USER_TOKEN_EMITTERS::remove); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 订阅SSE消息主题,并提供一个消费者函数来处理接收到的消息 | ||||
|      * | ||||
|   | ||||
| @@ -46,8 +46,14 @@ public class TranslationHandler extends JsonSerializer<Object> implements Contex | ||||
|                 gen.writeNull(); | ||||
|                 return; | ||||
|             } | ||||
|             Object result = trans.translation(value, translation.other()); | ||||
|             gen.writeObject(result); | ||||
|             try { | ||||
|                 Object result = trans.translation(value, translation.other()); | ||||
|                 gen.writeObject(result); | ||||
|             } catch (Exception e) { | ||||
|                 log.error("翻译处理异常,type: {}, value: {}", translation.type(), value, e); | ||||
|                 // 出现异常时输出原始值而不是中断序列化 | ||||
|                 gen.writeObject(value); | ||||
|             } | ||||
|         } else { | ||||
|             gen.writeObject(value); | ||||
|         } | ||||
|   | ||||
| @@ -21,11 +21,6 @@ | ||||
|             <artifactId>ruoyi-common-json</artifactId> | ||||
|         </dependency> | ||||
|  | ||||
|         <dependency> | ||||
|             <groupId>org.dromara</groupId> | ||||
|             <artifactId>ruoyi-common-redis</artifactId> | ||||
|         </dependency> | ||||
|  | ||||
|         <!-- SpringBoot Web容器 --> | ||||
|         <dependency> | ||||
|             <groupId>org.springframework.boot</groupId> | ||||
|   | ||||
| @@ -7,6 +7,7 @@ import org.dromara.common.web.filter.XssFilter; | ||||
| import org.springframework.boot.autoconfigure.AutoConfiguration; | ||||
| import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; | ||||
| import org.springframework.boot.context.properties.EnableConfigurationProperties; | ||||
| import org.springframework.boot.web.servlet.FilterRegistration; | ||||
| import org.springframework.boot.web.servlet.FilterRegistrationBean; | ||||
| import org.springframework.context.annotation.Bean; | ||||
|  | ||||
| @@ -21,24 +22,20 @@ public class FilterConfig { | ||||
|  | ||||
|     @Bean | ||||
|     @ConditionalOnProperty(value = "xss.enabled", havingValue = "true") | ||||
|     public FilterRegistrationBean<XssFilter> xssFilterRegistration() { | ||||
|         FilterRegistrationBean<XssFilter> registration = new FilterRegistrationBean<>(); | ||||
|         registration.setDispatcherTypes(DispatcherType.REQUEST); | ||||
|         registration.setFilter(new XssFilter()); | ||||
|         registration.addUrlPatterns("/*"); | ||||
|         registration.setName("xssFilter"); | ||||
|         registration.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE + 1); | ||||
|         return registration; | ||||
|     @FilterRegistration( | ||||
|         name = "xssFilter", | ||||
|         urlPatterns = "/*", | ||||
|         order = FilterRegistrationBean.HIGHEST_PRECEDENCE + 1, | ||||
|         dispatcherTypes = DispatcherType.REQUEST | ||||
|     ) | ||||
|     public XssFilter xssFilter() { | ||||
|         return new XssFilter(); | ||||
|     } | ||||
|  | ||||
|     @Bean | ||||
|     public FilterRegistrationBean<RepeatableFilter> someFilterRegistration() { | ||||
|         FilterRegistrationBean<RepeatableFilter> registration = new FilterRegistrationBean<>(); | ||||
|         registration.setFilter(new RepeatableFilter()); | ||||
|         registration.addUrlPatterns("/*"); | ||||
|         registration.setName("repeatableFilter"); | ||||
|         registration.setOrder(FilterRegistrationBean.LOWEST_PRECEDENCE); | ||||
|         return registration; | ||||
|     @FilterRegistration(name = "repeatableFilter", urlPatterns = "/*") | ||||
|     public RepeatableFilter repeatableFilter() { | ||||
|         return new RepeatableFilter(); | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -1,7 +1,8 @@ | ||||
| package org.dromara.common.web.config; | ||||
|  | ||||
| import cn.hutool.core.date.DateTime; | ||||
| import cn.hutool.core.date.DateUtil; | ||||
| import org.dromara.common.core.utils.StringUtils; | ||||
| import org.dromara.common.core.utils.ObjectUtils; | ||||
| import org.dromara.common.web.handler.GlobalExceptionHandler; | ||||
| import org.dromara.common.web.interceptor.PlusWebInvokeTimeInterceptor; | ||||
| import org.springframework.boot.autoconfigure.AutoConfiguration; | ||||
| @@ -34,10 +35,11 @@ public class ResourcesConfig implements WebMvcConfigurer { | ||||
|     public void addFormatters(FormatterRegistry registry) { | ||||
|         // 全局日期格式转换配置 | ||||
|         registry.addConverter(String.class, Date.class, source -> { | ||||
|             if (StringUtils.isBlank(source)) { | ||||
|             DateTime parse = DateUtil.parse(source); | ||||
|             if (ObjectUtils.isNull(parse)) { | ||||
|                 return null; | ||||
|             } | ||||
|             return DateUtil.parse(source); | ||||
|             return parse.toJdkDate(); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| package org.dromara.common.web.enums; | ||||
|  | ||||
| import cn.hutool.captcha.generator.CodeGenerator; | ||||
| import cn.hutool.captcha.generator.MathGenerator; | ||||
| import cn.hutool.captcha.generator.RandomGenerator; | ||||
| import org.dromara.common.web.utils.UnsignedMathGenerator; | ||||
| import lombok.AllArgsConstructor; | ||||
| import lombok.Getter; | ||||
|  | ||||
| @@ -18,7 +18,7 @@ public enum CaptchaType { | ||||
|     /** | ||||
|      * 数字 | ||||
|      */ | ||||
|     MATH(UnsignedMathGenerator.class), | ||||
|     MATH(MathGenerator.class), | ||||
|  | ||||
|     /** | ||||
|      * 字符 | ||||
|   | ||||
| @@ -23,6 +23,7 @@ import org.springframework.web.bind.MissingPathVariableException; | ||||
| import org.springframework.web.bind.annotation.ExceptionHandler; | ||||
| import org.springframework.web.bind.annotation.ResponseStatus; | ||||
| import org.springframework.web.bind.annotation.RestControllerAdvice; | ||||
| import org.springframework.web.context.request.async.AsyncRequestTimeoutException; | ||||
| import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; | ||||
| import org.springframework.web.servlet.NoHandlerFoundException; | ||||
|  | ||||
| @@ -123,7 +124,7 @@ public class GlobalExceptionHandler { | ||||
|      */ | ||||
|     @ResponseStatus(org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR) | ||||
|     @ExceptionHandler(IOException.class) | ||||
|     public void handleRuntimeException(IOException e, HttpServletRequest request) { | ||||
|     public void handleIoException(IOException e, HttpServletRequest request) { | ||||
|         String requestURI = request.getRequestURI(); | ||||
|         if (requestURI.contains("sse")) { | ||||
|             // sse 经常性连接中断 例如关闭浏览器 直接屏蔽 | ||||
| @@ -132,6 +133,13 @@ public class GlobalExceptionHandler { | ||||
|         log.error("请求地址'{}',连接中断", requestURI, e); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * sse 连接超时异常 不需要处理 | ||||
|      */ | ||||
|     @ExceptionHandler(AsyncRequestTimeoutException.class) | ||||
|     public void handleRuntimeException(AsyncRequestTimeoutException e) { | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 拦截未知的运行时异常 | ||||
|      */ | ||||
|   | ||||
| @@ -1,88 +0,0 @@ | ||||
| package org.dromara.common.web.utils; | ||||
|  | ||||
| import cn.hutool.captcha.generator.CodeGenerator; | ||||
| import cn.hutool.core.math.Calculator; | ||||
| import cn.hutool.core.util.CharUtil; | ||||
| import cn.hutool.core.util.RandomUtil; | ||||
| import org.dromara.common.core.utils.StringUtils; | ||||
|  | ||||
| import java.io.Serial; | ||||
|  | ||||
| /** | ||||
|  * 无符号计算生成器 | ||||
|  * | ||||
|  * @author Lion Li | ||||
|  */ | ||||
| public class UnsignedMathGenerator implements CodeGenerator { | ||||
|  | ||||
|     @Serial | ||||
|     private static final long serialVersionUID = -5514819971774091076L; | ||||
|  | ||||
|     private static final String OPERATORS = "+-*"; | ||||
|  | ||||
|     /** | ||||
|      * 参与计算数字最大长度 | ||||
|      */ | ||||
|     private final int numberLength; | ||||
|  | ||||
|     /** | ||||
|      * 构造 | ||||
|      */ | ||||
|     public UnsignedMathGenerator() { | ||||
|         this(2); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 构造 | ||||
|      * | ||||
|      * @param numberLength 参与计算最大数字位数 | ||||
|      */ | ||||
|     public UnsignedMathGenerator(int numberLength) { | ||||
|         this.numberLength = numberLength; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public String generate() { | ||||
|         final int limit = getLimit(); | ||||
|         int a = RandomUtil.randomInt(limit); | ||||
|         int b = RandomUtil.randomInt(limit); | ||||
|         String max = Integer.toString(Math.max(a,b)); | ||||
|         String min = Integer.toString(Math.min(a,b)); | ||||
|         max = StringUtils.rightPad(max, this.numberLength, CharUtil.SPACE); | ||||
|         min = StringUtils.rightPad(min, this.numberLength, CharUtil.SPACE); | ||||
|  | ||||
|         return max + RandomUtil.randomChar(OPERATORS) + min + '='; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public boolean verify(String code, String userInputCode) { | ||||
|         int result; | ||||
|         try { | ||||
|             result = Integer.parseInt(userInputCode); | ||||
|         } catch (NumberFormatException e) { | ||||
|             // 用户输入非数字 | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         final int calculateResult = (int) Calculator.conversion(code); | ||||
|         return result == calculateResult; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取验证码长度 | ||||
|      * | ||||
|      * @return 验证码长度 | ||||
|      */ | ||||
|     public int getLength() { | ||||
|         return this.numberLength * 2 + 2; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 根据长度获取参与计算数字最大值 | ||||
|      * | ||||
|      * @return 最大值 | ||||
|      */ | ||||
|     private int getLimit() { | ||||
|         return Integer.parseInt("1" + StringUtils.repeat('0', this.numberLength)); | ||||
|     } | ||||
| } | ||||
| @@ -1,6 +1,6 @@ | ||||
| # 贝尔实验室 Spring 官方推荐镜像 JDK下载地址 https://bell-sw.com/pages/downloads/ | ||||
| FROM bellsoft/liberica-openjdk-rocky:17.0.15-cds | ||||
| #FROM bellsoft/liberica-openjdk-rocky:21.0.7-cds | ||||
| FROM bellsoft/liberica-openjdk-rocky:17.0.16-cds | ||||
| #FROM bellsoft/liberica-openjdk-rocky:21.0.8-cds | ||||
| #FROM findepi/graalvm:java17-native | ||||
|  | ||||
| LABEL maintainer="Lion Li" | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| package org.dromara.monitor.admin; | ||||
|  | ||||
| import de.codecentric.boot.admin.server.config.EnableAdminServer; | ||||
| import org.springframework.boot.SpringApplication; | ||||
| import org.springframework.boot.autoconfigure.SpringBootApplication; | ||||
|  | ||||
| @@ -8,6 +9,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; | ||||
|  * | ||||
|  * @author Lion Li | ||||
|  */ | ||||
| @EnableAdminServer | ||||
| @SpringBootApplication | ||||
| public class MonitorAdminApplication { | ||||
|  | ||||
|   | ||||
| @@ -1,31 +0,0 @@ | ||||
| package org.dromara.monitor.admin.config; | ||||
|  | ||||
| import de.codecentric.boot.admin.server.config.EnableAdminServer; | ||||
| import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; | ||||
| import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration; | ||||
| import org.springframework.boot.task.ThreadPoolTaskExecutorBuilder; | ||||
| import org.springframework.context.annotation.Bean; | ||||
| import org.springframework.context.annotation.Configuration; | ||||
| import org.springframework.context.annotation.Lazy; | ||||
| import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; | ||||
|  | ||||
| import java.util.concurrent.Executor; | ||||
|  | ||||
| /** | ||||
|  * springboot-admin server配置类 | ||||
|  * | ||||
|  * @author Lion Li | ||||
|  */ | ||||
| @Configuration | ||||
| @EnableAdminServer | ||||
| public class AdminServerConfig { | ||||
|  | ||||
|     @Lazy | ||||
|     @Bean(name = TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME) | ||||
|     @ConditionalOnMissingBean(Executor.class) | ||||
|     public ThreadPoolTaskExecutor applicationTaskExecutor(ThreadPoolTaskExecutorBuilder builder) { | ||||
|         return builder.build(); | ||||
|     } | ||||
|  | ||||
|  | ||||
| } | ||||
| @@ -10,7 +10,7 @@ import org.springframework.security.config.annotation.web.configurers.AbstractHt | ||||
| import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; | ||||
| import org.springframework.security.web.SecurityFilterChain; | ||||
| import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; | ||||
| import org.springframework.security.web.util.matcher.AntPathRequestMatcher; | ||||
| import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher; | ||||
|  | ||||
| /** | ||||
|  * admin 监控 安全配置 | ||||
| @@ -32,14 +32,14 @@ public class SecurityConfig { | ||||
|         SavedRequestAwareAuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler(); | ||||
|         successHandler.setTargetUrlParameter("redirectTo"); | ||||
|         successHandler.setDefaultTargetUrl(adminContextPath + "/"); | ||||
|  | ||||
|         PathPatternRequestMatcher.Builder mvc = PathPatternRequestMatcher.withDefaults(); | ||||
|         return httpSecurity | ||||
|             .headers((header) -> | ||||
|                 header.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable)) | ||||
|             .authorizeHttpRequests((authorize) -> | ||||
|                 authorize.requestMatchers( | ||||
|                         new AntPathRequestMatcher(adminContextPath + "/assets/**"), | ||||
|                         new AntPathRequestMatcher(adminContextPath + "/login") | ||||
|                         mvc.matcher(adminContextPath + "/assets/**"), | ||||
|                         mvc.matcher(adminContextPath + "/login") | ||||
|                     ).permitAll() | ||||
|                     .anyRequest().authenticated()) | ||||
|             .formLogin((formLogin) -> | ||||
|   | ||||
| @@ -20,6 +20,9 @@ spring: | ||||
|       ui: | ||||
|         title: RuoYi-Vue-Plus服务监控中心 | ||||
|       context-path: /admin | ||||
|   # 忽略无用警告 | ||||
|   thymeleaf: | ||||
|     check-template-location: false | ||||
|  | ||||
| --- # Actuator 监控端点的配置项 | ||||
| management: | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| # 贝尔实验室 Spring 官方推荐镜像 JDK下载地址 https://bell-sw.com/pages/downloads/ | ||||
| FROM bellsoft/liberica-openjdk-rocky:17.0.15-cds | ||||
| #FROM bellsoft/liberica-openjdk-rocky:21.0.7-cds | ||||
| FROM bellsoft/liberica-openjdk-rocky:17.0.16-cds | ||||
| #FROM bellsoft/liberica-openjdk-rocky:21.0.8-cds | ||||
| #FROM findepi/graalvm:java17-native | ||||
|  | ||||
| LABEL maintainer="Lion Li" | ||||
|   | ||||
| @@ -0,0 +1,146 @@ | ||||
| package com.aizuda.snailjob.server.common.register; | ||||
|  | ||||
| import cn.hutool.core.collection.CollUtil; | ||||
| import cn.hutool.core.util.IdUtil; | ||||
| import cn.hutool.core.util.StrUtil; | ||||
| import com.aizuda.snailjob.common.core.enums.NodeTypeEnum; | ||||
| import com.aizuda.snailjob.common.core.util.JsonUtil; | ||||
| import com.aizuda.snailjob.common.core.util.NetUtil; | ||||
| import com.aizuda.snailjob.common.core.util.SnailJobVersion; | ||||
| import com.aizuda.snailjob.common.core.util.StreamUtils; | ||||
| import com.aizuda.snailjob.common.log.SnailJobLog; | ||||
| import com.aizuda.snailjob.server.common.cache.CacheConsumerGroup; | ||||
| import com.aizuda.snailjob.server.common.config.SystemProperties; | ||||
| import com.aizuda.snailjob.server.common.convert.RegisterNodeInfoConverter; | ||||
| import com.aizuda.snailjob.server.common.dto.ServerNodeExtAttrs; | ||||
| import com.aizuda.snailjob.server.common.handler.InstanceManager; | ||||
| import com.aizuda.snailjob.template.datasource.persistence.po.ServerNode; | ||||
| import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; | ||||
| import com.google.common.collect.Lists; | ||||
| import lombok.RequiredArgsConstructor; | ||||
| import org.springframework.boot.autoconfigure.web.ServerProperties; | ||||
| import org.springframework.stereotype.Component; | ||||
|  | ||||
| import java.time.LocalDateTime; | ||||
| import java.util.List; | ||||
| import java.util.Optional; | ||||
| import java.util.Set; | ||||
| import java.util.concurrent.ConcurrentMap; | ||||
| import java.util.concurrent.Executors; | ||||
| import java.util.concurrent.ScheduledExecutorService; | ||||
| import java.util.concurrent.TimeUnit; | ||||
|  | ||||
| /** | ||||
|  * 服务端注册 | ||||
|  * | ||||
|  * @author opensnail | ||||
|  * @date 2023-06-07 | ||||
|  * @since 1.6.0 | ||||
|  */ | ||||
| @Component(ServerRegister.BEAN_NAME) | ||||
| @RequiredArgsConstructor | ||||
| public class ServerRegister extends AbstractRegister { | ||||
|     public static final String BEAN_NAME = "serverRegister"; | ||||
|     private final ScheduledExecutorService serverRegisterNode = Executors.newSingleThreadScheduledExecutor(r -> new Thread(r, "server-register-node")); | ||||
|     public static final int DELAY_TIME = 30; | ||||
|     public static final String CURRENT_CID; | ||||
|     public static final String GROUP_NAME = "DEFAULT_SERVER"; | ||||
|     public static final String NAMESPACE_ID = "DEFAULT_SERVER_NAMESPACE_ID"; | ||||
|     private final InstanceManager instanceManager; | ||||
|     private final SystemProperties systemProperties; | ||||
|     private final ServerProperties serverProperties; | ||||
|  | ||||
|     static { | ||||
|         CURRENT_CID = IdUtil.getSnowflakeNextIdStr(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public boolean supports(int type) { | ||||
|         return getNodeType().equals(type); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected void beforeProcessor(RegisterContext context) { | ||||
|         // 新增扩展参数 | ||||
|         ServerNodeExtAttrs serverNodeExtAttrs = new ServerNodeExtAttrs(); | ||||
|         serverNodeExtAttrs.setWebPort(serverProperties.getPort()); | ||||
|         serverNodeExtAttrs.setSystemVersion(SnailJobVersion.getVersion()); | ||||
|  | ||||
|         context.setGroupName(GROUP_NAME); | ||||
|         context.setHostId(CURRENT_CID); | ||||
|         String serverHost = systemProperties.getServerHost(); | ||||
|         if (StrUtil.isEmptyIfStr(serverHost)) { | ||||
|             serverHost = NetUtil.getLocalIpStr(); | ||||
|         } | ||||
|         context.setHostIp(serverHost); | ||||
|         context.setHostPort(systemProperties.getServerPort()); | ||||
|         context.setContextPath(Optional.ofNullable(serverProperties.getServlet().getContextPath()).orElse(StrUtil.EMPTY)); | ||||
|         context.setNamespaceId(NAMESPACE_ID); | ||||
|         context.setExtAttrs(JsonUtil.toJsonString(serverNodeExtAttrs)); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected LocalDateTime getExpireAt() { | ||||
|         return LocalDateTime.now().plusSeconds(DELAY_TIME); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected boolean doRegister(RegisterContext context, ServerNode serverNode) { | ||||
|         refreshExpireAt(Lists.newArrayList(serverNode)); | ||||
|         return Boolean.TRUE; | ||||
|     } | ||||
|  | ||||
|  | ||||
|     @Override | ||||
|     protected void afterProcessor(final ServerNode serverNode) { | ||||
|         try { | ||||
|             // 同步当前POD消费的组的节点信息 | ||||
|             // netty的client只会注册到一个服务端,若组分配的和client连接的不是一个POD则会导致当前POD没有其他客户端的注册信息 | ||||
|             ConcurrentMap<String /*groupName*/, Set<String>/*namespaceId*/> allConsumerGroupName = CacheConsumerGroup.getAllConsumerGroupName(); | ||||
|             if (CollUtil.isNotEmpty(allConsumerGroupName)) { | ||||
|                 Set<String> namespaceIdSets = StreamUtils.toSetByFlatMap(allConsumerGroupName.values(), Set::stream); | ||||
|                 if (CollUtil.isEmpty(namespaceIdSets)) { | ||||
|                     return; | ||||
|                 } | ||||
|  | ||||
|                 List<ServerNode> serverNodes = serverNodeMapper.selectList( | ||||
|                         new LambdaQueryWrapper<ServerNode>() | ||||
|                                 .eq(ServerNode::getNodeType, NodeTypeEnum.CLIENT.getType()) | ||||
|                                 .in(ServerNode::getNamespaceId, namespaceIdSets) | ||||
|                                 .in(ServerNode::getGroupName, allConsumerGroupName.keySet())); | ||||
|                 for (final ServerNode node : serverNodes) { | ||||
|                     // 刷新全量本地缓存 | ||||
|                     instanceManager.registerOrUpdate(RegisterNodeInfoConverter.INSTANCE.toRegisterNodeInfo(node)); | ||||
|                     // 刷新过期时间 | ||||
|                     CacheConsumerGroup.addOrUpdate(node.getGroupName(), node.getNamespaceId()); | ||||
|                 } | ||||
|             } | ||||
|         } catch (Exception e) { | ||||
|             SnailJobLog.LOCAL.error("Client refresh failed", e); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected Integer getNodeType() { | ||||
|         return NodeTypeEnum.SERVER.getType(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void start() { | ||||
|         SnailJobLog.LOCAL.info("ServerRegister start"); | ||||
|  | ||||
|         serverRegisterNode.scheduleAtFixedRate(() -> { | ||||
|             try { | ||||
|                 this.register(new RegisterContext()); | ||||
|             } catch (Exception e) { | ||||
|                 SnailJobLog.LOCAL.error("Server-side registration failed", e); | ||||
|             } | ||||
|         }, 0, DELAY_TIME * 2 / 3, TimeUnit.SECONDS); | ||||
|  | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void close() { | ||||
|         SnailJobLog.LOCAL.info("ServerRegister close"); | ||||
|     } | ||||
| } | ||||
| @@ -16,15 +16,26 @@ spring: | ||||
|  | ||||
| --- # snail-job 服务端配置 | ||||
| snail-job: | ||||
|   # 拉取重试数据的每批次的大小 | ||||
|   retry-pull-page-size: 1000 | ||||
|   # 拉取重试数据的每批次的大小 | ||||
|   job-pull-page-size: 1000 | ||||
|   # 服务器端口 | ||||
|   # 服务端节点IP(默认按照`NetUtil.getLocalIpStr()`) | ||||
|   server-host: | ||||
|   # 服务端端口号 | ||||
|   server-port: 17888 | ||||
|   # 日志保存时间(单位: day) | ||||
|   # 合并日志默认保存天数 | ||||
|   merge-Log-days: 1 | ||||
|   # 合并日志默认的条数 | ||||
|   merge-Log-num: 500 | ||||
|   # 配置每批次拉取重试数据的大小 | ||||
|   retry-pull-page-size: 100 | ||||
|   # 配置日志保存时间(单位:天) | ||||
|   log-storage: 7 | ||||
|   rpc-type: grpc | ||||
|   # bucket的总数量 | ||||
|   bucket-total: 128 | ||||
|   # Dashboard 任务容错天数 | ||||
|   summary-day: 7 | ||||
|   # 配置负载均衡周期时间 | ||||
|   load-balance-cycle-time: 10 | ||||
|   # 重试任务拉取的并行度 | ||||
|   retry-max-pull-parallel: 2 | ||||
|  | ||||
| --- # 监控中心配置 | ||||
| spring.boot.admin.client: | ||||
|   | ||||
| @@ -16,15 +16,26 @@ spring: | ||||
|  | ||||
| --- # snail-job 服务端配置 | ||||
| snail-job: | ||||
|   # 拉取重试数据的每批次的大小 | ||||
|   retry-pull-page-size: 1000 | ||||
|   # 拉取重试数据的每批次的大小 | ||||
|   job-pull-page-size: 1000 | ||||
|   # 服务器端口 | ||||
|   # 服务端节点IP(默认按照`NetUtil.getLocalIpStr()`) | ||||
|   server-host: | ||||
|   # 服务端端口号 | ||||
|   server-port: 17888 | ||||
|   # 日志保存时间(单位: day) | ||||
|   # 合并日志默认保存天数 | ||||
|   merge-Log-days: 1 | ||||
|   # 合并日志默认的条数 | ||||
|   merge-Log-num: 500 | ||||
|   # 配置每批次拉取重试数据的大小 | ||||
|   retry-pull-page-size: 100 | ||||
|   # 配置日志保存时间(单位:天) | ||||
|   log-storage: 7 | ||||
|   rpc-type: grpc | ||||
|   # bucket的总数量 | ||||
|   bucket-total: 128 | ||||
|   # Dashboard 任务容错天数 | ||||
|   summary-day: 7 | ||||
|   # 配置负载均衡周期时间 | ||||
|   load-balance-cycle-time: 10 | ||||
|   # 重试任务拉取的并行度 | ||||
|   retry-max-pull-parallel: 2 | ||||
|  | ||||
| --- # 监控中心配置 | ||||
| spring.boot.admin.client: | ||||
|   | ||||
| @@ -85,6 +85,7 @@ | ||||
|     <!-- 控制台输出日志级别 --> | ||||
|     <root level="info"> | ||||
|         <appender-ref ref="console" /> | ||||
|         <appender-ref ref="file_console" /> | ||||
|         <appender-ref ref="async_info" /> | ||||
|         <appender-ref ref="async_error" /> | ||||
|         <appender-ref ref="snail_log_server_appender" /> | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
| 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.mail.utils.MailUtils; | ||||
| @@ -18,12 +17,11 @@ import java.util.Arrays; | ||||
|  * | ||||
|  * @author Michelle.Chung | ||||
|  */ | ||||
| @SaIgnore | ||||
| @Validated | ||||
| @RequiredArgsConstructor | ||||
| @RestController | ||||
| @RequestMapping("/demo/mail") | ||||
| public class MailController { | ||||
| public class MailSendController { | ||||
| 
 | ||||
|     /** | ||||
|      * 发送邮件 | ||||
| @@ -44,11 +42,11 @@ public class MailController { | ||||
|      * @param to       接收人 | ||||
|      * @param subject  标题 | ||||
|      * @param text     内容 | ||||
|      * @param filePath 附件路径 | ||||
|      */ | ||||
|     @GetMapping("/sendMessageWithAttachment") | ||||
|     public R<Void> sendMessageWithAttachment(String to, String subject, String text, String filePath) { | ||||
|         MailUtils.sendText(to, subject, text, new File(filePath)); | ||||
|     public R<Void> sendMessageWithAttachment(String to, String subject, String text) { | ||||
|         // 附件路径 禁止前端传递 有任意读取系统文件风险 | ||||
|         MailUtils.sendText(to, subject, text, new File("/xxx/xxx")); | ||||
|         return R.ok(); | ||||
|     } | ||||
| 
 | ||||
| @@ -58,10 +56,11 @@ public class MailController { | ||||
|      * @param to       接收人 | ||||
|      * @param subject  标题 | ||||
|      * @param text     内容 | ||||
|      * @param paths    附件路径 | ||||
|      */ | ||||
|     @GetMapping("/sendMessageWithAttachments") | ||||
|     public R<Void> sendMessageWithAttachments(String to, String subject, String text, String[] paths) { | ||||
|     public R<Void> sendMessageWithAttachments(String to, String subject, String text) { | ||||
|         // 附件路径 禁止前端传递 有任意读取系统文件风险 | ||||
|         String[] paths = new String[]{"/xxx/xxx", "/xxx/xxx"}; | ||||
|         File[] array = Arrays.stream(paths).map(File::new).toArray(File[]::new); | ||||
|         MailUtils.sendText(to, subject, text, array); | ||||
|         return R.ok(); | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user