From e61e42e56b2dcede08cd09acd86399cb04bb3c4a Mon Sep 17 00:00:00 2001
From: xuekang <914468783@qq.com>
Date: 星期五, 10 五月 2024 20:41:45 +0800
Subject: [PATCH] 初始化
---
ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/interceptor/PlusDataPermissionInterceptor.java | 129
ruoyi-common/ruoyi-common-translation/src/main/java/org/dromara/common/translation/core/impl/UserNameTranslationImpl.java | 26
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/LoginBody.java | 43
ruoyi-common/ruoyi-common-web/src/main/resources/logback-common.xml | 97
ruoyi-common/ruoyi-common-seata/src/main/java/org/dromara/common/seata/config/SeataConfiguration.java | 16
ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/properties/ApiDecryptProperties.java | 34
ruoyi-common/ruoyi-common-tenant/src/main/java/org/dromara/common/tenant/properties/TenantProperties.java | 27
ruoyi-common/ruoyi-common-log/src/main/java/org/dromara/common/log/annotation/Log.java | 48
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/CacheNames.java | 68
ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/I18nConfig.java | 22
ruoyi-common/ruoyi-common-translation/src/main/java/org/dromara/common/translation/core/impl/DeptNameTranslationImpl.java | 26
ruoyi-common/ruoyi-common-tenant/src/main/java/org/dromara/common/tenant/exception/TenantException.java | 20
ruoyi-common/ruoyi-common-ratelimiter/src/main/java/org/dromara/common/ratelimiter/enums/LimitType.java | 24
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/xss/XssValidator.java | 21
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/ServletUtils.java | 228
ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/config/properties/MailProperties.java | 69
ruoyi-common/ruoyi-common-log/src/main/java/org/dromara/common/log/enums/BusinessType.java | 58
ruoyi-common/ruoyi-common-loadbalancer/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports | 1
ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/enums/DataScopeType.java | 73
ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/DefaultExcelResult.java | 73
ruoyi-common/ruoyi-common-json/src/main/java/org/dromara/common/json/config/JacksonConfig.java | 47
ruoyi-common/ruoyi-common-satoken/src/main/java/org/dromara/common/satoken/config/SaTokenConfiguration.java | 44
ruoyi-common/ruoyi-common-mail/pom.xml | 34
ruoyi-common/ruoyi-common-loadbalancer/src/main/resources/META-INF/dubbo/org.apache.dubbo.rpc.cluster.LoadBalance | 1
ruoyi-common/ruoyi-common-redis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports | 1
ruoyi-common/ruoyi-common-log/src/main/java/org/dromara/common/log/event/LogEventListener.java | 103
ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/core/domain/BaseEntity.java | 71
ruoyi-common/ruoyi-common-websocket/src/main/java/org/dromara/common/websocket/handler/PlusWebSocketHandler.java | 102
ruoyi-common/ruoyi-common-log/src/main/java/org/dromara/common/log/event/OperLogEvent.java | 115
ruoyi-common/ruoyi-common-translation/src/main/java/org/dromara/common/translation/annotation/TranslationType.java | 23
ruoyi-common/ruoyi-common-redis/pom.xml | 37
ruoyi-common/ruoyi-common-elasticsearch/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports | 1
ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/convert/ExcelBigNumberConvert.java | 52
ruoyi-common/ruoyi-common-dubbo/pom.xml | 63
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/xss/Xss.java | 26
ruoyi-common/ruoyi-common-oss/pom.xml | 35
ruoyi-common/ruoyi-common-sensitive/src/main/java/org/dromara/common/sensitive/handler/SensitiveHandler.java | 58
ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/UndertowConfig.java | 30
ruoyi-common/ruoyi-common-core/src/main/resources/i18n/messages.properties | 61
ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/config/properties/SpringDocProperties.java | 94
ruoyi-common/ruoyi-common-translation/src/main/java/org/dromara/common/translation/config/TranslationConfig.java | 50
ruoyi-common/ruoyi-common-tenant/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports | 1
ruoyi-common/ruoyi-common-elasticsearch/src/main/java/org/dromara/common/elasticsearch/config/ActuatorEnvironmentPostProcessor.java | 25
ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/GlobalMailAccount.java | 46
ruoyi-common/ruoyi-common-dubbo/src/main/resources/META-INF/dubbo/org.apache.dubbo.rpc.Filter | 1
ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/core/encryptor/AbstractEncryptor.java | 18
ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/utils/SocialUtils.java | 70
ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/config/EncryptorAutoConfiguration.java | 41
ruoyi-common/ruoyi-common-loadbalancer/src/main/java/org/dromara/common/loadbalance/core/CustomSpringCloudLoadBalancer.java | 64
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/validate/EditGroup.java | 9
ruoyi-common/ruoyi-common-encrypt/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports | 2
ruoyi-common/ruoyi-common-seata/pom.xml | 53
ruoyi-common/ruoyi-common-security/src/main/java/org/dromara/common/security/config/SecurityConfiguration.java | 47
ruoyi-common/ruoyi-common-translation/pom.xml | 42
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/validate/QueryGroup.java | 9
ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/MailAccount.java | 659 ++
ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/maxkey/AuthMaxKeyRequest.java | 80
ruoyi-common/ruoyi-common-websocket/src/main/java/org/dromara/common/websocket/constant/WebSocketConstants.java | 28
ruoyi-common/ruoyi-common-mail/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports | 1
ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/core/encryptor/Sm2Encryptor.java | 61
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/GlobalConstants.java | 40
ruoyi-common/ruoyi-common-mybatis/pom.xml | 82
ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/config/RedisConfiguration.java | 144
ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/core/page/PageQuery.java | 114
ruoyi-common/ruoyi-common-job/src/main/java/org/dromara/common/job/config/properties/PowerJobProperties.java | 109
ruoyi-common/ruoyi-common-job/src/main/java/org/dromara/common/job/config/PowerJobConfig.java | 107
ruoyi-common/ruoyi-common-loadbalancer/src/main/java/org/dromara/common/loadbalance/config/CustomLoadBalanceAutoConfiguration.java | 13
ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/enumd/PolicyType.java | 35
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/DictService.java | 63
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/ValidatorUtils.java | 28
ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/enumd/AlgorithmType.java | 48
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/user/CaptchaException.java | 21
ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/config/MailConfiguration.java | 37
ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/convert/ExcelDictConvert.java | 73
ruoyi-common/ruoyi-common-log/src/main/java/org/dromara/common/log/aspect/LogAspect.java | 222
ruoyi-common/ruoyi-common-translation/src/main/java/org/dromara/common/translation/core/impl/OssUrlTranslationImpl.java | 26
ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/ExcelListener.java | 14
ruoyi-common/ruoyi-common-logstash/src/main/resources/logback-logstash.xml | 19
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/enums/UserType.java | 37
ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/handler/OpenApiHandler.java | 252
ruoyi-common/ruoyi-common-sensitive/pom.xml | 25
ruoyi-common/ruoyi-common-excel/pom.xml | 30
ruoyi-common/ruoyi-common-satoken/pom.xml | 57
ruoyi-common/ruoyi-common-social/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports | 1
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/config/ValidatorConfig.java | 40
ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/exception/OssException.java | 19
ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/service/SysDataScopeService.java | 28
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/R.java | 120
ruoyi-common/ruoyi-common-dict/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports | 1
ruoyi-common/ruoyi-common-elasticsearch/src/main/resources/META-INF/spring.factories | 2
ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/utils/RedisUtils.java | 538 +
ruoyi-common/ruoyi-common-ratelimiter/src/main/java/org/dromara/common/ratelimiter/config/RateLimiterConfig.java | 20
ruoyi-common/ruoyi-common-translation/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports | 6
ruoyi-common/ruoyi-common-dubbo/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports | 1
ruoyi-common/ruoyi-common-idempotent/src/main/java/org/dromara/common/idempotent/config/IdempotentAutoConfiguration.java | 21
ruoyi-common/ruoyi-common-tenant/src/main/java/org/dromara/common/tenant/helper/TenantHelper.java | 189
ruoyi-common/ruoyi-common-sensitive/src/main/java/org/dromara/common/sensitive/core/SensitiveStrategy.java | 49
ruoyi-common/ruoyi-common-translation/src/main/java/org/dromara/common/translation/core/handler/TranslationHandler.java | 65
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/MessageUtils.java | 33
ruoyi-common/ruoyi-common-translation/src/main/java/org/dromara/common/translation/constant/TransConstant.java | 36
ruoyi-common/ruoyi-common-encrypt/pom.xml | 47
ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/enums/DataBaseType.java | 49
ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/core/page/TableDataInfo.java | 81
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/StreamUtils.java | 254
ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/interceptor/MybatisDecryptInterceptor.java | 116
ruoyi-common/ruoyi-common-prometheus/pom.xml | 28
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/enums/LoginType.java | 44
ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/helper/DataPermissionHelper.java | 93
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/user/UserException.java | 20
ruoyi-common/ruoyi-common-sentinel/src/main/java/org/dromara/common/sentinel/config/properties/SentinelCustomProperties.java | 17
ruoyi-common/ruoyi-common-bom/pom.xml | 223
ruoyi-common/ruoyi-common-dubbo/src/main/java/org/dromara/common/dubbo/config/DubboConfiguration.java | 17
ruoyi-common/ruoyi-common-skylog/src/main/resources/logback-skylog.xml | 29
ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/handler/PlusDataPermissionHandler.java | 186
ruoyi-common/ruoyi-common-core/src/main/resources/i18n/messages_zh_CN.properties | 61
ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/utils/ExcelUtil.java | 436 +
ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/filter/DubboDataPermissionFilter.java | 28
ruoyi-common/ruoyi-common-idempotent/src/main/java/org/dromara/common/idempotent/annotation/RepeatSubmit.java | 29
ruoyi-common/ruoyi-common-dict/pom.xml | 42
ruoyi-common/ruoyi-common-log/src/main/java/org/dromara/common/log/event/LogininforEvent.java | 52
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/TenantConstants.java | 45
ruoyi-common/ruoyi-common-core/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports | 4
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/Threads.java | 75
ruoyi-common/ruoyi-common-skylog/pom.xml | 29
ruoyi-common/ruoyi-common-job/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports | 1
ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/UserPassAuthenticator.java | 33
ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/factory/OssFactory.java | 65
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/StringUtils.java | 321
ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/config/SocialAutoConfiguration.java | 23
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/sql/SqlUtil.java | 56
ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/core/IEncryptor.java | 35
ruoyi-common/ruoyi-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports | 2
ruoyi-common/ruoyi-common-translation/src/main/java/org/dromara/common/translation/core/impl/DictTypeTranslationImpl.java | 28
ruoyi-common/ruoyi-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports | 3
ruoyi-common/ruoyi-common-dict/src/main/java/org/dromara/common/dict/service/impl/DictServiceImpl.java | 92
ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/core/encryptor/Sm4Encryptor.java | 55
ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/manager/PlusSpringCacheManager.java | 192
ruoyi-common/ruoyi-common-sentinel/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports | 1
ruoyi-common/ruoyi-common-tenant/src/main/java/org/dromara/common/tenant/core/TenantSaTokenDao.java | 148
ruoyi-common/ruoyi-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports | 2
ruoyi-common/ruoyi-common-core/src/main/resources/i18n/messages_en_US.properties | 61
ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/utils/CacheUtils.java | 75
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/enums/DeviceType.java | 32
ruoyi-common/ruoyi-common-elasticsearch/pom.xml | 24
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/CacheConstants.java | 25
ruoyi-common/ruoyi-common-websocket/pom.xml | 40
ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/helper/DataBaseHelper.java | 82
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/reflect/ReflectUtils.java | 56
ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/DefaultExcelListener.java | 104
ruoyi-common/ruoyi-common-security/src/main/java/org/dromara/common/security/handler/GlobalExceptionHandler.java | 175
ruoyi-common/ruoyi-common-translation/src/main/java/org/dromara/common/translation/core/TranslationInterface.java | 20
ruoyi-common/ruoyi-common-ratelimiter/pom.xml | 30
ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/enumd/EncodeType.java | 26
ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/utils/AuthRedisStateCache.java | 61
ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/annotation/DataPermission.java | 18
ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/Mail.java | 483 +
ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/properties/EncryptorProperties.java | 48
ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/constant/OssConstant.java | 40
ruoyi-common/ruoyi-common-tenant/src/main/java/org/dromara/common/tenant/handle/TenantKeyPrefixHandler.java | 66
ruoyi-common/ruoyi-common-translation/src/main/java/org/dromara/common/translation/core/impl/NicknameTranslationImpl.java | 26
ruoyi-common/ruoyi-common-web/pom.xml | 74
ruoyi-common/ruoyi-common-log/src/main/java/org/dromara/common/log/enums/OperatorType.java | 23
ruoyi-common/ruoyi-common-loadbalancer/pom.xml | 40
ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/interceptor/MybatisEncryptInterceptor.java | 120
ruoyi-common/ruoyi-common-mybatis/src/main/resources/common-mybatis.yml | 33
ruoyi-common/ruoyi-common-translation/src/main/java/org/dromara/common/translation/annotation/Translation.java | 39
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/DateUtils.java | 168
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/ServiceException.java | 70
ruoyi-common/ruoyi-common-idempotent/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports | 1
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/file/FileSizeLimitExceededException.java | 18
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/file/MimeTypeUtils.java | 40
ruoyi-common/ruoyi-common-json/pom.xml | 37
ruoyi-common/ruoyi-common-social/pom.xml | 35
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/config/AsyncConfig.java | 111
ruoyi-common/ruoyi-common-alibaba-bom/pom.xml | 176
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/factory/YmlPropertySourceFactory.java | 31
ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/handler/InjectionMetaObjectHandler.java | 82
ruoyi-common/ruoyi-common-doc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports | 1
ruoyi-common/ruoyi-common-translation/src/main/java/org/dromara/common/translation/core/handler/TranslationBeanSerializerModifier.java | 29
ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/config/SpringDocAutoConfiguration.java | 117
ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/CellMergeStrategy.java | 142
ruoyi-common/ruoyi-common-dubbo/src/main/java/org/dromara/common/dubbo/enumd/RequestLogEnum.java | 18
ruoyi-common/ruoyi-common-tenant/src/main/java/org/dromara/common/tenant/config/TenantConfiguration.java | 106
ruoyi-common/ruoyi-common-json/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports | 1
ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/ExcelResult.java | 26
ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/MailException.java | 40
ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/core/mapper/BaseMapperPlus.java | 198
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/Constants.java | 81
ruoyi-common/ruoyi-common-seata/src/main/resources/common-seata.yml | 19
ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/core/encryptor/RsaEncryptor.java | 62
ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/filter/EncryptResponseBodyWrapper.java | 123
ruoyi-common/ruoyi-common-loadbalancer/src/main/java/org/dromara/common/loadbalance/config/CustomEnvironmentPostProcessor.java | 25
ruoyi-common/ruoyi-common-websocket/src/main/java/org/dromara/common/websocket/utils/WebSocketUtils.java | 110
ruoyi-common/ruoyi-common-tenant/pom.xml | 36
ruoyi-common/ruoyi-common-sms/pom.xml | 33
ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/core/I18nLocaleResolver.java | 31
ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/filter/CryptoFilter.java | 115
ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/config/properties/SocialProperties.java | 29
ruoyi-common/ruoyi-common-doc/pom.xml | 42
ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/config/MybatisPlusConfiguration.java | 116
ruoyi-common/ruoyi-common-ratelimiter/src/main/java/org/dromara/common/ratelimiter/annotation/RateLimiter.java | 41
ruoyi-common/ruoyi-common-websocket/src/main/java/org/dromara/common/websocket/config/WebSocketConfig.java | 60
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/ReUtil.java | 148
ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/annotation/CellMerge.java | 24
ruoyi-common/ruoyi-common-json/src/main/java/org/dromara/common/json/utils/JsonUtils.java | 113
ruoyi-common/ruoyi-common-ratelimiter/src/main/java/org/dromara/common/ratelimiter/aspectj/RateLimiterAspect.java | 127
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/validate/AddGroup.java | 9
ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/annotation/EncryptField.java | 44
ruoyi-common/ruoyi-common-logstash/pom.xml | 24
ruoyi-common/ruoyi-common-core/src/main/resources/ip2region.xdb | 0
ruoyi-common/ruoyi-common-dubbo/src/main/resources/common-dubbo.yml | 30
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/SpringUtils.java | 60
ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/annotation/ExcelDictFormat.java | 32
ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/config/MailConfig.java | 37
ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/config/properties/RedissonProperties.java | 135
ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/core/OssClient.java | 262
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/UserConstants.java | 142
ruoyi-common/ruoyi-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports | 2
ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/core/BaseController.java | 32
ruoyi-common/ruoyi-common-loadbalancer/src/main/java/org/dromara/common/loadbalance/config/CustomLoadBalanceClientConfiguration.java | 30
ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/core/encryptor/Base64Encryptor.java | 48
ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/core/EncryptContext.java | 41
ruoyi-common/ruoyi-common-prometheus/src/main/java/org/dromara/common/prometheus/config/PrometheusConfiguration.java | 22
ruoyi-common/ruoyi-common-loadbalancer/src/main/resources/META-INF/spring.factories | 2
ruoyi-common/ruoyi-common-security/pom.xml | 32
ruoyi-common/ruoyi-common-sms/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports | 1
ruoyi-common/ruoyi-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports | 1
ruoyi-common/ruoyi-common-prometheus/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports | 1
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/HttpStatus.java | 93
ruoyi-common/ruoyi-common-log/pom.xml | 43
ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/core/encryptor/AesEncryptor.java | 55
ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/utils/EncryptUtils.java | 311
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/file/FileUtils.java | 43
ruoyi-common/ruoyi-common-json/src/main/java/org/dromara/common/json/handler/BigNumberSerializer.java | 42
ruoyi-common/ruoyi-common-websocket/src/main/java/org/dromara/common/websocket/config/properties/WebSocketProperties.java | 26
ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/annotation/ApiEncrypt.java | 20
ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/config/properties/SocialLoginConfigProperties.java | 68
ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/convert/ExcelEnumConvert.java | 87
ruoyi-common/ruoyi-common-ratelimiter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports | 1
ruoyi-common/ruoyi-common-mybatis/src/main/resources/META-INF/dubbo/org.apache.dubbo.rpc.Filter | 1
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/file/FileException.java | 21
ruoyi-common/ruoyi-common-websocket/src/main/java/org/dromara/common/websocket/holder/WebSocketSessionHolder.java | 42
ruoyi-common/ruoyi-common-satoken/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports | 1
ruoyi-common/ruoyi-common-sentinel/src/main/java/com/alibaba/cloud/sentinel/custom/SentinelAutoConfiguration.java | 265
ruoyi-common/ruoyi-common-seata/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports | 1
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/ip/RegionUtils.java | 67
ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/properties/OssProperties.java | 63
ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/core/EncryptorManager.java | 100
ruoyi-common/ruoyi-common-dubbo/src/main/java/org/dromara/common/dubbo/filter/DubboRequestFilter.java | 58
ruoyi-common/ruoyi-common-websocket/src/main/java/org/dromara/common/websocket/listener/WebSocketTopicListener.java | 43
ruoyi-common/ruoyi-common-dict/src/main/java/org/dromara/common/dict/utils/DictUtils.java | 51
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/file/FileNameLengthLimitExceededException.java | 18
ruoyi-common/ruoyi-common-sms/src/main/java/org/dromara/common/sms/config/SmsAutoConfiguration.java | 14
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/ip/AddressUtils.java | 33
ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/annotation/ExcelEnumFormat.java | 30
ruoyi-common/ruoyi-common-loadbalancer/src/main/java/org/dromara/common/loadbalance/core/CustomDubboLoadBalancer.java | 30
ruoyi-common/ruoyi-common-tenant/src/main/java/org/dromara/common/tenant/manager/TenantSpringCacheManager.java | 32
ruoyi-common/ruoyi-common-satoken/src/main/java/org/dromara/common/satoken/core/dao/PlusSaTokenDao.java | 148
ruoyi-common/ruoyi-common-satoken/src/main/java/org/dromara/common/satoken/core/service/SaPermissionImpl.java | 47
ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/ExcelDownHandler.java | 371 +
ruoyi-common/ruoyi-common-idempotent/src/main/java/org/dromara/common/idempotent/aspectj/RepeatSubmitAspect.java | 146
ruoyi-common/ruoyi-common-sensitive/src/main/java/org/dromara/common/sensitive/core/SensitiveService.java | 18
ruoyi-common/ruoyi-common-tenant/src/main/java/org/dromara/common/tenant/handle/PlusTenantLineHandler.java | 56
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/enums/TenantStatus.java | 30
ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/handler/KeyPrefixHandler.java | 50
ruoyi-common/ruoyi-common-websocket/src/main/java/org/dromara/common/websocket/dto/WebSocketMessageDto.java | 29
ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/DropDownOptions.java | 149
ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/InternalMailUtil.java | 108
ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/config/ApiDecryptAutoConfiguration.java | 32
ruoyi-common/ruoyi-common-tenant/src/main/java/org/dromara/common/tenant/core/TenantEntity.java | 21
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/MapstructUtils.java | 92
ruoyi-common/ruoyi-common-log/src/main/java/org/dromara/common/log/enums/BusinessStatus.java | 18
ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/annotation/DataColumn.java | 28
ruoyi-common/ruoyi-common-websocket/src/main/java/org/dromara/common/websocket/interceptor/PlusWebSocketInterceptor.java | 51
ruoyi-common/ruoyi-common-sentinel/pom.xml | 51
ruoyi-common/ruoyi-common-dubbo/src/main/java/org/dromara/common/dubbo/properties/DubboCustomProperties.java | 22
ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/filter/DecryptRequestBodyWrapper.java | 94
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/base/BaseException.java | 73
ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/MailUtils.java | 467 +
ruoyi-common/ruoyi-common-satoken/src/main/java/org/dromara/common/satoken/utils/LoginHelper.java | 176
ruoyi-common/ruoyi-common-satoken/src/main/resources/common-satoken.yml | 13
ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/entity/UploadResult.java | 24
ruoyi-common/ruoyi-common-elasticsearch/src/main/java/org/dromara/common/elasticsearch/config/EasyEsConfiguration.java | 17
ruoyi-common/pom.xml | 54
ruoyi-common/ruoyi-common-job/pom.xml | 58
ruoyi-common/ruoyi-common-sensitive/src/main/java/org/dromara/common/sensitive/annotation/Sensitive.java | 28
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/TreeBuildUtils.java | 35
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/enums/UserStatus.java | 30
ruoyi-common/ruoyi-common-idempotent/pom.xml | 41
ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/config/PlusPaths.java | 15
ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/maxkey/AuthMaxKeySource.java | 52
ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/handler/MybatisExceptionHandler.java | 46
ruoyi-common/ruoyi-common-core/pom.xml | 104
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/user/CaptchaExpireException.java | 18
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/config/ApplicationConfig.java | 16
ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/enumd/AccessPolicyType.java | 55
296 files changed, 19,367 insertions(+), 0 deletions(-)
diff --git a/ruoyi-common/pom.xml b/ruoyi-common/pom.xml
new file mode 100644
index 0000000..ff3ea10
--- /dev/null
+++ b/ruoyi-common/pom.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-cloud-plus</artifactId>
+ <version>${revision}</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <modules>
+ <module>ruoyi-common-bom</module>
+ <module>ruoyi-common-alibaba-bom</module>
+ <module>ruoyi-common-log</module>
+ <module>ruoyi-common-dict</module>
+ <module>ruoyi-common-excel</module>
+ <module>ruoyi-common-core</module>
+ <module>ruoyi-common-redis</module>
+ <module>ruoyi-common-doc</module>
+ <module>ruoyi-common-security</module>
+ <module>ruoyi-common-satoken</module>
+ <module>ruoyi-common-web</module>
+ <module>ruoyi-common-mybatis</module>
+ <module>ruoyi-common-job</module>
+ <module>ruoyi-common-dubbo</module>
+ <module>ruoyi-common-seata</module>
+ <module>ruoyi-common-loadbalancer</module>
+ <module>ruoyi-common-oss</module>
+ <module>ruoyi-common-ratelimiter</module>
+ <module>ruoyi-common-idempotent</module>
+ <module>ruoyi-common-mail</module>
+ <module>ruoyi-common-sms</module>
+ <module>ruoyi-common-logstash</module>
+ <module>ruoyi-common-elasticsearch</module>
+ <module>ruoyi-common-sentinel</module>
+ <module>ruoyi-common-skylog</module>
+ <module>ruoyi-common-prometheus</module>
+ <module>ruoyi-common-translation</module>
+ <module>ruoyi-common-sensitive</module>
+ <module>ruoyi-common-json</module>
+ <module>ruoyi-common-encrypt</module>
+ <module>ruoyi-common-tenant</module>
+ <module>ruoyi-common-websocket</module>
+ <module>ruoyi-common-social</module>
+ </modules>
+
+ <artifactId>ruoyi-common</artifactId>
+ <packaging>pom</packaging>
+
+ <description>
+ ruoyi-common閫氱敤妯″潡
+ </description>
+
+</project>
diff --git a/ruoyi-common/ruoyi-common-alibaba-bom/pom.xml b/ruoyi-common/ruoyi-common-alibaba-bom/pom.xml
new file mode 100644
index 0000000..4a7cb38
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-alibaba-bom/pom.xml
@@ -0,0 +1,176 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns="http://maven.apache.org/POM/4.0.0"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-alibaba-bom</artifactId>
+ <version>${revision}</version>
+ <packaging>pom</packaging>
+
+ <description>
+ ruoyi-common-alibaba-bom alibaba渚濊禆椤�
+ </description>
+
+ <properties>
+ <revision>2.1.2</revision>
+ <spring-cloud-alibaba.version>2022.0.0.0</spring-cloud-alibaba.version>
+ <sentinel.version>1.8.6</sentinel.version>
+ <seata.version>1.7.1</seata.version>
+ <nacos.client.version>2.2.1</nacos.client.version>
+ <dubbo.version>3.2.7</dubbo.version>
+ <spring.context.support.version>1.0.11</spring.context.support.version>
+ </properties>
+ <dependencyManagement>
+ <dependencies>
+ <dependency>
+ <groupId>com.alibaba.cloud</groupId>
+ <artifactId>spring-cloud-alibaba-dependencies</artifactId>
+ <version>${spring-cloud-alibaba.version}</version>
+ <type>pom</type>
+ <scope>import</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.alibaba.nacos</groupId>
+ <artifactId>nacos-client</artifactId>
+ <version>${nacos.client.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.alibaba.csp</groupId>
+ <artifactId>sentinel-core</artifactId>
+ <version>${sentinel.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.alibaba.csp</groupId>
+ <artifactId>sentinel-parameter-flow-control</artifactId>
+ <version>${sentinel.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.alibaba.csp</groupId>
+ <artifactId>sentinel-datasource-extension</artifactId>
+ <version>${sentinel.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.alibaba.csp</groupId>
+ <artifactId>sentinel-datasource-apollo</artifactId>
+ <version>${sentinel.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.alibaba.csp</groupId>
+ <artifactId>sentinel-datasource-zookeeper</artifactId>
+ <version>${sentinel.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.alibaba.csp</groupId>
+ <artifactId>sentinel-datasource-nacos</artifactId>
+ <version>${sentinel.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.alibaba.csp</groupId>
+ <artifactId>sentinel-datasource-redis</artifactId>
+ <version>${sentinel.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.alibaba.csp</groupId>
+ <artifactId>sentinel-datasource-consul</artifactId>
+ <version>${sentinel.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.alibaba.csp</groupId>
+ <artifactId>sentinel-web-servlet</artifactId>
+ <version>${sentinel.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.alibaba.csp</groupId>
+ <artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
+ <version>${sentinel.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.alibaba.csp</groupId>
+ <artifactId>sentinel-transport-simple-http</artifactId>
+ <version>${sentinel.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.alibaba.csp</groupId>
+ <artifactId>sentinel-annotation-aspectj</artifactId>
+ <version>${sentinel.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.alibaba.csp</groupId>
+ <artifactId>sentinel-reactor-adapter</artifactId>
+ <version>${sentinel.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.alibaba.csp</groupId>
+ <artifactId>sentinel-cluster-server-default</artifactId>
+ <version>${sentinel.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.alibaba.csp</groupId>
+ <artifactId>sentinel-cluster-client-default</artifactId>
+ <version>${sentinel.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.alibaba.csp</groupId>
+ <artifactId>sentinel-spring-webflux-adapter</artifactId>
+ <version>${sentinel.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.alibaba.csp</groupId>
+ <artifactId>sentinel-api-gateway-adapter-common</artifactId>
+ <version>${sentinel.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.alibaba.csp</groupId>
+ <artifactId>sentinel-spring-webmvc-adapter</artifactId>
+ <version>${sentinel.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.alibaba.csp</groupId>
+ <artifactId>sentinel-dubbo-adapter</artifactId>
+ <version>${sentinel.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.alibaba.csp</groupId>
+ <artifactId>sentinel-apache-dubbo-adapter</artifactId>
+ <version>${sentinel.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.alibaba.csp</groupId>
+ <artifactId>sentinel-apache-dubbo3-adapter</artifactId>
+ <version>${sentinel.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>io.seata</groupId>
+ <artifactId>seata-spring-boot-starter</artifactId>
+ <version>${seata.version}</version>
+ </dependency>
+
+ <!-- Apache Dubbo 閰嶇疆 -->
+ <dependency>
+ <groupId>org.apache.dubbo</groupId>
+ <artifactId>dubbo-spring-boot-starter</artifactId>
+ <version>${dubbo.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.dubbo</groupId>
+ <artifactId>dubbo-spring-boot-actuator</artifactId>
+ <version>${dubbo.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.dubbo</groupId>
+ <artifactId>dubbo</artifactId>
+ <version>${dubbo.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>com.alibaba.spring</groupId>
+ <artifactId>spring-context-support</artifactId>
+ <version>${spring.context.support.version}</version>
+ </dependency>
+ </dependencies>
+ </dependencyManagement>
+</project>
diff --git a/ruoyi-common/ruoyi-common-bom/pom.xml b/ruoyi-common/ruoyi-common-bom/pom.xml
new file mode 100644
index 0000000..043824e
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-bom/pom.xml
@@ -0,0 +1,223 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns="http://maven.apache.org/POM/4.0.0"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-bom</artifactId>
+ <version>${revision}</version>
+ <packaging>pom</packaging>
+
+ <description>
+ ruoyi-common-bom common渚濊禆椤�
+ </description>
+
+ <properties>
+ <revision>2.1.2</revision>
+ </properties>
+
+ <dependencyManagement>
+ <dependencies>
+ <!-- 鏍稿績妯″潡 -->
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-core</artifactId>
+ <version>${revision}</version>
+ </dependency>
+
+ <!-- 鎺ュ彛妯″潡 -->
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-doc</artifactId>
+ <version>${revision}</version>
+ </dependency>
+
+ <!-- 瀹夊叏妯″潡 -->
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-security</artifactId>
+ <version>${revision}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-satoken</artifactId>
+ <version>${revision}</version>
+ </dependency>
+
+ <!-- 鏃ュ織璁板綍 -->
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-log</artifactId>
+ <version>${revision}</version>
+ </dependency>
+
+ <!-- 瀛楀吀 -->
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-dict</artifactId>
+ <version>${revision}</version>
+ </dependency>
+
+ <!-- excel -->
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-excel</artifactId>
+ <version>${revision}</version>
+ </dependency>
+
+ <!-- 缂撳瓨鏈嶅姟 -->
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-redis</artifactId>
+ <version>${revision}</version>
+ </dependency>
+
+ <!-- web鏈嶅姟 -->
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-web</artifactId>
+ <version>${revision}</version>
+ </dependency>
+
+ <!-- 鏁版嵁搴撴湇鍔� -->
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-mybatis</artifactId>
+ <version>${revision}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-job</artifactId>
+ <version>${revision}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-dubbo</artifactId>
+ <version>${revision}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-seata</artifactId>
+ <version>${revision}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-loadbalancer</artifactId>
+ <version>${revision}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-oss</artifactId>
+ <version>${revision}</version>
+ </dependency>
+
+ <!-- 闄愭祦 -->
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-ratelimiter</artifactId>
+ <version>${revision}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-idempotent</artifactId>
+ <version>${revision}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-mail</artifactId>
+ <version>${revision}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-sms</artifactId>
+ <version>${revision}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-logstash</artifactId>
+ <version>${revision}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-elasticsearch</artifactId>
+ <version>${revision}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-sentinel</artifactId>
+ <version>${revision}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-skylog</artifactId>
+ <version>${revision}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-prometheus</artifactId>
+ <version>${revision}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-translation</artifactId>
+ <version>${revision}</version>
+ </dependency>
+
+ <!-- 鑴辨晱妯″潡 -->
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-sensitive</artifactId>
+ <version>${revision}</version>
+ </dependency>
+
+ <!-- 搴忓垪鍖栨ā鍧� -->
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-json</artifactId>
+ <version>${revision}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-encrypt</artifactId>
+ <version>${revision}</version>
+ </dependency>
+
+ <!-- 绉熸埛妯″潡 -->
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-tenant</artifactId>
+ <version>${revision}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-websocket</artifactId>
+ <version>${revision}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-social</artifactId>
+ <version>${revision}</version>
+ </dependency>
+
+ </dependencies>
+ </dependencyManagement>
+</project>
diff --git a/ruoyi-common/ruoyi-common-core/pom.xml b/ruoyi-common/ruoyi-common-core/pom.xml
new file mode 100644
index 0000000..01d876e
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/pom.xml
@@ -0,0 +1,104 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common</artifactId>
+ <version>${revision}</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>ruoyi-common-core</artifactId>
+
+ <description>
+ ruoyi-common-core 鏍稿績妯″潡
+ </description>
+
+ <dependencies>
+ <!-- Spring妗嗘灦鍩烘湰鐨勬牳蹇冨伐鍏� -->
+ <dependency>
+ <groupId>org.springframework</groupId>
+ <artifactId>spring-context-support</artifactId>
+ </dependency>
+
+ <!-- SpringWeb妯″潡 -->
+ <dependency>
+ <groupId>org.springframework</groupId>
+ <artifactId>spring-web</artifactId>
+ </dependency>
+
+ <!-- Hibernate Validator -->
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-validation</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-aop</artifactId>
+ </dependency>
+
+ <!--甯哥敤宸ュ叿绫� -->
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-lang3</artifactId>
+ </dependency>
+
+ <!-- servlet鍖� -->
+ <dependency>
+ <groupId>jakarta.servlet</groupId>
+ <artifactId>jakarta.servlet-api</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>io.swagger.core.v3</groupId>
+ <artifactId>swagger-annotations</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>cn.hutool</groupId>
+ <artifactId>hutool-core</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>cn.hutool</groupId>
+ <artifactId>hutool-http</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>cn.hutool</groupId>
+ <artifactId>hutool-extra</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.projectlombok</groupId>
+ <artifactId>lombok</artifactId>
+ </dependency>
+
+ <!-- 鑷姩鐢熸垚YML閰嶇疆鍏宠仈JSON鏂囦欢 -->
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-configuration-processor</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-properties-migrator</artifactId>
+ <scope>runtime</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>io.github.linpeilie</groupId>
+ <artifactId>mapstruct-plus-spring-boot-starter</artifactId>
+ </dependency>
+
+ <!-- 绂荤嚎IP鍦板潃瀹氫綅搴� -->
+ <dependency>
+ <groupId>org.lionsoul</groupId>
+ <artifactId>ip2region</artifactId>
+ </dependency>
+
+ </dependencies>
+
+</project>
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/config/ApplicationConfig.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/config/ApplicationConfig.java
new file mode 100644
index 0000000..07500ba
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/config/ApplicationConfig.java
@@ -0,0 +1,16 @@
+package org.dromara.common.core.config;
+
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.context.annotation.EnableAspectJAutoProxy;
+
+/**
+ * 绋嬪簭娉ㄨВ閰嶇疆
+ *
+ * @author Lion Li
+ */
+@AutoConfiguration
+// 琛ㄧず閫氳繃aop妗嗘灦鏆撮湶璇ヤ唬鐞嗗璞�,AopContext鑳藉璁块棶
+@EnableAspectJAutoProxy(exposeProxy = true)
+public class ApplicationConfig {
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/config/AsyncConfig.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/config/AsyncConfig.java
new file mode 100644
index 0000000..5a8714e
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/config/AsyncConfig.java
@@ -0,0 +1,111 @@
+package org.dromara.common.core.config;
+
+import cn.hutool.core.util.ArrayUtil;
+import jakarta.annotation.PreDestroy;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.concurrent.BasicThreadFactory;
+import org.dromara.common.core.exception.ServiceException;
+import org.dromara.common.core.utils.SpringUtils;
+import org.dromara.common.core.utils.Threads;
+import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.scheduling.annotation.AsyncConfigurer;
+import org.springframework.scheduling.annotation.EnableAsync;
+
+import java.util.Arrays;
+import java.util.concurrent.*;
+
+/**
+ * 寮傛閰嶇疆
+ *
+ * @author Lion Li
+ */
+@Slf4j
+@EnableAsync(proxyTargetClass = true)
+@AutoConfiguration
+public class AsyncConfig implements AsyncConfigurer {
+
+ private final int corePoolSize = Runtime.getRuntime().availableProcessors() + 1;
+ private ScheduledExecutorService scheduledExecutorService;
+
+ /**
+ * 鎵ц鍛ㄦ湡鎬ф垨瀹氭椂浠诲姟
+ */
+ @Bean(name = "scheduledExecutorService")
+ public ScheduledExecutorService scheduledExecutorService() {
+ ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(corePoolSize,
+ new BasicThreadFactory.Builder().namingPattern("schedule-pool-%d").daemon(true).build(),
+ new ThreadPoolExecutor.CallerRunsPolicy()) {
+ @Override
+ protected void afterExecute(Runnable r, Throwable t) {
+ super.afterExecute(r, t);
+ printException(r, t);
+ }
+ };
+ this.scheduledExecutorService = scheduledThreadPoolExecutor;
+ return scheduledThreadPoolExecutor;
+ }
+
+ /**
+ * 閿�姣佷簨浠�
+ */
+ @PreDestroy
+ public void destroy() {
+ try {
+ log.info("====鍏抽棴鍚庡彴浠诲姟浠诲姟绾跨▼姹�====");
+ Threads.shutdownAndAwaitTermination(scheduledExecutorService);
+ } catch (Exception e) {
+ log.error(e.getMessage(), e);
+ }
+ }
+
+ /**
+ * 鑷畾涔� @Async 娉ㄨВ浣跨敤绯荤粺绾跨▼姹�
+ */
+ @Override
+ public Executor getAsyncExecutor() {
+ 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());
+ };
+ }
+
+ /**
+ * 鎵撳嵃绾跨▼寮傚父淇℃伅
+ */
+ public 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);
+ }
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/config/ValidatorConfig.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/config/ValidatorConfig.java
new file mode 100644
index 0000000..45c5bd1
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/config/ValidatorConfig.java
@@ -0,0 +1,40 @@
+package org.dromara.common.core.config;
+
+import jakarta.validation.Validator;
+import org.hibernate.validator.HibernateValidator;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.context.MessageSource;
+import org.springframework.context.annotation.Bean;
+import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
+
+import java.util.Properties;
+
+/**
+ * 鏍¢獙妗嗘灦閰嶇疆绫�
+ *
+ * @author Lion Li
+ */
+@AutoConfiguration
+public class ValidatorConfig {
+
+ /**
+ * 閰嶇疆鏍¢獙妗嗘灦 蹇�熻繑鍥炴ā寮�
+ */
+ @Bean
+ public Validator validator(MessageSource messageSource) {
+ try (LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean()) {
+ // 鍥介檯鍖�
+ factoryBean.setValidationMessageSource(messageSource);
+ // 璁剧疆浣跨敤 HibernateValidator 鏍¢獙鍣�
+ factoryBean.setProviderClass(HibernateValidator.class);
+ Properties properties = new Properties();
+ // 璁剧疆 蹇�熷紓甯歌繑鍥�
+ properties.setProperty("hibernate.validator.fail_fast", "true");
+ factoryBean.setValidationProperties(properties);
+ // 鍔犺浇閰嶇疆
+ factoryBean.afterPropertiesSet();
+ return factoryBean.getValidator();
+ }
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/CacheConstants.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/CacheConstants.java
new file mode 100644
index 0000000..67bc8e4
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/CacheConstants.java
@@ -0,0 +1,25 @@
+package org.dromara.common.core.constant;
+
+/**
+ * 缂撳瓨鐨刱ey 甯搁噺
+ *
+ * @author Lion Li
+ */
+public interface CacheConstants {
+
+ /**
+ * 鍦ㄧ嚎鐢ㄦ埛 redis key
+ */
+ String ONLINE_TOKEN_KEY = "online_tokens:";
+
+ /**
+ * 鍙傛暟绠$悊 cache key
+ */
+ String SYS_CONFIG_KEY = "sys_config:";
+
+ /**
+ * 瀛楀吀绠$悊 cache key
+ */
+ String SYS_DICT_KEY = "sys_dict:";
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/CacheNames.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/CacheNames.java
new file mode 100644
index 0000000..e59277a
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/CacheNames.java
@@ -0,0 +1,68 @@
+package org.dromara.common.core.constant;
+
+/**
+ * 缂撳瓨缁勫悕绉板父閲�
+ * <p>
+ * key 鏍煎紡涓� cacheNames#ttl#maxIdleTime#maxSize
+ * <p>
+ * ttl 杩囨湡鏃堕棿 濡傛灉璁剧疆涓�0鍒欎笉杩囨湡 榛樿涓�0
+ * maxIdleTime 鏈�澶х┖闂叉椂闂� 鏍规嵁LRU绠楁硶娓呯悊绌洪棽鏁版嵁 濡傛灉璁剧疆涓�0鍒欎笉妫�娴� 榛樿涓�0
+ * maxSize 缁勬渶澶ч暱搴� 鏍规嵁LRU绠楁硶娓呯悊婧㈠嚭鏁版嵁 濡傛灉璁剧疆涓�0鍒欐棤闄愰暱 榛樿涓�0
+ * <p>
+ * 渚嬪瓙: test#60s銆乼est#0#60s銆乼est#0#1m#1000銆乼est#1h#0#500
+ *
+ * @author Lion Li
+ */
+public interface CacheNames {
+
+ /**
+ * 婕旂ず妗堜緥
+ */
+ String DEMO_CACHE = "demo:cache#60s#10m#20";
+
+ /**
+ * 绯荤粺閰嶇疆
+ */
+ String SYS_CONFIG = "sys_config";
+
+ /**
+ * 鏁版嵁瀛楀吀
+ */
+ String SYS_DICT = "sys_dict";
+
+ /**
+ * 绉熸埛
+ */
+ String SYS_TENANT = GlobalConstants.GLOBAL_REDIS_KEY + "sys_tenant#30d";
+
+ /**
+ * 鐢ㄦ埛璐︽埛
+ */
+ String SYS_USER_NAME = "sys_user_name#30d";
+
+ /**
+ * 鐢ㄦ埛鍚嶇О
+ */
+ String SYS_NICKNAME = "sys_nickname#30d";
+
+ /**
+ * 閮ㄩ棬
+ */
+ String SYS_DEPT = "sys_dept#30d";
+
+ /**
+ * OSS鍐呭
+ */
+ String SYS_OSS = "sys_oss#30d";
+
+ /**
+ * OSS閰嶇疆
+ */
+ String SYS_OSS_CONFIG = GlobalConstants.GLOBAL_REDIS_KEY + "sys_oss_config";
+
+ /**
+ * 鍦ㄧ嚎鐢ㄦ埛
+ */
+ String ONLINE_TOKEN = "online_tokens";
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/Constants.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/Constants.java
new file mode 100644
index 0000000..cdbda89
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/Constants.java
@@ -0,0 +1,81 @@
+package org.dromara.common.core.constant;
+
+/**
+ * 閫氱敤甯搁噺淇℃伅
+ *
+ * @author ruoyi
+ */
+public interface Constants {
+
+ /**
+ * UTF-8 瀛楃闆�
+ */
+ String UTF8 = "UTF-8";
+
+ /**
+ * GBK 瀛楃闆�
+ */
+ String GBK = "GBK";
+
+ /**
+ * www涓诲煙
+ */
+ String WWW = "www.";
+
+ /**
+ * http璇锋眰
+ */
+ String HTTP = "http://";
+
+ /**
+ * https璇锋眰
+ */
+ String HTTPS = "https://";
+
+ /**
+ * 閫氱敤鎴愬姛鏍囪瘑
+ */
+ String SUCCESS = "0";
+
+ /**
+ * 閫氱敤澶辫触鏍囪瘑
+ */
+ String FAIL = "1";
+
+ /**
+ * 鐧诲綍鎴愬姛
+ */
+ String LOGIN_SUCCESS = "Success";
+
+ /**
+ * 娉ㄩ攢
+ */
+ String LOGOUT = "Logout";
+
+ /**
+ * 娉ㄥ唽
+ */
+ String REGISTER = "Register";
+
+ /**
+ * 鐧诲綍澶辫触
+ */
+ String LOGIN_FAIL = "Error";
+
+ /**
+ * 楠岃瘉鐮佹湁鏁堟湡锛堝垎閽燂級
+ */
+ Integer CAPTCHA_EXPIRATION = 2;
+
+ /**
+ * 浠ょ墝
+ */
+ String TOKEN = "token";
+
+ /**
+ * 椤剁骇閮ㄩ棬id
+ */
+ Long TOP_PARENT_ID = 0L;
+
+}
+
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/GlobalConstants.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/GlobalConstants.java
new file mode 100644
index 0000000..8e5a6eb
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/GlobalConstants.java
@@ -0,0 +1,40 @@
+package org.dromara.common.core.constant;
+
+/**
+ * 鍏ㄥ眬鐨刱ey甯搁噺 (涓氬姟鏃犲叧鐨刱ey)
+ *
+ * @author Lion Li
+ */
+public interface GlobalConstants {
+
+ /**
+ * 鍏ㄥ眬 redis key (涓氬姟鏃犲叧鐨刱ey)
+ */
+ String GLOBAL_REDIS_KEY = "global:";
+
+ /**
+ * 楠岃瘉鐮� redis key
+ */
+ String CAPTCHA_CODE_KEY = GLOBAL_REDIS_KEY + "captcha_codes:";
+
+ /**
+ * 闃查噸鎻愪氦 redis key
+ */
+ String REPEAT_SUBMIT_KEY = GLOBAL_REDIS_KEY + "repeat_submit:";
+
+ /**
+ * 闄愭祦 redis key
+ */
+ String RATE_LIMIT_KEY = GLOBAL_REDIS_KEY + "rate_limit:";
+
+ /**
+ * 鐧诲綍璐︽埛瀵嗙爜閿欒娆℃暟 redis key
+ */
+ String PWD_ERR_CNT_KEY = GLOBAL_REDIS_KEY + "pwd_err_cnt:";
+
+ /**
+ * 涓夋柟璁よ瘉 redis key
+ */
+ String SOCIAL_AUTH_CODE_KEY = GLOBAL_REDIS_KEY + "social_auth_codes:";
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/HttpStatus.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/HttpStatus.java
new file mode 100644
index 0000000..85566e8
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/HttpStatus.java
@@ -0,0 +1,93 @@
+package org.dromara.common.core.constant;
+
+/**
+ * 杩斿洖鐘舵�佺爜
+ *
+ * @author Lion Li
+ */
+public interface HttpStatus {
+ /**
+ * 鎿嶄綔鎴愬姛
+ */
+ int SUCCESS = 200;
+
+ /**
+ * 瀵硅薄鍒涘缓鎴愬姛
+ */
+ int CREATED = 201;
+
+ /**
+ * 璇锋眰宸茬粡琚帴鍙�
+ */
+ int ACCEPTED = 202;
+
+ /**
+ * 鎿嶄綔宸茬粡鎵ц鎴愬姛锛屼絾鏄病鏈夎繑鍥炴暟鎹�
+ */
+ int NO_CONTENT = 204;
+
+ /**
+ * 璧勬簮宸茶绉婚櫎
+ */
+ int MOVED_PERM = 301;
+
+ /**
+ * 閲嶅畾鍚�
+ */
+ int SEE_OTHER = 303;
+
+ /**
+ * 璧勬簮娌℃湁琚慨鏀�
+ */
+ int NOT_MODIFIED = 304;
+
+ /**
+ * 鍙傛暟鍒楄〃閿欒锛堢己灏戯紝鏍煎紡涓嶅尮閰嶏級
+ */
+ int BAD_REQUEST = 400;
+
+ /**
+ * 鏈巿鏉�
+ */
+ int UNAUTHORIZED = 401;
+
+ /**
+ * 璁块棶鍙楅檺锛屾巿鏉冭繃鏈�
+ */
+ int FORBIDDEN = 403;
+
+ /**
+ * 璧勬簮锛屾湇鍔℃湭鎵惧埌
+ */
+ int NOT_FOUND = 404;
+
+ /**
+ * 涓嶅厑璁哥殑http鏂规硶
+ */
+ int BAD_METHOD = 405;
+
+ /**
+ * 璧勬簮鍐茬獊锛屾垨鑰呰祫婧愯閿�
+ */
+ int CONFLICT = 409;
+
+ /**
+ * 涓嶆敮鎸佺殑鏁版嵁锛屽獟浣撶被鍨�
+ */
+ int UNSUPPORTED_TYPE = 415;
+
+ /**
+ * 绯荤粺鍐呴儴閿欒
+ */
+ int ERROR = 500;
+
+ /**
+ * 鎺ュ彛鏈疄鐜�
+ */
+ int NOT_IMPLEMENTED = 501;
+
+ /**
+ * 绯荤粺璀﹀憡娑堟伅
+ */
+ int WARN = 601;
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/TenantConstants.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/TenantConstants.java
new file mode 100644
index 0000000..86b63c9
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/TenantConstants.java
@@ -0,0 +1,45 @@
+package org.dromara.common.core.constant;
+
+/**
+ * 绉熸埛甯搁噺淇℃伅
+ *
+ * @author Lion Li
+ */
+public interface TenantConstants {
+
+ /**
+ * 绉熸埛姝e父鐘舵��
+ */
+ String NORMAL = "0";
+
+ /**
+ * 绉熸埛灏佺鐘舵��
+ */
+ String DISABLE = "1";
+
+ /**
+ * 瓒呯骇绠$悊鍛業D
+ */
+ Long SUPER_ADMIN_ID = 1L;
+
+ /**
+ * 瓒呯骇绠$悊鍛樿鑹� roleKey
+ */
+ String SUPER_ADMIN_ROLE_KEY = "superadmin";
+
+ /**
+ * 绉熸埛绠$悊鍛樿鑹� roleKey
+ */
+ String TENANT_ADMIN_ROLE_KEY = "admin";
+
+ /**
+ * 绉熸埛绠$悊鍛樿鑹插悕绉�
+ */
+ String TENANT_ADMIN_ROLE_NAME = "绠$悊鍛�";
+
+ /**
+ * 榛樿绉熸埛ID
+ */
+ String DEFAULT_TENANT_ID = "000000";
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/UserConstants.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/UserConstants.java
new file mode 100644
index 0000000..6f3b0b9
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/UserConstants.java
@@ -0,0 +1,142 @@
+package org.dromara.common.core.constant;
+
+/**
+ * 鐢ㄦ埛甯搁噺淇℃伅
+ *
+ * @author ruoyi
+ */
+public interface UserConstants {
+
+ /**
+ * 骞冲彴鍐呯郴缁熺敤鎴风殑鍞竴鏍囧織
+ */
+ String SYS_USER = "SYS_USER";
+
+ /**
+ * 姝e父鐘舵��
+ */
+ String NORMAL = "0";
+
+ /**
+ * 寮傚父鐘舵��
+ */
+ String EXCEPTION = "1";
+
+ /**
+ * 鐢ㄦ埛姝e父鐘舵��
+ */
+ String USER_NORMAL = "0";
+
+ /**
+ * 鐢ㄦ埛灏佺鐘舵��
+ */
+ String USER_DISABLE = "1";
+
+ /**
+ * 瑙掕壊姝e父鐘舵��
+ */
+ String ROLE_NORMAL = "0";
+
+ /**
+ * 瑙掕壊灏佺鐘舵��
+ */
+ String ROLE_DISABLE = "1";
+
+ /**
+ * 閮ㄩ棬姝e父鐘舵��
+ */
+ String DEPT_NORMAL = "0";
+
+ /**
+ * 閮ㄩ棬鍋滅敤鐘舵��
+ */
+ String DEPT_DISABLE = "1";
+
+ /**
+ * 宀椾綅姝e父鐘舵��
+ */
+ String POST_NORMAL = "0";
+
+ /**
+ * 宀椾綅鍋滅敤鐘舵��
+ */
+ String POST_DISABLE = "1";
+
+ /**
+ * 瀛楀吀姝e父鐘舵��
+ */
+ String DICT_NORMAL = "0";
+
+ /**
+ * 鏄惁涓虹郴缁熼粯璁わ紙鏄級
+ */
+ String YES = "Y";
+
+ /**
+ * 鏄惁鑿滃崟澶栭摼锛堟槸锛�
+ */
+ String YES_FRAME = "0";
+
+ /**
+ * 鏄惁鑿滃崟澶栭摼锛堝惁锛�
+ */
+ String NO_FRAME = "1";
+
+ /**
+ * 鑿滃崟姝e父鐘舵��
+ */
+ String MENU_NORMAL = "0";
+
+ /**
+ * 鑿滃崟鍋滅敤鐘舵��
+ */
+ String MENU_DISABLE = "1";
+
+ /**
+ * 鑿滃崟绫诲瀷锛堢洰褰曪級
+ */
+ String TYPE_DIR = "M";
+
+ /**
+ * 鑿滃崟绫诲瀷锛堣彍鍗曪級
+ */
+ String TYPE_MENU = "C";
+
+ /**
+ * 鑿滃崟绫诲瀷锛堟寜閽級
+ */
+ String TYPE_BUTTON = "F";
+
+ /**
+ * Layout缁勪欢鏍囪瘑
+ */
+ String LAYOUT = "Layout";
+
+ /**
+ * ParentView缁勪欢鏍囪瘑
+ */
+ String PARENT_VIEW = "ParentView";
+
+ /**
+ * InnerLink缁勪欢鏍囪瘑
+ */
+ String INNER_LINK = "InnerLink";
+
+ /**
+ * 鐢ㄦ埛鍚嶉暱搴﹂檺鍒�
+ */
+ int USERNAME_MIN_LENGTH = 2;
+ int USERNAME_MAX_LENGTH = 20;
+
+ /**
+ * 瀵嗙爜闀垮害闄愬埗
+ */
+ int PASSWORD_MIN_LENGTH = 5;
+ int PASSWORD_MAX_LENGTH = 20;
+
+ /**
+ * 瓒呯骇绠$悊鍛業D
+ */
+ Long SUPER_ADMIN_ID = 1L;
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/R.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/R.java
new file mode 100644
index 0000000..625f58b
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/R.java
@@ -0,0 +1,120 @@
+package org.dromara.common.core.domain;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.dromara.common.core.constant.HttpStatus;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 鍝嶅簲淇℃伅涓讳綋
+ *
+ * @author Lion Li
+ */
+@Data
+@NoArgsConstructor
+public class R<T> implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 鎴愬姛
+ */
+ public static final int SUCCESS = 200;
+
+ /**
+ * 澶辫触
+ */
+ public static final int FAIL = 500;
+
+ /**
+ * 娑堟伅鐘舵�佺爜
+ */
+ private int code;
+
+ /**
+ * 娑堟伅鍐呭
+ */
+ private String msg;
+
+ /**
+ * 鏁版嵁瀵硅薄
+ */
+ private T data;
+
+ public static <T> R<T> ok() {
+ return restResult(null, SUCCESS, "鎿嶄綔鎴愬姛");
+ }
+
+ public static <T> R<T> ok(T data) {
+ return restResult(data, SUCCESS, "鎿嶄綔鎴愬姛");
+ }
+
+ public static <T> R<T> ok(String msg) {
+ return restResult(null, SUCCESS, msg);
+ }
+
+ public static <T> R<T> ok(String msg, T data) {
+ return restResult(data, SUCCESS, msg);
+ }
+
+ public static <T> R<T> fail() {
+ return restResult(null, FAIL, "鎿嶄綔澶辫触");
+ }
+
+ public static <T> R<T> fail(String msg) {
+ return restResult(null, FAIL, msg);
+ }
+
+ public static <T> R<T> fail(T data) {
+ return restResult(data, FAIL, "鎿嶄綔澶辫触");
+ }
+
+ public static <T> R<T> fail(String msg, T data) {
+ return restResult(data, FAIL, msg);
+ }
+
+ public static <T> R<T> fail(int code, String msg) {
+ return restResult(null, code, msg);
+ }
+
+ /**
+ * 杩斿洖璀﹀憡娑堟伅
+ *
+ * @param msg 杩斿洖鍐呭
+ * @return 璀﹀憡娑堟伅
+ */
+ public static <T> R<T> warn(String msg) {
+ return restResult(null, HttpStatus.WARN, msg);
+ }
+
+ /**
+ * 杩斿洖璀﹀憡娑堟伅
+ *
+ * @param msg 杩斿洖鍐呭
+ * @param data 鏁版嵁瀵硅薄
+ * @return 璀﹀憡娑堟伅
+ */
+ public static <T> R<T> warn(String msg, T data) {
+ return restResult(data, HttpStatus.WARN, msg);
+ }
+
+ private static <T> R<T> restResult(T data, int code, String msg) {
+ R<T> r = new R<>();
+ r.setCode(code);
+ r.setData(data);
+ r.setMsg(msg);
+ return r;
+ }
+
+ public static <T> Boolean isError(R<T> ret) {
+ return !isSuccess(ret);
+ }
+
+ public static <T> Boolean isSuccess(R<T> ret) {
+ return R.SUCCESS == ret.getCode();
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/LoginBody.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/LoginBody.java
new file mode 100644
index 0000000..ee612fd
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/LoginBody.java
@@ -0,0 +1,43 @@
+package org.dromara.common.core.domain.model;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 鐢ㄦ埛鐧诲綍瀵硅薄
+ *
+ * @author Lion Li
+ */
+@Data
+@NoArgsConstructor
+public class LoginBody {
+
+ /**
+ * 瀹㈡埛绔痠d
+ */
+ @NotBlank(message = "{auth.clientid.not.blank}")
+ private String clientId;
+
+ /**
+ * 鎺堟潈绫诲瀷
+ */
+ @NotBlank(message = "{auth.grant.type.not.blank}")
+ private String grantType;
+
+ /**
+ * 绉熸埛ID
+ */
+ private String tenantId;
+
+ /**
+ * 楠岃瘉鐮�
+ */
+ private String code;
+
+ /**
+ * 鍞竴鏍囪瘑
+ */
+ private String uuid;
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/enums/DeviceType.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/enums/DeviceType.java
new file mode 100644
index 0000000..09bf44b
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/enums/DeviceType.java
@@ -0,0 +1,32 @@
+package org.dromara.common.core.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 璁惧绫诲瀷
+ * 閽堝涓�濂� 鐢ㄦ埛浣撶郴
+ *
+ * @author Lion Li
+ */
+@Getter
+@AllArgsConstructor
+public enum DeviceType {
+
+ /**
+ * pc绔�
+ */
+ PC("pc"),
+
+ /**
+ * app绔�
+ */
+ APP("app"),
+
+ /**
+ * 灏忕▼搴忕
+ */
+ XCX("xcx");
+
+ private final String device;
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/enums/LoginType.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/enums/LoginType.java
new file mode 100644
index 0000000..f9cac66
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/enums/LoginType.java
@@ -0,0 +1,44 @@
+package org.dromara.common.core.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 鐧诲綍绫诲瀷
+ *
+ * @author Lion Li
+ */
+@Getter
+@AllArgsConstructor
+public enum LoginType {
+
+ /**
+ * 瀵嗙爜鐧诲綍
+ */
+ PASSWORD("user.password.retry.limit.exceed", "user.password.retry.limit.count"),
+
+ /**
+ * 鐭俊鐧诲綍
+ */
+ SMS("sms.code.retry.limit.exceed", "sms.code.retry.limit.count"),
+
+ /**
+ * 閭鐧诲綍
+ */
+ EMAIL("email.code.retry.limit.exceed", "email.code.retry.limit.count"),
+
+ /**
+ * 灏忕▼搴忕櫥褰�
+ */
+ XCX("", "");
+
+ /**
+ * 鐧诲綍閲嶈瘯瓒呭嚭闄愬埗鎻愮ず
+ */
+ final String retryLimitExceed;
+
+ /**
+ * 鐧诲綍閲嶈瘯闄愬埗璁℃暟鎻愮ず
+ */
+ final String retryLimitCount;
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/enums/TenantStatus.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/enums/TenantStatus.java
new file mode 100644
index 0000000..400a399
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/enums/TenantStatus.java
@@ -0,0 +1,30 @@
+package org.dromara.common.core.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 鐢ㄦ埛鐘舵��
+ *
+ * @author LionLi
+ */
+@Getter
+@AllArgsConstructor
+public enum TenantStatus {
+ /**
+ * 姝e父
+ */
+ OK("0", "姝e父"),
+ /**
+ * 鍋滅敤
+ */
+ DISABLE("1", "鍋滅敤"),
+ /**
+ * 鍒犻櫎
+ */
+ DELETED("2", "鍒犻櫎");
+
+ private final String code;
+ private final String info;
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/enums/UserStatus.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/enums/UserStatus.java
new file mode 100644
index 0000000..be7e44d
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/enums/UserStatus.java
@@ -0,0 +1,30 @@
+package org.dromara.common.core.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 鐢ㄦ埛鐘舵��
+ *
+ * @author ruoyi
+ */
+@Getter
+@AllArgsConstructor
+public enum UserStatus {
+ /**
+ * 姝e父
+ */
+ OK("0", "姝e父"),
+ /**
+ * 鍋滅敤
+ */
+ DISABLE("1", "鍋滅敤"),
+ /**
+ * 鍒犻櫎
+ */
+ DELETED("2", "鍒犻櫎");
+
+ private final String code;
+ private final String info;
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/enums/UserType.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/enums/UserType.java
new file mode 100644
index 0000000..69e4753
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/enums/UserType.java
@@ -0,0 +1,37 @@
+package org.dromara.common.core.enums;
+
+import org.dromara.common.core.utils.StringUtils;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 璁惧绫诲瀷
+ * 閽堝澶氬 鐢ㄦ埛浣撶郴
+ *
+ * @author Lion Li
+ */
+@Getter
+@AllArgsConstructor
+public enum UserType {
+
+ /**
+ * pc绔�
+ */
+ SYS_USER("sys_user"),
+
+ /**
+ * app绔�
+ */
+ APP_USER("app_user");
+
+ private final String userType;
+
+ public static UserType getUserType(String str) {
+ for (UserType value : values()) {
+ if (StringUtils.contains(str, value.getUserType())) {
+ return value;
+ }
+ }
+ throw new RuntimeException("'UserType' not found By " + str);
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/ServiceException.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/ServiceException.java
new file mode 100644
index 0000000..4fb097a
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/ServiceException.java
@@ -0,0 +1,70 @@
+package org.dromara.common.core.exception;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+
+/**
+ * 涓氬姟寮傚父
+ *
+ * @author ruoyi
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@NoArgsConstructor
+@AllArgsConstructor
+public final class ServiceException extends RuntimeException {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 閿欒鐮�
+ */
+ private Integer code;
+
+ /**
+ * 閿欒鎻愮ず
+ */
+ private String message;
+
+ /**
+ * 閿欒鏄庣粏锛屽唴閮ㄨ皟璇曢敊璇�
+ */
+ private String detailMessage;
+
+ public ServiceException(String message) {
+ this.message = message;
+ }
+
+ public ServiceException(String message, Integer code) {
+ this.message = message;
+ this.code = code;
+ }
+
+ public String getDetailMessage() {
+ return detailMessage;
+ }
+
+ @Override
+ public String getMessage() {
+ return message;
+ }
+
+ public Integer getCode() {
+ return code;
+ }
+
+ public ServiceException setMessage(String message) {
+ this.message = message;
+ return this;
+ }
+
+ public ServiceException setDetailMessage(String detailMessage) {
+ this.detailMessage = detailMessage;
+ return this;
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/base/BaseException.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/base/BaseException.java
new file mode 100644
index 0000000..8df8acf
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/base/BaseException.java
@@ -0,0 +1,73 @@
+package org.dromara.common.core.exception.base;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import org.dromara.common.core.utils.MessageUtils;
+import org.dromara.common.core.utils.StringUtils;
+
+import java.io.Serial;
+
+/**
+ * 鍩虹寮傚父
+ *
+ * @author ruoyi
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@NoArgsConstructor
+@AllArgsConstructor
+public class BaseException extends RuntimeException {
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 鎵�灞炴ā鍧�
+ */
+ private String module;
+
+ /**
+ * 閿欒鐮�
+ */
+ private String code;
+
+ /**
+ * 閿欒鐮佸搴旂殑鍙傛暟
+ */
+ private Object[] args;
+
+ /**
+ * 閿欒娑堟伅
+ */
+ private String defaultMessage;
+
+ public BaseException(String module, String code, Object[] args) {
+ this(module, code, args, null);
+ }
+
+ public BaseException(String module, String defaultMessage) {
+ this(module, null, null, defaultMessage);
+ }
+
+ public BaseException(String code, Object[] args) {
+ this(null, code, args, null);
+ }
+
+ public BaseException(String defaultMessage) {
+ this(null, null, null, defaultMessage);
+ }
+
+ @Override
+ public String getMessage() {
+ String message = null;
+ if (!StringUtils.isEmpty(code)) {
+ message = MessageUtils.message(code, args);
+ }
+ if (message == null) {
+ message = defaultMessage;
+ }
+ return message;
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/file/FileException.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/file/FileException.java
new file mode 100644
index 0000000..d374fc0
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/file/FileException.java
@@ -0,0 +1,21 @@
+package org.dromara.common.core.exception.file;
+
+import org.dromara.common.core.exception.base.BaseException;
+
+import java.io.Serial;
+
+/**
+ * 鏂囦欢淇℃伅寮傚父绫�
+ *
+ * @author ruoyi
+ */
+public class FileException extends BaseException {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ public FileException(String code, Object[] args) {
+ super("file", code, args, null);
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/file/FileNameLengthLimitExceededException.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/file/FileNameLengthLimitExceededException.java
new file mode 100644
index 0000000..af98124
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/file/FileNameLengthLimitExceededException.java
@@ -0,0 +1,18 @@
+package org.dromara.common.core.exception.file;
+
+import java.io.Serial;
+
+/**
+ * 鏂囦欢鍚嶇О瓒呴暱闄愬埗寮傚父绫�
+ *
+ * @author ruoyi
+ */
+public class FileNameLengthLimitExceededException extends FileException {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ public FileNameLengthLimitExceededException(int defaultFileNameLength) {
+ super("upload.filename.exceed.length", new Object[]{defaultFileNameLength});
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/file/FileSizeLimitExceededException.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/file/FileSizeLimitExceededException.java
new file mode 100644
index 0000000..1eb8d40
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/file/FileSizeLimitExceededException.java
@@ -0,0 +1,18 @@
+package org.dromara.common.core.exception.file;
+
+import java.io.Serial;
+
+/**
+ * 鏂囦欢鍚嶅ぇ灏忛檺鍒跺紓甯哥被
+ *
+ * @author ruoyi
+ */
+public class FileSizeLimitExceededException extends FileException {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ public FileSizeLimitExceededException(long defaultMaxSize) {
+ super("upload.exceed.maxSize", new Object[]{defaultMaxSize});
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/user/CaptchaException.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/user/CaptchaException.java
new file mode 100644
index 0000000..a18e581
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/user/CaptchaException.java
@@ -0,0 +1,21 @@
+package org.dromara.common.core.exception.user;
+
+import java.io.Serial;
+
+/**
+ * 楠岃瘉鐮侀敊璇紓甯哥被
+ *
+ * @author Lion Li
+ */
+public class CaptchaException extends UserException {
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ public CaptchaException() {
+ super("user.jcaptcha.error");
+ }
+
+ public CaptchaException(String msg) {
+ super(msg);
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/user/CaptchaExpireException.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/user/CaptchaExpireException.java
new file mode 100644
index 0000000..f4b8cac
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/user/CaptchaExpireException.java
@@ -0,0 +1,18 @@
+package org.dromara.common.core.exception.user;
+
+import java.io.Serial;
+
+/**
+ * 楠岃瘉鐮佸け鏁堝紓甯哥被
+ *
+ * @author ruoyi
+ */
+public class CaptchaExpireException extends UserException {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ public CaptchaExpireException() {
+ super("user.jcaptcha.expire");
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/user/UserException.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/user/UserException.java
new file mode 100644
index 0000000..024fed6
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/user/UserException.java
@@ -0,0 +1,20 @@
+package org.dromara.common.core.exception.user;
+
+import org.dromara.common.core.exception.base.BaseException;
+
+import java.io.Serial;
+
+/**
+ * 鐢ㄦ埛淇℃伅寮傚父绫�
+ *
+ * @author ruoyi
+ */
+public class UserException extends BaseException {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ public UserException(String code, Object... args) {
+ super("user", code, args, null);
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/factory/YmlPropertySourceFactory.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/factory/YmlPropertySourceFactory.java
new file mode 100644
index 0000000..af61b90
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/factory/YmlPropertySourceFactory.java
@@ -0,0 +1,31 @@
+package org.dromara.common.core.factory;
+
+import org.dromara.common.core.utils.StringUtils;
+import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
+import org.springframework.core.env.PropertiesPropertySource;
+import org.springframework.core.env.PropertySource;
+import org.springframework.core.io.support.DefaultPropertySourceFactory;
+import org.springframework.core.io.support.EncodedResource;
+
+import java.io.IOException;
+
+/**
+ * yml 閰嶇疆婧愬伐鍘�
+ *
+ * @author Lion Li
+ */
+public class YmlPropertySourceFactory extends DefaultPropertySourceFactory {
+
+ @Override
+ public PropertySource<?> createPropertySource(String name, EncodedResource resource) throws IOException {
+ String sourceName = resource.getResource().getFilename();
+ if (StringUtils.isNotBlank(sourceName) && StringUtils.endsWithAny(sourceName, ".yml", ".yaml")) {
+ YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
+ factory.setResources(resource.getResource());
+ factory.afterPropertiesSet();
+ return new PropertiesPropertySource(sourceName, factory.getObject());
+ }
+ return super.createPropertySource(name, resource);
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/DictService.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/DictService.java
new file mode 100644
index 0000000..9f2632f
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/DictService.java
@@ -0,0 +1,63 @@
+package org.dromara.common.core.service;
+
+import org.dromara.common.core.utils.StringUtils;
+
+import java.util.Map;
+
+/**
+ * 瀛楀吀鏈嶅姟鏈嶅姟
+ *
+ * @author Lion Li
+ */
+public interface DictService {
+
+ /**
+ * 鏍规嵁瀛楀吀绫诲瀷鍜屽瓧鍏稿�艰幏鍙栧瓧鍏告爣绛�
+ *
+ * @param dictType 瀛楀吀绫诲瀷
+ * @param dictValue 瀛楀吀鍊�
+ * @return 瀛楀吀鏍囩
+ */
+ default String getDictLabel(String dictType, String dictValue) {
+ return getDictLabel(dictType, dictValue, StringUtils.SEPARATOR);
+ }
+
+ /**
+ * 鏍规嵁瀛楀吀绫诲瀷鍜屽瓧鍏告爣绛捐幏鍙栧瓧鍏稿��
+ *
+ * @param dictType 瀛楀吀绫诲瀷
+ * @param dictLabel 瀛楀吀鏍囩
+ * @return 瀛楀吀鍊�
+ */
+ default String getDictValue(String dictType, String dictLabel) {
+ return getDictValue(dictType, dictLabel, StringUtils.SEPARATOR);
+ }
+
+ /**
+ * 鏍规嵁瀛楀吀绫诲瀷鍜屽瓧鍏稿�艰幏鍙栧瓧鍏告爣绛�
+ *
+ * @param dictType 瀛楀吀绫诲瀷
+ * @param dictValue 瀛楀吀鍊�
+ * @param separator 鍒嗛殧绗�
+ * @return 瀛楀吀鏍囩
+ */
+ String getDictLabel(String dictType, String dictValue, String separator);
+
+ /**
+ * 鏍规嵁瀛楀吀绫诲瀷鍜屽瓧鍏告爣绛捐幏鍙栧瓧鍏稿��
+ *
+ * @param dictType 瀛楀吀绫诲瀷
+ * @param dictLabel 瀛楀吀鏍囩
+ * @param separator 鍒嗛殧绗�
+ * @return 瀛楀吀鍊�
+ */
+ String getDictValue(String dictType, String dictLabel, String separator);
+
+ /**
+ * 鑾峰彇瀛楀吀涓嬫墍鏈夌殑瀛楀吀鍊间笌鏍囩
+ *
+ * @param dictType 瀛楀吀绫诲瀷
+ * @return dictValue涓簁ey锛宒ictLabel涓哄�肩粍鎴愮殑Map
+ */
+ Map<String, String> getAllDictByDictType(String dictType);
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/DateUtils.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/DateUtils.java
new file mode 100644
index 0000000..72178a7
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/DateUtils.java
@@ -0,0 +1,168 @@
+package org.dromara.common.core.utils;
+
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import org.apache.commons.lang3.time.DateFormatUtils;
+
+import java.lang.management.ManagementFactory;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.Date;
+
+/**
+ * 鏃堕棿宸ュ叿绫�
+ *
+ * @author ruoyi
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class DateUtils extends org.apache.commons.lang3.time.DateUtils {
+
+ public static final String YYYY = "yyyy";
+
+ public static final String YYYY_MM = "yyyy-MM";
+
+ public static final String YYYY_MM_DD = "yyyy-MM-dd";
+
+ public static final String YYYYMMDDHHMMSS = "yyyyMMddHHmmss";
+
+ public static final String YYYY_MM_DD_HH_MM_SS = "yyyy-MM-dd HH:mm:ss";
+
+ private static final String[] PARSE_PATTERNS = {
+ "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyy-MM",
+ "yyyy/MM/dd", "yyyy/MM/dd HH:mm:ss", "yyyy/MM/dd HH:mm", "yyyy/MM",
+ "yyyy.MM.dd", "yyyy.MM.dd HH:mm:ss", "yyyy.MM.dd HH:mm", "yyyy.MM"};
+
+ /**
+ * 鑾峰彇褰撳墠Date鍨嬫棩鏈�
+ *
+ * @return Date() 褰撳墠鏃ユ湡
+ */
+ public static Date getNowDate() {
+ return new Date();
+ }
+
+ /**
+ * 鑾峰彇褰撳墠鏃ユ湡, 榛樿鏍煎紡涓簓yyy-MM-dd
+ *
+ * @return String
+ */
+ public static String getDate() {
+ return dateTimeNow(YYYY_MM_DD);
+ }
+
+ public static String getTime() {
+ return dateTimeNow(YYYY_MM_DD_HH_MM_SS);
+ }
+
+ public static String dateTimeNow() {
+ return dateTimeNow(YYYYMMDDHHMMSS);
+ }
+
+ public static String dateTimeNow(final String format) {
+ return parseDateToStr(format, new Date());
+ }
+
+ public static String dateTime(final Date date) {
+ return parseDateToStr(YYYY_MM_DD, date);
+ }
+
+ public static String parseDateToStr(final String format, final Date date) {
+ return new SimpleDateFormat(format).format(date);
+ }
+
+ public static Date dateTime(final String format, final String ts) {
+ try {
+ return new SimpleDateFormat(format).parse(ts);
+ } catch (ParseException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * 鏃ユ湡璺緞 鍗冲勾/鏈�/鏃� 濡�2018/08/08
+ */
+ public static String datePath() {
+ Date now = new Date();
+ return DateFormatUtils.format(now, "yyyy/MM/dd");
+ }
+
+ /**
+ * 鏃ユ湡璺緞 鍗冲勾/鏈�/鏃� 濡�20180808
+ */
+ public static String dateTime() {
+ Date now = new Date();
+ return DateFormatUtils.format(now, "yyyyMMdd");
+ }
+
+ /**
+ * 鏃ユ湡鍨嬪瓧绗︿覆杞寲涓烘棩鏈� 鏍煎紡
+ */
+ public static Date parseDate(Object str) {
+ if (str == null) {
+ return null;
+ }
+ try {
+ return parseDate(str.toString(), PARSE_PATTERNS);
+ } catch (ParseException e) {
+ return null;
+ }
+ }
+
+ /**
+ * 鑾峰彇鏈嶅姟鍣ㄥ惎鍔ㄦ椂闂�
+ */
+ public static Date getServerStartDate() {
+ long time = ManagementFactory.getRuntimeMXBean().getStartTime();
+ return new Date(time);
+ }
+
+ /**
+ * 璁$畻鐩稿樊澶╂暟
+ */
+ public static int differentDaysByMillisecond(Date date1, Date date2) {
+ return Math.abs((int) ((date2.getTime() - date1.getTime()) / (1000 * 3600 * 24)));
+ }
+
+ /**
+ * 璁$畻涓や釜鏃堕棿宸�
+ */
+ public static String getDatePoor(Date endDate, Date nowDate) {
+ long nd = 1000 * 24 * 60 * 60;
+ long nh = 1000 * 60 * 60;
+ long nm = 1000 * 60;
+ // long ns = 1000;
+ // 鑾峰緱涓や釜鏃堕棿鐨勬绉掓椂闂村樊寮�
+ long diff = endDate.getTime() - nowDate.getTime();
+ // 璁$畻宸灏戝ぉ
+ long day = diff / nd;
+ // 璁$畻宸灏戝皬鏃�
+ long hour = diff % nd / nh;
+ // 璁$畻宸灏戝垎閽�
+ long min = diff % nd % nh / nm;
+ // 璁$畻宸灏戠//杈撳嚭缁撴灉
+ // long sec = diff % nd % nh % nm / ns;
+ return day + "澶�" + hour + "灏忔椂" + min + "鍒嗛挓";
+ }
+
+ /**
+ * 澧炲姞 LocalDateTime ==> Date
+ */
+ public static Date toDate(LocalDateTime temporalAccessor) {
+ ZonedDateTime zdt = temporalAccessor.atZone(ZoneId.systemDefault());
+ return Date.from(zdt.toInstant());
+ }
+
+ /**
+ * 澧炲姞 LocalDate ==> Date
+ */
+ public static Date toDate(LocalDate temporalAccessor) {
+ LocalDateTime localDateTime = LocalDateTime.of(temporalAccessor, LocalTime.of(0, 0, 0));
+ ZonedDateTime zdt = localDateTime.atZone(ZoneId.systemDefault());
+ return Date.from(zdt.toInstant());
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/MapstructUtils.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/MapstructUtils.java
new file mode 100644
index 0000000..a3fcb8e
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/MapstructUtils.java
@@ -0,0 +1,92 @@
+package org.dromara.common.core.utils;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.ObjectUtil;
+import io.github.linpeilie.Converter;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Mapstruct 宸ュ叿绫�
+ * <p>鍙傝�冩枃妗o細<a href="https://mapstruct.plus/introduction/quick-start.html">mapstruct-plus</a></p>
+ *
+ * @author Michelle.Chung
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class MapstructUtils {
+
+ private final static Converter CONVERTER = SpringUtils.getBean(Converter.class);
+
+ /**
+ * 灏� T 绫诲瀷瀵硅薄锛岃浆鎹负 desc 绫诲瀷鐨勫璞″苟杩斿洖
+ *
+ * @param source 鏁版嵁鏉ユ簮瀹炰綋
+ * @param desc 鎻忚堪瀵硅薄 杞崲鍚庣殑瀵硅薄
+ * @return desc
+ */
+ public static <T, V> V convert(T source, Class<V> desc) {
+ if (ObjectUtil.isNull(source)) {
+ return null;
+ }
+ if (ObjectUtil.isNull(desc)) {
+ return null;
+ }
+ return CONVERTER.convert(source, desc);
+ }
+
+ /**
+ * 灏� T 绫诲瀷瀵硅薄锛屾寜鐓ч厤缃殑鏄犲皠瀛楁瑙勫垯锛岀粰 desc 绫诲瀷鐨勫璞¤祴鍊煎苟杩斿洖 desc 瀵硅薄
+ *
+ * @param source 鏁版嵁鏉ユ簮瀹炰綋
+ * @param desc 杞崲鍚庣殑瀵硅薄
+ * @return desc
+ */
+ public static <T, V> V convert(T source, V desc) {
+ if (ObjectUtil.isNull(source)) {
+ return null;
+ }
+ if (ObjectUtil.isNull(desc)) {
+ return null;
+ }
+ return CONVERTER.convert(source, desc);
+ }
+
+ /**
+ * 灏� T 绫诲瀷鐨勯泦鍚堬紝杞崲涓� desc 绫诲瀷鐨勯泦鍚堝苟杩斿洖
+ *
+ * @param sourceList 鏁版嵁鏉ユ簮瀹炰綋鍒楄〃
+ * @param desc 鎻忚堪瀵硅薄 杞崲鍚庣殑瀵硅薄
+ * @return desc
+ */
+ public static <T, V> List<V> convert(List<T> sourceList, Class<V> desc) {
+ if (ObjectUtil.isNull(sourceList)) {
+ return null;
+ }
+ if (CollUtil.isEmpty(sourceList)) {
+ return CollUtil.newArrayList();
+ }
+ return CONVERTER.convert(sourceList, desc);
+ }
+
+ /**
+ * 灏� Map 杞崲涓� beanClass 绫诲瀷鐨勯泦鍚堝苟杩斿洖
+ *
+ * @param map 鏁版嵁鏉ユ簮
+ * @param beanClass bean绫�
+ * @return bean瀵硅薄
+ */
+ public static <T> T convert(Map<String, Object> map, Class<T> beanClass) {
+ if (MapUtil.isEmpty(map)) {
+ return null;
+ }
+ if (ObjectUtil.isNull(beanClass)) {
+ return null;
+ }
+ return CONVERTER.convert(map, beanClass);
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/MessageUtils.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/MessageUtils.java
new file mode 100644
index 0000000..48dfc08
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/MessageUtils.java
@@ -0,0 +1,33 @@
+package org.dromara.common.core.utils;
+
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import org.springframework.context.MessageSource;
+import org.springframework.context.NoSuchMessageException;
+import org.springframework.context.i18n.LocaleContextHolder;
+
+/**
+ * 鑾峰彇i18n璧勬簮鏂囦欢
+ *
+ * @author Lion Li
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class MessageUtils {
+
+ private static final MessageSource MESSAGE_SOURCE = SpringUtils.getBean(MessageSource.class);
+
+ /**
+ * 鏍规嵁娑堟伅閿拰鍙傛暟 鑾峰彇娑堟伅 濮旀墭缁檚pring messageSource
+ *
+ * @param code 娑堟伅閿�
+ * @param args 鍙傛暟
+ * @return 鑾峰彇鍥介檯鍖栫炕璇戝��
+ */
+ public static String message(String code, Object... args) {
+ try {
+ return MESSAGE_SOURCE.getMessage(code, args, LocaleContextHolder.getLocale());
+ } catch (NoSuchMessageException e) {
+ return code;
+ }
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/ReUtil.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/ReUtil.java
new file mode 100644
index 0000000..2de7f4f
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/ReUtil.java
@@ -0,0 +1,148 @@
+package org.dromara.common.core.utils;
+
+
+import cn.hutool.core.convert.Convert;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class ReUtil {
+ public final static Pattern GROUP_VAR = Pattern.compile("\\$(\\d+)");
+
+ /**
+ * 姝e垯涓渶瑕佽杞箟鐨勫叧閿瓧
+ */
+ public final static Set<Character> RE_KEYS = new HashSet<>(
+ Arrays.asList('$', '(', ')', '*', '+', '.', '[', ']', '?', '\\', '^', '{', '}', '|'));
+ ;
+
+ /**
+ * 姝e垯鏇挎崲鎸囧畾鍊�<br>
+ * 閫氳繃姝e垯鏌ユ壘鍒板瓧绗︿覆锛岀劧鍚庢妸鍖归厤鍒扮殑瀛楃涓插姞鍏ュ埌replacementTemplate涓紝$1琛ㄧず鍒嗙粍1鐨勫瓧绗︿覆
+ *
+ * <p>
+ * 渚嬪锛氬師瀛楃涓叉槸锛氫腑鏂�1234锛屾垜鎯虫妸1234鎹㈡垚(1234)锛屽垯鍙互锛�
+ *
+ * <pre>
+ * ReUtil.replaceAll("涓枃1234", "(\\d+)", "($1)"))
+ *
+ * 缁撴灉锛氫腑鏂�(1234)
+ * </pre>
+ *
+ * @param content 鏂囨湰
+ * @param regex 姝e垯
+ * @param replacementTemplate 鏇挎崲鐨勬枃鏈ā鏉匡紝鍙互浣跨敤$1绫讳技鐨勫彉閲忔彁鍙栨鍒欏尮閰嶅嚭鐨勫唴瀹�
+ * @return 澶勭悊鍚庣殑鏂囨湰
+ */
+ public static String replaceAll(CharSequence content, String regex, String replacementTemplate) {
+ final Pattern pattern = Pattern.compile(regex, Pattern.DOTALL);
+ return replaceAll(content, pattern, replacementTemplate);
+ }
+
+ /**
+ * 姝e垯鏇挎崲鎸囧畾鍊�<br>
+ * 閫氳繃姝e垯鏌ユ壘鍒板瓧绗︿覆锛岀劧鍚庢妸鍖归厤鍒扮殑瀛楃涓插姞鍏ュ埌replacementTemplate涓紝$1琛ㄧず鍒嗙粍1鐨勫瓧绗︿覆
+ *
+ * @param content 鏂囨湰
+ * @param pattern {@link Pattern}
+ * @param replacementTemplate 鏇挎崲鐨勬枃鏈ā鏉匡紝鍙互浣跨敤$1绫讳技鐨勫彉閲忔彁鍙栨鍒欏尮閰嶅嚭鐨勫唴瀹�
+ * @return 澶勭悊鍚庣殑鏂囨湰
+ * @since 3.0.4
+ */
+ public static String replaceAll(CharSequence content, Pattern pattern, String replacementTemplate) {
+ if (StringUtils.isEmpty(content)) {
+ return StringUtils.EMPTY;
+ }
+
+ final Matcher matcher = pattern.matcher(content);
+ boolean result = matcher.find();
+ if (result) {
+ final Set<String> varNums = findAll(GROUP_VAR, replacementTemplate, 1, new HashSet<>());
+ final StringBuffer sb = new StringBuffer();
+ do {
+ String replacement = replacementTemplate;
+ for (String var : varNums) {
+ int group = Integer.parseInt(var);
+ replacement = replacement.replace("$" + var, matcher.group(group));
+ }
+ matcher.appendReplacement(sb, escape(replacement));
+ result = matcher.find();
+ }
+ while (result);
+ matcher.appendTail(sb);
+ return sb.toString();
+ }
+ return Convert.toStr(content);
+ }
+
+ /**
+ * 鍙栧緱鍐呭涓尮閰嶇殑鎵�鏈夌粨鏋�
+ *
+ * @param <T> 闆嗗悎绫诲瀷
+ * @param pattern 缂栬瘧鍚庣殑姝e垯妯″紡
+ * @param content 琚煡鎵剧殑鍐呭
+ * @param group 姝e垯鐨勫垎缁�
+ * @param collection 杩斿洖鐨勯泦鍚堢被鍨�
+ * @return 缁撴灉闆�
+ */
+ public static <T extends Collection<String>> T findAll(Pattern pattern, CharSequence content, int group,
+ T collection) {
+ if (null == pattern || null == content) {
+ return null;
+ }
+
+ if (null == collection) {
+ throw new NullPointerException("Null collection param provided!");
+ }
+
+ final Matcher matcher = pattern.matcher(content);
+ while (matcher.find()) {
+ collection.add(matcher.group(group));
+ }
+ return collection;
+ }
+
+ /**
+ * 杞箟瀛楃锛屽皢姝e垯鐨勫叧閿瓧杞箟
+ *
+ * @param c 瀛楃
+ * @return 杞箟鍚庣殑鏂囨湰
+ */
+ public static String escape(char c) {
+ final StringBuilder builder = new StringBuilder();
+ if (RE_KEYS.contains(c)) {
+ builder.append('\\');
+ }
+ builder.append(c);
+ return builder.toString();
+ }
+
+ /**
+ * 杞箟瀛楃涓诧紝灏嗘鍒欑殑鍏抽敭瀛楄浆涔�
+ *
+ * @param content 鏂囨湰
+ * @return 杞箟鍚庣殑鏂囨湰
+ */
+ public static String escape(CharSequence content) {
+ if (StringUtils.isBlank(content)) {
+ return StringUtils.EMPTY;
+ }
+
+ final StringBuilder builder = new StringBuilder();
+ int len = content.length();
+ char current;
+ for (int i = 0; i < len; i++) {
+ current = content.charAt(i);
+ if (RE_KEYS.contains(current)) {
+ builder.append('\\');
+ }
+ builder.append(current);
+ }
+ return builder.toString();
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/ServletUtils.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/ServletUtils.java
new file mode 100644
index 0000000..a1316eb
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/ServletUtils.java
@@ -0,0 +1,228 @@
+package org.dromara.common.core.utils;
+
+import cn.hutool.core.convert.Convert;
+import cn.hutool.extra.servlet.JakartaServletUtil;
+import cn.hutool.http.HttpStatus;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import org.springframework.http.MediaType;
+import org.springframework.util.LinkedCaseInsensitiveMap;
+import org.springframework.web.context.request.RequestAttributes;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import java.io.IOException;
+import java.net.URLDecoder;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 瀹㈡埛绔伐鍏风被
+ *
+ * @author ruoyi
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class ServletUtils extends JakartaServletUtil {
+
+ /**
+ * 鑾峰彇String鍙傛暟
+ */
+ public static String getParameter(String name) {
+ return getRequest().getParameter(name);
+ }
+
+ /**
+ * 鑾峰彇String鍙傛暟
+ */
+ public static String getParameter(String name, String defaultValue) {
+ return Convert.toStr(getRequest().getParameter(name), defaultValue);
+ }
+
+ /**
+ * 鑾峰彇Integer鍙傛暟
+ */
+ public static Integer getParameterToInt(String name) {
+ return Convert.toInt(getRequest().getParameter(name));
+ }
+
+ /**
+ * 鑾峰彇Integer鍙傛暟
+ */
+ public static Integer getParameterToInt(String name, Integer defaultValue) {
+ return Convert.toInt(getRequest().getParameter(name), defaultValue);
+ }
+
+ /**
+ * 鑾峰彇Boolean鍙傛暟
+ */
+ public static Boolean getParameterToBool(String name) {
+ return Convert.toBool(getRequest().getParameter(name));
+ }
+
+ /**
+ * 鑾峰彇Boolean鍙傛暟
+ */
+ public static Boolean getParameterToBool(String name, Boolean defaultValue) {
+ return Convert.toBool(getRequest().getParameter(name), defaultValue);
+ }
+
+ /**
+ * 鑾峰緱鎵�鏈夎姹傚弬鏁�
+ *
+ * @param request 璇锋眰瀵硅薄{@link ServletRequest}
+ * @return Map
+ */
+ public static Map<String, String[]> getParams(ServletRequest request) {
+ final Map<String, String[]> map = request.getParameterMap();
+ return Collections.unmodifiableMap(map);
+ }
+
+ /**
+ * 鑾峰緱鎵�鏈夎姹傚弬鏁�
+ *
+ * @param request 璇锋眰瀵硅薄{@link ServletRequest}
+ * @return Map
+ */
+ 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));
+ }
+ return params;
+ }
+
+ /**
+ * 鑾峰彇request
+ */
+ public static HttpServletRequest getRequest() {
+ try {
+ return getRequestAttributes().getRequest();
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ /**
+ * 鑾峰彇response
+ */
+ public static HttpServletResponse getResponse() {
+ try {
+ return getRequestAttributes().getResponse();
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ /**
+ * 鑾峰彇session
+ */
+ public static HttpSession getSession() {
+ return getRequest().getSession();
+ }
+
+ public static ServletRequestAttributes getRequestAttributes() {
+ try {
+ RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
+ return (ServletRequestAttributes) attributes;
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ public static String getHeader(HttpServletRequest request, String name) {
+ String value = request.getHeader(name);
+ if (StringUtils.isEmpty(value)) {
+ return StringUtils.EMPTY;
+ }
+ return urlDecode(value);
+ }
+
+ public static Map<String, String> getHeaders(HttpServletRequest request) {
+ Map<String, String> map = new LinkedCaseInsensitiveMap<>();
+ Enumeration<String> enumeration = request.getHeaderNames();
+ if (enumeration != null) {
+ while (enumeration.hasMoreElements()) {
+ String key = enumeration.nextElement();
+ String value = request.getHeader(key);
+ map.put(key, value);
+ }
+ }
+ return map;
+ }
+
+ /**
+ * 灏嗗瓧绗︿覆娓叉煋鍒板鎴风
+ *
+ * @param response 娓叉煋瀵硅薄
+ * @param string 寰呮覆鏌撶殑瀛楃涓�
+ */
+ public static void renderString(HttpServletResponse response, String string) {
+ try {
+ response.setStatus(HttpStatus.HTTP_OK);
+ response.setContentType(MediaType.APPLICATION_JSON_VALUE);
+ response.setCharacterEncoding(StandardCharsets.UTF_8.toString());
+ response.getWriter().print(string);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * 鏄惁鏄疉jax寮傛璇锋眰
+ *
+ * @param request
+ */
+ public static boolean isAjaxRequest(HttpServletRequest request) {
+
+ String accept = request.getHeader("accept");
+ if (accept != null && accept.contains(MediaType.APPLICATION_JSON_VALUE)) {
+ return true;
+ }
+
+ String xRequestedWith = request.getHeader("X-Requested-With");
+ if (xRequestedWith != null && xRequestedWith.contains("XMLHttpRequest")) {
+ return true;
+ }
+
+ String uri = request.getRequestURI();
+ if (StringUtils.equalsAnyIgnoreCase(uri, ".json", ".xml")) {
+ return true;
+ }
+
+ String ajax = request.getParameter("__ajax");
+ return StringUtils.equalsAnyIgnoreCase(ajax, "json", "xml");
+ }
+
+ public static String getClientIP() {
+ return getClientIP(getRequest());
+ }
+
+ /**
+ * 鍐呭缂栫爜
+ *
+ * @param str 鍐呭
+ * @return 缂栫爜鍚庣殑鍐呭
+ */
+ public static String urlEncode(String str) {
+ return URLEncoder.encode(str, StandardCharsets.UTF_8);
+ }
+
+ /**
+ * 鍐呭瑙g爜
+ *
+ * @param str 鍐呭
+ * @return 瑙g爜鍚庣殑鍐呭
+ */
+ public static String urlDecode(String str) {
+ return URLDecoder.decode(str, StandardCharsets.UTF_8);
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/SpringUtils.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/SpringUtils.java
new file mode 100644
index 0000000..0af4d51
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/SpringUtils.java
@@ -0,0 +1,60 @@
+package org.dromara.common.core.utils;
+
+import cn.hutool.extra.spring.SpringUtil;
+import org.springframework.aop.framework.AopContext;
+import org.springframework.beans.factory.NoSuchBeanDefinitionException;
+import org.springframework.context.ApplicationContext;
+
+/**
+ * spring宸ュ叿绫�
+ *
+ * @author Lion Li
+ */
+public final class SpringUtils extends SpringUtil {
+
+ /**
+ * 濡傛灉BeanFactory鍖呭惈涓�涓笌鎵�缁欏悕绉板尮閰嶇殑bean瀹氫箟锛屽垯杩斿洖true
+ */
+ public static boolean containsBean(String name) {
+ return getBeanFactory().containsBean(name);
+ }
+
+ /**
+ * 鍒ゆ柇浠ョ粰瀹氬悕瀛楁敞鍐岀殑bean瀹氫箟鏄竴涓猻ingleton杩樻槸涓�涓猵rototype銆�
+ * 濡傛灉涓庣粰瀹氬悕瀛楃浉搴旂殑bean瀹氫箟娌℃湁琚壘鍒帮紝灏嗕細鎶涘嚭涓�涓紓甯革紙NoSuchBeanDefinitionException锛�
+ */
+ public static boolean isSingleton(String name) throws NoSuchBeanDefinitionException {
+ return getBeanFactory().isSingleton(name);
+ }
+
+ /**
+ * @return Class 娉ㄥ唽瀵硅薄鐨勭被鍨�
+ */
+ public static Class<?> getType(String name) throws NoSuchBeanDefinitionException {
+ return getBeanFactory().getType(name);
+ }
+
+ /**
+ * 濡傛灉缁欏畾鐨刡ean鍚嶅瓧鍦╞ean瀹氫箟涓湁鍒悕锛屽垯杩斿洖杩欎簺鍒悕
+ */
+ public static String[] getAliases(String name) throws NoSuchBeanDefinitionException {
+ return getBeanFactory().getAliases(name);
+ }
+
+ /**
+ * 鑾峰彇aop浠g悊瀵硅薄
+ */
+ @SuppressWarnings("unchecked")
+ public static <T> T getAopProxy(T invoker) {
+ return (T) AopContext.currentProxy();
+ }
+
+
+ /**
+ * 鑾峰彇spring涓婁笅鏂�
+ */
+ public static ApplicationContext context() {
+ return getApplicationContext();
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/StreamUtils.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/StreamUtils.java
new file mode 100644
index 0000000..967612e
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/StreamUtils.java
@@ -0,0 +1,254 @@
+package org.dromara.common.core.utils;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.map.MapUtil;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+
+import java.util.*;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+/**
+ * stream 娴佸伐鍏风被
+ *
+ * @author Lion Li
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class StreamUtils {
+
+ /**
+ * 灏哻ollection杩囨护
+ *
+ * @param collection 闇�瑕佽浆鍖栫殑闆嗗悎
+ * @param function 杩囨护鏂规硶
+ * @return 杩囨护鍚庣殑list
+ */
+ public static <E> List<E> filter(Collection<E> collection, Predicate<E> function) {
+ if (CollUtil.isEmpty(collection)) {
+ return CollUtil.newArrayList();
+ }
+ // 娉ㄦ剰姝ゅ涓嶈浣跨敤 .toList() 鏂拌娉� 鍥犱负杩斿洖鐨勬槸涓嶅彲鍙楲ist 浼氬鑷村簭鍒楀寲闂
+ return collection.stream().filter(function).collect(Collectors.toList());
+ }
+
+ /**
+ * 灏哻ollection鎷兼帴
+ *
+ * @param collection 闇�瑕佽浆鍖栫殑闆嗗悎
+ * @param function 鎷兼帴鏂规硶
+ * @return 鎷兼帴鍚庣殑list
+ */
+ public static <E> String join(Collection<E> collection, Function<E, String> function) {
+ return join(collection, function, StringUtils.SEPARATOR);
+ }
+
+ /**
+ * 灏哻ollection鎷兼帴
+ *
+ * @param collection 闇�瑕佽浆鍖栫殑闆嗗悎
+ * @param function 鎷兼帴鏂规硶
+ * @param delimiter 鎷兼帴绗�
+ * @return 鎷兼帴鍚庣殑list
+ */
+ public static <E> String join(Collection<E> collection, Function<E, String> function, CharSequence delimiter) {
+ if (CollUtil.isEmpty(collection)) {
+ return StringUtils.EMPTY;
+ }
+ return collection.stream().map(function).filter(Objects::nonNull).collect(Collectors.joining(delimiter));
+ }
+
+ /**
+ * 灏哻ollection鎺掑簭
+ *
+ * @param collection 闇�瑕佽浆鍖栫殑闆嗗悎
+ * @param comparing 鎺掑簭鏂规硶
+ * @return 鎺掑簭鍚庣殑list
+ */
+ public static <E> List<E> sorted(Collection<E> collection, Comparator<E> comparing) {
+ if (CollUtil.isEmpty(collection)) {
+ return CollUtil.newArrayList();
+ }
+ // 娉ㄦ剰姝ゅ涓嶈浣跨敤 .toList() 鏂拌娉� 鍥犱负杩斿洖鐨勬槸涓嶅彲鍙楲ist 浼氬鑷村簭鍒楀寲闂
+ return collection.stream().filter(Objects::nonNull).sorted(comparing).collect(Collectors.toList());
+ }
+
+ /**
+ * 灏哻ollection杞寲涓虹被鍨嬩笉鍙樼殑map<br>
+ * <B>{@code Collection<V> ----> Map<K,V>}</B>
+ *
+ * @param collection 闇�瑕佽浆鍖栫殑闆嗗悎
+ * @param key V绫诲瀷杞寲涓篕绫诲瀷鐨刲ambda鏂规硶
+ * @param <V> collection涓殑娉涘瀷
+ * @param <K> map涓殑key绫诲瀷
+ * @return 杞寲鍚庣殑map
+ */
+ public static <V, K> Map<K, V> toIdentityMap(Collection<V> collection, Function<V, K> key) {
+ if (CollUtil.isEmpty(collection)) {
+ return MapUtil.newHashMap();
+ }
+ return collection.stream().filter(Objects::nonNull).collect(Collectors.toMap(key, Function.identity(), (l, r) -> l));
+ }
+
+ /**
+ * 灏咰ollection杞寲涓簃ap(value绫诲瀷涓巆ollection鐨勬硾鍨嬩笉鍚�)<br>
+ * <B>{@code Collection<E> -----> Map<K,V> }</B>
+ *
+ * @param collection 闇�瑕佽浆鍖栫殑闆嗗悎
+ * @param key E绫诲瀷杞寲涓篕绫诲瀷鐨刲ambda鏂规硶
+ * @param value E绫诲瀷杞寲涓篤绫诲瀷鐨刲ambda鏂规硶
+ * @param <E> collection涓殑娉涘瀷
+ * @param <K> map涓殑key绫诲瀷
+ * @param <V> map涓殑value绫诲瀷
+ * @return 杞寲鍚庣殑map
+ */
+ public static <E, K, V> Map<K, V> toMap(Collection<E> collection, Function<E, K> key, Function<E, V> value) {
+ if (CollUtil.isEmpty(collection)) {
+ return MapUtil.newHashMap();
+ }
+ return collection.stream().filter(Objects::nonNull).collect(Collectors.toMap(key, value, (l, r) -> l));
+ }
+
+ /**
+ * 灏哻ollection鎸夌収瑙勫垯(姣斿鏈夌浉鍚岀殑鐝骇id)鍒嗙被鎴恗ap<br>
+ * <B>{@code Collection<E> -------> Map<K,List<E>> } </B>
+ *
+ * @param collection 闇�瑕佸垎绫荤殑闆嗗悎
+ * @param key 鍒嗙被鐨勮鍒�
+ * @param <E> collection涓殑娉涘瀷
+ * @param <K> map涓殑key绫诲瀷
+ * @return 鍒嗙被鍚庣殑map
+ */
+ public static <E, K> Map<K, List<E>> groupByKey(Collection<E> collection, Function<E, K> key) {
+ if (CollUtil.isEmpty(collection)) {
+ return MapUtil.newHashMap();
+ }
+ return collection
+ .stream().filter(Objects::nonNull)
+ .collect(Collectors.groupingBy(key, LinkedHashMap::new, Collectors.toList()));
+ }
+
+ /**
+ * 灏哻ollection鎸夌収涓や釜瑙勫垯(姣斿鏈夌浉鍚岀殑骞寸骇id,鐝骇id)鍒嗙被鎴愬弻灞俶ap<br>
+ * <B>{@code Collection<E> ---> Map<T,Map<U,List<E>>> } </B>
+ *
+ * @param collection 闇�瑕佸垎绫荤殑闆嗗悎
+ * @param key1 绗竴涓垎绫荤殑瑙勫垯
+ * @param key2 绗簩涓垎绫荤殑瑙勫垯
+ * @param <E> 闆嗗悎鍏冪礌绫诲瀷
+ * @param <K> 绗竴涓猰ap涓殑key绫诲瀷
+ * @param <U> 绗簩涓猰ap涓殑key绫诲瀷
+ * @return 鍒嗙被鍚庣殑map
+ */
+ public static <E, K, U> Map<K, Map<U, List<E>>> groupBy2Key(Collection<E> collection, Function<E, K> key1, Function<E, U> key2) {
+ if (CollUtil.isEmpty(collection)) {
+ return MapUtil.newHashMap();
+ }
+ return collection
+ .stream().filter(Objects::nonNull)
+ .collect(Collectors.groupingBy(key1, LinkedHashMap::new, Collectors.groupingBy(key2, LinkedHashMap::new, Collectors.toList())));
+ }
+
+ /**
+ * 灏哻ollection鎸夌収涓や釜瑙勫垯(姣斿鏈夌浉鍚岀殑骞寸骇id,鐝骇id)鍒嗙被鎴愬弻灞俶ap<br>
+ * <B>{@code Collection<E> ---> Map<T,Map<U,E>> } </B>
+ *
+ * @param collection 闇�瑕佸垎绫荤殑闆嗗悎
+ * @param key1 绗竴涓垎绫荤殑瑙勫垯
+ * @param key2 绗簩涓垎绫荤殑瑙勫垯
+ * @param <T> 绗竴涓猰ap涓殑key绫诲瀷
+ * @param <U> 绗簩涓猰ap涓殑key绫诲瀷
+ * @param <E> collection涓殑娉涘瀷
+ * @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) {
+ return MapUtil.newHashMap();
+ }
+ return collection
+ .stream().filter(Objects::nonNull)
+ .collect(Collectors.groupingBy(key1, LinkedHashMap::new, Collectors.toMap(key2, Function.identity(), (l, r) -> l)));
+ }
+
+ /**
+ * 灏哻ollection杞寲涓篖ist闆嗗悎锛屼絾鏄袱鑰呯殑娉涘瀷涓嶅悓<br>
+ * <B>{@code Collection<E> ------> List<T> } </B>
+ *
+ * @param collection 闇�瑕佽浆鍖栫殑闆嗗悎
+ * @param function collection涓殑娉涘瀷杞寲涓簂ist娉涘瀷鐨刲ambda琛ㄨ揪寮�
+ * @param <E> collection涓殑娉涘瀷
+ * @param <T> List涓殑娉涘瀷
+ * @return 杞寲鍚庣殑list
+ */
+ public static <E, T> List<T> toList(Collection<E> collection, Function<E, T> function) {
+ if (CollUtil.isEmpty(collection)) {
+ return CollUtil.newArrayList();
+ }
+ return collection
+ .stream()
+ .map(function)
+ .filter(Objects::nonNull)
+ // 娉ㄦ剰姝ゅ涓嶈浣跨敤 .toList() 鏂拌娉� 鍥犱负杩斿洖鐨勬槸涓嶅彲鍙楲ist 浼氬鑷村簭鍒楀寲闂
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * 灏哻ollection杞寲涓篠et闆嗗悎锛屼絾鏄袱鑰呯殑娉涘瀷涓嶅悓<br>
+ * <B>{@code Collection<E> ------> Set<T> } </B>
+ *
+ * @param collection 闇�瑕佽浆鍖栫殑闆嗗悎
+ * @param function collection涓殑娉涘瀷杞寲涓簊et娉涘瀷鐨刲ambda琛ㄨ揪寮�
+ * @param <E> collection涓殑娉涘瀷
+ * @param <T> Set涓殑娉涘瀷
+ * @return 杞寲鍚庣殑Set
+ */
+ public static <E, T> Set<T> toSet(Collection<E> collection, Function<E, T> function) {
+ if (CollUtil.isEmpty(collection) || function == null) {
+ return CollUtil.newHashSet();
+ }
+ return collection
+ .stream()
+ .map(function)
+ .filter(Objects::nonNull)
+ .collect(Collectors.toSet());
+ }
+
+
+ /**
+ * 鍚堝苟涓や釜鐩稿悓key绫诲瀷鐨刴ap
+ *
+ * @param map1 绗竴涓渶瑕佸悎骞剁殑 map
+ * @param map2 绗簩涓渶瑕佸悎骞剁殑 map
+ * @param merge 鍚堝苟鐨刲ambda锛屽皢key value1 value2鍚堝苟鎴愭渶缁堢殑绫诲瀷,娉ㄦ剰value鍙兘涓虹┖鐨勬儏鍐�
+ * @param <K> map涓殑key绫诲瀷
+ * @param <X> 绗竴涓� map鐨剉alue绫诲瀷
+ * @param <Y> 绗簩涓� map鐨剉alue绫诲瀷
+ * @param <V> 鏈�缁坢ap鐨剉alue绫诲瀷
+ * @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)) {
+ return MapUtil.newHashMap();
+ } else if (MapUtil.isEmpty(map1)) {
+ map1 = MapUtil.newHashMap();
+ } else if (MapUtil.isEmpty(map2)) {
+ map2 = MapUtil.newHashMap();
+ }
+ 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;
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/StringUtils.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/StringUtils.java
new file mode 100644
index 0000000..5e4db50
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/StringUtils.java
@@ -0,0 +1,321 @@
+package org.dromara.common.core.utils;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.convert.Convert;
+import cn.hutool.core.lang.Validator;
+import cn.hutool.core.util.StrUtil;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import org.springframework.util.AntPathMatcher;
+
+import java.util.*;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * 瀛楃涓插伐鍏风被
+ *
+ * @author Lion Li
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class StringUtils extends org.apache.commons.lang3.StringUtils {
+
+ public static final String SEPARATOR = ",";
+
+ /**
+ * 鑾峰彇鍙傛暟涓嶄负绌哄��
+ *
+ * @param str defaultValue 瑕佸垽鏂殑value
+ * @return value 杩斿洖鍊�
+ */
+ public static String blankToDefault(String str, String defaultValue) {
+ return StrUtil.blankToDefault(str, defaultValue);
+ }
+
+ /**
+ * * 鍒ゆ柇涓�涓瓧绗︿覆鏄惁涓虹┖涓�
+ *
+ * @param str String
+ * @return true锛氫负绌� false锛氶潪绌�
+ */
+ public static boolean isEmpty(String str) {
+ return StrUtil.isEmpty(str);
+ }
+
+ /**
+ * * 鍒ゆ柇涓�涓瓧绗︿覆鏄惁涓洪潪绌轰覆
+ *
+ * @param str String
+ * @return true锛氶潪绌轰覆 false锛氱┖涓�
+ */
+ public static boolean isNotEmpty(String str) {
+ return !isEmpty(str);
+ }
+
+ /**
+ * 鍘荤┖鏍�
+ */
+ public static String trim(String str) {
+ return StrUtil.trim(str);
+ }
+
+ /**
+ * 鎴彇瀛楃涓�
+ *
+ * @param str 瀛楃涓�
+ * @param start 寮�濮�
+ * @return 缁撴灉
+ */
+ public static String substring(final String str, int start) {
+ return substring(str, start, str.length());
+ }
+
+ /**
+ * 鎴彇瀛楃涓�
+ *
+ * @param str 瀛楃涓�
+ * @param start 寮�濮�
+ * @param end 缁撴潫
+ * @return 缁撴灉
+ */
+ public static String substring(final String str, int start, int end) {
+ return StrUtil.sub(str, start, end);
+ }
+
+ /**
+ * 鏍煎紡鍖栨枃鏈�, {} 琛ㄧず鍗犱綅绗�<br>
+ * 姝ゆ柟娉曞彧鏄畝鍗曞皢鍗犱綅绗� {} 鎸夌収椤哄簭鏇挎崲涓哄弬鏁�<br>
+ * 濡傛灉鎯宠緭鍑� {} 浣跨敤 \\杞箟 { 鍗冲彲锛屽鏋滄兂杈撳嚭 {} 涔嬪墠鐨� \ 浣跨敤鍙岃浆涔夌 \\\\ 鍗冲彲<br>
+ * 渚嬶細<br>
+ * 閫氬父浣跨敤锛歠ormat("this is {} for {}", "a", "b") -> this is a for b<br>
+ * 杞箟{}锛� format("this is \\{} for {}", "a", "b") -> this is {} for a<br>
+ * 杞箟\锛� format("this is \\\\{} for {}", "a", "b") -> this is \a for b<br>
+ *
+ * @param template 鏂囨湰妯℃澘锛岃鏇挎崲鐨勯儴鍒嗙敤 {} 琛ㄧず
+ * @param params 鍙傛暟鍊�
+ * @return 鏍煎紡鍖栧悗鐨勬枃鏈�
+ */
+ public static String format(String template, Object... params) {
+ return StrUtil.format(template, params);
+ }
+
+ /**
+ * 鏄惁涓篽ttp(s)://寮�澶�
+ *
+ * @param link 閾炬帴
+ * @return 缁撴灉
+ */
+ public static boolean ishttp(String link) {
+ return Validator.isUrl(link);
+ }
+
+ /**
+ * 瀛楃涓茶浆set
+ *
+ * @param str 瀛楃涓�
+ * @param sep 鍒嗛殧绗�
+ * @return set闆嗗悎
+ */
+ public static Set<String> str2Set(String str, String sep) {
+ return new HashSet<>(str2List(str, sep, true, false));
+ }
+
+ /**
+ * 瀛楃涓茶浆list
+ *
+ * @param str 瀛楃涓�
+ * @param sep 鍒嗛殧绗�
+ * @param filterBlank 杩囨护绾┖鐧�
+ * @param trim 鍘绘帀棣栧熬绌虹櫧
+ * @return list闆嗗悎
+ */
+ public static List<String> str2List(String str, String sep, boolean filterBlank, boolean trim) {
+ List<String> list = new ArrayList<>();
+ if (isEmpty(str)) {
+ return list;
+ }
+
+ // 杩囨护绌虹櫧瀛楃涓�
+ if (filterBlank && isBlank(str)) {
+ return list;
+ }
+ String[] split = str.split(sep);
+ for (String string : split) {
+ if (filterBlank && isBlank(string)) {
+ continue;
+ }
+ if (trim) {
+ string = trim(string);
+ }
+ list.add(string);
+ }
+
+ return list;
+ }
+
+ /**
+ * 鏌ユ壘鎸囧畾瀛楃涓叉槸鍚﹀寘鍚寚瀹氬瓧绗︿覆鍒楄〃涓殑浠绘剰涓�涓瓧绗︿覆鍚屾椂涓插拷鐣ュぇ灏忓啓
+ *
+ * @param cs 鎸囧畾瀛楃涓�
+ * @param searchCharSequences 闇�瑕佹鏌ョ殑瀛楃涓叉暟缁�
+ * @return 鏄惁鍖呭惈浠绘剰涓�涓瓧绗︿覆
+ */
+ public static boolean containsAnyIgnoreCase(CharSequence cs, CharSequence... searchCharSequences) {
+ return StrUtil.containsAnyIgnoreCase(cs, searchCharSequences);
+ }
+
+ /**
+ * 椹煎嘲杞笅鍒掔嚎鍛藉悕
+ */
+ public static String toUnderScoreCase(String str) {
+ return StrUtil.toUnderlineCase(str);
+ }
+
+ /**
+ * 鏄惁鍖呭惈瀛楃涓�
+ *
+ * @param str 楠岃瘉瀛楃涓�
+ * @param strs 瀛楃涓茬粍
+ * @return 鍖呭惈杩斿洖true
+ */
+ public static boolean inStringIgnoreCase(String str, String... strs) {
+ return StrUtil.equalsAnyIgnoreCase(str, strs);
+ }
+
+ /**
+ * 灏嗕笅鍒掔嚎澶у啓鏂瑰紡鍛藉悕鐨勫瓧绗︿覆杞崲涓洪┘宄板紡銆傚鏋滆浆鎹㈠墠鐨勪笅鍒掔嚎澶у啓鏂瑰紡鍛藉悕鐨勫瓧绗︿覆涓虹┖锛屽垯杩斿洖绌哄瓧绗︿覆銆� 渚嬪锛欻ELLO_WORLD->HelloWorld
+ *
+ * @param name 杞崲鍓嶇殑涓嬪垝绾垮ぇ鍐欐柟寮忓懡鍚嶇殑瀛楃涓�
+ * @return 杞崲鍚庣殑椹煎嘲寮忓懡鍚嶇殑瀛楃涓�
+ */
+ public static String convertToCamelCase(String name) {
+ return StrUtil.upperFirst(StrUtil.toCamelCase(name));
+ }
+
+ /**
+ * 椹煎嘲寮忓懡鍚嶆硶 渚嬪锛歶ser_name->userName
+ */
+ public static String toCamelCase(String s) {
+ return StrUtil.toCamelCase(s);
+ }
+
+ /**
+ * 鏌ユ壘鎸囧畾瀛楃涓叉槸鍚﹀尮閰嶆寚瀹氬瓧绗︿覆鍒楄〃涓殑浠绘剰涓�涓瓧绗︿覆
+ *
+ * @param str 鎸囧畾瀛楃涓�
+ * @param strs 闇�瑕佹鏌ョ殑瀛楃涓叉暟缁�
+ * @return 鏄惁鍖归厤
+ */
+ public static boolean matches(String str, List<String> strs) {
+ if (isEmpty(str) || CollUtil.isEmpty(strs)) {
+ return false;
+ }
+ for (String pattern : strs) {
+ if (isMatch(pattern, str)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * 鍒ゆ柇url鏄惁涓庤鍒欓厤缃�:
+ * ? 琛ㄧず鍗曚釜瀛楃;
+ * * 琛ㄧず涓�灞傝矾寰勫唴鐨勪换鎰忓瓧绗︿覆锛屼笉鍙法灞傜骇;
+ * ** 琛ㄧず浠绘剰灞傝矾寰�;
+ *
+ * @param pattern 鍖归厤瑙勫垯
+ * @param url 闇�瑕佸尮閰嶇殑url
+ */
+ public static boolean isMatch(String pattern, String url) {
+ AntPathMatcher matcher = new AntPathMatcher();
+ return matcher.match(pattern, url);
+ }
+
+ /**
+ * 鏁板瓧宸﹁竟琛ラ綈0锛屼娇涔嬭揪鍒版寚瀹氶暱搴︺�傛敞鎰忥紝濡傛灉鏁板瓧杞崲涓哄瓧绗︿覆鍚庯紝闀垮害澶т簬size锛屽垯鍙繚鐣� 鏈�鍚巗ize涓瓧绗︺��
+ *
+ * @param num 鏁板瓧瀵硅薄
+ * @param size 瀛楃涓叉寚瀹氶暱搴�
+ * @return 杩斿洖鏁板瓧鐨勫瓧绗︿覆鏍煎紡锛岃瀛楃涓蹭负鎸囧畾闀垮害銆�
+ */
+ public static String padl(final Number num, final int size) {
+ return padl(num.toString(), size, '0');
+ }
+
+ /**
+ * 瀛楃涓插乏琛ラ綈銆傚鏋滃師濮嬪瓧绗︿覆s闀垮害澶т簬size锛屽垯鍙繚鐣欐渶鍚巗ize涓瓧绗︺��
+ *
+ * @param s 鍘熷瀛楃涓�
+ * @param size 瀛楃涓叉寚瀹氶暱搴�
+ * @param c 鐢ㄤ簬琛ラ綈鐨勫瓧绗�
+ * @return 杩斿洖鎸囧畾闀垮害鐨勫瓧绗︿覆锛岀敱鍘熷瓧绗︿覆宸﹁ˉ榻愭垨鎴彇寰楀埌銆�
+ */
+ public static String padl(final String s, final int size, final char c) {
+ final StringBuilder sb = new StringBuilder(size);
+ if (s != null) {
+ final int len = s.length();
+ if (s.length() <= size) {
+ sb.append(String.valueOf(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)));
+ }
+ return sb.toString();
+ }
+
+ /**
+ * 鍒囧垎瀛楃涓�(鍒嗛殧绗﹂粯璁ら�楀彿)
+ *
+ * @param str 琚垏鍒嗙殑瀛楃涓�
+ * @return 鍒嗗壊鍚庣殑鏁版嵁鍒楄〃
+ */
+ public static List<String> splitList(String str) {
+ return splitTo(str, Convert::toStr);
+ }
+
+ /**
+ * 鍒囧垎瀛楃涓�
+ *
+ * @param str 琚垏鍒嗙殑瀛楃涓�
+ * @param separator 鍒嗛殧绗�
+ * @return 鍒嗗壊鍚庣殑鏁版嵁鍒楄〃
+ */
+ public static List<String> splitList(String str, String separator) {
+ return splitTo(str, separator, Convert::toStr);
+ }
+
+ /**
+ * 鍒囧垎瀛楃涓茶嚜瀹氫箟杞崲(鍒嗛殧绗﹂粯璁ら�楀彿)
+ *
+ * @param str 琚垏鍒嗙殑瀛楃涓�
+ * @param mapper 鑷畾涔夎浆鎹�
+ * @return 鍒嗗壊鍚庣殑鏁版嵁鍒楄〃
+ */
+ public static <T> List<T> splitTo(String str, Function<? super Object, T> mapper) {
+ return splitTo(str, SEPARATOR, mapper);
+ }
+
+ /**
+ * 鍒囧垎瀛楃涓茶嚜瀹氫箟杞崲
+ *
+ * @param str 琚垏鍒嗙殑瀛楃涓�
+ * @param separator 鍒嗛殧绗�
+ * @param mapper 鑷畾涔夎浆鎹�
+ * @return 鍒嗗壊鍚庣殑鏁版嵁鍒楄〃
+ */
+ public static <T> List<T> splitTo(String str, String separator, Function<? super Object, T> mapper) {
+ if (isBlank(str)) {
+ return new ArrayList<>(0);
+ }
+ return StrUtil.split(str, separator)
+ .stream()
+ .filter(Objects::nonNull)
+ .map(mapper)
+ .collect(Collectors.toList());
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/Threads.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/Threads.java
new file mode 100644
index 0000000..ae6cfa3
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/Threads.java
@@ -0,0 +1,75 @@
+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 {
+
+ /**
+ * sleep绛夊緟,鍗曚綅涓烘绉�
+ */
+ public static void sleep(long milliseconds) {
+ try {
+ Thread.sleep(milliseconds);
+ } catch (InterruptedException e) {
+ return;
+ }
+ }
+
+ /**
+ * 鍋滄绾跨▼姹�
+ * 鍏堜娇鐢╯hutdown, 鍋滄鎺ユ敹鏂颁换鍔″苟灏濊瘯瀹屾垚鎵�鏈夊凡瀛樺湪浠诲姟.
+ * 濡傛灉瓒呮椂, 鍒欒皟鐢╯hutdownNow, 鍙栨秷鍦╳orkQueue涓璓ending鐨勪换鍔�,骞朵腑鏂墍鏈夐樆濉炲嚱鏁�.
+ * 濡傛灉浠嶇劧瓒呮檪锛屽墖寮峰埗閫�鍑�.
+ * 鍙﹀鍦╯hutdown鏃剁嚎绋嬫湰韬璋冪敤涓柇鍋氫簡澶勭悊.
+ */
+ 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);
+ }
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/TreeBuildUtils.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/TreeBuildUtils.java
new file mode 100644
index 0000000..d0163e6
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/TreeBuildUtils.java
@@ -0,0 +1,35 @@
+package org.dromara.common.core.utils;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.lang.tree.Tree;
+import cn.hutool.core.lang.tree.TreeNodeConfig;
+import cn.hutool.core.lang.tree.TreeUtil;
+import cn.hutool.core.lang.tree.parser.NodeParser;
+import org.dromara.common.core.utils.reflect.ReflectUtils;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+/**
+ * 鎵╁睍 hutool TreeUtil 灏佽绯荤粺鏍戞瀯寤�
+ *
+ * @author Lion Li
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class TreeBuildUtils extends TreeUtil {
+
+ /**
+ * 鏍规嵁鍓嶇瀹氬埗宸紓鍖栧瓧娈�
+ */
+ public static final TreeNodeConfig DEFAULT_CONFIG = TreeNodeConfig.DEFAULT_CONFIG.setNameKey("label");
+
+ public static <T, K> List<Tree<K>> build(List<T> list, NodeParser<T, K> nodeParser) {
+ if (CollUtil.isEmpty(list)) {
+ return null;
+ }
+ K k = ReflectUtils.invokeGetter(list.get(0), "parentId");
+ return TreeUtil.build(list, k, DEFAULT_CONFIG, nodeParser);
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/ValidatorUtils.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/ValidatorUtils.java
new file mode 100644
index 0000000..f94f916
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/ValidatorUtils.java
@@ -0,0 +1,28 @@
+package org.dromara.common.core.utils;
+
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+
+import jakarta.validation.ConstraintViolation;
+import jakarta.validation.ConstraintViolationException;
+import jakarta.validation.Validator;
+import java.util.Set;
+
+/**
+ * Validator 鏍¢獙妗嗘灦宸ュ叿
+ *
+ * @author Lion Li
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class ValidatorUtils {
+
+ private static final Validator VALID = SpringUtils.getBean(Validator.class);
+
+ public static <T> void validate(T object, Class<?>... groups) {
+ Set<ConstraintViolation<T>> validate = VALID.validate(object, groups);
+ if (!validate.isEmpty()) {
+ throw new ConstraintViolationException("鍙傛暟鏍¢獙寮傚父", validate);
+ }
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/file/FileUtils.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/file/FileUtils.java
new file mode 100644
index 0000000..573b207
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/file/FileUtils.java
@@ -0,0 +1,43 @@
+package org.dromara.common.core.utils.file;
+
+import cn.hutool.core.io.FileUtil;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * 鏂囦欢澶勭悊宸ュ叿绫�
+ *
+ * @author Lion Li
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class FileUtils extends FileUtil {
+
+ /**
+ * 涓嬭浇鏂囦欢鍚嶉噸鏂扮紪鐮�
+ *
+ * @param response 鍝嶅簲瀵硅薄
+ * @param realFileName 鐪熷疄鏂囦欢鍚�
+ */
+ public static void setAttachmentResponseHeader(HttpServletResponse response, String realFileName) {
+ String percentEncodedFileName = percentEncode(realFileName);
+ String contentDispositionValue = "attachment; filename=%s;filename*=utf-8''%s".formatted(percentEncodedFileName, percentEncodedFileName);
+ response.addHeader("Access-Control-Expose-Headers", "Content-Disposition,download-filename");
+ response.setHeader("Content-disposition", contentDispositionValue);
+ response.setHeader("download-filename", percentEncodedFileName);
+ }
+
+ /**
+ * 鐧惧垎鍙风紪鐮佸伐鍏锋柟娉�
+ *
+ * @param s 闇�瑕佺櫨鍒嗗彿缂栫爜鐨勫瓧绗︿覆
+ * @return 鐧惧垎鍙风紪鐮佸悗鐨勫瓧绗︿覆
+ */
+ public static String percentEncode(String s) {
+ String encode = URLEncoder.encode(s, StandardCharsets.UTF_8);
+ return encode.replaceAll("\\+", "%20");
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/file/MimeTypeUtils.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/file/MimeTypeUtils.java
new file mode 100644
index 0000000..23fa2cf
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/file/MimeTypeUtils.java
@@ -0,0 +1,40 @@
+package org.dromara.common.core.utils.file;
+
+/**
+ * 濯掍綋绫诲瀷宸ュ叿绫�
+ *
+ * @author ruoyi
+ */
+public class MimeTypeUtils {
+ public static final String IMAGE_PNG = "image/png";
+
+ public static final String IMAGE_JPG = "image/jpg";
+
+ public static final String IMAGE_JPEG = "image/jpeg";
+
+ public static final String IMAGE_BMP = "image/bmp";
+
+ public static final String IMAGE_GIF = "image/gif";
+
+ public static final String[] IMAGE_EXTENSION = {"bmp", "gif", "jpg", "jpeg", "png"};
+
+ public static final String[] FLASH_EXTENSION = {"swf", "flv"};
+
+ public static final String[] MEDIA_EXTENSION = {"swf", "flv", "mp3", "wav", "wma", "wmv", "mid", "avi", "mpg",
+ "asf", "rm", "rmvb"};
+
+ public static final String[] VIDEO_EXTENSION = {"mp4", "avi", "rmvb"};
+
+ public static final String[] DEFAULT_ALLOWED_EXTENSION = {
+ // 鍥剧墖
+ "bmp", "gif", "jpg", "jpeg", "png",
+ // word excel powerpoint
+ "doc", "docx", "xls", "xlsx", "ppt", "pptx", "html", "htm", "txt",
+ // 鍘嬬缉鏂囦欢
+ "rar", "zip", "gz", "bz2",
+ // 瑙嗛鏍煎紡
+ "mp4", "avi", "rmvb",
+ // pdf
+ "pdf"};
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/ip/AddressUtils.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/ip/AddressUtils.java
new file mode 100644
index 0000000..3f7cd57
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/ip/AddressUtils.java
@@ -0,0 +1,33 @@
+package org.dromara.common.core.utils.ip;
+
+import cn.hutool.core.net.NetUtil;
+import cn.hutool.http.HtmlUtil;
+import org.dromara.common.core.utils.StringUtils;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * 鑾峰彇鍦板潃绫�
+ *
+ * @author Lion Li
+ */
+@Slf4j
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class AddressUtils {
+
+ // 鏈煡鍦板潃
+ public static final String UNKNOWN = "XX XX";
+
+ public static String getRealAddressByIP(String ip) {
+ if (StringUtils.isBlank(ip)) {
+ return UNKNOWN;
+ }
+ // 鍐呯綉涓嶆煡璇�
+ ip = StringUtils.contains(ip, "0:0:0:0:0:0:0:1") ? "127.0.0.1" : HtmlUtil.cleanHtmlTag(ip);
+ if (NetUtil.isInnerIP(ip)) {
+ return "鍐呯綉IP";
+ }
+ return RegionUtils.getCityInfo(ip);
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/ip/RegionUtils.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/ip/RegionUtils.java
new file mode 100644
index 0000000..6e2a44e
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/ip/RegionUtils.java
@@ -0,0 +1,67 @@
+package org.dromara.common.core.utils.ip;
+
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.io.resource.ClassPathResource;
+import cn.hutool.core.util.ObjectUtil;
+import org.dromara.common.core.exception.ServiceException;
+import org.dromara.common.core.utils.file.FileUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.lionsoul.ip2region.xdb.Searcher;
+
+import java.io.File;
+
+/**
+ * 鏍规嵁ip鍦板潃瀹氫綅宸ュ叿绫伙紝绂荤嚎鏂瑰紡
+ * 鍙傝�冨湴鍧�锛�<a href="https://gitee.com/lionsoul/ip2region/tree/master/binding/java">闆嗘垚 ip2region 瀹炵幇绂荤嚎IP鍦板潃瀹氫綅搴�</a>
+ *
+ * @author lishuyan
+ */
+@Slf4j
+public class RegionUtils {
+
+ private static final Searcher SEARCHER;
+
+ static {
+ String fileName = "/ip2region.xdb";
+ File existFile = FileUtils.file(FileUtil.getTmpDir() + FileUtil.FILE_SEPARATOR + fileName);
+ if (!FileUtils.exist(existFile)) {
+ ClassPathResource fileStream = new ClassPathResource(fileName);
+ if (ObjectUtil.isEmpty(fileStream.getStream())) {
+ throw new ServiceException("RegionUtils鍒濆鍖栧け璐ワ紝鍘熷洜锛欼P鍦板潃搴撴暟鎹笉瀛樺湪锛�");
+ }
+ FileUtils.writeFromStream(fileStream.getStream(), existFile);
+ }
+
+ String dbPath = existFile.getPath();
+
+ // 1銆佷粠 dbPath 鍔犺浇鏁翠釜 xdb 鍒板唴瀛樸��
+ byte[] cBuff;
+ try {
+ cBuff = Searcher.loadContentFromFile(dbPath);
+ } catch (Exception e) {
+ throw new ServiceException("RegionUtils鍒濆鍖栧け璐ワ紝鍘熷洜锛氫粠ip2region.xdb鏂囦欢鍔犺浇鍐呭澶辫触锛�" + e.getMessage());
+ }
+ // 2銆佷娇鐢ㄤ笂杩扮殑 cBuff 鍒涘缓涓�涓畬鍏ㄥ熀浜庡唴瀛樼殑鏌ヨ瀵硅薄銆�
+ try {
+ SEARCHER = Searcher.newWithBuffer(cBuff);
+ } catch (Exception e) {
+ throw new ServiceException("RegionUtils鍒濆鍖栧け璐ワ紝鍘熷洜锛�" + e.getMessage());
+ }
+ }
+
+ /**
+ * 鏍规嵁IP鍦板潃绂荤嚎鑾峰彇鍩庡競
+ */
+ public static String getCityInfo(String ip) {
+ try {
+ ip = ip.trim();
+ // 3銆佹墽琛屾煡璇�
+ String region = SEARCHER.search(ip);
+ return region.replace("0|", "").replace("|0", "");
+ } catch (Exception e) {
+ log.error("IP鍦板潃绂荤嚎鑾峰彇鍩庡競寮傚父 {}", ip);
+ return "鏈煡";
+ }
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/reflect/ReflectUtils.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/reflect/ReflectUtils.java
new file mode 100644
index 0000000..367e8c9
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/reflect/ReflectUtils.java
@@ -0,0 +1,56 @@
+package org.dromara.common.core.utils.reflect;
+
+import cn.hutool.core.util.ReflectUtil;
+import org.dromara.common.core.utils.StringUtils;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+
+import java.lang.reflect.Method;
+
+/**
+ * 鍙嶅皠宸ュ叿绫�. 鎻愪緵璋冪敤getter/setter鏂规硶, 璁块棶绉佹湁鍙橀噺, 璋冪敤绉佹湁鏂规硶, 鑾峰彇娉涘瀷绫诲瀷Class, 琚獳OP杩囩殑鐪熷疄绫荤瓑宸ュ叿鍑芥暟.
+ *
+ * @author Lion Li
+ */
+@SuppressWarnings("rawtypes")
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class ReflectUtils extends ReflectUtil {
+
+ private static final String SETTER_PREFIX = "set";
+
+ private static final String GETTER_PREFIX = "get";
+
+ /**
+ * 璋冪敤Getter鏂规硶.
+ * 鏀寔澶氱骇锛屽锛氬璞″悕.瀵硅薄鍚�.鏂规硶
+ */
+ @SuppressWarnings("unchecked")
+ public static <E> E invokeGetter(Object obj, String propertyName) {
+ Object object = obj;
+ for (String name : StringUtils.split(propertyName, ".")) {
+ String getterMethodName = GETTER_PREFIX + StringUtils.capitalize(name);
+ object = invoke(object, getterMethodName);
+ }
+ return (E) object;
+ }
+
+ /**
+ * 璋冪敤Setter鏂规硶, 浠呭尮閰嶆柟娉曞悕銆�
+ * 鏀寔澶氱骇锛屽锛氬璞″悕.瀵硅薄鍚�.鏂规硶
+ */
+ public static <E> void invokeSetter(Object obj, String propertyName, E value) {
+ Object object = obj;
+ String[] names = StringUtils.split(propertyName, ".");
+ for (int i = 0; i < names.length; i++) {
+ if (i < names.length - 1) {
+ String getterMethodName = GETTER_PREFIX + StringUtils.capitalize(names[i]);
+ object = invoke(object, getterMethodName);
+ } else {
+ String setterMethodName = SETTER_PREFIX + StringUtils.capitalize(names[i]);
+ Method method = getMethodByName(object.getClass(), setterMethodName);
+ invoke(object, method, value);
+ }
+ }
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/sql/SqlUtil.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/sql/SqlUtil.java
new file mode 100644
index 0000000..3e109b2
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/sql/SqlUtil.java
@@ -0,0 +1,56 @@
+package org.dromara.common.core.utils.sql;
+
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import org.dromara.common.core.utils.StringUtils;
+
+/**
+ * sql鎿嶄綔宸ュ叿绫�
+ *
+ * @author ruoyi
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class SqlUtil {
+
+ /**
+ * 瀹氫箟甯哥敤鐨� sql鍏抽敭瀛�
+ */
+ public static final String SQL_REGEX = "select |insert |delete |update |drop |count |exec |chr |mid |master |truncate |char |and |declare ";
+
+ /**
+ * 浠呮敮鎸佸瓧姣嶃�佹暟瀛椼�佷笅鍒掔嚎銆佺┖鏍笺�侀�楀彿銆佸皬鏁扮偣锛堟敮鎸佸涓瓧娈垫帓搴忥級
+ */
+ public static final String SQL_PATTERN = "[a-zA-Z0-9_\\ \\,\\.]+";
+
+ /**
+ * 妫�鏌ュ瓧绗︼紝闃叉娉ㄥ叆缁曡繃
+ */
+ public static String escapeOrderBySql(String value) {
+ if (StringUtils.isNotEmpty(value) && !isValidOrderBySql(value)) {
+ throw new IllegalArgumentException("鍙傛暟涓嶇鍚堣鑼冿紝涓嶈兘杩涜鏌ヨ");
+ }
+ return value;
+ }
+
+ /**
+ * 楠岃瘉 order by 璇硶鏄惁绗﹀悎瑙勮寖
+ */
+ public static boolean isValidOrderBySql(String value) {
+ return value.matches(SQL_PATTERN);
+ }
+
+ /**
+ * SQL鍏抽敭瀛楁鏌�
+ */
+ public static void filterKeyword(String value) {
+ if (StringUtils.isEmpty(value)) {
+ return;
+ }
+ String[] sqlKeywords = StringUtils.split(SQL_REGEX, "\\|");
+ for (String sqlKeyword : sqlKeywords) {
+ if (StringUtils.indexOfIgnoreCase(value, sqlKeyword) > -1) {
+ throw new IllegalArgumentException("鍙傛暟瀛樺湪SQL娉ㄥ叆椋庨櫓");
+ }
+ }
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/validate/AddGroup.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/validate/AddGroup.java
new file mode 100644
index 0000000..0275899
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/validate/AddGroup.java
@@ -0,0 +1,9 @@
+package org.dromara.common.core.validate;
+
+/**
+ * 鏍¢獙鍒嗙粍 add
+ *
+ * @author Lion Li
+ */
+public interface AddGroup {
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/validate/EditGroup.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/validate/EditGroup.java
new file mode 100644
index 0000000..77c5040
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/validate/EditGroup.java
@@ -0,0 +1,9 @@
+package org.dromara.common.core.validate;
+
+/**
+ * 鏍¢獙鍒嗙粍 edit
+ *
+ * @author Lion Li
+ */
+public interface EditGroup {
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/validate/QueryGroup.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/validate/QueryGroup.java
new file mode 100644
index 0000000..02a0ac2
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/validate/QueryGroup.java
@@ -0,0 +1,9 @@
+package org.dromara.common.core.validate;
+
+/**
+ * 鏍¢獙鍒嗙粍 query
+ *
+ * @author Lion Li
+ */
+public interface QueryGroup {
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/xss/Xss.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/xss/Xss.java
new file mode 100644
index 0000000..eed495f
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/xss/Xss.java
@@ -0,0 +1,26 @@
+package org.dromara.common.core.xss;
+
+import jakarta.validation.Constraint;
+import jakarta.validation.Payload;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 鑷畾涔墄ss鏍¢獙娉ㄨВ
+ *
+ * @author Lion Li
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(value = {ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
+@Constraint(validatedBy = {XssValidator.class})
+public @interface Xss {
+
+ String message() default "涓嶅厑璁镐换浣曡剼鏈繍琛�";
+
+ Class<?>[] groups() default {};
+
+ Class<? extends Payload>[] payload() default {};
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/xss/XssValidator.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/xss/XssValidator.java
new file mode 100644
index 0000000..9c32563
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/xss/XssValidator.java
@@ -0,0 +1,21 @@
+package org.dromara.common.core.xss;
+
+import cn.hutool.core.util.ReUtil;
+import cn.hutool.http.HtmlUtil;
+
+import jakarta.validation.ConstraintValidator;
+import jakarta.validation.ConstraintValidatorContext;
+
+/**
+ * 鑷畾涔墄ss鏍¢獙娉ㄨВ瀹炵幇
+ *
+ * @author Lion Li
+ */
+public class XssValidator implements ConstraintValidator<Xss, String> {
+
+ @Override
+ public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
+ return !ReUtil.contains(HtmlUtil.RE_HTML_MARK, value);
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/ruoyi-common/ruoyi-common-core/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..ddd3025
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1,4 @@
+org.dromara.common.core.utils.SpringUtils
+org.dromara.common.core.config.ApplicationConfig
+org.dromara.common.core.config.ValidatorConfig
+org.dromara.common.core.config.AsyncConfig
diff --git a/ruoyi-common/ruoyi-common-core/src/main/resources/i18n/messages.properties b/ruoyi-common/ruoyi-common-core/src/main/resources/i18n/messages.properties
new file mode 100644
index 0000000..cce11c8
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/resources/i18n/messages.properties
@@ -0,0 +1,61 @@
+#閿欒娑堟伅
+not.null=* 蹇呴』濉啓
+user.jcaptcha.error=楠岃瘉鐮侀敊璇�
+user.jcaptcha.expire=楠岃瘉鐮佸凡澶辨晥
+user.not.exists=瀵逛笉璧�, 鎮ㄧ殑璐﹀彿锛歿0} 涓嶅瓨鍦�.
+user.password.not.match=鐢ㄦ埛涓嶅瓨鍦�/瀵嗙爜閿欒
+user.password.retry.limit.count=瀵嗙爜杈撳叆閿欒{0}娆�
+user.password.retry.limit.exceed=瀵嗙爜杈撳叆閿欒{0}娆★紝甯愭埛閿佸畾{1}鍒嗛挓
+user.password.delete=瀵逛笉璧凤紝鎮ㄧ殑璐﹀彿锛歿0} 宸茶鍒犻櫎
+user.blocked=瀵逛笉璧凤紝鎮ㄧ殑璐﹀彿锛歿0} 宸茬鐢紝璇疯仈绯荤鐞嗗憳
+role.blocked=瑙掕壊宸插皝绂侊紝璇疯仈绯荤鐞嗗憳
+user.logout.success=閫�鍑烘垚鍔�
+length.not.valid=闀垮害蹇呴』鍦▄min}鍒皗max}涓瓧绗︿箣闂�
+user.username.not.blank=鐢ㄦ埛鍚嶄笉鑳戒负绌�
+user.username.not.valid=* 2鍒�20涓眽瀛椼�佸瓧姣嶃�佹暟瀛楁垨涓嬪垝绾跨粍鎴愶紝涓斿繀椤讳互闈炴暟瀛楀紑澶�
+user.username.length.valid=璐︽埛闀垮害蹇呴』鍦▄min}鍒皗max}涓瓧绗︿箣闂�
+user.password.not.blank=鐢ㄦ埛瀵嗙爜涓嶈兘涓虹┖
+user.password.length.valid=鐢ㄦ埛瀵嗙爜闀垮害蹇呴』鍦▄min}鍒皗max}涓瓧绗︿箣闂�
+user.password.not.valid=* 5-50涓瓧绗�
+user.email.not.valid=閭鏍煎紡閿欒
+user.email.not.blank=閭涓嶈兘涓虹┖
+user.phonenumber.not.blank=鐢ㄦ埛鎵嬫満鍙蜂笉鑳戒负绌�
+user.mobile.phone.number.not.valid=鎵嬫満鍙锋牸寮忛敊璇�
+user.login.success=鐧诲綍鎴愬姛
+user.register.success=娉ㄥ唽鎴愬姛
+user.register.save.error=淇濆瓨鐢ㄦ埛 {0} 澶辫触锛屾敞鍐岃处鍙峰凡瀛樺湪
+user.register.error=娉ㄥ唽澶辫触锛岃鑱旂郴绯荤粺绠$悊浜哄憳
+user.notfound=璇烽噸鏂扮櫥褰�
+user.forcelogout=绠$悊鍛樺己鍒堕��鍑猴紝璇烽噸鏂扮櫥褰�
+user.unknown.error=鏈煡閿欒锛岃閲嶆柊鐧诲綍
+auth.grant.type.error=璁よ瘉鏉冮檺绫诲瀷閿欒
+auth.grant.type.blocked=璁よ瘉鏉冮檺绫诲瀷宸茬鐢�
+auth.grant.type.not.blank=璁よ瘉鏉冮檺绫诲瀷涓嶈兘涓虹┖
+auth.clientid.not.blank=璁よ瘉瀹㈡埛绔痠d涓嶈兘涓虹┖
+##鏂囦欢涓婁紶娑堟伅
+upload.exceed.maxSize=涓婁紶鐨勬枃浠跺ぇ灏忚秴鍑洪檺鍒剁殑鏂囦欢澶у皬锛�<br/>鍏佽鐨勬枃浠舵渶澶уぇ灏忔槸锛歿0}MB锛�
+upload.filename.exceed.length=涓婁紶鐨勬枃浠跺悕鏈�闀縶0}涓瓧绗�
+##鏉冮檺
+no.permission=鎮ㄦ病鏈夋暟鎹殑鏉冮檺锛岃鑱旂郴绠$悊鍛樻坊鍔犳潈闄� [{0}]
+no.create.permission=鎮ㄦ病鏈夊垱寤烘暟鎹殑鏉冮檺锛岃鑱旂郴绠$悊鍛樻坊鍔犳潈闄� [{0}]
+no.update.permission=鎮ㄦ病鏈変慨鏀规暟鎹殑鏉冮檺锛岃鑱旂郴绠$悊鍛樻坊鍔犳潈闄� [{0}]
+no.delete.permission=鎮ㄦ病鏈夊垹闄ゆ暟鎹殑鏉冮檺锛岃鑱旂郴绠$悊鍛樻坊鍔犳潈闄� [{0}]
+no.export.permission=鎮ㄦ病鏈夊鍑烘暟鎹殑鏉冮檺锛岃鑱旂郴绠$悊鍛樻坊鍔犳潈闄� [{0}]
+no.view.permission=鎮ㄦ病鏈夋煡鐪嬫暟鎹殑鏉冮檺锛岃鑱旂郴绠$悊鍛樻坊鍔犳潈闄� [{0}]
+repeat.submit.message=涓嶅厑璁搁噸澶嶆彁浜わ紝璇风◢鍊欏啀璇�
+rate.limiter.message=璁块棶杩囦簬棰戠箒锛岃绋嶅�欏啀璇�
+sms.code.not.blank=鐭俊楠岃瘉鐮佷笉鑳戒负绌�
+sms.code.retry.limit.count=鐭俊楠岃瘉鐮佽緭鍏ラ敊璇瘂0}娆�
+sms.code.retry.limit.exceed=鐭俊楠岃瘉鐮佽緭鍏ラ敊璇瘂0}娆★紝甯愭埛閿佸畾{1}鍒嗛挓
+email.code.not.blank=閭楠岃瘉鐮佷笉鑳戒负绌�
+email.code.retry.limit.count=閭楠岃瘉鐮佽緭鍏ラ敊璇瘂0}娆�
+email.code.retry.limit.exceed=閭楠岃瘉鐮佽緭鍏ラ敊璇瘂0}娆★紝甯愭埛閿佸畾{1}鍒嗛挓
+xcx.code.not.blank=灏忕▼搴廩code]涓嶈兘涓虹┖
+social.source.not.blank=绗笁鏂圭櫥褰曞钩鍙癧source]涓嶈兘涓虹┖
+social.code.not.blank=绗笁鏂圭櫥褰曞钩鍙癧code]涓嶈兘涓虹┖
+social.state.not.blank=绗笁鏂圭櫥褰曞钩鍙癧state]涓嶈兘涓虹┖
+##绉熸埛
+tenant.number.not.blank=绉熸埛缂栧彿涓嶈兘涓虹┖
+tenant.not.exists=瀵逛笉璧�, 鎮ㄧ殑绉熸埛涓嶅瓨鍦紝璇疯仈绯荤鐞嗗憳
+tenant.blocked=瀵逛笉璧凤紝鎮ㄧ殑绉熸埛宸茬鐢紝璇疯仈绯荤鐞嗗憳
+tenant.expired=瀵逛笉璧凤紝鎮ㄧ殑绉熸埛宸茶繃鏈燂紝璇疯仈绯荤鐞嗗憳
diff --git a/ruoyi-common/ruoyi-common-core/src/main/resources/i18n/messages_en_US.properties b/ruoyi-common/ruoyi-common-core/src/main/resources/i18n/messages_en_US.properties
new file mode 100644
index 0000000..f948c4a
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/resources/i18n/messages_en_US.properties
@@ -0,0 +1,61 @@
+#閿欒娑堟伅
+not.null=* Required fill in
+user.jcaptcha.error=Captcha error
+user.jcaptcha.expire=Captcha invalid
+user.not.exists=Sorry, your account: {0} does not exist
+user.password.not.match=User does not exist/Password error
+user.password.retry.limit.count=Password input error {0} times
+user.password.retry.limit.exceed=Password input error {0} times, account locked for {1} minutes
+user.password.delete=Sorry, your account锛歿0} has been deleted
+user.blocked=Sorry, your account: {0} has been disabled. Please contact the administrator
+role.blocked=Role disabled锛宲lease contact administrators
+user.logout.success=Exit successful
+length.not.valid=The length must be between {min} and {max} characters
+user.username.not.blank=Username cannot be blank
+user.username.not.valid=* 2 to 20 chinese characters, letters, numbers or underscores, and must start with a non number
+user.username.length.valid=Account length must be between {min} and {max} characters
+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.email.not.valid=Mailbox format error
+user.email.not.blank=Mailbox cannot be blank
+user.phonenumber.not.blank=Phone number cannot be blank
+user.mobile.phone.number.not.valid=Phone number format error
+user.login.success=Login successful
+user.register.success=Register successful
+user.register.save.error=Failed to save user {0}, The registered account already exists
+user.register.error=Register failed, please contact system administrator
+user.notfound=Please login again
+user.forcelogout=The administrator is forced to exit锛宲lease login again
+user.unknown.error=Unknown error, please login again
+auth.grant.type.error=Auth grant type error
+auth.grant.type.blocked=Auth grant type disabled
+auth.grant.type.not.blank=Auth grant type cannot be blank
+auth.clientid.not.blank=Auth clientid cannot be blank
+##鏂囦欢涓婁紶娑堟伅
+upload.exceed.maxSize=The uploaded file size exceeds the limit file size锛�<br/>the maximum allowed file size is锛歿0}MB锛�
+upload.filename.exceed.length=The maximum length of uploaded file name is {0} characters
+##鏉冮檺
+no.permission=You do not have permission to the data锛宲lease contact your administrator to add permissions [{0}]
+no.create.permission=You do not have permission to create data锛宲lease contact your administrator to add permissions [{0}]
+no.update.permission=You do not have permission to modify data锛宲lease contact your administrator to add permissions [{0}]
+no.delete.permission=You do not have permission to delete data锛宲lease contact your administrator to add permissions [{0}]
+no.export.permission=You do not have permission to export data锛宲lease contact your administrator to add permissions [{0}]
+no.view.permission=You do not have permission to view data锛宲lease contact your administrator to add permissions [{0}]
+repeat.submit.message=Repeat submit is not allowed, please try again later
+rate.limiter.message=Visit too frequently, please try again later
+sms.code.not.blank=Sms code cannot be blank
+sms.code.retry.limit.count=Sms code input error {0} times
+sms.code.retry.limit.exceed=Sms code input error {0} times, account locked for {1} minutes
+email.code.not.blank=Email code cannot be blank
+email.code.retry.limit.count=Email code input error {0} times
+email.code.retry.limit.exceed=Email code input error {0} times, account locked for {1} minutes
+xcx.code.not.blank=Mini program [code] cannot be blank
+social.source.not.blank=Social login platform [source] cannot be blank
+social.code.not.blank=Social login platform [code] cannot be blank
+social.state.not.blank=Social login platform [state] cannot be blank
+##绉熸埛
+tenant.number.not.blank=Tenant number cannot be blank
+tenant.not.exists=Sorry, your tenant does not exist. Please contact the administrator
+tenant.blocked=Sorry, your tenant is disabled. Please contact the administrator
+tenant.expired=Sorry, your tenant has expired. Please contact the administrator.
diff --git a/ruoyi-common/ruoyi-common-core/src/main/resources/i18n/messages_zh_CN.properties b/ruoyi-common/ruoyi-common-core/src/main/resources/i18n/messages_zh_CN.properties
new file mode 100644
index 0000000..cce11c8
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/resources/i18n/messages_zh_CN.properties
@@ -0,0 +1,61 @@
+#閿欒娑堟伅
+not.null=* 蹇呴』濉啓
+user.jcaptcha.error=楠岃瘉鐮侀敊璇�
+user.jcaptcha.expire=楠岃瘉鐮佸凡澶辨晥
+user.not.exists=瀵逛笉璧�, 鎮ㄧ殑璐﹀彿锛歿0} 涓嶅瓨鍦�.
+user.password.not.match=鐢ㄦ埛涓嶅瓨鍦�/瀵嗙爜閿欒
+user.password.retry.limit.count=瀵嗙爜杈撳叆閿欒{0}娆�
+user.password.retry.limit.exceed=瀵嗙爜杈撳叆閿欒{0}娆★紝甯愭埛閿佸畾{1}鍒嗛挓
+user.password.delete=瀵逛笉璧凤紝鎮ㄧ殑璐﹀彿锛歿0} 宸茶鍒犻櫎
+user.blocked=瀵逛笉璧凤紝鎮ㄧ殑璐﹀彿锛歿0} 宸茬鐢紝璇疯仈绯荤鐞嗗憳
+role.blocked=瑙掕壊宸插皝绂侊紝璇疯仈绯荤鐞嗗憳
+user.logout.success=閫�鍑烘垚鍔�
+length.not.valid=闀垮害蹇呴』鍦▄min}鍒皗max}涓瓧绗︿箣闂�
+user.username.not.blank=鐢ㄦ埛鍚嶄笉鑳戒负绌�
+user.username.not.valid=* 2鍒�20涓眽瀛椼�佸瓧姣嶃�佹暟瀛楁垨涓嬪垝绾跨粍鎴愶紝涓斿繀椤讳互闈炴暟瀛楀紑澶�
+user.username.length.valid=璐︽埛闀垮害蹇呴』鍦▄min}鍒皗max}涓瓧绗︿箣闂�
+user.password.not.blank=鐢ㄦ埛瀵嗙爜涓嶈兘涓虹┖
+user.password.length.valid=鐢ㄦ埛瀵嗙爜闀垮害蹇呴』鍦▄min}鍒皗max}涓瓧绗︿箣闂�
+user.password.not.valid=* 5-50涓瓧绗�
+user.email.not.valid=閭鏍煎紡閿欒
+user.email.not.blank=閭涓嶈兘涓虹┖
+user.phonenumber.not.blank=鐢ㄦ埛鎵嬫満鍙蜂笉鑳戒负绌�
+user.mobile.phone.number.not.valid=鎵嬫満鍙锋牸寮忛敊璇�
+user.login.success=鐧诲綍鎴愬姛
+user.register.success=娉ㄥ唽鎴愬姛
+user.register.save.error=淇濆瓨鐢ㄦ埛 {0} 澶辫触锛屾敞鍐岃处鍙峰凡瀛樺湪
+user.register.error=娉ㄥ唽澶辫触锛岃鑱旂郴绯荤粺绠$悊浜哄憳
+user.notfound=璇烽噸鏂扮櫥褰�
+user.forcelogout=绠$悊鍛樺己鍒堕��鍑猴紝璇烽噸鏂扮櫥褰�
+user.unknown.error=鏈煡閿欒锛岃閲嶆柊鐧诲綍
+auth.grant.type.error=璁よ瘉鏉冮檺绫诲瀷閿欒
+auth.grant.type.blocked=璁よ瘉鏉冮檺绫诲瀷宸茬鐢�
+auth.grant.type.not.blank=璁よ瘉鏉冮檺绫诲瀷涓嶈兘涓虹┖
+auth.clientid.not.blank=璁よ瘉瀹㈡埛绔痠d涓嶈兘涓虹┖
+##鏂囦欢涓婁紶娑堟伅
+upload.exceed.maxSize=涓婁紶鐨勬枃浠跺ぇ灏忚秴鍑洪檺鍒剁殑鏂囦欢澶у皬锛�<br/>鍏佽鐨勬枃浠舵渶澶уぇ灏忔槸锛歿0}MB锛�
+upload.filename.exceed.length=涓婁紶鐨勬枃浠跺悕鏈�闀縶0}涓瓧绗�
+##鏉冮檺
+no.permission=鎮ㄦ病鏈夋暟鎹殑鏉冮檺锛岃鑱旂郴绠$悊鍛樻坊鍔犳潈闄� [{0}]
+no.create.permission=鎮ㄦ病鏈夊垱寤烘暟鎹殑鏉冮檺锛岃鑱旂郴绠$悊鍛樻坊鍔犳潈闄� [{0}]
+no.update.permission=鎮ㄦ病鏈変慨鏀规暟鎹殑鏉冮檺锛岃鑱旂郴绠$悊鍛樻坊鍔犳潈闄� [{0}]
+no.delete.permission=鎮ㄦ病鏈夊垹闄ゆ暟鎹殑鏉冮檺锛岃鑱旂郴绠$悊鍛樻坊鍔犳潈闄� [{0}]
+no.export.permission=鎮ㄦ病鏈夊鍑烘暟鎹殑鏉冮檺锛岃鑱旂郴绠$悊鍛樻坊鍔犳潈闄� [{0}]
+no.view.permission=鎮ㄦ病鏈夋煡鐪嬫暟鎹殑鏉冮檺锛岃鑱旂郴绠$悊鍛樻坊鍔犳潈闄� [{0}]
+repeat.submit.message=涓嶅厑璁搁噸澶嶆彁浜わ紝璇风◢鍊欏啀璇�
+rate.limiter.message=璁块棶杩囦簬棰戠箒锛岃绋嶅�欏啀璇�
+sms.code.not.blank=鐭俊楠岃瘉鐮佷笉鑳戒负绌�
+sms.code.retry.limit.count=鐭俊楠岃瘉鐮佽緭鍏ラ敊璇瘂0}娆�
+sms.code.retry.limit.exceed=鐭俊楠岃瘉鐮佽緭鍏ラ敊璇瘂0}娆★紝甯愭埛閿佸畾{1}鍒嗛挓
+email.code.not.blank=閭楠岃瘉鐮佷笉鑳戒负绌�
+email.code.retry.limit.count=閭楠岃瘉鐮佽緭鍏ラ敊璇瘂0}娆�
+email.code.retry.limit.exceed=閭楠岃瘉鐮佽緭鍏ラ敊璇瘂0}娆★紝甯愭埛閿佸畾{1}鍒嗛挓
+xcx.code.not.blank=灏忕▼搴廩code]涓嶈兘涓虹┖
+social.source.not.blank=绗笁鏂圭櫥褰曞钩鍙癧source]涓嶈兘涓虹┖
+social.code.not.blank=绗笁鏂圭櫥褰曞钩鍙癧code]涓嶈兘涓虹┖
+social.state.not.blank=绗笁鏂圭櫥褰曞钩鍙癧state]涓嶈兘涓虹┖
+##绉熸埛
+tenant.number.not.blank=绉熸埛缂栧彿涓嶈兘涓虹┖
+tenant.not.exists=瀵逛笉璧�, 鎮ㄧ殑绉熸埛涓嶅瓨鍦紝璇疯仈绯荤鐞嗗憳
+tenant.blocked=瀵逛笉璧凤紝鎮ㄧ殑绉熸埛宸茬鐢紝璇疯仈绯荤鐞嗗憳
+tenant.expired=瀵逛笉璧凤紝鎮ㄧ殑绉熸埛宸茶繃鏈燂紝璇疯仈绯荤鐞嗗憳
diff --git a/ruoyi-common/ruoyi-common-core/src/main/resources/ip2region.xdb b/ruoyi-common/ruoyi-common-core/src/main/resources/ip2region.xdb
new file mode 100644
index 0000000..31f96a1
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/resources/ip2region.xdb
Binary files differ
diff --git a/ruoyi-common/ruoyi-common-dict/pom.xml b/ruoyi-common/ruoyi-common-dict/pom.xml
new file mode 100644
index 0000000..6cf4efc
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-dict/pom.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns="http://maven.apache.org/POM/4.0.0"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common</artifactId>
+ <version>${revision}</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>ruoyi-common-dict</artifactId>
+
+ <description>
+ ruoyi-common-dict 瀛楀吀
+ </description>
+
+ <dependencies>
+
+ <!-- RuoYi Common Security -->
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-redis</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-api-system</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.dubbo</groupId>
+ <artifactId>dubbo-spring-boot-starter</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>cn.dev33</groupId>
+ <artifactId>sa-token-core</artifactId>
+ </dependency>
+
+ </dependencies>
+</project>
diff --git a/ruoyi-common/ruoyi-common-dict/src/main/java/org/dromara/common/dict/service/impl/DictServiceImpl.java b/ruoyi-common/ruoyi-common-dict/src/main/java/org/dromara/common/dict/service/impl/DictServiceImpl.java
new file mode 100644
index 0000000..2de6c55
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-dict/src/main/java/org/dromara/common/dict/service/impl/DictServiceImpl.java
@@ -0,0 +1,92 @@
+package org.dromara.common.dict.service.impl;
+
+import cn.dev33.satoken.context.SaHolder;
+import cn.hutool.core.util.ObjectUtil;
+import org.dromara.common.core.constant.CacheConstants;
+import org.dromara.common.core.service.DictService;
+import org.dromara.common.core.utils.StreamUtils;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.system.api.RemoteDictService;
+import org.apache.dubbo.config.annotation.DubboReference;
+import org.dromara.system.api.domain.vo.RemoteDictDataVo;
+import org.springframework.stereotype.Service;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * 瀛楀吀鏈嶅姟鏈嶅姟
+ *
+ * @author Lion Li
+ */
+@Service
+public class DictServiceImpl implements DictService {
+
+ @DubboReference
+ private RemoteDictService remoteDictService;
+
+ /**
+ * 鏍规嵁瀛楀吀绫诲瀷鍜屽瓧鍏稿�艰幏鍙栧瓧鍏告爣绛�
+ *
+ * @param dictType 瀛楀吀绫诲瀷
+ * @param dictValue 瀛楀吀鍊�
+ * @param separator 鍒嗛殧绗�
+ * @return 瀛楀吀鏍囩
+ */
+ @SuppressWarnings("unchecked cast")
+ @Override
+ public String getDictLabel(String dictType, String dictValue, String separator) {
+ // 浼樺厛浠庢湰鍦扮紦瀛樿幏鍙�
+ List<RemoteDictDataVo> datas = (List<RemoteDictDataVo>) SaHolder.getStorage().get(CacheConstants.SYS_DICT_KEY + dictType);
+ if (ObjectUtil.isNull(datas)) {
+ datas = remoteDictService.selectDictDataByType(dictType);
+ SaHolder.getStorage().set(CacheConstants.SYS_DICT_KEY + dictType, datas);
+ }
+
+ Map<String, String> map = StreamUtils.toMap(datas, RemoteDictDataVo::getDictValue, RemoteDictDataVo::getDictLabel);
+ if (StringUtils.containsAny(dictValue, separator)) {
+ return Arrays.stream(dictValue.split(separator))
+ .map(v -> map.getOrDefault(v, StringUtils.EMPTY))
+ .collect(Collectors.joining(separator));
+ } else {
+ return map.getOrDefault(dictValue, StringUtils.EMPTY);
+ }
+ }
+
+ /**
+ * 鏍规嵁瀛楀吀绫诲瀷鍜屽瓧鍏告爣绛捐幏鍙栧瓧鍏稿��
+ *
+ * @param dictType 瀛楀吀绫诲瀷
+ * @param dictLabel 瀛楀吀鏍囩
+ * @param separator 鍒嗛殧绗�
+ * @return 瀛楀吀鍊�
+ */
+ @SuppressWarnings("unchecked cast")
+ @Override
+ public String getDictValue(String dictType, String dictLabel, String separator) {
+ // 浼樺厛浠庢湰鍦扮紦瀛樿幏鍙�
+ List<RemoteDictDataVo> datas = (List<RemoteDictDataVo>) SaHolder.getStorage().get(CacheConstants.SYS_DICT_KEY + dictType);
+ if (ObjectUtil.isNull(datas)) {
+ datas = remoteDictService.selectDictDataByType(dictType);
+ SaHolder.getStorage().set(CacheConstants.SYS_DICT_KEY + dictType, datas);
+ }
+
+ Map<String, String> map = StreamUtils.toMap(datas, RemoteDictDataVo::getDictLabel, RemoteDictDataVo::getDictValue);
+ if (StringUtils.containsAny(dictLabel, separator)) {
+ return Arrays.stream(dictLabel.split(separator))
+ .map(l -> map.getOrDefault(l, StringUtils.EMPTY))
+ .collect(Collectors.joining(separator));
+ } else {
+ return map.getOrDefault(dictLabel, StringUtils.EMPTY);
+ }
+ }
+
+ @Override
+ public Map<String, String> getAllDictByDictType(String dictType) {
+ List<RemoteDictDataVo> list = remoteDictService.selectDictDataByType(dictType);
+ return StreamUtils.toMap(list, RemoteDictDataVo::getDictValue, RemoteDictDataVo::getDictLabel);
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-dict/src/main/java/org/dromara/common/dict/utils/DictUtils.java b/ruoyi-common/ruoyi-common-dict/src/main/java/org/dromara/common/dict/utils/DictUtils.java
new file mode 100644
index 0000000..93b2403
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-dict/src/main/java/org/dromara/common/dict/utils/DictUtils.java
@@ -0,0 +1,51 @@
+package org.dromara.common.dict.utils;
+
+import org.dromara.common.core.constant.CacheNames;
+import org.dromara.common.redis.utils.CacheUtils;
+import org.dromara.system.api.domain.vo.RemoteDictDataVo;
+
+import java.util.List;
+
+/**
+ * 瀛楀吀宸ュ叿绫�
+ *
+ * @author ruoyi
+ */
+public class DictUtils {
+ /**
+ * 璁剧疆瀛楀吀缂撳瓨
+ *
+ * @param key 鍙傛暟閿�
+ * @param dictDatas 瀛楀吀鏁版嵁鍒楄〃
+ */
+ public static void setDictCache(String key, List<RemoteDictDataVo> dictDatas) {
+ CacheUtils.put(CacheNames.SYS_DICT, key, dictDatas);
+ }
+
+ /**
+ * 鑾峰彇瀛楀吀缂撳瓨
+ *
+ * @param key 鍙傛暟閿�
+ * @return dictDatas 瀛楀吀鏁版嵁鍒楄〃
+ */
+ public static List<RemoteDictDataVo> getDictCache(String key) {
+ return CacheUtils.get(CacheNames.SYS_DICT, key);
+ }
+
+ /**
+ * 鍒犻櫎鎸囧畾瀛楀吀缂撳瓨
+ *
+ * @param key 瀛楀吀閿�
+ */
+ public static void removeDictCache(String key) {
+ CacheUtils.evict(CacheNames.SYS_DICT, key);
+ }
+
+ /**
+ * 娓呯┖瀛楀吀缂撳瓨
+ */
+ public static void clearDictCache() {
+ CacheUtils.clear(CacheNames.SYS_DICT);
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-dict/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/ruoyi-common/ruoyi-common-dict/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..2b0acfe
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-dict/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+org.dromara.common.dict.service.impl.DictServiceImpl
diff --git a/ruoyi-common/ruoyi-common-doc/pom.xml b/ruoyi-common/ruoyi-common-doc/pom.xml
new file mode 100644
index 0000000..26a35fc
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-doc/pom.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common</artifactId>
+ <version>${revision}</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>ruoyi-common-doc</artifactId>
+
+ <description>
+ ruoyi-common-doc 绯荤粺鎺ュ彛
+ </description>
+
+ <dependencies>
+
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-core</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.springdoc</groupId>
+ <artifactId>springdoc-openapi-starter-webmvc-api</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>com.github.therapi</groupId>
+ <artifactId>therapi-runtime-javadoc</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>com.fasterxml.jackson.module</groupId>
+ <artifactId>jackson-module-kotlin</artifactId>
+ </dependency>
+
+ </dependencies>
+
+</project>
diff --git a/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/config/PlusPaths.java b/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/config/PlusPaths.java
new file mode 100644
index 0000000..23075d2
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/config/PlusPaths.java
@@ -0,0 +1,15 @@
+package org.dromara.common.doc.config;
+
+import io.swagger.v3.oas.models.Paths;
+
+/**
+ * 鍗曠嫭浣跨敤涓�涓被渚夸簬鍒ゆ柇 瑙e喅springdoc璺緞鎷兼帴閲嶅闂
+ *
+ * @author Lion Li
+ */
+public class PlusPaths extends Paths {
+
+ public PlusPaths() {
+ super();
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/config/SpringDocAutoConfiguration.java b/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/config/SpringDocAutoConfiguration.java
new file mode 100644
index 0000000..a6adaec
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/config/SpringDocAutoConfiguration.java
@@ -0,0 +1,117 @@
+package org.dromara.common.doc.config;
+
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.Paths;
+import io.swagger.v3.oas.models.info.Info;
+import io.swagger.v3.oas.models.security.SecurityRequirement;
+import lombok.RequiredArgsConstructor;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.doc.config.properties.SpringDocProperties;
+import org.dromara.common.doc.handler.OpenApiHandler;
+import org.springdoc.core.configuration.SpringDocConfiguration;
+import org.springdoc.core.customizers.OpenApiBuilderCustomizer;
+import org.springdoc.core.customizers.OpenApiCustomizer;
+import org.springdoc.core.customizers.ServerBaseUrlCustomizer;
+import org.springdoc.core.properties.SpringDocConfigProperties;
+import org.springdoc.core.providers.JavadocProvider;
+import org.springdoc.core.service.OpenAPIService;
+import org.springdoc.core.service.SecurityService;
+import org.springdoc.core.utils.PropertyResolverUtils;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.autoconfigure.web.ServerProperties;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+
+import java.util.*;
+
+/**
+ * Swagger 鏂囨。閰嶇疆
+ *
+ * @author Lion Li
+ */
+@RequiredArgsConstructor
+@AutoConfiguration(before = SpringDocConfiguration.class)
+@EnableConfigurationProperties(SpringDocProperties.class)
+@ConditionalOnProperty(name = "springdoc.api-docs.enabled", havingValue = "true", matchIfMissing = true)
+public class SpringDocAutoConfiguration {
+
+ private final ServerProperties serverProperties;
+
+ @Value("${spring.application.name}")
+ private String appName;
+
+ @Bean
+ @ConditionalOnMissingBean(OpenAPI.class)
+ public OpenAPI openApi(SpringDocProperties properties) {
+ OpenAPI openApi = new OpenAPI();
+ // 鏂囨。鍩烘湰淇℃伅
+ SpringDocProperties.InfoProperties infoProperties = properties.getInfo();
+ Info info = convertInfo(infoProperties);
+ openApi.info(info);
+ // 鎵╁睍鏂囨。淇℃伅
+ 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);
+
+ return openApi;
+ }
+
+ private Info convertInfo(SpringDocProperties.InfoProperties infoProperties) {
+ Info info = new Info();
+ info.setTitle(infoProperties.getTitle());
+ info.setDescription(infoProperties.getDescription());
+ info.setContact(infoProperties.getContact());
+ info.setLicense(infoProperties.getLicense());
+ info.setVersion(infoProperties.getVersion());
+ return info;
+ }
+
+ /**
+ * 鑷畾涔� openapi 澶勭悊鍣�
+ */
+ @Bean
+ public OpenAPIService openApiBuilder(Optional<OpenAPI> openAPI,
+ SecurityService securityParser,
+ SpringDocConfigProperties springDocConfigProperties, PropertyResolverUtils propertyResolverUtils,
+ Optional<List<OpenApiBuilderCustomizer>> openApiBuilderCustomisers,
+ Optional<List<ServerBaseUrlCustomizer>> serverBaseUrlCustomisers, Optional<JavadocProvider> javadocProvider) {
+ return new OpenApiHandler(openAPI, securityParser, springDocConfigProperties, propertyResolverUtils, openApiBuilderCustomisers, serverBaseUrlCustomisers, javadocProvider);
+ }
+
+ /**
+ * 瀵瑰凡缁忕敓鎴愬ソ鐨� OpenApi 杩涜鑷畾涔夋搷浣�
+ */
+ @Bean
+ public OpenApiCustomizer openApiCustomizer() {
+ // 鎷兼帴鏈嶅姟璺緞
+ String appPath = "/" + StringUtils.substring(appName, appName.indexOf("-") + 1);
+ String contextPath = serverProperties.getServlet().getContextPath();
+ String finalContextPath;
+ if (StringUtils.isBlank(contextPath) || "/".equals(contextPath)) {
+ finalContextPath = appPath;
+ } else {
+ finalContextPath = appPath + contextPath;
+ }
+ // 瀵规墍鏈夎矾寰勫鍔犲墠缃笂涓嬫枃璺緞
+ return openApi -> {
+ Paths oldPaths = openApi.getPaths();
+ if (oldPaths instanceof PlusPaths) {
+ return;
+ }
+ PlusPaths newPaths = new PlusPaths();
+ oldPaths.forEach((k, v) -> newPaths.addPathItem(finalContextPath + k, v));
+ openApi.setPaths(newPaths);
+ };
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/config/properties/SpringDocProperties.java b/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/config/properties/SpringDocProperties.java
new file mode 100644
index 0000000..eae3b4c
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/config/properties/SpringDocProperties.java
@@ -0,0 +1,94 @@
+package org.dromara.common.doc.config.properties;
+
+import io.swagger.v3.oas.models.Components;
+import io.swagger.v3.oas.models.ExternalDocumentation;
+import io.swagger.v3.oas.models.Paths;
+import io.swagger.v3.oas.models.info.Contact;
+import io.swagger.v3.oas.models.info.License;
+import io.swagger.v3.oas.models.tags.Tag;
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.NestedConfigurationProperty;
+
+import java.util.List;
+
+/**
+ * swagger 閰嶇疆灞炴��
+ *
+ * @author Lion Li
+ */
+@Data
+@ConfigurationProperties(prefix = "springdoc")
+public class SpringDocProperties {
+
+ /**
+ * 鏂囨。鍩烘湰淇℃伅
+ */
+ @NestedConfigurationProperty
+ private InfoProperties info = new InfoProperties();
+
+ /**
+ * 鎵╁睍鏂囨。鍦板潃
+ */
+ @NestedConfigurationProperty
+ private ExternalDocumentation externalDocs;
+
+ /**
+ * 鏍囩
+ */
+ private List<Tag> tags = null;
+
+ /**
+ * 璺緞
+ */
+ @NestedConfigurationProperty
+ private Paths paths = null;
+
+ /**
+ * 缁勪欢
+ */
+ @NestedConfigurationProperty
+ private Components components = null;
+
+ /**
+ * <p>
+ * 鏂囨。鐨勫熀纭�灞炴�т俊鎭�
+ * </p>
+ *
+ * @see io.swagger.v3.oas.models.info.Info
+ *
+ * 涓轰簡 springboot 鑷姩鐢熶骇閰嶇疆鎻愮ず淇℃伅锛屾墍浠ヨ繖閲屽鍒朵竴涓被鍑烘潵
+ */
+ @Data
+ public static class InfoProperties {
+
+ /**
+ * 鏍囬
+ */
+ private String title = null;
+
+ /**
+ * 鎻忚堪
+ */
+ private String description = null;
+
+ /**
+ * 鑱旂郴浜轰俊鎭�
+ */
+ @NestedConfigurationProperty
+ private Contact contact = null;
+
+ /**
+ * 璁稿彲璇�
+ */
+ @NestedConfigurationProperty
+ private License license = null;
+
+ /**
+ * 鐗堟湰
+ */
+ private String version = null;
+
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/handler/OpenApiHandler.java b/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/handler/OpenApiHandler.java
new file mode 100644
index 0000000..1ee9e73
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/handler/OpenApiHandler.java
@@ -0,0 +1,252 @@
+package org.dromara.common.doc.handler;
+
+import cn.hutool.core.io.IoUtil;
+import io.swagger.v3.core.jackson.TypeNameResolver;
+import io.swagger.v3.core.util.AnnotationsUtils;
+import io.swagger.v3.oas.annotations.tags.Tags;
+import io.swagger.v3.oas.models.Components;
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.Operation;
+import io.swagger.v3.oas.models.Paths;
+import io.swagger.v3.oas.models.tags.Tag;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springdoc.core.customizers.OpenApiBuilderCustomizer;
+import org.springdoc.core.customizers.ServerBaseUrlCustomizer;
+import org.springdoc.core.properties.SpringDocConfigProperties;
+import org.springdoc.core.providers.JavadocProvider;
+import org.springdoc.core.service.OpenAPIService;
+import org.springdoc.core.service.SecurityService;
+import org.springdoc.core.utils.PropertyResolverUtils;
+import org.springframework.context.ApplicationContext;
+import org.springframework.core.annotation.AnnotatedElementUtils;
+import org.springframework.util.CollectionUtils;
+import org.springframework.web.method.HandlerMethod;
+
+import java.io.StringReader;
+import java.lang.reflect.Method;
+import java.util.*;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * 鑷畾涔� openapi 澶勭悊鍣�
+ * 瀵规簮鐮佸姛鑳借繘琛屼慨鏀� 澧炲己浣跨敤
+ */
+@Slf4j
+@SuppressWarnings("all")
+public class OpenApiHandler extends OpenAPIService {
+
+ /**
+ * The Basic error controller.
+ */
+ private static Class<?> basicErrorController;
+
+ /**
+ * The Security parser.
+ */
+ private final SecurityService securityParser;
+
+ /**
+ * The Mappings map.
+ */
+ private final Map<String, Object> mappingsMap = new HashMap<>();
+
+ /**
+ * The Springdoc tags.
+ */
+ private final Map<HandlerMethod, Tag> springdocTags = new HashMap<>();
+
+ /**
+ * The Open api builder customisers.
+ */
+ private final Optional<List<OpenApiBuilderCustomizer>> openApiBuilderCustomisers;
+
+ /**
+ * The server base URL customisers.
+ */
+ private final Optional<List<ServerBaseUrlCustomizer>> serverBaseUrlCustomizers;
+
+ /**
+ * The Spring doc config properties.
+ */
+ private final SpringDocConfigProperties springDocConfigProperties;
+
+ /**
+ * The Cached open api map.
+ */
+ private final Map<String, OpenAPI> cachedOpenAPI = new HashMap<>();
+
+ /**
+ * The Property resolver utils.
+ */
+ private final PropertyResolverUtils propertyResolverUtils;
+
+ /**
+ * The javadoc provider.
+ */
+ private final Optional<JavadocProvider> javadocProvider;
+
+ /**
+ * The Context.
+ */
+ private ApplicationContext context;
+
+ /**
+ * The Open api.
+ */
+ private OpenAPI openAPI;
+
+ /**
+ * The Is servers present.
+ */
+ private boolean isServersPresent;
+
+ /**
+ * The Server base url.
+ */
+ private String serverBaseUrl;
+
+ /**
+ * Instantiates a new Open api builder.
+ *
+ * @param openAPI the open api
+ * @param securityParser the security parser
+ * @param springDocConfigProperties the spring doc config properties
+ * @param propertyResolverUtils the property resolver utils
+ * @param openApiBuilderCustomizers the open api builder customisers
+ * @param serverBaseUrlCustomizers the server base url customizers
+ * @param javadocProvider the javadoc provider
+ */
+ public OpenApiHandler(Optional<OpenAPI> openAPI, SecurityService securityParser,
+ SpringDocConfigProperties springDocConfigProperties, PropertyResolverUtils propertyResolverUtils,
+ Optional<List<OpenApiBuilderCustomizer>> openApiBuilderCustomizers,
+ Optional<List<ServerBaseUrlCustomizer>> serverBaseUrlCustomizers,
+ Optional<JavadocProvider> javadocProvider) {
+ super(openAPI, securityParser, springDocConfigProperties, propertyResolverUtils, openApiBuilderCustomizers, serverBaseUrlCustomizers, javadocProvider);
+ if (openAPI.isPresent()) {
+ this.openAPI = openAPI.get();
+ if (this.openAPI.getComponents() == null)
+ this.openAPI.setComponents(new Components());
+ if (this.openAPI.getPaths() == null)
+ this.openAPI.setPaths(new Paths());
+ if (!CollectionUtils.isEmpty(this.openAPI.getServers()))
+ this.isServersPresent = true;
+ }
+ this.propertyResolverUtils = propertyResolverUtils;
+ this.securityParser = securityParser;
+ this.springDocConfigProperties = springDocConfigProperties;
+ this.openApiBuilderCustomisers = openApiBuilderCustomizers;
+ this.serverBaseUrlCustomizers = serverBaseUrlCustomizers;
+ this.javadocProvider = javadocProvider;
+ if (springDocConfigProperties.isUseFqn())
+ TypeNameResolver.std.setUseFqn(true);
+ }
+
+ @Override
+ public Operation buildTags(HandlerMethod handlerMethod, Operation operation, OpenAPI openAPI, Locale locale) {
+
+ Set<Tag> tags = new HashSet<>();
+ Set<String> tagsStr = new HashSet<>();
+
+ buildTagsFromMethod(handlerMethod.getMethod(), tags, tagsStr, locale);
+ buildTagsFromClass(handlerMethod.getBeanType(), tags, tagsStr, locale);
+
+ if (!CollectionUtils.isEmpty(tagsStr))
+ tagsStr = tagsStr.stream()
+ .map(str -> propertyResolverUtils.resolve(str, locale))
+ .collect(Collectors.toSet());
+
+ if (springdocTags.containsKey(handlerMethod)) {
+ Tag tag = springdocTags.get(handlerMethod);
+ tagsStr.add(tag.getName());
+ if (openAPI.getTags() == null || !openAPI.getTags().contains(tag)) {
+ openAPI.addTagsItem(tag);
+ }
+ }
+
+ if (!CollectionUtils.isEmpty(tagsStr)) {
+ if (CollectionUtils.isEmpty(operation.getTags()))
+ operation.setTags(new ArrayList<>(tagsStr));
+ else {
+ Set<String> operationTagsSet = new HashSet<>(operation.getTags());
+ operationTagsSet.addAll(tagsStr);
+ operation.getTags().clear();
+ operation.getTags().addAll(operationTagsSet);
+ }
+ }
+
+ if (isAutoTagClasses(operation)) {
+
+
+ if (javadocProvider.isPresent()) {
+ String description = javadocProvider.get().getClassJavadoc(handlerMethod.getBeanType());
+ if (StringUtils.isNotBlank(description)) {
+ Tag tag = new Tag();
+
+ // 鑷畾涔夐儴鍒� 淇敼浣跨敤java娉ㄩ噴褰搕ag鍚�
+ List<String> list = IoUtil.readLines(new StringReader(description), new ArrayList<>());
+ // tag.setName(tagAutoName);
+ tag.setName(list.get(0));
+ operation.addTagsItem(list.get(0));
+
+ tag.setDescription(description);
+ if (openAPI.getTags() == null || !openAPI.getTags().contains(tag)) {
+ openAPI.addTagsItem(tag);
+ }
+ }
+ } else {
+ String tagAutoName = splitCamelCase(handlerMethod.getBeanType().getSimpleName());
+ operation.addTagsItem(tagAutoName);
+ }
+ }
+
+ if (!CollectionUtils.isEmpty(tags)) {
+ // Existing tags
+ List<Tag> openApiTags = openAPI.getTags();
+ if (!CollectionUtils.isEmpty(openApiTags))
+ tags.addAll(openApiTags);
+ openAPI.setTags(new ArrayList<>(tags));
+ }
+
+ // Handle SecurityRequirement at operation level
+ io.swagger.v3.oas.annotations.security.SecurityRequirement[] securityRequirements = securityParser
+ .getSecurityRequirements(handlerMethod);
+ if (securityRequirements != null) {
+ if (securityRequirements.length == 0)
+ operation.setSecurity(Collections.emptyList());
+ else
+ securityParser.buildSecurityRequirement(securityRequirements, operation);
+ }
+
+ return operation;
+ }
+
+ private void buildTagsFromMethod(Method method, Set<Tag> tags, Set<String> tagsStr, Locale locale) {
+ // method tags
+ Set<Tags> tagsSet = AnnotatedElementUtils
+ .findAllMergedAnnotations(method, Tags.class);
+ Set<io.swagger.v3.oas.annotations.tags.Tag> methodTags = tagsSet.stream()
+ .flatMap(x -> Stream.of(x.value())).collect(Collectors.toSet());
+ methodTags.addAll(AnnotatedElementUtils.findAllMergedAnnotations(method, io.swagger.v3.oas.annotations.tags.Tag.class));
+ if (!CollectionUtils.isEmpty(methodTags)) {
+ tagsStr.addAll(methodTags.stream().map(tag -> propertyResolverUtils.resolve(tag.name(), locale)).collect(Collectors.toSet()));
+ List<io.swagger.v3.oas.annotations.tags.Tag> allTags = new ArrayList<>(methodTags);
+ addTags(allTags, tags, locale);
+ }
+ }
+
+ private void addTags(List<io.swagger.v3.oas.annotations.tags.Tag> sourceTags, Set<Tag> tags, Locale locale) {
+ Optional<Set<Tag>> optionalTagSet = AnnotationsUtils
+ .getTags(sourceTags.toArray(new io.swagger.v3.oas.annotations.tags.Tag[0]), true);
+ optionalTagSet.ifPresent(tagsSet -> {
+ tagsSet.forEach(tag -> {
+ tag.name(propertyResolverUtils.resolve(tag.getName(), locale));
+ tag.description(propertyResolverUtils.resolve(tag.getDescription(), locale));
+ if (tags.stream().noneMatch(t -> t.getName().equals(tag.getName())))
+ tags.add(tag);
+ });
+ });
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-doc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/ruoyi-common/ruoyi-common-doc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..f039b2b
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-doc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+org.dromara.common.doc.config.SpringDocAutoConfiguration
diff --git a/ruoyi-common/ruoyi-common-dubbo/pom.xml b/ruoyi-common/ruoyi-common-dubbo/pom.xml
new file mode 100644
index 0000000..ce5bb3e
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-dubbo/pom.xml
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns="http://maven.apache.org/POM/4.0.0"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common</artifactId>
+ <version>${revision}</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>ruoyi-common-dubbo</artifactId>
+
+ <description>
+ ruoyi-common-dubbo
+ </description>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-json</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.springframework.cloud</groupId>
+ <artifactId>spring-cloud-context</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.dubbo</groupId>
+ <artifactId>dubbo-spring-boot-starter</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.dubbo</groupId>
+ <artifactId>dubbo-spring-boot-actuator</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.projectlombok</groupId>
+ <artifactId>lombok</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>cn.dev33</groupId>
+ <artifactId>sa-token-spring-boot3-starter</artifactId>
+ </dependency>
+
+ <!-- Sa-Token 鏁村悎 Dubbo -->
+ <dependency>
+ <groupId>cn.dev33</groupId>
+ <artifactId>sa-token-dubbo3</artifactId>
+ <version>${satoken.version}</version>
+ <exclusions>
+ <exclusion>
+ <groupId>org.apache.dubbo</groupId>
+ <artifactId>dubbo</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+
+ </dependencies>
+</project>
diff --git a/ruoyi-common/ruoyi-common-dubbo/src/main/java/org/dromara/common/dubbo/config/DubboConfiguration.java b/ruoyi-common/ruoyi-common-dubbo/src/main/java/org/dromara/common/dubbo/config/DubboConfiguration.java
new file mode 100644
index 0000000..6fc622e
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-dubbo/src/main/java/org/dromara/common/dubbo/config/DubboConfiguration.java
@@ -0,0 +1,17 @@
+package org.dromara.common.dubbo.config;
+
+import org.dromara.common.core.factory.YmlPropertySourceFactory;
+import org.dromara.common.dubbo.properties.DubboCustomProperties;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.PropertySource;
+
+/**
+ * dubbo 閰嶇疆绫�
+ */
+@AutoConfiguration
+@EnableConfigurationProperties(DubboCustomProperties.class)
+@PropertySource(value = "classpath:common-dubbo.yml", factory = YmlPropertySourceFactory.class)
+public class DubboConfiguration {
+
+}
diff --git a/ruoyi-common/ruoyi-common-dubbo/src/main/java/org/dromara/common/dubbo/enumd/RequestLogEnum.java b/ruoyi-common/ruoyi-common-dubbo/src/main/java/org/dromara/common/dubbo/enumd/RequestLogEnum.java
new file mode 100644
index 0000000..f238eeb
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-dubbo/src/main/java/org/dromara/common/dubbo/enumd/RequestLogEnum.java
@@ -0,0 +1,18 @@
+package org.dromara.common.dubbo.enumd;
+
+import lombok.AllArgsConstructor;
+
+/**
+ * 璇锋眰鏃ュ織娉涘瀷
+ *
+ * @author Lion Li
+ */
+@AllArgsConstructor
+public enum RequestLogEnum {
+
+ /**
+ * info 鍩虹淇℃伅 param 鍙傛暟淇℃伅 full 鍏ㄩ儴
+ */
+ INFO, PARAM, FULL;
+
+}
diff --git a/ruoyi-common/ruoyi-common-dubbo/src/main/java/org/dromara/common/dubbo/filter/DubboRequestFilter.java b/ruoyi-common/ruoyi-common-dubbo/src/main/java/org/dromara/common/dubbo/filter/DubboRequestFilter.java
new file mode 100644
index 0000000..382dc94
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-dubbo/src/main/java/org/dromara/common/dubbo/filter/DubboRequestFilter.java
@@ -0,0 +1,58 @@
+package org.dromara.common.dubbo.filter;
+
+import org.dromara.common.core.utils.SpringUtils;
+import org.dromara.common.dubbo.enumd.RequestLogEnum;
+import org.dromara.common.dubbo.properties.DubboCustomProperties;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.dubbo.common.constants.CommonConstants;
+import org.apache.dubbo.common.extension.Activate;
+import org.apache.dubbo.rpc.*;
+import org.apache.dubbo.rpc.service.GenericService;
+import org.dromara.common.json.utils.JsonUtils;
+
+/**
+ * dubbo鏃ュ織杩囨护鍣�
+ *
+ * @author Lion Li
+ */
+@Slf4j
+@Activate(group = {CommonConstants.PROVIDER, CommonConstants.CONSUMER}, order = Integer.MAX_VALUE)
+public class DubboRequestFilter implements Filter {
+
+ @Override
+ public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
+ DubboCustomProperties properties = SpringUtils.getBean(DubboCustomProperties.class);
+ if (!properties.getRequestLog()) {
+ // 鏈紑鍚垯璺宠繃鏃ュ織閫昏緫
+ return invoker.invoke(invocation);
+ }
+ String client = CommonConstants.PROVIDER;
+ if (RpcContext.getServiceContext().isConsumerSide()) {
+ client = CommonConstants.CONSUMER;
+ }
+ String baselog = "Client[" + client + "],InterfaceName=[" + invocation.getInvoker().getInterface().getSimpleName() + "],MethodName=[" + invocation.getMethodName() + "]";
+ if (properties.getLogLevel() == RequestLogEnum.INFO) {
+ log.info("DUBBO - 鏈嶅姟璋冪敤: {}", baselog);
+ } else {
+ log.info("DUBBO - 鏈嶅姟璋冪敤: {},Parameter={}", baselog, invocation.getArguments());
+ }
+
+ long startTime = System.currentTimeMillis();
+ // 鎵ц鎺ュ彛璋冪敤閫昏緫
+ Result result = invoker.invoke(invocation);
+ // 璋冪敤鑰楁椂
+ long elapsed = System.currentTimeMillis() - startTime;
+ // 濡傛灉鍙戠敓寮傚父 鍒欐墦鍗板紓甯告棩蹇�
+ if (result.hasException() && invoker.getInterface().equals(GenericService.class)) {
+ log.error("DUBBO - 鏈嶅姟寮傚父: {},Exception={}", baselog, result.getException());
+ } else {
+ if (properties.getLogLevel() == RequestLogEnum.INFO) {
+ log.info("DUBBO - 鏈嶅姟鍝嶅簲: {},SpendTime=[{}ms]", baselog, elapsed);
+ } else if (properties.getLogLevel() == RequestLogEnum.FULL) {
+ log.info("DUBBO - 鏈嶅姟鍝嶅簲: {},SpendTime=[{}ms],Response={}", baselog, elapsed, JsonUtils.toJsonString(new Object[]{result.getValue()}));
+ }
+ }
+ return result;
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-dubbo/src/main/java/org/dromara/common/dubbo/properties/DubboCustomProperties.java b/ruoyi-common/ruoyi-common-dubbo/src/main/java/org/dromara/common/dubbo/properties/DubboCustomProperties.java
new file mode 100644
index 0000000..8918088
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-dubbo/src/main/java/org/dromara/common/dubbo/properties/DubboCustomProperties.java
@@ -0,0 +1,22 @@
+package org.dromara.common.dubbo.properties;
+
+import lombok.Data;
+import org.dromara.common.dubbo.enumd.RequestLogEnum;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.cloud.context.config.annotation.RefreshScope;
+
+/**
+ * 鑷畾涔夐厤缃�
+ *
+ * @author Lion Li
+ */
+@Data
+@RefreshScope
+@ConfigurationProperties(prefix = "dubbo.custom")
+public class DubboCustomProperties {
+
+ private Boolean requestLog;
+
+ private RequestLogEnum logLevel;
+
+}
diff --git a/ruoyi-common/ruoyi-common-dubbo/src/main/resources/META-INF/dubbo/org.apache.dubbo.rpc.Filter b/ruoyi-common/ruoyi-common-dubbo/src/main/resources/META-INF/dubbo/org.apache.dubbo.rpc.Filter
new file mode 100644
index 0000000..6f766ab
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-dubbo/src/main/resources/META-INF/dubbo/org.apache.dubbo.rpc.Filter
@@ -0,0 +1 @@
+dubboRequestFilter=org.dromara.common.dubbo.filter.DubboRequestFilter
diff --git a/ruoyi-common/ruoyi-common-dubbo/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/ruoyi-common/ruoyi-common-dubbo/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..f60bd3a
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-dubbo/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+org.dromara.common.dubbo.config.DubboConfiguration
diff --git a/ruoyi-common/ruoyi-common-dubbo/src/main/resources/common-dubbo.yml b/ruoyi-common/ruoyi-common-dubbo/src/main/resources/common-dubbo.yml
new file mode 100644
index 0000000..fdc21f6
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-dubbo/src/main/resources/common-dubbo.yml
@@ -0,0 +1,30 @@
+# 鍐呯疆閰嶇疆 涓嶅厑璁镐慨鏀� 濡傞渶淇敼璇峰湪 nacos 涓婂啓鐩稿悓閰嶇疆瑕嗙洊
+dubbo:
+ application:
+ logger: slf4j
+ # 鍏冩暟鎹腑蹇� local 鏈湴 remote 杩滅▼ 杩欓噷浣跨敤杩滅▼渚夸簬鍏朵粬鏈嶅姟鑾峰彇
+ metadataType: remote
+ # 鍙�夊�� interface銆乮nstance銆乤ll锛岄粯璁ゆ槸 all锛屽嵆鎺ュ彛绾у湴鍧�銆佸簲鐢ㄧ骇鍦板潃閮芥敞鍐�
+ register-mode: instance
+ service-discovery:
+ # FORCE_INTERFACE锛屽彧娑堣垂鎺ュ彛绾у湴鍧�锛屽鏃犲湴鍧�鍒欐姤閿欙紝鍗曡闃� 2.x 鍦板潃
+ # APPLICATION_FIRST锛屾櫤鑳藉喅绛栨帴鍙g骇/搴旂敤绾у湴鍧�锛屽弻璁㈤槄
+ # FORCE_APPLICATION锛屽彧娑堣垂搴旂敤绾у湴鍧�锛屽鏃犲湴鍧�鍒欐姤閿欙紝鍗曡闃� 3.x 鍦板潃
+ migration: FORCE_APPLICATION
+ # 娉ㄥ唽涓績閰嶇疆
+ registry:
+ address: nacos://${spring.cloud.nacos.server-addr}
+ group: DUBBO_GROUP
+ parameters:
+ namespace: ${spring.profiles.active}
+ # 娑堣垂鑰呯浉鍏抽厤缃�
+ consumer:
+ # 缁撴灉缂撳瓨(LRU绠楁硶)
+ # 浼氭湁鏁版嵁涓嶄竴鑷撮棶棰� 寤鸿鍦ㄦ敞瑙e眬閮ㄥ紑鍚�
+ cache: false
+ # 鏀寔鏍¢獙娉ㄨВ
+ validation: jvalidationNew
+ # 璋冪敤閲嶈瘯 涓嶅寘鎷涓�娆� 0涓轰笉闇�瑕侀噸璇�
+ retries: 0
+ # 鍒濆鍖栨鏌�
+ check: false
diff --git a/ruoyi-common/ruoyi-common-elasticsearch/pom.xml b/ruoyi-common/ruoyi-common-elasticsearch/pom.xml
new file mode 100644
index 0000000..401320d
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-elasticsearch/pom.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns="http://maven.apache.org/POM/4.0.0"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common</artifactId>
+ <version>${revision}</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>ruoyi-common-elasticsearch</artifactId>
+
+ <description>
+ ruoyi-common-elasticsearch ES鎼滅储寮曟搸鏈嶅姟
+ </description>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.dromara.easy-es</groupId>
+ <artifactId>easy-es-boot-starter</artifactId>
+ </dependency>
+ </dependencies>
+</project>
diff --git a/ruoyi-common/ruoyi-common-elasticsearch/src/main/java/org/dromara/common/elasticsearch/config/ActuatorEnvironmentPostProcessor.java b/ruoyi-common/ruoyi-common-elasticsearch/src/main/java/org/dromara/common/elasticsearch/config/ActuatorEnvironmentPostProcessor.java
new file mode 100644
index 0000000..f2fbc5f
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-elasticsearch/src/main/java/org/dromara/common/elasticsearch/config/ActuatorEnvironmentPostProcessor.java
@@ -0,0 +1,25 @@
+package org.dromara.common.elasticsearch.config;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.env.EnvironmentPostProcessor;
+import org.springframework.core.Ordered;
+import org.springframework.core.env.ConfigurableEnvironment;
+
+/**
+ * 鍋ュ悍妫�鏌ラ厤缃敞鍏�
+ *
+ * @author Lion Li
+ */
+public class ActuatorEnvironmentPostProcessor implements EnvironmentPostProcessor, Ordered {
+
+ @Override
+ public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
+ System.setProperty("management.health.elasticsearch.enabled", "false");
+ }
+
+ @Override
+ public int getOrder() {
+ return Ordered.HIGHEST_PRECEDENCE;
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-elasticsearch/src/main/java/org/dromara/common/elasticsearch/config/EasyEsConfiguration.java b/ruoyi-common/ruoyi-common-elasticsearch/src/main/java/org/dromara/common/elasticsearch/config/EasyEsConfiguration.java
new file mode 100644
index 0000000..3d6354a
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-elasticsearch/src/main/java/org/dromara/common/elasticsearch/config/EasyEsConfiguration.java
@@ -0,0 +1,17 @@
+package org.dromara.common.elasticsearch.config;
+
+import org.dromara.easyes.starter.register.EsMapperScan;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+
+/**
+ * easy-es 閰嶇疆
+ *
+ * @author Lion Li
+ */
+@AutoConfiguration
+@ConditionalOnProperty(value = "easy-es.enable", havingValue = "true")
+@EsMapperScan("org.dromara.**.esmapper")
+public class EasyEsConfiguration {
+
+}
diff --git a/ruoyi-common/ruoyi-common-elasticsearch/src/main/resources/META-INF/spring.factories b/ruoyi-common/ruoyi-common-elasticsearch/src/main/resources/META-INF/spring.factories
new file mode 100644
index 0000000..1a18290
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-elasticsearch/src/main/resources/META-INF/spring.factories
@@ -0,0 +1,2 @@
+org.springframework.boot.env.EnvironmentPostProcessor=\
+ org.dromara.common.elasticsearch.config.ActuatorEnvironmentPostProcessor
diff --git a/ruoyi-common/ruoyi-common-elasticsearch/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/ruoyi-common/ruoyi-common-elasticsearch/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..b1a5918
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-elasticsearch/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+org.dromara.common.elasticsearch.config.EasyEsConfiguration
diff --git a/ruoyi-common/ruoyi-common-encrypt/pom.xml b/ruoyi-common/ruoyi-common-encrypt/pom.xml
new file mode 100644
index 0000000..df3222b
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-encrypt/pom.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common</artifactId>
+ <version>${revision}</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>ruoyi-common-encrypt</artifactId>
+
+ <description>
+ ruoyi-common-encrypt 鏁版嵁鍔犺В瀵嗘ā鍧�
+ </description>
+
+ <dependencies>
+
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-core</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.mybatis.spring.boot</groupId>
+ <artifactId>mybatis-spring-boot-starter</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.bouncycastle</groupId>
+ <artifactId>bcprov-jdk15to18</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>cn.hutool</groupId>
+ <artifactId>hutool-crypto</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.springframework</groupId>
+ <artifactId>spring-webmvc</artifactId>
+ </dependency>
+
+ </dependencies>
+
+</project>
diff --git a/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/annotation/ApiEncrypt.java b/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/annotation/ApiEncrypt.java
new file mode 100644
index 0000000..7f52de8
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/annotation/ApiEncrypt.java
@@ -0,0 +1,20 @@
+package org.dromara.common.encrypt.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 寮哄埗鍔犲瘑娉ㄨВ
+ *
+ * @author Michelle.Chung
+ */
+@Documented
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface ApiEncrypt {
+
+ /**
+ * 鍝嶅簲鍔犲瘑蹇界暐锛岄粯璁や笉鍔犲瘑锛屼负 true 鏃跺姞瀵�
+ */
+ boolean response() default false;
+
+}
diff --git a/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/annotation/EncryptField.java b/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/annotation/EncryptField.java
new file mode 100644
index 0000000..d357d72
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/annotation/EncryptField.java
@@ -0,0 +1,44 @@
+package org.dromara.common.encrypt.annotation;
+
+import org.dromara.common.encrypt.enumd.AlgorithmType;
+import org.dromara.common.encrypt.enumd.EncodeType;
+
+import java.lang.annotation.*;
+
+/**
+ * 瀛楁鍔犲瘑娉ㄨВ
+ *
+ * @author 鑰侀┈
+ */
+@Documented
+@Inherited
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface EncryptField {
+
+ /**
+ * 鍔犲瘑绠楁硶
+ */
+ AlgorithmType algorithm() default AlgorithmType.DEFAULT;
+
+ /**
+ * 绉橀挜銆侫ES銆丼M4闇�瑕�
+ */
+ String password() default "";
+
+ /**
+ * 鍏挜銆俁SA銆丼M2闇�瑕�
+ */
+ String publicKey() default "";
+
+ /**
+ * 绉侀挜銆俁SA銆丼M2闇�瑕�
+ */
+ String privateKey() default "";
+
+ /**
+ * 缂栫爜鏂瑰紡銆傚鍔犲瘑绠楁硶涓築ASE64鐨勪笉璧蜂綔鐢�
+ */
+ EncodeType encode() default EncodeType.DEFAULT;
+
+}
diff --git a/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/config/ApiDecryptAutoConfiguration.java b/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/config/ApiDecryptAutoConfiguration.java
new file mode 100644
index 0000000..098f6bc
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/config/ApiDecryptAutoConfiguration.java
@@ -0,0 +1,32 @@
+package org.dromara.common.encrypt.config;
+
+import jakarta.servlet.DispatcherType;
+import org.dromara.common.encrypt.filter.CryptoFilter;
+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.FilterRegistrationBean;
+import org.springframework.context.annotation.Bean;
+
+/**
+ * api 瑙e瘑鑷姩閰嶇疆
+ *
+ * @author wdhcr
+ */
+@AutoConfiguration
+@EnableConfigurationProperties(ApiDecryptProperties.class)
+@ConditionalOnProperty(value = "api-decrypt.enabled", havingValue = "true")
+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;
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/config/EncryptorAutoConfiguration.java b/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/config/EncryptorAutoConfiguration.java
new file mode 100644
index 0000000..e988a3a
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/config/EncryptorAutoConfiguration.java
@@ -0,0 +1,41 @@
+package org.dromara.common.encrypt.config;
+
+import org.dromara.common.encrypt.core.EncryptorManager;
+import org.dromara.common.encrypt.interceptor.MybatisDecryptInterceptor;
+import org.dromara.common.encrypt.interceptor.MybatisEncryptInterceptor;
+import org.dromara.common.encrypt.properties.EncryptorProperties;
+import org.springframework.beans.factory.annotation.Autowired;
+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;
+
+/**
+ * 鍔犺В瀵嗛厤缃�
+ *
+ * @author 鑰侀┈
+ * @version 4.6.0
+ */
+@AutoConfiguration
+@EnableConfigurationProperties(EncryptorProperties.class)
+@ConditionalOnProperty(value = "mybatis-encryptor.enable", havingValue = "true")
+public class EncryptorAutoConfiguration {
+
+ @Autowired
+ private EncryptorProperties properties;
+
+ @Bean
+ public EncryptorManager encryptorManager() {
+ return new EncryptorManager();
+ }
+
+ @Bean
+ public MybatisEncryptInterceptor mybatisEncryptInterceptor(EncryptorManager encryptorManager) {
+ return new MybatisEncryptInterceptor(encryptorManager, properties);
+ }
+
+ @Bean
+ public MybatisDecryptInterceptor mybatisDecryptInterceptor(EncryptorManager encryptorManager) {
+ return new MybatisDecryptInterceptor(encryptorManager, properties);
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/core/EncryptContext.java b/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/core/EncryptContext.java
new file mode 100644
index 0000000..2f02eaf
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/core/EncryptContext.java
@@ -0,0 +1,41 @@
+package org.dromara.common.encrypt.core;
+
+import org.dromara.common.encrypt.enumd.AlgorithmType;
+import org.dromara.common.encrypt.enumd.EncodeType;
+import lombok.Data;
+
+/**
+ * 鍔犲瘑涓婁笅鏂� 鐢ㄤ簬encryptor浼犻�掑繀瑕佺殑鍙傛暟銆�
+ *
+ * @author 鑰侀┈
+ * @version 4.6.0
+ */
+@Data
+public class EncryptContext {
+
+ /**
+ * 榛樿绠楁硶
+ */
+ private AlgorithmType algorithm;
+
+ /**
+ * 瀹夊叏绉橀挜
+ */
+ private String password;
+
+ /**
+ * 鍏挜
+ */
+ private String publicKey;
+
+ /**
+ * 绉侀挜
+ */
+ private String privateKey;
+
+ /**
+ * 缂栫爜鏂瑰紡锛宐ase64/hex
+ */
+ private EncodeType encode;
+
+}
diff --git a/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/core/EncryptorManager.java b/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/core/EncryptorManager.java
new file mode 100644
index 0000000..782995d
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/core/EncryptorManager.java
@@ -0,0 +1,100 @@
+package org.dromara.common.encrypt.core;
+
+import cn.hutool.core.util.ReflectUtil;
+import org.dromara.common.encrypt.annotation.EncryptField;
+import lombok.extern.slf4j.Slf4j;
+
+import java.lang.reflect.Field;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+
+/**
+ * 鍔犲瘑绠$悊绫�
+ *
+ * @author 鑰侀┈
+ * @version 4.6.0
+ */
+@Slf4j
+public class EncryptorManager {
+
+ /**
+ * 缂撳瓨鍔犲瘑鍣�
+ */
+ Map<EncryptContext, IEncryptor> encryptorMap = new ConcurrentHashMap<>();
+
+ /**
+ * 绫诲姞瀵嗗瓧娈电紦瀛�
+ */
+ Map<Class<?>, Set<Field>> fieldCache = new ConcurrentHashMap<>();
+
+ /**
+ * 鑾峰彇绫诲姞瀵嗗瓧娈电紦瀛�
+ */
+ public Set<Field> getFieldCache(Class<?> sourceClazz) {
+ return fieldCache.computeIfAbsent(sourceClazz, clazz -> {
+ Set<Field> fieldSet = new HashSet<>();
+ while (clazz != null) {
+ Field[] fields = clazz.getDeclaredFields();
+ fieldSet.addAll(Arrays.asList(fields));
+ clazz = clazz.getSuperclass();
+ }
+ fieldSet = fieldSet.stream().filter(field ->
+ field.isAnnotationPresent(EncryptField.class) && field.getType() == String.class)
+ .collect(Collectors.toSet());
+ for (Field field : fieldSet) {
+ field.setAccessible(true);
+ }
+ return fieldSet;
+ });
+ }
+
+ /**
+ * 娉ㄥ唽鍔犲瘑鎵ц鑰呭埌缂撳瓨
+ *
+ * @param encryptContext 鍔犲瘑鎵ц鑰呴渶瑕佺殑鐩稿叧閰嶇疆鍙傛暟
+ */
+ public IEncryptor registAndGetEncryptor(EncryptContext encryptContext) {
+ if (encryptorMap.containsKey(encryptContext)) {
+ return encryptorMap.get(encryptContext);
+ }
+ IEncryptor encryptor = ReflectUtil.newInstance(encryptContext.getAlgorithm().getClazz(), encryptContext);
+ encryptorMap.put(encryptContext, encryptor);
+ return encryptor;
+ }
+
+ /**
+ * 绉婚櫎缂撳瓨涓殑鍔犲瘑鎵ц鑰�
+ *
+ * @param encryptContext 鍔犲瘑鎵ц鑰呴渶瑕佺殑鐩稿叧閰嶇疆鍙傛暟
+ */
+ public void removeEncryptor(EncryptContext encryptContext) {
+ this.encryptorMap.remove(encryptContext);
+ }
+
+ /**
+ * 鏍规嵁閰嶇疆杩涜鍔犲瘑銆備細杩涜鏈湴缂撳瓨瀵瑰簲鐨勭畻娉曞拰瀵瑰簲鐨勭閽ヤ俊鎭��
+ *
+ * @param value 寰呭姞瀵嗙殑鍊�
+ * @param encryptContext 鍔犲瘑鐩稿叧鐨勯厤缃俊鎭�
+ */
+ public String encrypt(String value, EncryptContext encryptContext) {
+ IEncryptor encryptor = this.registAndGetEncryptor(encryptContext);
+ return encryptor.encrypt(value, encryptContext.getEncode());
+ }
+
+ /**
+ * 鏍规嵁閰嶇疆杩涜瑙e瘑
+ *
+ * @param value 寰呰В瀵嗙殑鍊�
+ * @param encryptContext 鍔犲瘑鐩稿叧鐨勯厤缃俊鎭�
+ */
+ public String decrypt(String value, EncryptContext encryptContext) {
+ IEncryptor encryptor = this.registAndGetEncryptor(encryptContext);
+ return encryptor.decrypt(value);
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/core/IEncryptor.java b/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/core/IEncryptor.java
new file mode 100644
index 0000000..dbc4420
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/core/IEncryptor.java
@@ -0,0 +1,35 @@
+package org.dromara.common.encrypt.core;
+
+import org.dromara.common.encrypt.enumd.AlgorithmType;
+import org.dromara.common.encrypt.enumd.EncodeType;
+
+/**
+ * 鍔犺В鑰�
+ *
+ * @author 鑰侀┈
+ * @version 4.6.0
+ */
+public interface IEncryptor {
+
+ /**
+ * 鑾峰緱褰撳墠绠楁硶
+ */
+ AlgorithmType algorithm();
+
+ /**
+ * 鍔犲瘑
+ *
+ * @param value 寰呭姞瀵嗗瓧绗︿覆
+ * @param encodeType 鍔犲瘑鍚庣殑缂栫爜鏍煎紡
+ * @return 鍔犲瘑鍚庣殑瀛楃涓�
+ */
+ String encrypt(String value, EncodeType encodeType);
+
+ /**
+ * 瑙e瘑
+ *
+ * @param value 寰呭姞瀵嗗瓧绗︿覆
+ * @return 瑙e瘑鍚庣殑瀛楃涓�
+ */
+ String decrypt(String value);
+}
diff --git a/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/core/encryptor/AbstractEncryptor.java b/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/core/encryptor/AbstractEncryptor.java
new file mode 100644
index 0000000..858d229
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/core/encryptor/AbstractEncryptor.java
@@ -0,0 +1,18 @@
+package org.dromara.common.encrypt.core.encryptor;
+
+import org.dromara.common.encrypt.core.EncryptContext;
+import org.dromara.common.encrypt.core.IEncryptor;
+
+/**
+ * 鎵�鏈夊姞瀵嗘墽琛岃�呯殑鍩虹被
+ *
+ * @author 鑰侀┈
+ * @version 4.6.0
+ */
+public abstract class AbstractEncryptor implements IEncryptor {
+
+ public AbstractEncryptor(EncryptContext context) {
+ // 鐢ㄦ埛閰嶇疆鏍¢獙涓庨厤缃敞鍏�
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/core/encryptor/AesEncryptor.java b/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/core/encryptor/AesEncryptor.java
new file mode 100644
index 0000000..e4dc597
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/core/encryptor/AesEncryptor.java
@@ -0,0 +1,55 @@
+package org.dromara.common.encrypt.core.encryptor;
+
+import org.dromara.common.encrypt.core.EncryptContext;
+import org.dromara.common.encrypt.enumd.AlgorithmType;
+import org.dromara.common.encrypt.enumd.EncodeType;
+import org.dromara.common.encrypt.utils.EncryptUtils;
+
+/**
+ * AES绠楁硶瀹炵幇
+ *
+ * @author 鑰侀┈
+ * @version 4.6.0
+ */
+public class AesEncryptor extends AbstractEncryptor {
+
+ private final EncryptContext context;
+
+ public AesEncryptor(EncryptContext context) {
+ super(context);
+ this.context = context;
+ }
+
+ /**
+ * 鑾峰緱褰撳墠绠楁硶
+ */
+ @Override
+ public AlgorithmType algorithm() {
+ return AlgorithmType.AES;
+ }
+
+ /**
+ * 鍔犲瘑
+ *
+ * @param value 寰呭姞瀵嗗瓧绗︿覆
+ * @param encodeType 鍔犲瘑鍚庣殑缂栫爜鏍煎紡
+ */
+ @Override
+ public String encrypt(String value, EncodeType encodeType) {
+ if (encodeType == EncodeType.HEX) {
+ return EncryptUtils.encryptByAesHex(value, context.getPassword());
+ } else {
+ return EncryptUtils.encryptByAes(value, context.getPassword());
+ }
+ }
+
+ /**
+ * 瑙e瘑
+ *
+ * @param value 寰呭姞瀵嗗瓧绗︿覆
+ */
+ @Override
+ public String decrypt(String value) {
+ return EncryptUtils.decryptByAes(value, context.getPassword());
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/core/encryptor/Base64Encryptor.java b/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/core/encryptor/Base64Encryptor.java
new file mode 100644
index 0000000..0028548
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/core/encryptor/Base64Encryptor.java
@@ -0,0 +1,48 @@
+package org.dromara.common.encrypt.core.encryptor;
+
+import org.dromara.common.encrypt.core.EncryptContext;
+import org.dromara.common.encrypt.enumd.AlgorithmType;
+import org.dromara.common.encrypt.enumd.EncodeType;
+import org.dromara.common.encrypt.utils.EncryptUtils;
+
+/**
+ * Base64绠楁硶瀹炵幇
+ *
+ * @author 鑰侀┈
+ * @version 4.6.0
+ */
+public class Base64Encryptor extends AbstractEncryptor {
+
+ public Base64Encryptor(EncryptContext context) {
+ super(context);
+ }
+
+ /**
+ * 鑾峰緱褰撳墠绠楁硶
+ */
+ @Override
+ public AlgorithmType algorithm() {
+ return AlgorithmType.BASE64;
+ }
+
+ /**
+ * 鍔犲瘑
+ *
+ * @param value 寰呭姞瀵嗗瓧绗︿覆
+ * @param encodeType 鍔犲瘑鍚庣殑缂栫爜鏍煎紡
+ */
+ @Override
+ public String encrypt(String value, EncodeType encodeType) {
+ return EncryptUtils.encryptByBase64(value);
+ }
+
+ /**
+ * 瑙e瘑
+ *
+ * @param value 寰呭姞瀵嗗瓧绗︿覆
+ */
+ @Override
+ public String decrypt(String value) {
+ return EncryptUtils.decryptByBase64(value);
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/core/encryptor/RsaEncryptor.java b/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/core/encryptor/RsaEncryptor.java
new file mode 100644
index 0000000..5f03a4b
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/core/encryptor/RsaEncryptor.java
@@ -0,0 +1,62 @@
+package org.dromara.common.encrypt.core.encryptor;
+
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.encrypt.core.EncryptContext;
+import org.dromara.common.encrypt.enumd.AlgorithmType;
+import org.dromara.common.encrypt.enumd.EncodeType;
+import org.dromara.common.encrypt.utils.EncryptUtils;
+
+
+/**
+ * RSA绠楁硶瀹炵幇
+ *
+ * @author 鑰侀┈
+ * @version 4.6.0
+ */
+public class RsaEncryptor extends AbstractEncryptor {
+
+ private final EncryptContext context;
+
+ public RsaEncryptor(EncryptContext context) {
+ super(context);
+ String privateKey = context.getPrivateKey();
+ String publicKey = context.getPublicKey();
+ if (StringUtils.isAnyEmpty(privateKey, publicKey)) {
+ throw new IllegalArgumentException("RSA鍏閽ュ潎闇�瑕佹彁渚涳紝鍏挜鍔犲瘑锛岀閽ヨВ瀵嗐��");
+ }
+ this.context = context;
+ }
+
+ /**
+ * 鑾峰緱褰撳墠绠楁硶
+ */
+ @Override
+ public AlgorithmType algorithm() {
+ return AlgorithmType.RSA;
+ }
+
+ /**
+ * 鍔犲瘑
+ *
+ * @param value 寰呭姞瀵嗗瓧绗︿覆
+ * @param encodeType 鍔犲瘑鍚庣殑缂栫爜鏍煎紡
+ */
+ @Override
+ public String encrypt(String value, EncodeType encodeType) {
+ if (encodeType == EncodeType.HEX) {
+ return EncryptUtils.encryptByRsaHex(value, context.getPublicKey());
+ } else {
+ return EncryptUtils.encryptByRsa(value, context.getPublicKey());
+ }
+ }
+
+ /**
+ * 瑙e瘑
+ *
+ * @param value 寰呭姞瀵嗗瓧绗︿覆
+ */
+ @Override
+ public String decrypt(String value) {
+ return EncryptUtils.decryptByRsa(value, context.getPrivateKey());
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/core/encryptor/Sm2Encryptor.java b/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/core/encryptor/Sm2Encryptor.java
new file mode 100644
index 0000000..aec5d82
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/core/encryptor/Sm2Encryptor.java
@@ -0,0 +1,61 @@
+package org.dromara.common.encrypt.core.encryptor;
+
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.encrypt.core.EncryptContext;
+import org.dromara.common.encrypt.enumd.AlgorithmType;
+import org.dromara.common.encrypt.enumd.EncodeType;
+import org.dromara.common.encrypt.utils.EncryptUtils;
+
+/**
+ * sm2绠楁硶瀹炵幇
+ *
+ * @author 鑰侀┈
+ * @version 4.6.0
+ */
+public class Sm2Encryptor extends AbstractEncryptor {
+
+ private final EncryptContext context;
+
+ public Sm2Encryptor(EncryptContext context) {
+ super(context);
+ String privateKey = context.getPrivateKey();
+ String publicKey = context.getPublicKey();
+ if (StringUtils.isAnyEmpty(privateKey, publicKey)) {
+ throw new IllegalArgumentException("SM2鍏閽ュ潎闇�瑕佹彁渚涳紝鍏挜鍔犲瘑锛岀閽ヨВ瀵嗐��");
+ }
+ this.context = context;
+ }
+
+ /**
+ * 鑾峰緱褰撳墠绠楁硶
+ */
+ @Override
+ public AlgorithmType algorithm() {
+ return AlgorithmType.SM2;
+ }
+
+ /**
+ * 鍔犲瘑
+ *
+ * @param value 寰呭姞瀵嗗瓧绗︿覆
+ * @param encodeType 鍔犲瘑鍚庣殑缂栫爜鏍煎紡
+ */
+ @Override
+ public String encrypt(String value, EncodeType encodeType) {
+ if (encodeType == EncodeType.HEX) {
+ return EncryptUtils.encryptBySm2Hex(value, context.getPublicKey());
+ } else {
+ return EncryptUtils.encryptBySm2(value, context.getPublicKey());
+ }
+ }
+
+ /**
+ * 瑙e瘑
+ *
+ * @param value 寰呭姞瀵嗗瓧绗︿覆
+ */
+ @Override
+ public String decrypt(String value) {
+ return EncryptUtils.decryptBySm2(value, context.getPrivateKey());
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/core/encryptor/Sm4Encryptor.java b/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/core/encryptor/Sm4Encryptor.java
new file mode 100644
index 0000000..adaf674
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/core/encryptor/Sm4Encryptor.java
@@ -0,0 +1,55 @@
+package org.dromara.common.encrypt.core.encryptor;
+
+import org.dromara.common.encrypt.core.EncryptContext;
+import org.dromara.common.encrypt.enumd.AlgorithmType;
+import org.dromara.common.encrypt.enumd.EncodeType;
+import org.dromara.common.encrypt.utils.EncryptUtils;
+
+/**
+ * sm4绠楁硶瀹炵幇
+ *
+ * @author 鑰侀┈
+ * @version 4.6.0
+ */
+public class Sm4Encryptor extends AbstractEncryptor {
+
+ private final EncryptContext context;
+
+ public Sm4Encryptor(EncryptContext context) {
+ super(context);
+ this.context = context;
+ }
+
+ /**
+ * 鑾峰緱褰撳墠绠楁硶
+ */
+ @Override
+ public AlgorithmType algorithm() {
+ return AlgorithmType.SM4;
+ }
+
+ /**
+ * 鍔犲瘑
+ *
+ * @param value 寰呭姞瀵嗗瓧绗︿覆
+ * @param encodeType 鍔犲瘑鍚庣殑缂栫爜鏍煎紡
+ */
+ @Override
+ public String encrypt(String value, EncodeType encodeType) {
+ if (encodeType == EncodeType.HEX) {
+ return EncryptUtils.encryptBySm4Hex(value, context.getPassword());
+ } else {
+ return EncryptUtils.encryptBySm4(value, context.getPassword());
+ }
+ }
+
+ /**
+ * 瑙e瘑
+ *
+ * @param value 寰呭姞瀵嗗瓧绗︿覆
+ */
+ @Override
+ public String decrypt(String value) {
+ return EncryptUtils.decryptBySm4(value, context.getPassword());
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/enumd/AlgorithmType.java b/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/enumd/AlgorithmType.java
new file mode 100644
index 0000000..7f54939
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/enumd/AlgorithmType.java
@@ -0,0 +1,48 @@
+package org.dromara.common.encrypt.enumd;
+
+import org.dromara.common.encrypt.core.encryptor.*;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 绠楁硶鍚嶇О
+ *
+ * @author 鑰侀┈
+ * @version 4.6.0
+ */
+@Getter
+@AllArgsConstructor
+public enum AlgorithmType {
+
+ /**
+ * 榛樿璧皔ml閰嶇疆
+ */
+ DEFAULT(null),
+
+ /**
+ * base64
+ */
+ BASE64(Base64Encryptor.class),
+
+ /**
+ * aes
+ */
+ AES(AesEncryptor.class),
+
+ /**
+ * rsa
+ */
+ RSA(RsaEncryptor.class),
+
+ /**
+ * sm2
+ */
+ SM2(Sm2Encryptor.class),
+
+ /**
+ * sm4
+ */
+ SM4(Sm4Encryptor.class);
+
+ private final Class<? extends AbstractEncryptor> clazz;
+}
diff --git a/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/enumd/EncodeType.java b/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/enumd/EncodeType.java
new file mode 100644
index 0000000..f471221
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/enumd/EncodeType.java
@@ -0,0 +1,26 @@
+package org.dromara.common.encrypt.enumd;
+
+/**
+ * 缂栫爜绫诲瀷
+ *
+ * @author 鑰侀┈
+ * @version 4.6.0
+ */
+public enum EncodeType {
+
+ /**
+ * 榛樿浣跨敤yml閰嶇疆
+ */
+ DEFAULT,
+
+ /**
+ * base64缂栫爜
+ */
+ BASE64,
+
+ /**
+ * 16杩涘埗缂栫爜
+ */
+ HEX;
+
+}
diff --git a/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/filter/CryptoFilter.java b/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/filter/CryptoFilter.java
new file mode 100644
index 0000000..8d898c0
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/filter/CryptoFilter.java
@@ -0,0 +1,115 @@
+package org.dromara.common.encrypt.filter;
+
+import cn.hutool.core.util.ObjectUtil;
+import jakarta.servlet.*;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.dromara.common.core.constant.HttpStatus;
+import org.dromara.common.core.exception.ServiceException;
+import org.dromara.common.core.utils.SpringUtils;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.encrypt.annotation.ApiEncrypt;
+import org.dromara.common.encrypt.properties.ApiDecryptProperties;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.MediaType;
+import org.springframework.web.method.HandlerMethod;
+import org.springframework.web.servlet.HandlerExceptionResolver;
+import org.springframework.web.servlet.HandlerExecutionChain;
+import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
+
+import java.io.IOException;
+
+
+/**
+ * Crypto 杩囨护鍣�
+ *
+ * @author wdhcr
+ */
+public class CryptoFilter implements Filter {
+ private final ApiDecryptProperties properties;
+
+ public CryptoFilter(ApiDecryptProperties properties) {
+ this.properties = properties;
+ }
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+ HttpServletRequest servletRequest = (HttpServletRequest) request;
+ HttpServletResponse servletResponse = (HttpServletResponse) response;
+
+ boolean responseFlag = false;
+ ServletRequest requestWrapper = null;
+ ServletResponse responseWrapper = null;
+ EncryptResponseBodyWrapper responseBodyWrapper = null;
+
+ // 鏄惁涓� json 璇锋眰
+ if (StringUtils.startsWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE)) {
+ // 鏄惁涓� put 鎴栬�� post 璇锋眰
+ if (HttpMethod.PUT.matches(servletRequest.getMethod()) || HttpMethod.POST.matches(servletRequest.getMethod())) {
+ // 鏄惁瀛樺湪鍔犲瘑鏍囧ご
+ String headerValue = servletRequest.getHeader(properties.getHeaderFlag());
+ // 鑾峰彇鍔犲瘑娉ㄨВ
+ ApiEncrypt apiEncrypt = this.getApiEncryptAnnotation(servletRequest);
+ responseFlag = apiEncrypt != null && apiEncrypt.response();
+ if (StringUtils.isNotBlank(headerValue)) {
+ // 璇锋眰瑙e瘑
+ requestWrapper = new DecryptRequestBodyWrapper(servletRequest, properties.getPrivateKey(), properties.getHeaderFlag());
+ } else {
+ // 鏄惁鏈夋敞瑙o紝鏈夊氨鎶ラ敊锛屾病鏈夋斁琛�
+ if (ObjectUtil.isNotNull(apiEncrypt)) {
+ HandlerExceptionResolver exceptionResolver = SpringUtils.getBean("handlerExceptionResolver", HandlerExceptionResolver.class);
+ exceptionResolver.resolveException(
+ servletRequest, servletResponse, null,
+ new ServiceException("娌℃湁璁块棶鏉冮檺锛岃鑱旂郴绠$悊鍛樻巿鏉�", HttpStatus.FORBIDDEN));
+ return;
+ }
+ }
+ // 鍒ゆ柇鏄惁鍝嶅簲鍔犲瘑
+ if (responseFlag) {
+ responseBodyWrapper = new EncryptResponseBodyWrapper(servletResponse);
+ responseWrapper = responseBodyWrapper;
+ }
+ }
+ }
+
+ chain.doFilter(
+ ObjectUtil.defaultIfNull(requestWrapper, request),
+ ObjectUtil.defaultIfNull(responseWrapper, response));
+
+ if (responseFlag) {
+ servletResponse.reset();
+ // 瀵瑰師濮嬪唴瀹瑰姞瀵�
+ String encryptContent = responseBodyWrapper.getEncryptContent(
+ servletResponse, properties.getPublicKey(), properties.getHeaderFlag());
+ // 瀵瑰姞瀵嗗悗鐨勫唴瀹瑰啓鍑�
+ servletResponse.getWriter().write(encryptContent);
+ }
+ }
+
+ /**
+ * 鑾峰彇 ApiEncrypt 娉ㄨВ
+ */
+ private ApiEncrypt getApiEncryptAnnotation(HttpServletRequest servletRequest) {
+ RequestMappingHandlerMapping handlerMapping = SpringUtils.getBean("requestMappingHandlerMapping", RequestMappingHandlerMapping.class);
+ // 鑾峰彇娉ㄨВ
+ try {
+ HandlerExecutionChain mappingHandler = handlerMapping.getHandler(servletRequest);
+ if (ObjectUtil.isNotNull(mappingHandler)) {
+ Object handler = mappingHandler.getHandler();
+ if (ObjectUtil.isNotNull(handler)) {
+ // 浠巋andler鑾峰彇娉ㄨВ
+ if (handler instanceof HandlerMethod handlerMethod) {
+ return handlerMethod.getMethodAnnotation(ApiEncrypt.class);
+ }
+ }
+ }
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ return null;
+ }
+
+ @Override
+ public void destroy() {
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/filter/DecryptRequestBodyWrapper.java b/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/filter/DecryptRequestBodyWrapper.java
new file mode 100644
index 0000000..98f4bc7
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/filter/DecryptRequestBodyWrapper.java
@@ -0,0 +1,94 @@
+package org.dromara.common.encrypt.filter;
+
+import cn.hutool.core.io.IoUtil;
+import jakarta.servlet.ReadListener;
+import jakarta.servlet.ServletInputStream;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletRequestWrapper;
+import org.dromara.common.core.constant.Constants;
+import org.dromara.common.encrypt.utils.EncryptUtils;
+import org.springframework.http.MediaType;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * 瑙e瘑璇锋眰鍙傛暟宸ュ叿绫�
+ *
+ * @author wdhcr
+ */
+public class DecryptRequestBodyWrapper extends HttpServletRequestWrapper {
+
+ private final byte[] body;
+
+ public DecryptRequestBodyWrapper(HttpServletRequest request, String privateKey, String headerFlag) throws IOException {
+ super(request);
+ // 鑾峰彇 AES 瀵嗙爜 閲囩敤 RSA 鍔犲瘑
+ String headerRsa = request.getHeader(headerFlag);
+ String decryptAes = EncryptUtils.decryptByRsa(headerRsa, privateKey);
+ // 瑙e瘑 AES 瀵嗙爜
+ String aesPassword = EncryptUtils.decryptByBase64(decryptAes);
+ request.setCharacterEncoding(Constants.UTF8);
+ byte[] readBytes = IoUtil.readBytes(request.getInputStream(), false);
+ String requestBody = new String(readBytes, StandardCharsets.UTF_8);
+ // 瑙e瘑 body 閲囩敤 AES 鍔犲瘑
+ String decryptBody = EncryptUtils.decryptByAes(requestBody, aesPassword);
+ body = decryptBody.getBytes(StandardCharsets.UTF_8);
+ }
+
+ @Override
+ public BufferedReader getReader() {
+ return new BufferedReader(new InputStreamReader(getInputStream()));
+ }
+
+
+ @Override
+ public int getContentLength() {
+ return body.length;
+ }
+
+ @Override
+ public long getContentLengthLong() {
+ return body.length;
+ }
+
+ @Override
+ public String getContentType() {
+ return MediaType.APPLICATION_JSON_VALUE;
+ }
+
+
+ @Override
+ public ServletInputStream getInputStream() {
+ final ByteArrayInputStream bais = new ByteArrayInputStream(body);
+ return new ServletInputStream() {
+ @Override
+ public int read() {
+ return bais.read();
+ }
+
+ @Override
+ public int available() {
+ return body.length;
+ }
+
+ @Override
+ public boolean isFinished() {
+ return false;
+ }
+
+ @Override
+ public boolean isReady() {
+ return false;
+ }
+
+ @Override
+ public void setReadListener(ReadListener readListener) {
+
+ }
+ };
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/filter/EncryptResponseBodyWrapper.java b/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/filter/EncryptResponseBodyWrapper.java
new file mode 100644
index 0000000..5eb34a7
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/filter/EncryptResponseBodyWrapper.java
@@ -0,0 +1,123 @@
+package org.dromara.common.encrypt.filter;
+
+import cn.hutool.core.util.RandomUtil;
+import jakarta.servlet.ServletOutputStream;
+import jakarta.servlet.WriteListener;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpServletResponseWrapper;
+import org.dromara.common.encrypt.utils.EncryptUtils;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * 鍔犲瘑鍝嶅簲鍙傛暟鍖呰绫�
+ *
+ * @author Michelle.Chung
+ */
+public class EncryptResponseBodyWrapper extends HttpServletResponseWrapper {
+
+ private final ByteArrayOutputStream byteArrayOutputStream;
+ private final ServletOutputStream servletOutputStream;
+ private final PrintWriter printWriter;
+
+ public EncryptResponseBodyWrapper(HttpServletResponse response) throws IOException {
+ super(response);
+ this.byteArrayOutputStream = new ByteArrayOutputStream();
+ this.servletOutputStream = this.getOutputStream();
+ this.printWriter = new PrintWriter(new OutputStreamWriter(byteArrayOutputStream));
+ }
+
+ @Override
+ public PrintWriter getWriter() {
+ return printWriter;
+ }
+
+ @Override
+ public void flushBuffer() throws IOException {
+ if (servletOutputStream != null) {
+ servletOutputStream.flush();
+ }
+ if (printWriter != null) {
+ printWriter.flush();
+ }
+ }
+
+ @Override
+ public void reset() {
+ byteArrayOutputStream.reset();
+ }
+
+ public byte[] getResponseData() throws IOException {
+ flushBuffer();
+ return byteArrayOutputStream.toByteArray();
+ }
+
+ public String getContent() throws IOException {
+ flushBuffer();
+ return byteArrayOutputStream.toString();
+ }
+
+ /**
+ * 鑾峰彇鍔犲瘑鍐呭
+ *
+ * @param servletResponse response
+ * @param publicKey RSA鍏挜 (鐢ㄤ簬鍔犲瘑 AES 绉橀挜)
+ * @param headerFlag 璇锋眰澶存爣蹇�
+ * @return 鍔犲瘑鍐呭
+ * @throws IOException
+ */
+ public String getEncryptContent(HttpServletResponse servletResponse, String publicKey, String headerFlag) throws IOException {
+ // 鐢熸垚绉橀挜
+ String aesPassword = RandomUtil.randomString(32);
+ // 绉橀挜浣跨敤 Base64 缂栫爜
+ String encryptAes = EncryptUtils.encryptByBase64(aesPassword);
+ // Rsa 鍏挜鍔犲瘑 Base64 缂栫爜
+ String encryptPassword = EncryptUtils.encryptByRsa(encryptAes, publicKey);
+
+ // 璁剧疆鍝嶅簲澶�
+ servletResponse.setHeader(headerFlag, encryptPassword);
+ servletResponse.setHeader("Access-Control-Allow-Origin", "*");
+ servletResponse.setHeader("Access-Control-Allow-Methods", "*");
+ servletResponse.setCharacterEncoding(StandardCharsets.UTF_8.toString());
+
+ // 鑾峰彇鍘熷鍐呭
+ String originalBody = this.getContent();
+ // 瀵瑰唴瀹硅繘琛屽姞瀵�
+ return EncryptUtils.encryptByAes(originalBody, aesPassword);
+ }
+
+ @Override
+ public ServletOutputStream getOutputStream() throws IOException {
+ return new ServletOutputStream() {
+ @Override
+ public boolean isReady() {
+ return false;
+ }
+
+ @Override
+ public void setWriteListener(WriteListener writeListener) {
+
+ }
+
+ @Override
+ public void write(int b) throws IOException {
+ byteArrayOutputStream.write(b);
+ }
+
+ @Override
+ public void write(byte[] b) throws IOException {
+ byteArrayOutputStream.write(b);
+ }
+
+ @Override
+ public void write(byte[] b, int off, int len) throws IOException {
+ byteArrayOutputStream.write(b, off, len);
+ }
+ };
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/interceptor/MybatisDecryptInterceptor.java b/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/interceptor/MybatisDecryptInterceptor.java
new file mode 100644
index 0000000..7c2508f
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/interceptor/MybatisDecryptInterceptor.java
@@ -0,0 +1,116 @@
+package org.dromara.common.encrypt.interceptor;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.convert.Convert;
+import cn.hutool.core.util.ObjectUtil;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.ibatis.executor.resultset.ResultSetHandler;
+import org.apache.ibatis.plugin.*;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.encrypt.annotation.EncryptField;
+import org.dromara.common.encrypt.core.EncryptContext;
+import org.dromara.common.encrypt.core.EncryptorManager;
+import org.dromara.common.encrypt.enumd.AlgorithmType;
+import org.dromara.common.encrypt.enumd.EncodeType;
+import org.dromara.common.encrypt.properties.EncryptorProperties;
+
+import java.lang.reflect.Field;
+import java.sql.Statement;
+import java.util.*;
+
+/**
+ * 鍑哄弬瑙e瘑鎷︽埅鍣�
+ *
+ * @author 鑰侀┈
+ * @version 4.6.0
+ */
+@Slf4j
+@Intercepts({@Signature(
+ type = ResultSetHandler.class,
+ method = "handleResultSets",
+ args = {Statement.class})
+})
+@AllArgsConstructor
+public class MybatisDecryptInterceptor implements Interceptor {
+
+ private final EncryptorManager encryptorManager;
+ private final EncryptorProperties defaultProperties;
+
+ @Override
+ public Object intercept(Invocation invocation) throws Throwable {
+ // 鑾峰彇鎵цmysql鎵ц缁撴灉
+ Object result = invocation.proceed();
+ if (result == null) {
+ return null;
+ }
+ decryptHandler(result);
+ return result;
+ }
+
+ /**
+ * 瑙e瘑瀵硅薄
+ *
+ * @param sourceObject 寰呭姞瀵嗗璞�
+ */
+ private void decryptHandler(Object sourceObject) {
+ if (ObjectUtil.isNull(sourceObject)) {
+ return;
+ }
+ if (sourceObject instanceof Map<?, ?> map) {
+ new HashSet<>(map.values()).forEach(this::decryptHandler);
+ return;
+ }
+ if (sourceObject instanceof List<?> list) {
+ if(CollUtil.isEmpty(list)) {
+ return;
+ }
+ // 鍒ゆ柇绗竴涓厓绱犳槸鍚﹀惈鏈夋敞瑙c�傚鏋滄病鏈夌洿鎺ヨ繑鍥烇紝鎻愰珮鏁堢巼
+ Object firstItem = list.get(0);
+ if (ObjectUtil.isNull(firstItem) || CollUtil.isEmpty(encryptorManager.getFieldCache(firstItem.getClass()))) {
+ return;
+ }
+ list.forEach(this::decryptHandler);
+ return;
+ }
+ Set<Field> fields = encryptorManager.getFieldCache(sourceObject.getClass());
+ try {
+ for (Field field : fields) {
+ field.set(sourceObject, this.decryptField(Convert.toStr(field.get(sourceObject)), field));
+ }
+ } catch (Exception e) {
+ log.error("澶勭悊瑙e瘑瀛楁鏃跺嚭閿�", e);
+ }
+ }
+
+ /**
+ * 瀛楁鍊艰繘琛屽姞瀵嗐�傞�氳繃瀛楁鐨勬壒娉ㄦ敞鍐屾柊鐨勫姞瀵嗙畻娉�
+ *
+ * @param value 寰呭姞瀵嗙殑鍊�
+ * @param field 寰呭姞瀵嗗瓧娈�
+ * @return 鍔犲瘑鍚庣粨鏋�
+ */
+ private String decryptField(String value, Field field) {
+ if (ObjectUtil.isNull(value)) {
+ return null;
+ }
+ EncryptField encryptField = field.getAnnotation(EncryptField.class);
+ EncryptContext encryptContext = new EncryptContext();
+ encryptContext.setAlgorithm(encryptField.algorithm() == AlgorithmType.DEFAULT ? defaultProperties.getAlgorithm() : encryptField.algorithm());
+ encryptContext.setEncode(encryptField.encode() == EncodeType.DEFAULT ? defaultProperties.getEncode() : encryptField.encode());
+ encryptContext.setPassword(StringUtils.isBlank(encryptField.password()) ? defaultProperties.getPassword() : encryptField.password());
+ encryptContext.setPrivateKey(StringUtils.isBlank(encryptField.privateKey()) ? defaultProperties.getPrivateKey() : encryptField.privateKey());
+ encryptContext.setPublicKey(StringUtils.isBlank(encryptField.publicKey()) ? defaultProperties.getPublicKey() : encryptField.publicKey());
+ return this.encryptorManager.decrypt(value, encryptContext);
+ }
+
+ @Override
+ public Object plugin(Object target) {
+ return Plugin.wrap(target, this);
+ }
+
+ @Override
+ public void setProperties(Properties properties) {
+
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/interceptor/MybatisEncryptInterceptor.java b/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/interceptor/MybatisEncryptInterceptor.java
new file mode 100644
index 0000000..152f7db
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/interceptor/MybatisEncryptInterceptor.java
@@ -0,0 +1,120 @@
+package org.dromara.common.encrypt.interceptor;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.convert.Convert;
+import cn.hutool.core.util.ObjectUtil;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.ibatis.executor.parameter.ParameterHandler;
+import org.apache.ibatis.plugin.Interceptor;
+import org.apache.ibatis.plugin.Intercepts;
+import org.apache.ibatis.plugin.Invocation;
+import org.apache.ibatis.plugin.Signature;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.encrypt.annotation.EncryptField;
+import org.dromara.common.encrypt.core.EncryptContext;
+import org.dromara.common.encrypt.core.EncryptorManager;
+import org.dromara.common.encrypt.enumd.AlgorithmType;
+import org.dromara.common.encrypt.enumd.EncodeType;
+import org.dromara.common.encrypt.properties.EncryptorProperties;
+
+import java.lang.reflect.Field;
+import java.sql.PreparedStatement;
+import java.util.*;
+
+/**
+ * 鍏ュ弬鍔犲瘑鎷︽埅鍣�
+ *
+ * @author 鑰侀┈
+ * @version 4.6.0
+ */
+@Slf4j
+@Intercepts({@Signature(
+ type = ParameterHandler.class,
+ method = "setParameters",
+ args = {PreparedStatement.class})
+})
+@AllArgsConstructor
+public class MybatisEncryptInterceptor implements Interceptor {
+
+ private final EncryptorManager encryptorManager;
+ private final EncryptorProperties defaultProperties;
+
+ @Override
+ public Object intercept(Invocation invocation) throws Throwable {
+ return invocation;
+ }
+
+ @Override
+ public Object plugin(Object target) {
+ if (target instanceof ParameterHandler parameterHandler) {
+ // 杩涜鍔犲瘑鎿嶄綔
+ Object parameterObject = parameterHandler.getParameterObject();
+ if (ObjectUtil.isNotNull(parameterObject) && !(parameterObject instanceof String)) {
+ this.encryptHandler(parameterObject);
+ }
+ }
+ return target;
+ }
+
+ /**
+ * 鍔犲瘑瀵硅薄
+ *
+ * @param sourceObject 寰呭姞瀵嗗璞�
+ */
+ private void encryptHandler(Object sourceObject) {
+ if (ObjectUtil.isNull(sourceObject)) {
+ return;
+ }
+ if (sourceObject instanceof Map<?, ?> map) {
+ new HashSet<>(map.values()).forEach(this::encryptHandler);
+ return;
+ }
+ if (sourceObject instanceof List<?> list) {
+ if(CollUtil.isEmpty(list)) {
+ return;
+ }
+ // 鍒ゆ柇绗竴涓厓绱犳槸鍚﹀惈鏈夋敞瑙c�傚鏋滄病鏈夌洿鎺ヨ繑鍥烇紝鎻愰珮鏁堢巼
+ Object firstItem = list.get(0);
+ if (ObjectUtil.isNull(firstItem) || CollUtil.isEmpty(encryptorManager.getFieldCache(firstItem.getClass()))) {
+ return;
+ }
+ list.forEach(this::encryptHandler);
+ return;
+ }
+ Set<Field> fields = encryptorManager.getFieldCache(sourceObject.getClass());
+ try {
+ for (Field field : fields) {
+ field.set(sourceObject, this.encryptField(Convert.toStr(field.get(sourceObject)), field));
+ }
+ } catch (Exception e) {
+ log.error("澶勭悊鍔犲瘑瀛楁鏃跺嚭閿�", e);
+ }
+ }
+
+ /**
+ * 瀛楁鍊艰繘琛屽姞瀵嗐�傞�氳繃瀛楁鐨勬壒娉ㄦ敞鍐屾柊鐨勫姞瀵嗙畻娉�
+ *
+ * @param value 寰呭姞瀵嗙殑鍊�
+ * @param field 寰呭姞瀵嗗瓧娈�
+ * @return 鍔犲瘑鍚庣粨鏋�
+ */
+ private String encryptField(String value, Field field) {
+ if (ObjectUtil.isNull(value)) {
+ return null;
+ }
+ EncryptField encryptField = field.getAnnotation(EncryptField.class);
+ EncryptContext encryptContext = new EncryptContext();
+ encryptContext.setAlgorithm(encryptField.algorithm() == AlgorithmType.DEFAULT ? defaultProperties.getAlgorithm() : encryptField.algorithm());
+ encryptContext.setEncode(encryptField.encode() == EncodeType.DEFAULT ? defaultProperties.getEncode() : encryptField.encode());
+ encryptContext.setPassword(StringUtils.isBlank(encryptField.password()) ? defaultProperties.getPassword() : encryptField.password());
+ encryptContext.setPrivateKey(StringUtils.isBlank(encryptField.privateKey()) ? defaultProperties.getPrivateKey() : encryptField.privateKey());
+ encryptContext.setPublicKey(StringUtils.isBlank(encryptField.publicKey()) ? defaultProperties.getPublicKey() : encryptField.publicKey());
+ return this.encryptorManager.encrypt(value, encryptContext);
+ }
+
+
+ @Override
+ public void setProperties(Properties properties) {
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/properties/ApiDecryptProperties.java b/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/properties/ApiDecryptProperties.java
new file mode 100644
index 0000000..6aadb3e
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/properties/ApiDecryptProperties.java
@@ -0,0 +1,34 @@
+package org.dromara.common.encrypt.properties;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * api瑙e瘑灞炴�ч厤缃被
+ * @author wdhcr
+ */
+@Data
+@ConfigurationProperties(prefix = "api-decrypt")
+public class ApiDecryptProperties {
+
+ /**
+ * 鍔犲瘑寮�鍏�
+ */
+ private Boolean enabled;
+
+ /**
+ * 澶撮儴鏍囪瘑
+ */
+ private String headerFlag;
+
+ /**
+ * 鍝嶅簲鍔犲瘑鍏挜
+ */
+ private String publicKey;
+
+ /**
+ * 璇锋眰瑙e瘑绉侀挜
+ */
+ private String privateKey;
+
+}
diff --git a/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/properties/EncryptorProperties.java b/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/properties/EncryptorProperties.java
new file mode 100644
index 0000000..ba445c1
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/properties/EncryptorProperties.java
@@ -0,0 +1,48 @@
+package org.dromara.common.encrypt.properties;
+
+import org.dromara.common.encrypt.enumd.AlgorithmType;
+import org.dromara.common.encrypt.enumd.EncodeType;
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * 鍔犺В瀵嗗睘鎬ч厤缃被
+ *
+ * @author 鑰侀┈
+ * @version 4.6.0
+ */
+@Data
+@ConfigurationProperties(prefix = "mybatis-encryptor")
+public class EncryptorProperties {
+
+ /**
+ * 杩囨护寮�鍏�
+ */
+ private Boolean enable;
+
+ /**
+ * 榛樿绠楁硶
+ */
+ private AlgorithmType algorithm;
+
+ /**
+ * 瀹夊叏绉橀挜
+ */
+ private String password;
+
+ /**
+ * 鍏挜
+ */
+ private String publicKey;
+
+ /**
+ * 绉侀挜
+ */
+ private String privateKey;
+
+ /**
+ * 缂栫爜鏂瑰紡锛宐ase64/hex
+ */
+ private EncodeType encode;
+
+}
diff --git a/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/utils/EncryptUtils.java b/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/utils/EncryptUtils.java
new file mode 100644
index 0000000..8e34843
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/utils/EncryptUtils.java
@@ -0,0 +1,311 @@
+package org.dromara.common.encrypt.utils;
+
+import cn.hutool.core.codec.Base64;
+import cn.hutool.core.util.ArrayUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.crypto.SecureUtil;
+import cn.hutool.crypto.SmUtil;
+import cn.hutool.crypto.asymmetric.KeyType;
+import cn.hutool.crypto.asymmetric.RSA;
+import cn.hutool.crypto.asymmetric.SM2;
+
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 瀹夊叏鐩稿叧宸ュ叿绫�
+ *
+ * @author 鑰侀┈
+ */
+public class EncryptUtils {
+ /**
+ * 鍏挜
+ */
+ public static final String PUBLIC_KEY = "publicKey";
+ /**
+ * 绉侀挜
+ */
+ public static final String PRIVATE_KEY = "privateKey";
+
+ /**
+ * Base64鍔犲瘑
+ *
+ * @param data 寰呭姞瀵嗘暟鎹�
+ * @return 鍔犲瘑鍚庡瓧绗︿覆
+ */
+ public static String encryptByBase64(String data) {
+ return Base64.encode(data, StandardCharsets.UTF_8);
+ }
+
+ /**
+ * Base64瑙e瘑
+ *
+ * @param data 寰呰В瀵嗘暟鎹�
+ * @return 瑙e瘑鍚庡瓧绗︿覆
+ */
+ public static String decryptByBase64(String data) {
+ return Base64.decodeStr(data, StandardCharsets.UTF_8);
+ }
+
+ /**
+ * AES鍔犲瘑
+ *
+ * @param data 寰呰В瀵嗘暟鎹�
+ * @param password 绉橀挜瀛楃涓�
+ * @return 鍔犲瘑鍚庡瓧绗︿覆, 閲囩敤Base64缂栫爜
+ */
+ public static String encryptByAes(String data, String password) {
+ if (StrUtil.isBlank(password)) {
+ throw new IllegalArgumentException("AES闇�瑕佷紶鍏ョ閽ヤ俊鎭�");
+ }
+ // aes绠楁硶鐨勭閽ヨ姹傛槸16浣嶃��24浣嶃��32浣�
+ int[] array = {16, 24, 32};
+ if (!ArrayUtil.contains(array, password.length())) {
+ throw new IllegalArgumentException("AES绉橀挜闀垮害瑕佹眰涓�16浣嶃��24浣嶃��32浣�");
+ }
+ return SecureUtil.aes(password.getBytes(StandardCharsets.UTF_8)).encryptBase64(data, StandardCharsets.UTF_8);
+ }
+
+ /**
+ * AES鍔犲瘑
+ *
+ * @param data 寰呰В瀵嗘暟鎹�
+ * @param password 绉橀挜瀛楃涓�
+ * @return 鍔犲瘑鍚庡瓧绗︿覆, 閲囩敤Hex缂栫爜
+ */
+ public static String encryptByAesHex(String data, String password) {
+ if (StrUtil.isBlank(password)) {
+ throw new IllegalArgumentException("AES闇�瑕佷紶鍏ョ閽ヤ俊鎭�");
+ }
+ // aes绠楁硶鐨勭閽ヨ姹傛槸16浣嶃��24浣嶃��32浣�
+ int[] array = {16, 24, 32};
+ if (!ArrayUtil.contains(array, password.length())) {
+ throw new IllegalArgumentException("AES绉橀挜闀垮害瑕佹眰涓�16浣嶃��24浣嶃��32浣�");
+ }
+ return SecureUtil.aes(password.getBytes(StandardCharsets.UTF_8)).encryptHex(data, StandardCharsets.UTF_8);
+ }
+
+ /**
+ * AES瑙e瘑
+ *
+ * @param data 寰呰В瀵嗘暟鎹�
+ * @param password 绉橀挜瀛楃涓�
+ * @return 瑙e瘑鍚庡瓧绗︿覆
+ */
+ public static String decryptByAes(String data, String password) {
+ if (StrUtil.isBlank(password)) {
+ throw new IllegalArgumentException("AES闇�瑕佷紶鍏ョ閽ヤ俊鎭�");
+ }
+ // aes绠楁硶鐨勭閽ヨ姹傛槸16浣嶃��24浣嶃��32浣�
+ int[] array = {16, 24, 32};
+ if (!ArrayUtil.contains(array, password.length())) {
+ throw new IllegalArgumentException("AES绉橀挜闀垮害瑕佹眰涓�16浣嶃��24浣嶃��32浣�");
+ }
+ return SecureUtil.aes(password.getBytes(StandardCharsets.UTF_8)).decryptStr(data, StandardCharsets.UTF_8);
+ }
+
+ /**
+ * sm4鍔犲瘑
+ *
+ * @param data 寰呭姞瀵嗘暟鎹�
+ * @param password 绉橀挜瀛楃涓�
+ * @return 鍔犲瘑鍚庡瓧绗︿覆, 閲囩敤Base64缂栫爜
+ */
+ public static String encryptBySm4(String data, String password) {
+ if (StrUtil.isBlank(password)) {
+ throw new IllegalArgumentException("SM4闇�瑕佷紶鍏ョ閽ヤ俊鎭�");
+ }
+ // sm4绠楁硶鐨勭閽ヨ姹傛槸16浣嶉暱搴�
+ int sm4PasswordLength = 16;
+ if (sm4PasswordLength != password.length()) {
+ throw new IllegalArgumentException("SM4绉橀挜闀垮害瑕佹眰涓�16浣�");
+ }
+ return SmUtil.sm4(password.getBytes(StandardCharsets.UTF_8)).encryptBase64(data, StandardCharsets.UTF_8);
+ }
+
+ /**
+ * sm4鍔犲瘑
+ *
+ * @param data 寰呭姞瀵嗘暟鎹�
+ * @param password 绉橀挜瀛楃涓�
+ * @return 鍔犲瘑鍚庡瓧绗︿覆, 閲囩敤Base64缂栫爜
+ */
+ public static String encryptBySm4Hex(String data, String password) {
+ if (StrUtil.isBlank(password)) {
+ throw new IllegalArgumentException("SM4闇�瑕佷紶鍏ョ閽ヤ俊鎭�");
+ }
+ // sm4绠楁硶鐨勭閽ヨ姹傛槸16浣嶉暱搴�
+ int sm4PasswordLength = 16;
+ if (sm4PasswordLength != password.length()) {
+ throw new IllegalArgumentException("SM4绉橀挜闀垮害瑕佹眰涓�16浣�");
+ }
+ return SmUtil.sm4(password.getBytes(StandardCharsets.UTF_8)).encryptHex(data, StandardCharsets.UTF_8);
+ }
+
+ /**
+ * sm4瑙e瘑
+ *
+ * @param data 寰呰В瀵嗘暟鎹�
+ * @param password 绉橀挜瀛楃涓�
+ * @return 瑙e瘑鍚庡瓧绗︿覆
+ */
+ public static String decryptBySm4(String data, String password) {
+ if (StrUtil.isBlank(password)) {
+ throw new IllegalArgumentException("SM4闇�瑕佷紶鍏ョ閽ヤ俊鎭�");
+ }
+ // sm4绠楁硶鐨勭閽ヨ姹傛槸16浣嶉暱搴�
+ int sm4PasswordLength = 16;
+ if (sm4PasswordLength != password.length()) {
+ throw new IllegalArgumentException("SM4绉橀挜闀垮害瑕佹眰涓�16浣�");
+ }
+ return SmUtil.sm4(password.getBytes(StandardCharsets.UTF_8)).decryptStr(data, StandardCharsets.UTF_8);
+ }
+
+ /**
+ * 浜х敓sm2鍔犺В瀵嗛渶瑕佺殑鍏挜鍜岀閽�
+ *
+ * @return 鍏閽ap
+ */
+ public static Map<String, String> generateSm2Key() {
+ Map<String, String> keyMap = new HashMap<>(2);
+ SM2 sm2 = SmUtil.sm2();
+ keyMap.put(PRIVATE_KEY, sm2.getPrivateKeyBase64());
+ keyMap.put(PUBLIC_KEY, sm2.getPublicKeyBase64());
+ return keyMap;
+ }
+
+ /**
+ * sm2鍏挜鍔犲瘑
+ *
+ * @param data 寰呭姞瀵嗘暟鎹�
+ * @param publicKey 鍏挜
+ * @return 鍔犲瘑鍚庡瓧绗︿覆, 閲囩敤Base64缂栫爜
+ */
+ public static String encryptBySm2(String data, String publicKey) {
+ if (StrUtil.isBlank(publicKey)) {
+ throw new IllegalArgumentException("SM2闇�瑕佷紶鍏ュ叕閽ヨ繘琛屽姞瀵�");
+ }
+ SM2 sm2 = SmUtil.sm2(null, publicKey);
+ return sm2.encryptBase64(data, StandardCharsets.UTF_8, KeyType.PublicKey);
+ }
+
+ /**
+ * sm2鍏挜鍔犲瘑
+ *
+ * @param data 寰呭姞瀵嗘暟鎹�
+ * @param publicKey 鍏挜
+ * @return 鍔犲瘑鍚庡瓧绗︿覆, 閲囩敤Hex缂栫爜
+ */
+ public static String encryptBySm2Hex(String data, String publicKey) {
+ if (StrUtil.isBlank(publicKey)) {
+ throw new IllegalArgumentException("SM2闇�瑕佷紶鍏ュ叕閽ヨ繘琛屽姞瀵�");
+ }
+ SM2 sm2 = SmUtil.sm2(null, publicKey);
+ return sm2.encryptHex(data, StandardCharsets.UTF_8, KeyType.PublicKey);
+ }
+
+ /**
+ * sm2绉侀挜瑙e瘑
+ *
+ * @param data 寰呭姞瀵嗘暟鎹�
+ * @param privateKey 绉侀挜
+ * @return 瑙e瘑鍚庡瓧绗︿覆
+ */
+ public static String decryptBySm2(String data, String privateKey) {
+ if (StrUtil.isBlank(privateKey)) {
+ throw new IllegalArgumentException("SM2闇�瑕佷紶鍏ョ閽ヨ繘琛岃В瀵�");
+ }
+ SM2 sm2 = SmUtil.sm2(privateKey, null);
+ return sm2.decryptStr(data, KeyType.PrivateKey, StandardCharsets.UTF_8);
+ }
+
+ /**
+ * 浜х敓RSA鍔犺В瀵嗛渶瑕佺殑鍏挜鍜岀閽�
+ *
+ * @return 鍏閽ap
+ */
+ public static Map<String, String> generateRsaKey() {
+ Map<String, String> keyMap = new HashMap<>(2);
+ RSA rsa = SecureUtil.rsa();
+ keyMap.put(PRIVATE_KEY, rsa.getPrivateKeyBase64());
+ keyMap.put(PUBLIC_KEY, rsa.getPublicKeyBase64());
+ return keyMap;
+ }
+
+ /**
+ * rsa鍏挜鍔犲瘑
+ *
+ * @param data 寰呭姞瀵嗘暟鎹�
+ * @param publicKey 鍏挜
+ * @return 鍔犲瘑鍚庡瓧绗︿覆, 閲囩敤Base64缂栫爜
+ */
+ public static String encryptByRsa(String data, String publicKey) {
+ if (StrUtil.isBlank(publicKey)) {
+ throw new IllegalArgumentException("RSA闇�瑕佷紶鍏ュ叕閽ヨ繘琛屽姞瀵�");
+ }
+ RSA rsa = SecureUtil.rsa(null, publicKey);
+ return rsa.encryptBase64(data, StandardCharsets.UTF_8, KeyType.PublicKey);
+ }
+
+ /**
+ * rsa鍏挜鍔犲瘑
+ *
+ * @param data 寰呭姞瀵嗘暟鎹�
+ * @param publicKey 鍏挜
+ * @return 鍔犲瘑鍚庡瓧绗︿覆, 閲囩敤Hex缂栫爜
+ */
+ public static String encryptByRsaHex(String data, String publicKey) {
+ if (StrUtil.isBlank(publicKey)) {
+ throw new IllegalArgumentException("RSA闇�瑕佷紶鍏ュ叕閽ヨ繘琛屽姞瀵�");
+ }
+ RSA rsa = SecureUtil.rsa(null, publicKey);
+ return rsa.encryptHex(data, StandardCharsets.UTF_8, KeyType.PublicKey);
+ }
+
+ /**
+ * rsa绉侀挜瑙e瘑
+ *
+ * @param data 寰呭姞瀵嗘暟鎹�
+ * @param privateKey 绉侀挜
+ * @return 瑙e瘑鍚庡瓧绗︿覆
+ */
+ public static String decryptByRsa(String data, String privateKey) {
+ if (StrUtil.isBlank(privateKey)) {
+ throw new IllegalArgumentException("RSA闇�瑕佷紶鍏ョ閽ヨ繘琛岃В瀵�");
+ }
+ RSA rsa = SecureUtil.rsa(privateKey, null);
+ return rsa.decryptStr(data, KeyType.PrivateKey, StandardCharsets.UTF_8);
+ }
+
+ /**
+ * md5鍔犲瘑
+ *
+ * @param data 寰呭姞瀵嗘暟鎹�
+ * @return 鍔犲瘑鍚庡瓧绗︿覆, 閲囩敤Hex缂栫爜
+ */
+ public static String encryptByMd5(String data) {
+ return SecureUtil.md5(data);
+ }
+
+ /**
+ * sha256鍔犲瘑
+ *
+ * @param data 寰呭姞瀵嗘暟鎹�
+ * @return 鍔犲瘑鍚庡瓧绗︿覆, 閲囩敤Hex缂栫爜
+ */
+ public static String encryptBySha256(String data) {
+ return SecureUtil.sha256(data);
+ }
+
+ /**
+ * sm3鍔犲瘑
+ *
+ * @param data 寰呭姞瀵嗘暟鎹�
+ * @return 鍔犲瘑鍚庡瓧绗︿覆, 閲囩敤Hex缂栫爜
+ */
+ public static String encryptBySm3(String data) {
+ return SmUtil.sm3(data);
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-encrypt/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/ruoyi-common/ruoyi-common-encrypt/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..b73a234
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-encrypt/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1,2 @@
+org.dromara.common.encrypt.config.EncryptorAutoConfiguration
+org.dromara.common.encrypt.config.ApiDecryptAutoConfiguration
diff --git a/ruoyi-common/ruoyi-common-excel/pom.xml b/ruoyi-common/ruoyi-common-excel/pom.xml
new file mode 100644
index 0000000..dd4a5ee
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-excel/pom.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common</artifactId>
+ <version>${revision}</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>ruoyi-common-excel</artifactId>
+
+ <description>
+ ruoyi-common-excel
+ </description>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-json</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>com.alibaba</groupId>
+ <artifactId>easyexcel</artifactId>
+ </dependency>
+ </dependencies>
+
+</project>
diff --git a/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/annotation/CellMerge.java b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/annotation/CellMerge.java
new file mode 100644
index 0000000..bbdaaa1
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/annotation/CellMerge.java
@@ -0,0 +1,24 @@
+package org.dromara.common.excel.annotation;
+
+import org.dromara.common.excel.core.CellMergeStrategy;
+
+import java.lang.annotation.*;
+
+/**
+ * excel 鍒楀崟鍏冩牸鍚堝苟(鍚堝苟鍒楃浉鍚岄」)
+ *
+ * 闇�鎼厤 {@link CellMergeStrategy} 绛栫暐浣跨敤
+ *
+ * @author Lion Li
+ */
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface CellMerge {
+
+ /**
+ * col index
+ */
+ int index() default -1;
+
+}
diff --git a/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/annotation/ExcelDictFormat.java b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/annotation/ExcelDictFormat.java
new file mode 100644
index 0000000..5c51842
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/annotation/ExcelDictFormat.java
@@ -0,0 +1,32 @@
+package org.dromara.common.excel.annotation;
+
+import org.dromara.common.core.utils.StringUtils;
+
+import java.lang.annotation.*;
+
+/**
+ * 瀛楀吀鏍煎紡鍖�
+ *
+ * @author Lion Li
+ */
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface ExcelDictFormat {
+
+ /**
+ * 濡傛灉鏄瓧鍏哥被鍨嬶紝璇疯缃瓧鍏哥殑type鍊� (濡�: sys_user_sex)
+ */
+ String dictType() default "";
+
+ /**
+ * 璇诲彇鍐呭杞〃杈惧紡 (濡�: 0=鐢�,1=濂�,2=鏈煡)
+ */
+ String readConverterExp() default "";
+
+ /**
+ * 鍒嗛殧绗︼紝璇诲彇瀛楃涓茬粍鍐呭
+ */
+ String separator() default StringUtils.SEPARATOR;
+
+}
diff --git a/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/annotation/ExcelEnumFormat.java b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/annotation/ExcelEnumFormat.java
new file mode 100644
index 0000000..290379d
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/annotation/ExcelEnumFormat.java
@@ -0,0 +1,30 @@
+package org.dromara.common.excel.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 鏋氫妇鏍煎紡鍖�
+ *
+ * @author Liang
+ */
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface ExcelEnumFormat {
+
+ /**
+ * 瀛楀吀鏋氫妇绫诲瀷
+ */
+ Class<? extends Enum<?>> enumClass();
+
+ /**
+ * 瀛楀吀鏋氫妇绫讳腑瀵瑰簲鐨刢ode灞炴�у悕绉帮紝榛樿涓篶ode
+ */
+ String codeField() default "code";
+
+ /**
+ * 瀛楀吀鏋氫妇绫讳腑瀵瑰簲鐨則ext灞炴�у悕绉帮紝榛樿涓簍ext
+ */
+ String textField() default "text";
+
+}
diff --git a/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/convert/ExcelBigNumberConvert.java b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/convert/ExcelBigNumberConvert.java
new file mode 100644
index 0000000..07cc4c4
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/convert/ExcelBigNumberConvert.java
@@ -0,0 +1,52 @@
+package org.dromara.common.excel.convert;
+
+import cn.hutool.core.convert.Convert;
+import cn.hutool.core.util.ObjectUtil;
+import com.alibaba.excel.converters.Converter;
+import com.alibaba.excel.enums.CellDataTypeEnum;
+import com.alibaba.excel.metadata.GlobalConfiguration;
+import com.alibaba.excel.metadata.data.ReadCellData;
+import com.alibaba.excel.metadata.data.WriteCellData;
+import com.alibaba.excel.metadata.property.ExcelContentProperty;
+import lombok.extern.slf4j.Slf4j;
+
+import java.math.BigDecimal;
+
+/**
+ * 澶ф暟鍊艰浆鎹�
+ * Excel 鏁板�奸暱搴︿綅15浣� 澶т簬15浣嶇殑鏁板�艰浆鎹綅瀛楃涓�
+ *
+ * @author Lion Li
+ */
+@Slf4j
+public class ExcelBigNumberConvert implements Converter<Long> {
+
+ @Override
+ public Class<Long> supportJavaTypeKey() {
+ return Long.class;
+ }
+
+ @Override
+ public CellDataTypeEnum supportExcelTypeKey() {
+ return CellDataTypeEnum.STRING;
+ }
+
+ @Override
+ public Long convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
+ return Convert.toLong(cellData.getData());
+ }
+
+ @Override
+ public WriteCellData<Object> convertToExcelData(Long object, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
+ if (ObjectUtil.isNotNull(object)) {
+ String str = Convert.toStr(object);
+ if (str.length() > 15) {
+ return new WriteCellData<>(str);
+ }
+ }
+ WriteCellData<Object> cellData = new WriteCellData<>(new BigDecimal(object));
+ cellData.setType(CellDataTypeEnum.NUMBER);
+ return cellData;
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/convert/ExcelDictConvert.java b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/convert/ExcelDictConvert.java
new file mode 100644
index 0000000..61eeabf
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/convert/ExcelDictConvert.java
@@ -0,0 +1,73 @@
+package org.dromara.common.excel.convert;
+
+import cn.hutool.core.annotation.AnnotationUtil;
+import cn.hutool.core.convert.Convert;
+import cn.hutool.core.util.ObjectUtil;
+import com.alibaba.excel.converters.Converter;
+import com.alibaba.excel.enums.CellDataTypeEnum;
+import com.alibaba.excel.metadata.GlobalConfiguration;
+import com.alibaba.excel.metadata.data.ReadCellData;
+import com.alibaba.excel.metadata.data.WriteCellData;
+import com.alibaba.excel.metadata.property.ExcelContentProperty;
+import org.dromara.common.excel.annotation.ExcelDictFormat;
+import org.dromara.common.core.service.DictService;
+import org.dromara.common.core.utils.SpringUtils;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.excel.utils.ExcelUtil;
+import lombok.extern.slf4j.Slf4j;
+
+import java.lang.reflect.Field;
+
+/**
+ * 瀛楀吀鏍煎紡鍖栬浆鎹㈠鐞�
+ *
+ * @author Lion Li
+ */
+@Slf4j
+public class ExcelDictConvert implements Converter<Object> {
+
+ @Override
+ public Class<Object> supportJavaTypeKey() {
+ return Object.class;
+ }
+
+ @Override
+ public CellDataTypeEnum supportExcelTypeKey() {
+ return null;
+ }
+
+ @Override
+ public Object convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
+ ExcelDictFormat anno = getAnnotation(contentProperty.getField());
+ String type = anno.dictType();
+ String label = cellData.getStringValue();
+ String value;
+ if (StringUtils.isBlank(type)) {
+ value = ExcelUtil.reverseByExp(label, anno.readConverterExp(), anno.separator());
+ } else {
+ value = SpringUtils.getBean(DictService.class).getDictValue(type, label, anno.separator());
+ }
+ return Convert.convert(contentProperty.getField().getType(), value);
+ }
+
+ @Override
+ public WriteCellData<String> convertToExcelData(Object object, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
+ if (ObjectUtil.isNull(object)) {
+ return new WriteCellData<>("");
+ }
+ ExcelDictFormat anno = getAnnotation(contentProperty.getField());
+ String type = anno.dictType();
+ String value = Convert.toStr(object);
+ String label;
+ if (StringUtils.isBlank(type)) {
+ label = ExcelUtil.convertByExp(value, anno.readConverterExp(), anno.separator());
+ } else {
+ label = SpringUtils.getBean(DictService.class).getDictLabel(type, value, anno.separator());
+ }
+ return new WriteCellData<>(label);
+ }
+
+ private ExcelDictFormat getAnnotation(Field field) {
+ return AnnotationUtil.getAnnotation(field, ExcelDictFormat.class);
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/convert/ExcelEnumConvert.java b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/convert/ExcelEnumConvert.java
new file mode 100644
index 0000000..b948ea7
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/convert/ExcelEnumConvert.java
@@ -0,0 +1,87 @@
+package org.dromara.common.excel.convert;
+
+import cn.hutool.core.annotation.AnnotationUtil;
+import cn.hutool.core.convert.Convert;
+import cn.hutool.core.util.ObjectUtil;
+import com.alibaba.excel.converters.Converter;
+import com.alibaba.excel.enums.CellDataTypeEnum;
+import com.alibaba.excel.metadata.GlobalConfiguration;
+import com.alibaba.excel.metadata.data.ReadCellData;
+import com.alibaba.excel.metadata.data.WriteCellData;
+import com.alibaba.excel.metadata.property.ExcelContentProperty;
+import org.dromara.common.core.utils.reflect.ReflectUtils;
+import org.dromara.common.excel.annotation.ExcelEnumFormat;
+import lombok.extern.slf4j.Slf4j;
+
+import java.lang.reflect.Field;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 鏋氫妇鏍煎紡鍖栬浆鎹㈠鐞�
+ *
+ * @author Liang
+ */
+@Slf4j
+public class ExcelEnumConvert implements Converter<Object> {
+
+ @Override
+ public Class<Object> supportJavaTypeKey() {
+ return Object.class;
+ }
+
+ @Override
+ public CellDataTypeEnum supportExcelTypeKey() {
+ return null;
+ }
+
+ @Override
+ public Object convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
+ cellData.checkEmpty();
+ // Excel涓~鍏ョ殑鏄灇涓句腑鎸囧畾鐨勬弿杩�
+ Object textValue = switch (cellData.getType()) {
+ case STRING, DIRECT_STRING, RICH_TEXT_STRING -> cellData.getStringValue();
+ case NUMBER -> cellData.getNumberValue();
+ case BOOLEAN -> cellData.getBooleanValue();
+ default -> throw new IllegalArgumentException("鍗曞厓鏍肩被鍨嬪紓甯�!");
+ };
+ // 濡傛灉鏄┖鍊�
+ if (ObjectUtil.isNull(textValue)) {
+ return null;
+ }
+ Map<Object, String> enumCodeToTextMap = beforeConvert(contentProperty);
+ // 浠嶫ava杈撳嚭鑷矱xcel鏄痗ode杞瑃ext
+ // 鍥犳浠嶦xcel杞琂ava搴旇灏唗ext涓巆ode瀵硅皟
+ Map<Object, Object> enumTextToCodeMap = new HashMap<>();
+ enumCodeToTextMap.forEach((key, value) -> enumTextToCodeMap.put(value, key));
+ // 搴旇浠巘ext -> code涓煡鎵�
+ Object codeValue = enumTextToCodeMap.get(textValue);
+ return Convert.convert(contentProperty.getField().getType(), codeValue);
+ }
+
+ @Override
+ public WriteCellData<String> convertToExcelData(Object object, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
+ if (ObjectUtil.isNull(object)) {
+ return new WriteCellData<>("");
+ }
+ Map<Object, String> enumValueMap = beforeConvert(contentProperty);
+ String value = Convert.toStr(enumValueMap.get(object), "");
+ return new WriteCellData<>(value);
+ }
+
+ private Map<Object, String> beforeConvert(ExcelContentProperty contentProperty) {
+ ExcelEnumFormat anno = getAnnotation(contentProperty.getField());
+ Map<Object, String> enumValueMap = new HashMap<>();
+ Enum<?>[] enumConstants = anno.enumClass().getEnumConstants();
+ for (Enum<?> enumConstant : enumConstants) {
+ Object codeValue = ReflectUtils.invokeGetter(enumConstant, anno.codeField());
+ String textValue = ReflectUtils.invokeGetter(enumConstant, anno.textField());
+ enumValueMap.put(codeValue, textValue);
+ }
+ return enumValueMap;
+ }
+
+ private ExcelEnumFormat getAnnotation(Field field) {
+ return AnnotationUtil.getAnnotation(field, ExcelEnumFormat.class);
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/CellMergeStrategy.java b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/CellMergeStrategy.java
new file mode 100644
index 0000000..bcb5be7
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/CellMergeStrategy.java
@@ -0,0 +1,142 @@
+package org.dromara.common.excel.core;
+
+import cn.hutool.core.collection.CollUtil;
+import com.alibaba.excel.annotation.ExcelProperty;
+import com.alibaba.excel.metadata.Head;
+import com.alibaba.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.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 鍒楀�奸噸澶嶅悎骞剁瓥鐣�
+ *
+ * @author Lion Li
+ */
+@Slf4j
+public class CellMergeStrategy extends AbstractMergeStrategy {
+
+ private final List<CellRangeAddress> cellList;
+ private final boolean hasTitle;
+ private int rowIndex;
+
+ public CellMergeStrategy(List<?> list, boolean hasTitle) {
+ this.hasTitle = hasTitle;
+ // 琛屽悎骞跺紑濮嬩笅鏍�
+ this.rowIndex = hasTitle ? 1 : 0;
+ this.cellList = handle(list, hasTitle);
+ }
+
+ @Override
+ protected void merge(Sheet sheet, Cell cell, Head head, Integer relativeRowIndex) {
+ // judge the list is not null
+ if (CollUtil.isNotEmpty(cellList)) {
+ // the judge is necessary
+ if (cell.getRowIndex() == rowIndex && cell.getColumnIndex() == 0) {
+ for (CellRangeAddress item : cellList) {
+ sheet.addMergedRegion(item);
+ }
+ }
+ }
+ }
+
+ @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()));
+
+ // 鏈夋敞瑙g殑瀛楁
+ 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 (j == 0) {
+ if (i == list.size() - 1) {
+ if (i > repeatCell.getCurrent()) {
+ cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex, colNum, colNum));
+ }
+ }
+ } else {
+ // 鍒ゆ柇鍓嶉潰鐨勬槸鍚﹀悎骞朵簡
+ RepeatCell firstCell = map.get(mergeFields.get(0));
+ if (repeatCell.getCurrent() != firstCell.getCurrent()) {
+ if (i == list.size() - 1) {
+ if (i > repeatCell.getCurrent()) {
+ cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex, colNum, colNum));
+ }
+ } else if (repeatCell.getCurrent() < firstCell.getCurrent()) {
+ if (i - repeatCell.getCurrent() > 1) {
+ cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex - 1, colNum, colNum));
+ }
+ map.put(field, new RepeatCell(val, i));
+ }
+ } else if (i == list.size() - 1) {
+ if (i > repeatCell.getCurrent()) {
+ cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex, colNum, colNum));
+ }
+ }
+ }
+ }
+ }
+ }
+ return cellList;
+ }
+
+ @Data
+ @AllArgsConstructor
+ static class RepeatCell {
+
+ private Object value;
+
+ private int current;
+
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/DefaultExcelListener.java b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/DefaultExcelListener.java
new file mode 100644
index 0000000..b6fa0b4
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/DefaultExcelListener.java
@@ -0,0 +1,104 @@
+package org.dromara.common.excel.core;
+
+import cn.hutool.core.util.StrUtil;
+import com.alibaba.excel.context.AnalysisContext;
+import com.alibaba.excel.event.AnalysisEventListener;
+import com.alibaba.excel.exception.ExcelAnalysisException;
+import com.alibaba.excel.exception.ExcelDataConvertException;
+import org.dromara.common.core.utils.StreamUtils;
+import org.dromara.common.core.utils.ValidatorUtils;
+import org.dromara.common.json.utils.JsonUtils;
+import jakarta.validation.ConstraintViolation;
+import jakarta.validation.ConstraintViolationException;
+import lombok.NoArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Excel 瀵煎叆鐩戝惉
+ *
+ * @author Yjoioooo
+ * @author Lion Li
+ */
+@Slf4j
+@NoArgsConstructor
+public class DefaultExcelListener<T> extends AnalysisEventListener<T> implements ExcelListener<T> {
+
+ /**
+ * 鏄惁Validator妫�楠岋紝榛樿涓烘槸
+ */
+ private Boolean isValidate = Boolean.TRUE;
+
+ /**
+ * excel 琛ㄥご鏁版嵁
+ */
+ private Map<Integer, String> headMap;
+
+ /**
+ * 瀵煎叆鍥炴墽
+ */
+ private ExcelResult<T> excelResult;
+
+ public DefaultExcelListener(boolean isValidate) {
+ this.excelResult = new DefaultExcelResult<>();
+ this.isValidate = isValidate;
+ }
+
+ /**
+ * 澶勭悊寮傚父
+ *
+ * @param exception ExcelDataConvertException
+ * @param context Excel 涓婁笅鏂�
+ */
+ @Override
+ public void onException(Exception exception, AnalysisContext context) throws Exception {
+ String errMsg = null;
+ if (exception instanceof ExcelDataConvertException excelDataConvertException) {
+ // 濡傛灉鏄煇涓�涓崟鍏冩牸鐨勮浆鎹㈠紓甯� 鑳借幏鍙栧埌鍏蜂綋琛屽彿
+ Integer rowIndex = excelDataConvertException.getRowIndex();
+ Integer columnIndex = excelDataConvertException.getColumnIndex();
+ errMsg = StrUtil.format("绗瑊}琛�-绗瑊}鍒�-琛ㄥご{}: 瑙f瀽寮傚父<br/>",
+ rowIndex + 1, columnIndex + 1, headMap.get(columnIndex));
+ if (log.isDebugEnabled()) {
+ log.error(errMsg);
+ }
+ }
+ if (exception instanceof ConstraintViolationException constraintViolationException) {
+ Set<ConstraintViolation<?>> constraintViolations = constraintViolationException.getConstraintViolations();
+ String constraintViolationsMsg = StreamUtils.join(constraintViolations, ConstraintViolation::getMessage, ", ");
+ errMsg = StrUtil.format("绗瑊}琛屾暟鎹牎楠屽紓甯�: {}", context.readRowHolder().getRowIndex() + 1, constraintViolationsMsg);
+ if (log.isDebugEnabled()) {
+ log.error(errMsg);
+ }
+ }
+ excelResult.getErrorList().add(errMsg);
+ throw new ExcelAnalysisException(errMsg);
+ }
+
+ @Override
+ public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
+ this.headMap = headMap;
+ log.debug("瑙f瀽鍒颁竴鏉¤〃澶存暟鎹�: {}", JsonUtils.toJsonString(headMap));
+ }
+
+ @Override
+ public void invoke(T data, AnalysisContext context) {
+ if (isValidate) {
+ ValidatorUtils.validate(data);
+ }
+ excelResult.getList().add(data);
+ }
+
+ @Override
+ public void doAfterAllAnalysed(AnalysisContext context) {
+ log.debug("鎵�鏈夋暟鎹В鏋愬畬鎴愶紒");
+ }
+
+ @Override
+ public ExcelResult<T> getExcelResult() {
+ return excelResult;
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/DefaultExcelResult.java b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/DefaultExcelResult.java
new file mode 100644
index 0000000..7373e12
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/DefaultExcelResult.java
@@ -0,0 +1,73 @@
+package org.dromara.common.excel.core;
+
+import cn.hutool.core.util.StrUtil;
+import lombok.Setter;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 榛樿excel杩斿洖瀵硅薄
+ *
+ * @author Yjoioooo
+ * @author Lion Li
+ */
+public class DefaultExcelResult<T> implements ExcelResult<T> {
+
+ /**
+ * 鏁版嵁瀵硅薄list
+ */
+ @Setter
+ private List<T> list;
+
+ /**
+ * 閿欒淇℃伅鍒楄〃
+ */
+ @Setter
+ private List<String> errorList;
+
+ public DefaultExcelResult() {
+ this.list = new ArrayList<>();
+ this.errorList = new ArrayList<>();
+ }
+
+ public DefaultExcelResult(List<T> list, List<String> errorList) {
+ this.list = list;
+ this.errorList = errorList;
+ }
+
+ public DefaultExcelResult(ExcelResult<T> excelResult) {
+ this.list = excelResult.getList();
+ this.errorList = excelResult.getErrorList();
+ }
+
+ @Override
+ public List<T> getList() {
+ return list;
+ }
+
+ @Override
+ public List<String> getErrorList() {
+ return errorList;
+ }
+
+ /**
+ * 鑾峰彇瀵煎叆鍥炴墽
+ *
+ * @return 瀵煎叆鍥炴墽
+ */
+ @Override
+ public String getAnalysis() {
+ int successCount = list.size();
+ int errorCount = errorList.size();
+ if (successCount == 0) {
+ return "璇诲彇澶辫触锛屾湭瑙f瀽鍒版暟鎹�";
+ } else {
+ if (errorCount == 0) {
+ return StrUtil.format("鎭枩鎮紝鍏ㄩ儴璇诲彇鎴愬姛锛佸叡{}鏉�", successCount);
+ } else {
+ return "";
+ }
+ }
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/DropDownOptions.java b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/DropDownOptions.java
new file mode 100644
index 0000000..8b53a0c
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/DropDownOptions.java
@@ -0,0 +1,149 @@
+package org.dromara.common.excel.core;
+
+import cn.hutool.core.util.StrUtil;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.dromara.common.core.exception.ServiceException;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * <h1>Excel涓嬫媺鍙�夐」</h1>
+ * 娉ㄦ剰锛氫负纭繚涓嬫媺妗嗚В鏋愭纭紝浼犲�煎姟蹇呬娇鐢╟reateOptionValue()鍋氫负鍊肩殑鎷兼帴
+ *
+ * @author Emil.Zhang
+ */
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@SuppressWarnings("unused")
+public class DropDownOptions {
+ /**
+ * 涓�绾т笅鎷夋墍鍦ㄥ垪index锛屼粠0寮�濮嬬畻
+ */
+ private int index = 0;
+ /**
+ * 浜岀骇涓嬫媺鎵�鍦ㄧ殑index锛屼粠0寮�濮嬬畻锛屼笉鑳戒笌涓�绾х浉鍚�
+ */
+ private int nextIndex = 0;
+ /**
+ * 涓�绾т笅鎷夋墍鍖呭惈鐨勬暟鎹�
+ */
+ private List<String> options = new ArrayList<>();
+ /**
+ * 浜岀骇涓嬫媺鎵�鍖呭惈鐨勬暟鎹甅ap
+ * <p>浠ユ瘡涓�涓竴绾ч�夐」鍊间负Key锛屾瘡涓竴绾ч�夐」瀵瑰簲鐨勪簩绾ф暟鎹负Value</p>
+ */
+ private Map<String, List<String>> nextOptions = new HashMap<>();
+ /**
+ * 鍒嗛殧绗�
+ */
+ private static final String DELIMITER = "_";
+
+ /**
+ * 鍒涘缓鍙湁涓�绾х殑涓嬫媺閫�
+ */
+ public DropDownOptions(int index, List<String> options) {
+ this.index = index;
+ this.options = options;
+ }
+
+ /**
+ * <h2>鍒涘缓姣忎釜閫夐」鍙�夊��</h2>
+ * <p>娉ㄦ剰锛氫笉鑳戒互鏁板瓧锛岀壒娈婄鍙峰紑澶达紝閫夐」涓笉鍙互鍖呭惈浠讳綍杩愮畻绗﹀彿</p>
+ *
+ * @param vars 鍙�夊�煎唴鍖呭惈鐨勫弬鏁�
+ * @return 鍚堣鐨勫彲閫夊��
+ */
+ public static String createOptionValue(Object... vars) {
+ 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]));
+ if (!var.matches(regex)) {
+ throw new ServiceException("閫夐」鏁版嵁涓嶇鍚堣鍒欙紝浠呭厑璁镐娇鐢ㄤ腑鑻辨枃瀛楃浠ュ強鏁板瓧");
+ }
+ stringBuffer.append(var);
+ if (i < vars.length - 1) {
+ // 鐩磋嚦鏈�鍚庝竴涓墠锛岄兘浠浣滀负鍒囧壊绾�
+ stringBuffer.append(DELIMITER);
+ }
+ }
+ if (stringBuffer.toString().matches("^\\d_*$")) {
+ throw new ServiceException("绂佹浠ユ暟瀛楀紑澶�");
+ }
+ return stringBuffer.toString();
+ }
+
+ /**
+ * 灏嗗鐞嗗悗鍚堢悊鐨勫彲閫夊�艰В鏋愪负鍘熷鐨勫弬鏁�
+ *
+ * @param option 缁忚繃澶勭悊鍚庣殑鍚堢悊鐨勫彲閫夐」
+ * @return 鍘熷鐨勫弬鏁�
+ */
+ public static List<String> analyzeOptionValue(String option) {
+ return StrUtil.split(option, DELIMITER, true, true);
+ }
+
+ /**
+ * 鍒涘缓绾ц仈涓嬫媺閫夐」
+ *
+ * @param parentList 鐖跺疄浣撳彲閫夐」鍘熷鏁版嵁
+ * @param parentIndex 鐖朵笅鎷夐�変綅缃�
+ * @param sonList 瀛愬疄浣撳彲閫夐」鍘熷鏁版嵁
+ * @param sonIndex 瀛愪笅鎷夐�変綅缃�
+ * @param parentHowToGetIdFunction 鐖剁被濡備綍鑾峰彇鍞竴鏍囪瘑
+ * @param sonHowToGetParentIdFunction 瀛愮被濡備綍鑾峰彇鐖剁被鐨勫敮涓�鏍囪瘑
+ * @param howToBuildEveryOption 濡備綍鐢熸垚涓嬫媺閫夊唴瀹�
+ * @return 绾ц仈涓嬫媺閫夐」
+ */
+ public static <T> DropDownOptions buildLinkedOptions(List<T> parentList,
+ int parentIndex,
+ List<T> sonList,
+ int sonIndex,
+ Function<T, Number> parentHowToGetIdFunction,
+ Function<T, Number> sonHowToGetParentIdFunction,
+ Function<T, String> howToBuildEveryOption) {
+ DropDownOptions parentLinkSonOptions = new DropDownOptions();
+ // 鍏堝垱寤虹埗绫荤殑涓嬫媺
+ parentLinkSonOptions.setIndex(parentIndex);
+ parentLinkSonOptions.setOptions(
+ parentList.stream()
+ .map(howToBuildEveryOption)
+ .collect(Collectors.toList())
+ );
+ // 鎻愬彇鐖�-瀛愮骇鑱斾笅鎷�
+ Map<String, List<String>> sonOptions = new HashMap<>();
+ // 鐖剁骇渚濇嵁鑷繁鐨処D鍒嗙粍
+ Map<Number, List<T>> parentGroupByIdMap =
+ parentList.stream().collect(Collectors.groupingBy(parentHowToGetIdFunction));
+ // 閬嶅巻姣忎釜瀛愰泦锛屾彁鍙栧埌Map涓�
+ sonList.forEach(everySon -> {
+ if (parentGroupByIdMap.containsKey(sonHowToGetParentIdFunction.apply(everySon))) {
+ // 鎵惧埌瀵瑰簲鐨勪笂绾�
+ T parentObj = parentGroupByIdMap.get(sonHowToGetParentIdFunction.apply(everySon)).get(0);
+ // 鎻愬彇鍚嶇О鍜孖D浣滀负Key
+ String key = howToBuildEveryOption.apply(parentObj);
+ // Key瀵瑰簲鐨刅alue
+ List<String> thisParentSonOptionList;
+ if (sonOptions.containsKey(key)) {
+ thisParentSonOptionList = sonOptions.get(key);
+ } else {
+ thisParentSonOptionList = new ArrayList<>();
+ sonOptions.put(key, thisParentSonOptionList);
+ }
+ // 寰�Value涓坊鍔犲綋鍓嶅瓙闆嗛�夐」
+ thisParentSonOptionList.add(howToBuildEveryOption.apply(everySon));
+ }
+ });
+ parentLinkSonOptions.setNextIndex(sonIndex);
+ parentLinkSonOptions.setNextOptions(sonOptions);
+ return parentLinkSonOptions;
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/ExcelDownHandler.java b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/ExcelDownHandler.java
new file mode 100644
index 0000000..3b791ea
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/ExcelDownHandler.java
@@ -0,0 +1,371 @@
+package org.dromara.common.excel.core;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ArrayUtil;
+import cn.hutool.core.util.EnumUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
+import com.alibaba.excel.metadata.FieldCache;
+import com.alibaba.excel.metadata.FieldWrapper;
+import com.alibaba.excel.util.ClassUtils;
+import com.alibaba.excel.write.handler.SheetWriteHandler;
+import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
+import com.alibaba.excel.write.metadata.holder.WriteWorkbookHolder;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.poi.ss.usermodel.*;
+import org.apache.poi.ss.util.CellRangeAddressList;
+import org.apache.poi.ss.util.WorkbookUtil;
+import org.apache.poi.xssf.usermodel.XSSFDataValidation;
+import org.dromara.common.core.exception.ServiceException;
+import org.dromara.common.core.service.DictService;
+import org.dromara.common.core.utils.SpringUtils;
+import org.dromara.common.core.utils.StreamUtils;
+import org.dromara.common.excel.annotation.ExcelDictFormat;
+import org.dromara.common.excel.annotation.ExcelEnumFormat;
+
+import java.lang.reflect.Field;
+import java.util.*;
+
+/**
+ * <h1>Excel琛ㄦ牸涓嬫媺閫夋搷浣�</h1>
+ * 鑰冭檻鍒颁笅鎷夐�夎繃澶氬彲鑳藉鑷碋xcel鎵撳紑缂撴參鐨勯棶棰橈紝鍙牎楠屽墠1000琛�
+ * <p>
+ * 鍗冲彧鏈夊墠1000琛岀殑鏁版嵁鍙互鐢ㄤ笅鎷夋锛岃秴鍑虹殑鑷閫氳繃闄愬埗鏁版嵁閲忕殑褰㈠紡锛岀浜屾杈撳嚭
+ *
+ * @author Emil.Zhang
+ */
+@Slf4j
+public class ExcelDownHandler implements SheetWriteHandler {
+
+ /**
+ * Excel琛ㄦ牸涓殑鍒楀悕鑻辨枃
+ * 浠呬负浜嗚В鏋愬垪鑻辨枃锛岀姝慨鏀�
+ */
+ private static final String EXCEL_COLUMN_NAME = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+ /**
+ * 鍗曢�夋暟鎹甋heet鍚�
+ */
+ private static final String OPTIONS_SHEET_NAME = "options";
+ /**
+ * 鑱斿姩閫夋嫨鏁版嵁Sheet鍚嶇殑澶�
+ */
+ private static final String LINKED_OPTIONS_SHEET_NAME = "linkedOptions";
+ /**
+ * 涓嬫媺鍙�夐」
+ */
+ private final List<DropDownOptions> dropDownOptions;
+ /**
+ * 褰撳墠鍗曢�夎繘搴�
+ */
+ private int currentOptionsColumnIndex;
+ /**
+ * 褰撳墠鑱斿姩閫夋嫨杩涘害
+ */
+ private int currentLinkedOptionsSheetIndex;
+ private final DictService dictService;
+
+ public ExcelDownHandler(List<DropDownOptions> options) {
+ this.dropDownOptions = options;
+ this.currentOptionsColumnIndex = 0;
+ this.currentLinkedOptionsSheetIndex = 0;
+ this.dictService = SpringUtils.getBean(DictService.class);
+ }
+
+ /**
+ * <h2>寮�濮嬪垱寤轰笅鎷夋暟鎹�</h2>
+ * 1.閫氳繃瑙f瀽浼犲叆鐨凘ExcelProperty鍚岀骇鏄惁鏍囨敞鏈堾DropDown閫夐」
+ * 濡傛灉鏈変笖璁剧疆浜唙alue鍊硷紝鍒欏皢鍏剁洿鎺ョ疆涓轰笅鎷夊彲閫夐」
+ * <p>
+ * 2.鎴栬�呭湪璋冪敤ExcelUtil鏃舵寚瀹氫簡鍙�夐」锛屽皢渚濇嵁浼犲叆鐨勫彲閫夐」鍋氫笅鎷�
+ * <p>
+ * 3.浜岃�呭苟瀛橈紝娉ㄦ剰璋冪敤鏂瑰紡
+ */
+ @Override
+ public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {
+ Sheet sheet = writeSheetHolder.getSheet();
+ // 寮�濮嬭缃笅鎷夋 HSSFWorkbook
+ DataValidationHelper helper = sheet.getDataValidationHelper();
+ Workbook workbook = writeWorkbookHolder.getWorkbook();
+ FieldCache fieldCache = ClassUtils.declaredFields(writeWorkbookHolder.getClazz(), writeWorkbookHolder);
+ for (Map.Entry<Integer, FieldWrapper> entry : fieldCache.getSortedFieldMap().entrySet()) {
+ Integer index = entry.getKey();
+ FieldWrapper wrapper = entry.getValue();
+ Field field = wrapper.getField();
+ // 寰幆瀹炰綋涓殑姣忎釜灞炴��
+ // 鍙�夌殑涓嬫媺鍊�
+ List<String> options = new ArrayList<>();
+ if (field.isAnnotationPresent(ExcelDictFormat.class)) {
+ // 濡傛灉鎸囧畾浜咢ExcelDictFormat锛屽垯浣跨敤瀛楀吀鐨勯�昏緫
+ ExcelDictFormat format = field.getDeclaredAnnotation(ExcelDictFormat.class);
+ String dictType = format.dictType();
+ String converterExp = format.readConverterExp();
+ if (StrUtil.isNotBlank(dictType)) {
+ // 濡傛灉浼犻�掍簡瀛楀吀鍚嶏紝鍒欎緷鎹瓧鍏稿缓绔嬩笅鎷�
+ Collection<String> values = Optional.ofNullable(dictService.getAllDictByDictType(dictType))
+ .orElseThrow(() -> new ServiceException(String.format("瀛楀吀 %s 涓嶅瓨鍦�", dictType)))
+ .values();
+ options = new ArrayList<>(values);
+ } else if (StrUtil.isNotBlank(converterExp)) {
+ // 濡傛灉鎸囧畾浜嗙‘鍒囩殑鍊硷紝鍒欑洿鎺ヨВ鏋愮‘鍒囩殑鍊�
+ options = StrUtil.split(converterExp, format.separator(), true, true);
+ }
+ } else if (field.isAnnotationPresent(ExcelEnumFormat.class)) {
+ // 鍚﹀垯濡傛灉鎸囧畾浜咢ExcelEnumFormat锛屽垯浣跨敤鏋氫妇鐨勯�昏緫
+ ExcelEnumFormat format = field.getDeclaredAnnotation(ExcelEnumFormat.class);
+ List<Object> values = EnumUtil.getFieldValues(format.enumClass(), format.textField());
+ options = StreamUtils.toList(values, String::valueOf);
+ }
+ if (ObjectUtil.isNotEmpty(options)) {
+ // 浠呭綋涓嬫媺鍙�夐」涓嶄负绌烘椂鎵ц
+ if (options.size() > 20) {
+ // 杩欓噷闄愬埗濡傛灉鍙�夐」澶т簬20锛屽垯浣跨敤棰濆琛ㄥ舰寮�
+ dropDownWithSheet(helper, workbook, sheet, index, options);
+ } else {
+ // 鍚﹀垯浣跨敤鍥哄畾鍊煎舰寮�
+ dropDownWithSimple(helper, sheet, index, options);
+ }
+ }
+ }
+ if (CollUtil.isEmpty(dropDownOptions)) {
+ return;
+ }
+ dropDownOptions.forEach(everyOptions -> {
+ // 濡傛灉浼犻�掍簡涓嬫媺妗嗛�夋嫨鍣ㄥ弬鏁�
+ if (!everyOptions.getNextOptions().isEmpty()) {
+ // 褰撲簩绾ч�夐」涓嶄负绌烘椂锛屼娇鐢ㄩ澶栧叧鑱旇〃鐨勫舰寮�
+ dropDownLinkedOptions(helper, workbook, sheet, everyOptions);
+ } else if (everyOptions.getOptions().size() > 10) {
+ // 褰撲竴绾ч�夐」鍙傛暟涓暟澶т簬10锛屼娇鐢ㄩ澶栬〃鐨勫舰寮�
+ dropDownWithSheet(helper, workbook, sheet, everyOptions.getIndex(), everyOptions.getOptions());
+ } else if (everyOptions.getOptions().size() != 0) {
+ // 褰撲竴绾ч�夐」涓暟涓嶄负绌猴紝浣跨敤榛樿褰㈠紡
+ dropDownWithSimple(helper, sheet, everyOptions.getIndex(), everyOptions.getOptions());
+ }
+ });
+ }
+
+ /**
+ * <h2>绠�鍗曚笅鎷夋</h2>
+ * 鐩存帴灏嗗彲閫夐」鎷兼帴涓烘寚瀹氬垪鐨勬暟鎹牎楠屽��
+ *
+ * @param celIndex 鍒梚ndex
+ * @param value 涓嬫媺閫夊彲閫夊��
+ */
+ private void dropDownWithSimple(DataValidationHelper helper, Sheet sheet, Integer celIndex, List<String> value) {
+ if (ObjectUtil.isEmpty(value)) {
+ return;
+ }
+ this.markOptionsToSheet(helper, sheet, celIndex, helper.createExplicitListConstraint(ArrayUtil.toArray(value, String.class)));
+ }
+
+ /**
+ * <h2>棰濆琛ㄦ牸褰㈠紡鐨勭骇鑱斾笅鎷夋</h2>
+ *
+ * @param options 棰濆琛ㄦ牸褰㈠紡瀛樺偍鐨勪笅鎷夊彲閫夐」
+ */
+ private void dropDownLinkedOptions(DataValidationHelper helper, Workbook workbook, Sheet sheet, DropDownOptions options) {
+ String linkedOptionsSheetName = String.format("%s_%d", LINKED_OPTIONS_SHEET_NAME, currentLinkedOptionsSheetIndex);
+ // 鍒涘缓鑱斿姩涓嬫媺鏁版嵁琛�
+ Sheet linkedOptionsDataSheet = workbook.createSheet(WorkbookUtil.createSafeSheetName(linkedOptionsSheetName));
+ // 灏嗕笅鎷夎〃闅愯棌
+ workbook.setSheetHidden(workbook.getSheetIndex(linkedOptionsDataSheet), true);
+ // 瀹屽杽妯悜鐨勪竴绾ч�夐」鏁版嵁琛�
+ List<String> firstOptions = options.getOptions();
+ Map<String, List<String>> secoundOptionsMap = options.getNextOptions();
+
+ // 鍒涘缓鍚嶇О绠$悊鍣�
+ Name name = workbook.createName();
+ // 璁剧疆鍚嶇О绠$悊鍣ㄧ殑鍒悕
+ name.setNameName(linkedOptionsSheetName);
+ // 浠ユí鍚戠涓�琛屽垱寤轰竴绾т笅鎷夋嫾鎺ュ紩鐢ㄤ綅缃�
+ String firstOptionsFunction = String.format("%s!$%s$1:$%s$1",
+ linkedOptionsSheetName,
+ getExcelColumnName(0),
+ getExcelColumnName(firstOptions.size())
+ );
+ // 璁剧疆鍚嶇О绠$悊鍣ㄧ殑寮曠敤浣嶇疆
+ name.setRefersToFormula(firstOptionsFunction);
+ // 璁剧疆鏁版嵁鏍¢獙涓哄簭鍒楁ā寮忥紝寮曠敤鐨勬槸鍚嶇О绠$悊鍣ㄤ腑鐨勫埆鍚�
+ this.markOptionsToSheet(helper, sheet, options.getIndex(), helper.createFormulaListConstraint(linkedOptionsSheetName));
+
+ for (int columIndex = 0; columIndex < firstOptions.size(); columIndex++) {
+ // 鍏堟彁鍙栦富琛ㄤ腑涓�绾т笅鎷夌殑鍒楀悕
+ String firstOptionsColumnName = getExcelColumnName(columIndex);
+ // 涓�娆″惊鐜槸姣忎竴涓竴绾ч�夐」
+ int finalI = columIndex;
+ // 鏈寰幆鐨勪竴绾ч�夐」鍊�
+ String thisFirstOptionsValue = firstOptions.get(columIndex);
+ // 鍒涘缓绗竴琛岀殑鏁版嵁
+ Optional.ofNullable(linkedOptionsDataSheet.getRow(0))
+ // 濡傛灉涓嶅瓨鍦ㄥ垯鍒涘缓绗竴琛�
+ .orElseGet(() -> linkedOptionsDataSheet.createRow(finalI))
+ // 绗竴琛屽綋鍓嶅垪
+ .createCell(columIndex)
+ // 璁剧疆鍊间负褰撳墠涓�绾ч�夐」鍊�
+ .setCellValue(thisFirstOptionsValue);
+
+ // 绗簩琛屽紑濮嬶紝璁剧疆绗簩绾у埆閫夐」鍙傛暟
+ List<String> secondOptions = secoundOptionsMap.get(thisFirstOptionsValue);
+ if (CollUtil.isEmpty(secondOptions)) {
+ // 蹇呴』淇濊瘉鑷冲皯鏈変竴涓叧鑱旈�夐」锛屽惁鍒欏皢瀵艰嚧Excel瑙f瀽閿欒
+ secondOptions = Collections.singletonList("鏆傛棤_0");
+ }
+
+ // 浠ヨ涓�绾ч�夐」鍊煎垱寤哄瓙鍚嶇О绠$悊鍣�
+ Name sonName = workbook.createName();
+ // 璁剧疆鍚嶇О绠$悊鍣ㄧ殑鍒悕
+ sonName.setNameName(thisFirstOptionsValue);
+ // 浠ョ浜岃璇ュ垪鏁版嵁鎷兼帴寮曠敤浣嶇疆
+ String sonFunction = String.format("%s!$%s$2:$%s$%d",
+ linkedOptionsSheetName,
+ firstOptionsColumnName,
+ firstOptionsColumnName,
+ secondOptions.size() + 1
+ );
+ // 璁剧疆鍚嶇О绠$悊鍣ㄧ殑寮曠敤浣嶇疆
+ sonName.setRefersToFormula(sonFunction);
+ // 鏁版嵁楠岃瘉涓哄簭鍒楁ā寮忥紝寮曠敤鍒版瘡涓�涓富琛ㄤ腑鐨勪簩绾ч�夐」浣嶇疆
+ // 鍒涘缓瀛愰」鐨勫悕绉扮鐞嗗櫒锛屽彧鏄负浜嗕娇寰桬xcel鍙互璇嗗埆鍒版暟鎹�
+ String mainSheetFirstOptionsColumnName = getExcelColumnName(options.getIndex());
+ for (int i = 0; i < 100; i++) {
+ // 浠ヤ竴绾ч�夐」瀵瑰簲鐨勪富浣撴墍鍦ㄤ綅缃垱寤轰簩绾т笅鎷�
+ String secondOptionsFunction = String.format("=INDIRECT(%s%d)", mainSheetFirstOptionsColumnName, i + 1);
+ // 浜岀骇鍙兘涓昏〃姣忎竴琛岀殑姣忎竴鍒楁坊鍔犱簩绾ф牎楠�
+ markLinkedOptionsToSheet(helper, sheet, i, options.getNextIndex(), helper.createFormulaListConstraint(secondOptionsFunction));
+ }
+
+ for (int rowIndex = 0; rowIndex < secondOptions.size(); rowIndex++) {
+ // 浠庣浜岃寮�濮嬪~鍏呬簩绾ч�夐」
+ int finalRowIndex = rowIndex + 1;
+ int finalColumIndex = columIndex;
+
+ Row row = Optional.ofNullable(linkedOptionsDataSheet.getRow(finalRowIndex))
+ // 娌℃湁鍒欏垱寤�
+ .orElseGet(() -> linkedOptionsDataSheet.createRow(finalRowIndex));
+ Optional
+ // 鍦ㄦ湰绾т竴绾ч�夐」鎵�鍦ㄧ殑鍒�
+ .ofNullable(row.getCell(finalColumIndex))
+ // 涓嶅瓨鍦ㄥ垯鍒涘缓
+ .orElseGet(() -> row.createCell(finalColumIndex))
+ // 璁剧疆浜岀骇閫夐」鍊�
+ .setCellValue(secondOptions.get(rowIndex));
+ }
+ }
+
+ currentLinkedOptionsSheetIndex++;
+ }
+
+ /**
+ * <h2>棰濆琛ㄦ牸褰㈠紡鐨勬櫘閫氫笅鎷夋</h2>
+ * 鐢变簬涓嬫媺妗嗗彲閫夊�兼暟閲忚繃澶氾紝涓烘彁鍗嘐xcel鎵撳紑鏁堢巼锛屼娇鐢ㄩ澶栬〃鏍煎舰寮忓仛涓嬫媺
+ *
+ * @param celIndex 涓嬫媺閫�
+ * @param value 涓嬫媺閫夊彲閫夊��
+ */
+ private void dropDownWithSheet(DataValidationHelper helper, Workbook workbook, Sheet sheet, Integer celIndex, List<String> value) {
+ // 鍒涘缓涓嬫媺鏁版嵁琛�
+ Sheet simpleDataSheet = Optional.ofNullable(workbook.getSheet(WorkbookUtil.createSafeSheetName(OPTIONS_SHEET_NAME)))
+ .orElseGet(() -> workbook.createSheet(WorkbookUtil.createSafeSheetName(OPTIONS_SHEET_NAME)));
+ // 灏嗕笅鎷夎〃闅愯棌
+ workbook.setSheetHidden(workbook.getSheetIndex(simpleDataSheet), true);
+ // 瀹屽杽绾靛悜鐨勪竴绾ч�夐」鏁版嵁琛�
+ for (int i = 0; i < value.size(); i++) {
+ int finalI = i;
+ // 鑾峰彇姣忎竴閫夐」琛岋紝濡傛灉娌℃湁鍒欏垱寤�
+ Row row = Optional.ofNullable(simpleDataSheet.getRow(i))
+ .orElseGet(() -> simpleDataSheet.createRow(finalI));
+ // 鑾峰彇鏈骇閫夐」瀵瑰簲鐨勯�夐」鍒楋紝濡傛灉娌℃湁鍒欏垱寤�
+ Cell cell = Optional.ofNullable(row.getCell(currentOptionsColumnIndex))
+ .orElseGet(() -> row.createCell(currentOptionsColumnIndex));
+ // 璁剧疆鍊�
+ cell.setCellValue(value.get(i));
+ }
+
+ // 鍒涘缓鍚嶇О绠$悊鍣�
+ Name name = workbook.createName();
+ // 璁剧疆鍚嶇О绠$悊鍣ㄧ殑鍒悕
+ String nameName = String.format("%s_%d", OPTIONS_SHEET_NAME, celIndex);
+ name.setNameName(nameName);
+ // 浠ョ旱鍚戠涓�鍒楀垱寤轰竴绾т笅鎷夋嫾鎺ュ紩鐢ㄤ綅缃�
+ String function = String.format("%s!$%s$1:$%s$%d",
+ OPTIONS_SHEET_NAME,
+ getExcelColumnName(currentOptionsColumnIndex),
+ getExcelColumnName(currentOptionsColumnIndex),
+ value.size());
+ // 璁剧疆鍚嶇О绠$悊鍣ㄧ殑寮曠敤浣嶇疆
+ name.setRefersToFormula(function);
+ // 璁剧疆鏁版嵁鏍¢獙涓哄簭鍒楁ā寮忥紝寮曠敤鐨勬槸鍚嶇О绠$悊鍣ㄤ腑鐨勫埆鍚�
+ this.markOptionsToSheet(helper, sheet, celIndex, helper.createFormulaListConstraint(nameName));
+ currentOptionsColumnIndex++;
+ }
+
+ /**
+ * 鎸傝浇涓嬫媺鐨勫垪锛屼粎闄愪竴绾ч�夐」
+ */
+ private void markOptionsToSheet(DataValidationHelper helper, Sheet sheet, Integer celIndex,
+ DataValidationConstraint constraint) {
+ // 璁剧疆鏁版嵁鏈夋晥鎬у姞杞藉湪鍝釜鍗曞厓鏍间笂,鍥涗釜鍙傛暟鍒嗗埆鏄細璧峰琛屻�佺粓姝㈣銆佽捣濮嬪垪銆佺粓姝㈠垪
+ CellRangeAddressList addressList = new CellRangeAddressList(1, 1000, celIndex, celIndex);
+ markDataValidationToSheet(helper, sheet, constraint, addressList);
+ }
+
+ /**
+ * 鎸傝浇涓嬫媺鐨勫垪锛屼粎闄愪簩绾ч�夐」
+ */
+ private void markLinkedOptionsToSheet(DataValidationHelper helper, Sheet sheet, Integer rowIndex,
+ Integer celIndex, DataValidationConstraint constraint) {
+ // 璁剧疆鏁版嵁鏈夋晥鎬у姞杞藉湪鍝釜鍗曞厓鏍间笂,鍥涗釜鍙傛暟鍒嗗埆鏄細璧峰琛屻�佺粓姝㈣銆佽捣濮嬪垪銆佺粓姝㈠垪
+ CellRangeAddressList addressList = new CellRangeAddressList(rowIndex, rowIndex, celIndex, celIndex);
+ markDataValidationToSheet(helper, sheet, constraint, addressList);
+ }
+
+ /**
+ * 搴旂敤鏁版嵁鏍¢獙
+ */
+ private void markDataValidationToSheet(DataValidationHelper helper, Sheet sheet,
+ DataValidationConstraint constraint, CellRangeAddressList addressList) {
+ // 鏁版嵁鏈夋晥鎬у璞�
+ DataValidation dataValidation = helper.createValidation(constraint, addressList);
+ // 澶勭悊Excel鍏煎鎬ч棶棰�
+ if (dataValidation instanceof XSSFDataValidation) {
+ //鏁版嵁鏍¢獙
+ dataValidation.setSuppressDropDownArrow(true);
+ //閿欒鎻愮ず
+ dataValidation.setErrorStyle(DataValidation.ErrorStyle.STOP);
+ dataValidation.createErrorBox("鎻愮ず", "姝ゅ�间笌鍗曞厓鏍煎畾涔夋暟鎹笉涓�鑷�");
+ dataValidation.setShowErrorBox(true);
+ //閫夊畾鎻愮ず
+ dataValidation.createPromptBox("濉啓璇存槑锛�", "濉啓鍐呭鍙兘涓轰笅鎷変腑鏁版嵁锛屽叾浠栨暟鎹皢瀵艰嚧瀵煎叆澶辫触");
+ dataValidation.setShowPromptBox(true);
+ sheet.addValidationData(dataValidation);
+ } else {
+ dataValidation.setSuppressDropDownArrow(false);
+ }
+ sheet.addValidationData(dataValidation);
+ }
+
+ /**
+ * <h2>渚濇嵁鍒梚ndex鑾峰彇鍒楀悕鑻辨枃</h2>
+ * 渚濇嵁鍒梚ndex杞崲涓篍xcel涓殑鍒楀悕鑻辨枃
+ * <p>渚嬪绗�1鍒楋紝index涓�0锛岃В鏋愬嚭鏉ヤ负A鍒�</p>
+ * 绗�27鍒楋紝index涓�26锛岃В鏋愪负AA鍒�
+ * <p>绗�28鍒楋紝index涓�27锛岃В鏋愪负AB鍒�</p>
+ *
+ * @param columnIndex 鍒梚ndex
+ * @return 鍒梚ndex鎵�鍦ㄥ緱鑻辨枃鍚�
+ */
+ private String getExcelColumnName(int columnIndex) {
+ // 26涓�寰幆鐨勬鏁�
+ int columnCircleCount = columnIndex / 26;
+ // 26涓�寰幆鍐呯殑浣嶇疆
+ int thisCircleColumnIndex = columnIndex % 26;
+ // 26涓�寰幆鐨勬鏁板ぇ浜�0锛屽垯瑙嗕负鏍忓悕鑷冲皯涓や綅
+ String columnPrefix = columnCircleCount == 0
+ ? StrUtil.EMPTY
+ : StrUtil.subWithLength(EXCEL_COLUMN_NAME, columnCircleCount - 1, 1);
+ // 浠�26涓�寰幆鍐呭彇瀵瑰簲鐨勬爮浣嶅悕
+ String columnNext = StrUtil.subWithLength(EXCEL_COLUMN_NAME, thisCircleColumnIndex, 1);
+ // 灏嗕簩鑰呮嫾鎺ュ嵆涓烘渶缁堢殑鏍忎綅鍚�
+ return columnPrefix + columnNext;
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/ExcelListener.java b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/ExcelListener.java
new file mode 100644
index 0000000..2d0340f
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/ExcelListener.java
@@ -0,0 +1,14 @@
+package org.dromara.common.excel.core;
+
+import com.alibaba.excel.read.listener.ReadListener;
+
+/**
+ * Excel 瀵煎叆鐩戝惉
+ *
+ * @author Lion Li
+ */
+public interface ExcelListener<T> extends ReadListener<T> {
+
+ ExcelResult<T> getExcelResult();
+
+}
diff --git a/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/ExcelResult.java b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/ExcelResult.java
new file mode 100644
index 0000000..0c2a418
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/ExcelResult.java
@@ -0,0 +1,26 @@
+package org.dromara.common.excel.core;
+
+import java.util.List;
+
+/**
+ * excel杩斿洖瀵硅薄
+ *
+ * @author Lion Li
+ */
+public interface ExcelResult<T> {
+
+ /**
+ * 瀵硅薄鍒楄〃
+ */
+ List<T> getList();
+
+ /**
+ * 閿欒鍒楄〃
+ */
+ List<String> getErrorList();
+
+ /**
+ * 瀵煎叆鍥炴墽
+ */
+ String getAnalysis();
+}
diff --git a/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/utils/ExcelUtil.java b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/utils/ExcelUtil.java
new file mode 100644
index 0000000..a6c14ad
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/utils/ExcelUtil.java
@@ -0,0 +1,436 @@
+package org.dromara.common.excel.utils;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.io.resource.ClassPathResource;
+import cn.hutool.core.util.IdUtil;
+import com.alibaba.excel.EasyExcel;
+import com.alibaba.excel.ExcelWriter;
+import com.alibaba.excel.write.builder.ExcelWriterSheetBuilder;
+import com.alibaba.excel.write.metadata.WriteSheet;
+import com.alibaba.excel.write.metadata.fill.FillConfig;
+import com.alibaba.excel.write.metadata.fill.FillWrapper;
+import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
+import jakarta.servlet.ServletOutputStream;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.core.utils.file.FileUtils;
+import org.dromara.common.excel.convert.ExcelBigNumberConvert;
+import org.dromara.common.excel.core.*;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Excel鐩稿叧澶勭悊
+ *
+ * @author Lion Li
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class ExcelUtil {
+
+ /**
+ * 鍚屾瀵煎叆(閫傜敤浜庡皬鏁版嵁閲�)
+ *
+ * @param is 杈撳叆娴�
+ * @return 杞崲鍚庨泦鍚�
+ */
+ public static <T> List<T> importExcel(InputStream is, Class<T> clazz) {
+ return EasyExcel.read(is).head(clazz).autoCloseStream(false).sheet().doReadSync();
+ }
+
+
+ /**
+ * 浣跨敤鏍¢獙鐩戝惉鍣� 寮傛瀵煎叆 鍚屾杩斿洖
+ *
+ * @param is 杈撳叆娴�
+ * @param clazz 瀵硅薄绫诲瀷
+ * @param isValidate 鏄惁 Validator 妫�楠� 榛樿涓烘槸
+ * @return 杞崲鍚庨泦鍚�
+ */
+ public static <T> ExcelResult<T> importExcel(InputStream is, Class<T> clazz, boolean isValidate) {
+ DefaultExcelListener<T> listener = new DefaultExcelListener<>(isValidate);
+ EasyExcel.read(is, clazz, listener).sheet().doRead();
+ return listener.getExcelResult();
+ }
+
+ /**
+ * 浣跨敤鑷畾涔夌洃鍚櫒 寮傛瀵煎叆 鑷畾涔夎繑鍥�
+ *
+ * @param is 杈撳叆娴�
+ * @param clazz 瀵硅薄绫诲瀷
+ * @param listener 鑷畾涔夌洃鍚櫒
+ * @return 杞崲鍚庨泦鍚�
+ */
+ public static <T> ExcelResult<T> importExcel(InputStream is, Class<T> clazz, ExcelListener<T> listener) {
+ EasyExcel.read(is, clazz, listener).sheet().doRead();
+ return listener.getExcelResult();
+ }
+
+ /**
+ * 瀵煎嚭excel
+ *
+ * @param list 瀵煎嚭鏁版嵁闆嗗悎
+ * @param sheetName 宸ヤ綔琛ㄧ殑鍚嶇О
+ * @param clazz 瀹炰綋绫�
+ * @param response 鍝嶅簲浣�
+ */
+ public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, HttpServletResponse response) {
+ try {
+ resetResponse(sheetName, response);
+ ServletOutputStream os = response.getOutputStream();
+ exportExcel(list, sheetName, clazz, false, os, null);
+ } catch (IOException e) {
+ throw new RuntimeException("瀵煎嚭Excel寮傚父");
+ }
+ }
+
+ /**
+ * 瀵煎嚭excel
+ *
+ * @param list 瀵煎嚭鏁版嵁闆嗗悎
+ * @param sheetName 宸ヤ綔琛ㄧ殑鍚嶇О
+ * @param clazz 瀹炰綋绫�
+ * @param response 鍝嶅簲浣�
+ * @param options 绾ц仈涓嬫媺閫�
+ */
+ public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, HttpServletResponse response, List<DropDownOptions> options) {
+ try {
+ resetResponse(sheetName, response);
+ ServletOutputStream os = response.getOutputStream();
+ exportExcel(list, sheetName, clazz, false, os, options);
+ } catch (IOException e) {
+ throw new RuntimeException("瀵煎嚭Excel寮傚父");
+ }
+ }
+
+ /**
+ * 瀵煎嚭excel
+ *
+ * @param list 瀵煎嚭鏁版嵁闆嗗悎
+ * @param sheetName 宸ヤ綔琛ㄧ殑鍚嶇О
+ * @param clazz 瀹炰綋绫�
+ * @param merge 鏄惁鍚堝苟鍗曞厓鏍�
+ * @param response 鍝嶅簲浣�
+ */
+ public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, boolean merge, HttpServletResponse response) {
+ try {
+ resetResponse(sheetName, response);
+ ServletOutputStream os = response.getOutputStream();
+ exportExcel(list, sheetName, clazz, merge, os, null);
+ } catch (IOException e) {
+ throw new RuntimeException("瀵煎嚭Excel寮傚父");
+ }
+ }
+
+ /**
+ * 瀵煎嚭excel
+ *
+ * @param list 瀵煎嚭鏁版嵁闆嗗悎
+ * @param sheetName 宸ヤ綔琛ㄧ殑鍚嶇О
+ * @param clazz 瀹炰綋绫�
+ * @param merge 鏄惁鍚堝苟鍗曞厓鏍�
+ * @param response 鍝嶅簲浣�
+ * @param options 绾ц仈涓嬫媺閫�
+ */
+ public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, boolean merge, HttpServletResponse response, List<DropDownOptions> options) {
+ try {
+ resetResponse(sheetName, response);
+ ServletOutputStream os = response.getOutputStream();
+ exportExcel(list, sheetName, clazz, merge, os, options);
+ } catch (IOException e) {
+ throw new RuntimeException("瀵煎嚭Excel寮傚父");
+ }
+ }
+
+ /**
+ * 瀵煎嚭excel
+ *
+ * @param list 瀵煎嚭鏁版嵁闆嗗悎
+ * @param sheetName 宸ヤ綔琛ㄧ殑鍚嶇О
+ * @param clazz 瀹炰綋绫�
+ * @param os 杈撳嚭娴�
+ */
+ public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, OutputStream os) {
+ exportExcel(list, sheetName, clazz, false, os, null);
+ }
+
+ /**
+ * 瀵煎嚭excel
+ *
+ * @param list 瀵煎嚭鏁版嵁闆嗗悎
+ * @param sheetName 宸ヤ綔琛ㄧ殑鍚嶇О
+ * @param clazz 瀹炰綋绫�
+ * @param os 杈撳嚭娴�
+ * @param options 绾ц仈涓嬫媺閫夊唴瀹�
+ */
+ public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, OutputStream os, List<DropDownOptions> options) {
+ exportExcel(list, sheetName, clazz, false, os, options);
+ }
+
+ /**
+ * 瀵煎嚭excel
+ *
+ * @param list 瀵煎嚭鏁版嵁闆嗗悎
+ * @param sheetName 宸ヤ綔琛ㄧ殑鍚嶇О
+ * @param clazz 瀹炰綋绫�
+ * @param merge 鏄惁鍚堝苟鍗曞厓鏍�
+ * @param os 杈撳嚭娴�
+ */
+ public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, boolean merge,
+ OutputStream os, List<DropDownOptions> options) {
+ ExcelWriterSheetBuilder builder = EasyExcel.write(os, clazz)
+ .autoCloseStream(false)
+ // 鑷姩閫傞厤
+ .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
+ // 澶ф暟鍊艰嚜鍔ㄨ浆鎹� 闃叉澶辩湡
+ .registerConverter(new ExcelBigNumberConvert())
+ .sheet(sheetName);
+ if (merge) {
+ // 鍚堝苟澶勭悊鍣�
+ builder.registerWriteHandler(new CellMergeStrategy(list, true));
+ }
+ // 娣诲姞涓嬫媺妗嗘搷浣�
+ builder.registerWriteHandler(new ExcelDownHandler(options));
+ builder.doWrite(list);
+ }
+
+ /**
+ * 鍗曡〃澶氭暟鎹ā鏉垮鍑� 妯℃澘鏍煎紡涓� {.灞炴�
+ *
+ * @param filename 鏂囦欢鍚�
+ * @param templatePath 妯℃澘璺緞 resource 鐩綍涓嬬殑璺緞鍖呮嫭妯℃澘鏂囦欢鍚�
+ * 渚嬪: excel/temp.xlsx
+ * 閲嶇偣: 妯℃澘鏂囦欢蹇呴』鏀剧疆鍒板惎鍔ㄧ被瀵瑰簲鐨� resource 鐩綍涓�
+ * @param data 妯℃澘闇�瑕佺殑鏁版嵁
+ * @param response 鍝嶅簲浣�
+ */
+ public static void exportTemplate(List<Object> data, String filename, String templatePath, HttpServletResponse response) {
+ try {
+ resetResponse(filename, response);
+ ServletOutputStream os = response.getOutputStream();
+ exportTemplate(data, templatePath, os);
+ } catch (IOException e) {
+ throw new RuntimeException("瀵煎嚭Excel寮傚父");
+ }
+ }
+
+ /**
+ * 鍗曡〃澶氭暟鎹ā鏉垮鍑� 妯℃澘鏍煎紡涓� {.灞炴�
+ *
+ * @param templatePath 妯℃澘璺緞 resource 鐩綍涓嬬殑璺緞鍖呮嫭妯℃澘鏂囦欢鍚�
+ * 渚嬪: excel/temp.xlsx
+ * 閲嶇偣: 妯℃澘鏂囦欢蹇呴』鏀剧疆鍒板惎鍔ㄧ被瀵瑰簲鐨� resource 鐩綍涓�
+ * @param data 妯℃澘闇�瑕佺殑鏁版嵁
+ * @param os 杈撳嚭娴�
+ */
+ public static void exportTemplate(List<Object> data, String templatePath, OutputStream os) {
+ ClassPathResource templateResource = new ClassPathResource(templatePath);
+ ExcelWriter excelWriter = EasyExcel.write(os)
+ .withTemplate(templateResource.getStream())
+ .autoCloseStream(false)
+ // 澶ф暟鍊艰嚜鍔ㄨ浆鎹� 闃叉澶辩湡
+ .registerConverter(new ExcelBigNumberConvert())
+ .build();
+ WriteSheet writeSheet = EasyExcel.writerSheet().build();
+ if (CollUtil.isEmpty(data)) {
+ throw new IllegalArgumentException("鏁版嵁涓虹┖");
+ }
+ // 鍗曡〃澶氭暟鎹鍑� 妯℃澘鏍煎紡涓� {.灞炴�
+ for (Object d : data) {
+ excelWriter.fill(d, writeSheet);
+ }
+ excelWriter.finish();
+ }
+
+ /**
+ * 澶氳〃澶氭暟鎹ā鏉垮鍑� 妯℃澘鏍煎紡涓� {key.灞炴�
+ *
+ * @param filename 鏂囦欢鍚�
+ * @param templatePath 妯℃澘璺緞 resource 鐩綍涓嬬殑璺緞鍖呮嫭妯℃澘鏂囦欢鍚�
+ * 渚嬪: excel/temp.xlsx
+ * 閲嶇偣: 妯℃澘鏂囦欢蹇呴』鏀剧疆鍒板惎鍔ㄧ被瀵瑰簲鐨� resource 鐩綍涓�
+ * @param data 妯℃澘闇�瑕佺殑鏁版嵁
+ * @param response 鍝嶅簲浣�
+ */
+ public static void exportTemplateMultiList(Map<String, Object> data, String filename, String templatePath, HttpServletResponse response) {
+ try {
+ resetResponse(filename, response);
+ ServletOutputStream os = response.getOutputStream();
+ exportTemplateMultiList(data, templatePath, os);
+ } catch (IOException e) {
+ throw new RuntimeException("瀵煎嚭Excel寮傚父");
+ }
+ }
+
+ /**
+ * 澶歴heet妯℃澘瀵煎嚭 妯℃澘鏍煎紡涓� {key.灞炴�
+ *
+ * @param filename 鏂囦欢鍚�
+ * @param templatePath 妯℃澘璺緞 resource 鐩綍涓嬬殑璺緞鍖呮嫭妯℃澘鏂囦欢鍚�
+ * 渚嬪: excel/temp.xlsx
+ * 閲嶇偣: 妯℃澘鏂囦欢蹇呴』鏀剧疆鍒板惎鍔ㄧ被瀵瑰簲鐨� resource 鐩綍涓�
+ * @param data 妯℃澘闇�瑕佺殑鏁版嵁
+ * @param response 鍝嶅簲浣�
+ */
+ public static void exportTemplateMultiSheet(List<Map<String, Object>> data, String filename, String templatePath, HttpServletResponse response) {
+ try {
+ resetResponse(filename, response);
+ ServletOutputStream os = response.getOutputStream();
+ exportTemplateMultiSheet(data, templatePath, os);
+ } catch (IOException e) {
+ throw new RuntimeException("瀵煎嚭Excel寮傚父");
+ }
+ }
+
+ /**
+ * 澶氳〃澶氭暟鎹ā鏉垮鍑� 妯℃澘鏍煎紡涓� {key.灞炴�
+ *
+ * @param templatePath 妯℃澘璺緞 resource 鐩綍涓嬬殑璺緞鍖呮嫭妯℃澘鏂囦欢鍚�
+ * 渚嬪: excel/temp.xlsx
+ * 閲嶇偣: 妯℃澘鏂囦欢蹇呴』鏀剧疆鍒板惎鍔ㄧ被瀵瑰簲鐨� resource 鐩綍涓�
+ * @param data 妯℃澘闇�瑕佺殑鏁版嵁
+ * @param os 杈撳嚭娴�
+ */
+ public static void exportTemplateMultiList(Map<String, Object> data, String templatePath, OutputStream os) {
+ ClassPathResource templateResource = new ClassPathResource(templatePath);
+ ExcelWriter excelWriter = EasyExcel.write(os)
+ .withTemplate(templateResource.getStream())
+ .autoCloseStream(false)
+ // 澶ф暟鍊艰嚜鍔ㄨ浆鎹� 闃叉澶辩湡
+ .registerConverter(new ExcelBigNumberConvert())
+ .build();
+ WriteSheet writeSheet = EasyExcel.writerSheet().build();
+ if (CollUtil.isEmpty(data)) {
+ throw new IllegalArgumentException("鏁版嵁涓虹┖");
+ }
+ for (Map.Entry<String, Object> map : data.entrySet()) {
+ // 璁剧疆鍒楄〃鍚庣画杩樻湁鏁版嵁
+ FillConfig fillConfig = FillConfig.builder().forceNewRow(Boolean.TRUE).build();
+ if (map.getValue() instanceof Collection) {
+ // 澶氳〃瀵煎嚭蹇呴』浣跨敤 FillWrapper
+ excelWriter.fill(new FillWrapper(map.getKey(), (Collection<?>) map.getValue()), fillConfig, writeSheet);
+ } else {
+ excelWriter.fill(map.getValue(), writeSheet);
+ }
+ }
+ excelWriter.finish();
+ }
+
+ /**
+ * 澶歴heet妯℃澘瀵煎嚭 妯℃澘鏍煎紡涓� {key.灞炴�
+ *
+ * @param templatePath 妯℃澘璺緞 resource 鐩綍涓嬬殑璺緞鍖呮嫭妯℃澘鏂囦欢鍚�
+ * 渚嬪: excel/temp.xlsx
+ * 閲嶇偣: 妯℃澘鏂囦欢蹇呴』鏀剧疆鍒板惎鍔ㄧ被瀵瑰簲鐨� resource 鐩綍涓�
+ * @param data 妯℃澘闇�瑕佺殑鏁版嵁
+ * @param os 杈撳嚭娴�
+ */
+ public static void exportTemplateMultiSheet(List<Map<String, Object>> data, String templatePath, OutputStream os) {
+ ClassPathResource templateResource = new ClassPathResource(templatePath);
+ ExcelWriter excelWriter = EasyExcel.write(os)
+ .withTemplate(templateResource.getStream())
+ .autoCloseStream(false)
+ // 澶ф暟鍊艰嚜鍔ㄨ浆鎹� 闃叉澶辩湡
+ .registerConverter(new ExcelBigNumberConvert())
+ .build();
+ if (CollUtil.isEmpty(data)) {
+ throw new IllegalArgumentException("鏁版嵁涓虹┖");
+ }
+ for (int i = 0; i < data.size(); i++) {
+ WriteSheet writeSheet = EasyExcel.writerSheet(i).build();
+ for (Map.Entry<String, Object> map : data.get(i).entrySet()) {
+ // 璁剧疆鍒楄〃鍚庣画杩樻湁鏁版嵁
+ FillConfig fillConfig = FillConfig.builder().forceNewRow(Boolean.TRUE).build();
+ if (map.getValue() instanceof Collection) {
+ // 澶氳〃瀵煎嚭蹇呴』浣跨敤 FillWrapper
+ excelWriter.fill(new FillWrapper(map.getKey(), (Collection<?>) map.getValue()), fillConfig, writeSheet);
+ } else {
+ excelWriter.fill(map.getValue(), writeSheet);
+ }
+ }
+ }
+ excelWriter.finish();
+ }
+
+ /**
+ * 閲嶇疆鍝嶅簲浣�
+ */
+ private static void resetResponse(String sheetName, HttpServletResponse response) throws UnsupportedEncodingException {
+ String filename = encodingFilename(sheetName);
+ FileUtils.setAttachmentResponseHeader(response, filename);
+ response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8");
+ }
+
+ /**
+ * 瑙f瀽瀵煎嚭鍊� 0=鐢�,1=濂�,2=鏈煡
+ *
+ * @param propertyValue 鍙傛暟鍊�
+ * @param converterExp 缈昏瘧娉ㄨВ
+ * @param separator 鍒嗛殧绗�
+ * @return 瑙f瀽鍚庡��
+ */
+ public static String convertByExp(String propertyValue, String converterExp, String separator) {
+ StringBuilder propertyString = new StringBuilder();
+ String[] convertSource = converterExp.split(StringUtils.SEPARATOR);
+ for (String item : convertSource) {
+ String[] itemArray = item.split("=");
+ if (StringUtils.containsAny(propertyValue, separator)) {
+ for (String value : propertyValue.split(separator)) {
+ if (itemArray[0].equals(value)) {
+ propertyString.append(itemArray[1] + separator);
+ break;
+ }
+ }
+ } else {
+ if (itemArray[0].equals(propertyValue)) {
+ return itemArray[1];
+ }
+ }
+ }
+ return StringUtils.stripEnd(propertyString.toString(), separator);
+ }
+
+ /**
+ * 鍙嶅悜瑙f瀽鍊� 鐢�=0,濂�=1,鏈煡=2
+ *
+ * @param propertyValue 鍙傛暟鍊�
+ * @param converterExp 缈昏瘧娉ㄨВ
+ * @param separator 鍒嗛殧绗�
+ * @return 瑙f瀽鍚庡��
+ */
+ public static String reverseByExp(String propertyValue, String converterExp, String separator) {
+ StringBuilder propertyString = new StringBuilder();
+ String[] convertSource = converterExp.split(StringUtils.SEPARATOR);
+ for (String item : convertSource) {
+ String[] itemArray = item.split("=");
+ if (StringUtils.containsAny(propertyValue, separator)) {
+ for (String value : propertyValue.split(separator)) {
+ if (itemArray[1].equals(value)) {
+ propertyString.append(itemArray[0] + separator);
+ break;
+ }
+ }
+ } else {
+ if (itemArray[1].equals(propertyValue)) {
+ return itemArray[0];
+ }
+ }
+ }
+ return StringUtils.stripEnd(propertyString.toString(), separator);
+ }
+
+ /**
+ * 缂栫爜鏂囦欢鍚�
+ */
+ public static String encodingFilename(String filename) {
+ return IdUtil.fastSimpleUUID() + "_" + filename + ".xlsx";
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-idempotent/pom.xml b/ruoyi-common/ruoyi-common-idempotent/pom.xml
new file mode 100644
index 0000000..64418b4
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-idempotent/pom.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common</artifactId>
+ <version>${revision}</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>ruoyi-common-idempotent</artifactId>
+
+ <description>
+ ruoyi-common-idempotent 骞傜瓑鍔熻兘
+ </description>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-json</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-redis</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>cn.hutool</groupId>
+ <artifactId>hutool-crypto</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>cn.dev33</groupId>
+ <artifactId>sa-token-core</artifactId>
+ </dependency>
+
+ </dependencies>
+
+</project>
diff --git a/ruoyi-common/ruoyi-common-idempotent/src/main/java/org/dromara/common/idempotent/annotation/RepeatSubmit.java b/ruoyi-common/ruoyi-common-idempotent/src/main/java/org/dromara/common/idempotent/annotation/RepeatSubmit.java
new file mode 100644
index 0000000..42ae802
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-idempotent/src/main/java/org/dromara/common/idempotent/annotation/RepeatSubmit.java
@@ -0,0 +1,29 @@
+package org.dromara.common.idempotent.annotation;
+
+import java.lang.annotation.*;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 鑷畾涔夋敞瑙i槻姝㈣〃鍗曢噸澶嶆彁浜�
+ *
+ * @author Lion Li
+ */
+@Inherited
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface RepeatSubmit {
+
+ /**
+ * 闂撮殧鏃堕棿(ms)锛屽皬浜庢鏃堕棿瑙嗕负閲嶅鎻愪氦
+ */
+ int interval() default 5000;
+
+ TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
+
+ /**
+ * 鎻愮ず娑堟伅 鏀寔鍥介檯鍖� 鏍煎紡涓� {code}
+ */
+ String message() default "{repeat.submit.message}";
+
+}
diff --git a/ruoyi-common/ruoyi-common-idempotent/src/main/java/org/dromara/common/idempotent/aspectj/RepeatSubmitAspect.java b/ruoyi-common/ruoyi-common-idempotent/src/main/java/org/dromara/common/idempotent/aspectj/RepeatSubmitAspect.java
new file mode 100644
index 0000000..fc53b1b
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-idempotent/src/main/java/org/dromara/common/idempotent/aspectj/RepeatSubmitAspect.java
@@ -0,0 +1,146 @@
+package org.dromara.common.idempotent.aspectj;
+
+import cn.dev33.satoken.SaManager;
+import cn.hutool.core.util.ArrayUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.crypto.SecureUtil;
+import org.dromara.common.core.constant.GlobalConstants;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.core.exception.ServiceException;
+import org.dromara.common.core.utils.MessageUtils;
+import org.dromara.common.core.utils.ServletUtils;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.idempotent.annotation.RepeatSubmit;
+import org.dromara.common.json.utils.JsonUtils;
+import org.dromara.common.redis.utils.RedisUtils;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+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.springframework.validation.BindingResult;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.time.Duration;
+import java.util.Collection;
+import java.util.Map;
+import java.util.StringJoiner;
+
+/**
+ * 闃叉閲嶅鎻愪氦(鍙傝�冪編鍥TIS闃查噸绯荤粺)
+ *
+ * @author Lion Li
+ */
+@Aspect
+public class RepeatSubmitAspect {
+
+ private static final ThreadLocal<String> KEY_CACHE = new ThreadLocal<>();
+
+ @Before("@annotation(repeatSubmit)")
+ public void doBefore(JoinPoint point, RepeatSubmit repeatSubmit) throws Throwable {
+ // 濡傛灉娉ㄨВ涓嶄负0 鍒欎娇鐢ㄦ敞瑙f暟鍊�
+ long interval = repeatSubmit.timeUnit().toMillis(repeatSubmit.interval());
+
+ if (interval < 1000) {
+ throw new ServiceException("閲嶅鎻愪氦闂撮殧鏃堕棿涓嶈兘灏忎簬'1'绉�");
+ }
+ HttpServletRequest request = ServletUtils.getRequest();
+ String nowParams = argsArrayToString(point.getArgs());
+
+ // 璇锋眰鍦板潃锛堜綔涓哄瓨鏀綾ache鐨刱ey鍊硷級
+ String url = request.getRequestURI();
+
+ // 鍞竴鍊硷紙娌℃湁娑堟伅澶村垯浣跨敤璇锋眰鍦板潃锛�
+ String submitKey = StringUtils.trimToEmpty(request.getHeader(SaManager.getConfig().getTokenName()));
+
+ submitKey = SecureUtil.md5(submitKey + ":" + nowParams);
+ // 鍞竴鏍囪瘑锛堟寚瀹歬ey + url + 娑堟伅澶达級
+ String cacheRepeatKey = GlobalConstants.REPEAT_SUBMIT_KEY + url + submitKey;
+ if (RedisUtils.setObjectIfAbsent(cacheRepeatKey, "", Duration.ofMillis(interval))) {
+ KEY_CACHE.set(cacheRepeatKey);
+ } else {
+ String message = repeatSubmit.message();
+ if (StringUtils.startsWith(message, "{") && StringUtils.endsWith(message, "}")) {
+ message = MessageUtils.message(StringUtils.substring(message, 1, message.length() - 1));
+ }
+ throw new ServiceException(message);
+ }
+ }
+
+ /**
+ * 澶勭悊瀹岃姹傚悗鎵ц
+ *
+ * @param joinPoint 鍒囩偣
+ */
+ @AfterReturning(pointcut = "@annotation(repeatSubmit)", returning = "jsonResult")
+ public void doAfterReturning(JoinPoint joinPoint, RepeatSubmit repeatSubmit, Object jsonResult) {
+ if (jsonResult instanceof R<?> r) {
+ try {
+ // 鎴愬姛鍒欎笉鍒犻櫎redis鏁版嵁 淇濊瘉鍦ㄦ湁鏁堟椂闂村唴鏃犳硶閲嶅鎻愪氦
+ if (r.getCode() == R.SUCCESS) {
+ return;
+ }
+ RedisUtils.deleteObject(KEY_CACHE.get());
+ } finally {
+ KEY_CACHE.remove();
+ }
+ }
+ }
+
+ /**
+ * 鎷︽埅寮傚父鎿嶄綔
+ *
+ * @param joinPoint 鍒囩偣
+ * @param e 寮傚父
+ */
+ @AfterThrowing(value = "@annotation(repeatSubmit)", throwing = "e")
+ public void doAfterThrowing(JoinPoint joinPoint, RepeatSubmit repeatSubmit, Exception e) {
+ RedisUtils.deleteObject(KEY_CACHE.get());
+ KEY_CACHE.remove();
+ }
+
+ /**
+ * 鍙傛暟鎷艰
+ */
+ private String argsArrayToString(Object[] paramsArray) {
+ StringJoiner params = new StringJoiner(" ");
+ if (ArrayUtil.isEmpty(paramsArray)) {
+ return params.toString();
+ }
+ for (Object o : paramsArray) {
+ if (ObjectUtil.isNotNull(o) && !isFilterObject(o)) {
+ params.add(JsonUtils.toJsonString(o));
+ }
+ }
+ return params.toString();
+ }
+
+ /**
+ * 鍒ゆ柇鏄惁闇�瑕佽繃婊ょ殑瀵硅薄銆�
+ *
+ * @param o 瀵硅薄淇℃伅銆�
+ * @return 濡傛灉鏄渶瑕佽繃婊ょ殑瀵硅薄锛屽垯杩斿洖true锛涘惁鍒欒繑鍥瀎alse銆�
+ */
+ @SuppressWarnings("rawtypes")
+ public boolean isFilterObject(final Object o) {
+ Class<?> clazz = o.getClass();
+ if (clazz.isArray()) {
+ return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
+ } else if (Collection.class.isAssignableFrom(clazz)) {
+ Collection collection = (Collection) o;
+ for (Object value : collection) {
+ return value instanceof MultipartFile;
+ }
+ } else if (Map.class.isAssignableFrom(clazz)) {
+ Map map = (Map) o;
+ for (Object value : map.values()) {
+ return value instanceof MultipartFile;
+ }
+ }
+ return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
+ || o instanceof BindingResult;
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-idempotent/src/main/java/org/dromara/common/idempotent/config/IdempotentAutoConfiguration.java b/ruoyi-common/ruoyi-common-idempotent/src/main/java/org/dromara/common/idempotent/config/IdempotentAutoConfiguration.java
new file mode 100644
index 0000000..e8b785c
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-idempotent/src/main/java/org/dromara/common/idempotent/config/IdempotentAutoConfiguration.java
@@ -0,0 +1,21 @@
+package org.dromara.common.idempotent.config;
+
+import org.dromara.common.idempotent.aspectj.RepeatSubmitAspect;
+import org.dromara.common.redis.config.RedisConfiguration;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.context.annotation.Bean;
+
+/**
+ * 骞傜瓑鍔熻兘閰嶇疆
+ *
+ * @author Lion Li
+ */
+@AutoConfiguration(after = RedisConfiguration.class)
+public class IdempotentAutoConfiguration {
+
+ @Bean
+ public RepeatSubmitAspect repeatSubmitAspect() {
+ return new RepeatSubmitAspect();
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-idempotent/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/ruoyi-common/ruoyi-common-idempotent/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..fc57da7
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-idempotent/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+org.dromara.common.idempotent.config.IdempotentAutoConfiguration
diff --git a/ruoyi-common/ruoyi-common-job/pom.xml b/ruoyi-common/ruoyi-common-job/pom.xml
new file mode 100644
index 0000000..0df7fee
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-job/pom.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns="http://maven.apache.org/POM/4.0.0"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common</artifactId>
+ <version>${revision}</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>ruoyi-common-job</artifactId>
+
+ <description>
+ ruoyi-common-job 瀹氭椂浠诲姟
+ </description>
+
+ <dependencies>
+
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-autoconfigure</artifactId>
+ </dependency>
+
+ <!-- 鏈嶅姟鍙戠幇缁勪欢 -->
+ <dependency>
+ <groupId>org.springframework.cloud</groupId>
+ <artifactId>spring-cloud-commons</artifactId>
+ </dependency>
+
+ <!--PowerJob-->
+ <dependency>
+ <groupId>tech.powerjob</groupId>
+ <artifactId>powerjob-worker</artifactId>
+ <exclusions>
+ <exclusion>
+ <artifactId>powerjob-remote-impl-akka</artifactId>
+ <groupId>tech.powerjob</groupId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ <dependency>
+ <groupId>tech.powerjob</groupId>
+ <artifactId>powerjob-official-processors</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.projectlombok</groupId>
+ <artifactId>lombok</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-core</artifactId>
+ </dependency>
+
+ </dependencies>
+</project>
diff --git a/ruoyi-common/ruoyi-common-job/src/main/java/org/dromara/common/job/config/PowerJobConfig.java b/ruoyi-common/ruoyi-common-job/src/main/java/org/dromara/common/job/config/PowerJobConfig.java
new file mode 100644
index 0000000..35612f3
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-job/src/main/java/org/dromara/common/job/config/PowerJobConfig.java
@@ -0,0 +1,107 @@
+package org.dromara.common.job.config;
+
+import cn.hutool.core.collection.CollUtil;
+import org.dromara.common.core.utils.StreamUtils;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.job.config.properties.PowerJobProperties;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.cloud.client.ServiceInstance;
+import org.springframework.cloud.client.discovery.DiscoveryClient;
+import org.springframework.context.annotation.Bean;
+import tech.powerjob.common.utils.CommonUtils;
+import tech.powerjob.common.utils.NetUtils;
+import tech.powerjob.worker.PowerJobSpringWorker;
+import tech.powerjob.worker.common.PowerJobWorkerConfig;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Autoconfiguration class for PowerJob-worker.
+ *
+ * @author songyinyin
+ * @since 2020/7/26 16:37
+ */
+@AutoConfiguration
+@EnableConfigurationProperties(PowerJobProperties.class)
+@ConditionalOnProperty(prefix = "powerjob.worker", name = "enabled", havingValue = "true", matchIfMissing = true)
+public class PowerJobConfig{
+
+ @Bean
+ public PowerJobSpringWorker initPowerJob(PowerJobProperties properties, DiscoveryClient discoveryClient) {
+
+ PowerJobProperties.Worker worker = properties.getWorker();
+
+ /*
+ * Address of PowerJob-server node(s). Do not mistake for ActorSystem port. Do not add
+ * any prefix, i.e. http://.
+ */
+ List<String> serverAddress;
+ if (StringUtils.isNotBlank(worker.getServerName())) {
+ List<ServiceInstance> instances = discoveryClient.getInstances(worker.getServerName());
+ if (CollUtil.isEmpty(instances)) {
+ throw new RuntimeException("璋冨害涓績涓嶅瓨鍦�!");
+ }
+ serverAddress = StreamUtils.toList(instances, instance ->
+ String.format("%s:%s", instance.getHost(), instance.getPort()));
+ } else {
+ CommonUtils.requireNonNull(worker.getServerAddress(), "serverAddress can't be empty! " +
+ "if you don't want to enable powerjob, please config program arguments: powerjob.worker.enabled=false");
+ serverAddress = Arrays.asList(worker.getServerAddress().split(","));
+ }
+ /*
+ * Create OhMyConfig object for setting properties.
+ */
+ PowerJobWorkerConfig config = new PowerJobWorkerConfig();
+ /*
+ * Configuration of worker port. Random port is enabled when port is set with non-positive number.
+ */
+ if (worker.getPort() != null) {
+ config.setPort(worker.getPort());
+ } else {
+ int port = worker.getAkkaPort();
+ if (port <= 0) {
+ port = NetUtils.getRandomPort();
+ }
+ config.setPort(port);
+ }
+ /*
+ * appName, name of the application. Applications should be registered in advance to prevent
+ * error. This property should be the same with what you entered for appName when getting
+ * registered.
+ */
+ config.setAppName(worker.getAppName());
+ config.setServerAddress(serverAddress);
+ config.setProtocol(worker.getProtocol());
+ /*
+ * For non-Map/MapReduce tasks, {@code memory} is recommended for speeding up calculation.
+ * Map/MapReduce tasks may produce batches of subtasks, which could lead to OutOfMemory
+ * exception or error, {@code disk} should be applied.
+ */
+ config.setStoreStrategy(worker.getStoreStrategy());
+ /*
+ * When enabledTestMode is set as true, PowerJob-worker no longer connects to PowerJob-server
+ * or validate appName.
+ */
+ config.setAllowLazyConnectServer(worker.isAllowLazyConnectServer());
+ /*
+ * Max length of appended workflow context . Appended workflow context value that is longer than the value will be ignored.
+ */
+ config.setMaxAppendedWfContextLength(worker.getMaxAppendedWfContextLength());
+
+ config.setTag(worker.getTag());
+
+ config.setMaxHeavyweightTaskNum(worker.getMaxHeavyweightTaskNum());
+
+ config.setMaxLightweightTaskNum(worker.getMaxLightweightTaskNum());
+
+ config.setHealthReportInterval(worker.getHealthReportInterval());
+ /*
+ * Create PowerJobSpringWorker object and set properties.
+ */
+ return new PowerJobSpringWorker(config);
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-job/src/main/java/org/dromara/common/job/config/properties/PowerJobProperties.java b/ruoyi-common/ruoyi-common-job/src/main/java/org/dromara/common/job/config/properties/PowerJobProperties.java
new file mode 100644
index 0000000..df5f285
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-job/src/main/java/org/dromara/common/job/config/properties/PowerJobProperties.java
@@ -0,0 +1,109 @@
+package org.dromara.common.job.config.properties;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import tech.powerjob.common.RemoteConstant;
+import tech.powerjob.common.enums.Protocol;
+import tech.powerjob.worker.common.constants.StoreStrategy;
+import tech.powerjob.worker.core.processor.ProcessResult;
+import tech.powerjob.worker.core.processor.WorkflowContext;
+
+/**
+ * PowerJob properties configuration class.
+ *
+ * @author songyinyin
+ * @since 2020/7/26 16:37
+ */
+@ConfigurationProperties(prefix = "powerjob")
+public class PowerJobProperties {
+
+ private final Worker worker = new Worker();
+
+ public Worker getWorker() {
+ return worker;
+ }
+
+ /**
+ * Powerjob worker configuration properties.
+ */
+ @Setter
+ @Getter
+ public static class Worker {
+
+ /**
+ * Whether to enable PowerJob Worker
+ */
+ private boolean enabled = true;
+
+ /**
+ * Name of application, String type. Total length of this property should be no more than 255
+ * characters. This is one of the required properties when registering a new application. This
+ * property should be assigned with the same value as what you entered for the appName.
+ */
+ private String appName;
+ /**
+ * Akka port of Powerjob-worker, optional value. Default value of this property is 27777.
+ * If multiple PowerJob-worker nodes were deployed, different, unique ports should be assigned.
+ * Deprecated, please use 'port'
+ */
+ @Deprecated
+ private int akkaPort = RemoteConstant.DEFAULT_WORKER_PORT;
+ /**
+ * port
+ */
+ private Integer port;
+ /**
+ * Address(es) of Powerjob-server node(s). Ip:port or domain.
+ * Example of single Powerjob-server node:
+ * <p>
+ * 127.0.0.1:7700
+ * </p>
+ * Example of Powerjob-server cluster:
+ * <p>
+ * 192.168.0.10:7700,192.168.0.11:7700,192.168.0.12:7700
+ * </p>
+ */
+ private String serverAddress;
+
+ private String serverName;
+ /**
+ * Protocol for communication between WORKER and server
+ */
+ private Protocol protocol = Protocol.AKKA;
+ /**
+ * Local store strategy for H2 database. {@code disk} or {@code memory}.
+ */
+ private StoreStrategy storeStrategy = StoreStrategy.DISK;
+ /**
+ * Max length of response result. Result that is longer than the value will be truncated.
+ * {@link ProcessResult} max length for #msg
+ */
+ private int maxResultLength = 8192;
+ /**
+ * If allowLazyConnectServer is set as true, PowerJob worker allows launching without a direct connection to the server.
+ * allowLazyConnectServer is used for conditions that your have no powerjob-server in your develop env so you can't startup the application
+ */
+ private boolean allowLazyConnectServer = false;
+ /**
+ * Max length of appended workflow context value length. Appended workflow context value that is longer than the value will be ignored.
+ * {@link WorkflowContext} max length for #appendedContextData
+ */
+ private int maxAppendedWfContextLength = 8192;
+
+ private String tag;
+ /**
+ * Max numbers of LightTaskTacker
+ */
+ private Integer maxLightweightTaskNum = 1024;
+ /**
+ * Max numbers of HeavyTaskTacker
+ */
+ private Integer maxHeavyweightTaskNum = 64;
+ /**
+ * Interval(s) of worker health report
+ */
+ private Integer healthReportInterval = 10;
+
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-job/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/ruoyi-common/ruoyi-common-job/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..222bcc1
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-job/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+org.dromara.common.job.config.PowerJobConfig
diff --git a/ruoyi-common/ruoyi-common-json/pom.xml b/ruoyi-common/ruoyi-common-json/pom.xml
new file mode 100644
index 0000000..870df5c
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-json/pom.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common</artifactId>
+ <version>${revision}</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>ruoyi-common-json</artifactId>
+
+ <description>
+ ruoyi-common-json 搴忓垪鍖栨ā鍧�
+ </description>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-core</artifactId>
+ </dependency>
+
+ <!-- JSON宸ュ叿绫� -->
+ <dependency>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-databind</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>com.fasterxml.jackson.datatype</groupId>
+ <artifactId>jackson-datatype-jsr310</artifactId>
+ </dependency>
+
+ </dependencies>
+
+</project>
diff --git a/ruoyi-common/ruoyi-common-json/src/main/java/org/dromara/common/json/config/JacksonConfig.java b/ruoyi-common/ruoyi-common-json/src/main/java/org/dromara/common/json/config/JacksonConfig.java
new file mode 100644
index 0000000..8f5a45d
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-json/src/main/java/org/dromara/common/json/config/JacksonConfig.java
@@ -0,0 +1,47 @@
+package org.dromara.common.json.config;
+
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
+import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
+import org.dromara.common.json.handler.BigNumberSerializer;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
+import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
+import org.springframework.context.annotation.Bean;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.TimeZone;
+
+/**
+ * jackson 閰嶇疆
+ *
+ * @author Lion Li
+ */
+@Slf4j
+@AutoConfiguration(before = JacksonAutoConfiguration.class)
+public class JacksonConfig {
+
+ @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));
+ builder.modules(javaTimeModule);
+ builder.timeZone(TimeZone.getDefault());
+ log.info("鍒濆鍖� jackson 閰嶇疆");
+ };
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-json/src/main/java/org/dromara/common/json/handler/BigNumberSerializer.java b/ruoyi-common/ruoyi-common-json/src/main/java/org/dromara/common/json/handler/BigNumberSerializer.java
new file mode 100644
index 0000000..f2a7c2d
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-json/src/main/java/org/dromara/common/json/handler/BigNumberSerializer.java
@@ -0,0 +1,42 @@
+package org.dromara.common.json.handler;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.annotation.JacksonStdImpl;
+import com.fasterxml.jackson.databind.ser.std.NumberSerializer;
+
+import java.io.IOException;
+
+/**
+ * 瓒呭嚭 JS 鏈�澶ф渶灏忓�� 澶勭悊
+ *
+ * @author Lion Li
+ */
+@JacksonStdImpl
+public class BigNumberSerializer extends NumberSerializer {
+
+ /**
+ * 鏍规嵁 JS Number.MAX_SAFE_INTEGER 涓� Number.MIN_SAFE_INTEGER 寰楁潵
+ */
+ private static final long MAX_SAFE_INTEGER = 9007199254740991L;
+ private static final long MIN_SAFE_INTEGER = -9007199254740991L;
+
+ /**
+ * 鎻愪緵瀹炰緥
+ */
+ public static final BigNumberSerializer INSTANCE = new BigNumberSerializer(Number.class);
+
+ public BigNumberSerializer(Class<? extends Number> rawType) {
+ super(rawType);
+ }
+
+ @Override
+ public void serialize(Number value, JsonGenerator gen, SerializerProvider provider) throws IOException {
+ // 瓒呭嚭鑼冨洿 搴忓垪鍖栦綅瀛楃涓�
+ if (value.longValue() > MIN_SAFE_INTEGER && value.longValue() < MAX_SAFE_INTEGER) {
+ super.serialize(value, gen, provider);
+ } else {
+ gen.writeString(value.toString());
+ }
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-json/src/main/java/org/dromara/common/json/utils/JsonUtils.java b/ruoyi-common/ruoyi-common-json/src/main/java/org/dromara/common/json/utils/JsonUtils.java
new file mode 100644
index 0000000..42af8da
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-json/src/main/java/org/dromara/common/json/utils/JsonUtils.java
@@ -0,0 +1,113 @@
+package org.dromara.common.json.utils;
+
+import cn.hutool.core.lang.Dict;
+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.ObjectMapper;
+import com.fasterxml.jackson.databind.exc.MismatchedInputException;
+import org.dromara.common.core.utils.SpringUtils;
+import org.dromara.common.core.utils.StringUtils;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * JSON 宸ュ叿绫�
+ *
+ * @author 鑺嬮亾婧愮爜
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class JsonUtils {
+
+ private static final ObjectMapper OBJECT_MAPPER = SpringUtils.getBean(ObjectMapper.class);
+
+ public static ObjectMapper getObjectMapper() {
+ return OBJECT_MAPPER;
+ }
+
+ public static String toJsonString(Object object) {
+ if (ObjectUtil.isNull(object)) {
+ return null;
+ }
+ try {
+ return OBJECT_MAPPER.writeValueAsString(object);
+ } catch (JsonProcessingException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static <T> T parseObject(String text, Class<T> clazz) {
+ if (StringUtils.isEmpty(text)) {
+ return null;
+ }
+ try {
+ return OBJECT_MAPPER.readValue(text, clazz);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static <T> T parseObject(byte[] bytes, Class<T> clazz) {
+ if (ArrayUtil.isEmpty(bytes)) {
+ return null;
+ }
+ try {
+ return OBJECT_MAPPER.readValue(bytes, clazz);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static <T> T parseObject(String text, TypeReference<T> typeReference) {
+ if (StringUtils.isBlank(text)) {
+ return null;
+ }
+ try {
+ return OBJECT_MAPPER.readValue(text, typeReference);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static Dict parseMap(String text) {
+ if (StringUtils.isBlank(text)) {
+ return null;
+ }
+ try {
+ return OBJECT_MAPPER.readValue(text, OBJECT_MAPPER.getTypeFactory().constructType(Dict.class));
+ } catch (MismatchedInputException e) {
+ // 绫诲瀷涓嶅尮閰嶈鏄庝笉鏄痡son
+ return null;
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static List<Dict> parseArrayMap(String text) {
+ if (StringUtils.isBlank(text)) {
+ return null;
+ }
+ try {
+ return OBJECT_MAPPER.readValue(text, OBJECT_MAPPER.getTypeFactory().constructCollectionType(List.class, Dict.class));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static <T> List<T> parseArray(String text, Class<T> clazz) {
+ if (StringUtils.isEmpty(text)) {
+ return new ArrayList<>();
+ }
+ try {
+ return OBJECT_MAPPER.readValue(text, OBJECT_MAPPER.getTypeFactory().constructCollectionType(List.class, clazz));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-json/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/ruoyi-common/ruoyi-common-json/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..1625397
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-json/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+org.dromara.common.json.config.JacksonConfig
diff --git a/ruoyi-common/ruoyi-common-loadbalancer/pom.xml b/ruoyi-common/ruoyi-common-loadbalancer/pom.xml
new file mode 100644
index 0000000..1509a44
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-loadbalancer/pom.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns="http://maven.apache.org/POM/4.0.0"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common</artifactId>
+ <version>${revision}</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>ruoyi-common-loadbalancer</artifactId>
+
+ <description>
+ ruoyi-common-loadbalancer 鑷畾涔夎礋杞藉潎琛�(澶氬洟闃熷紑鍙戜娇鐢�)
+ </description>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.springframework.cloud</groupId>
+ <artifactId>spring-cloud-starter-loadbalancer</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.dubbo</groupId>
+ <artifactId>dubbo-spring-boot-starter</artifactId>
+ <scope>provided</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.projectlombok</groupId>
+ <artifactId>lombok</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-core</artifactId>
+ </dependency>
+
+ </dependencies>
+</project>
diff --git a/ruoyi-common/ruoyi-common-loadbalancer/src/main/java/org/dromara/common/loadbalance/config/CustomEnvironmentPostProcessor.java b/ruoyi-common/ruoyi-common-loadbalancer/src/main/java/org/dromara/common/loadbalance/config/CustomEnvironmentPostProcessor.java
new file mode 100644
index 0000000..7811238
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-loadbalancer/src/main/java/org/dromara/common/loadbalance/config/CustomEnvironmentPostProcessor.java
@@ -0,0 +1,25 @@
+package org.dromara.common.loadbalance.config;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.env.EnvironmentPostProcessor;
+import org.springframework.core.Ordered;
+import org.springframework.core.env.ConfigurableEnvironment;
+
+/**
+ * dubbo鑷畾涔夎礋杞藉潎琛¢厤缃敞鍏�
+ *
+ * @author Lion Li
+ */
+public class CustomEnvironmentPostProcessor implements EnvironmentPostProcessor, Ordered {
+
+ @Override
+ public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
+ System.setProperty("dubbo.consumer.loadbalance", "customDubboLoadBalancer");
+ }
+
+ @Override
+ public int getOrder() {
+ return Ordered.HIGHEST_PRECEDENCE;
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-loadbalancer/src/main/java/org/dromara/common/loadbalance/config/CustomLoadBalanceAutoConfiguration.java b/ruoyi-common/ruoyi-common-loadbalancer/src/main/java/org/dromara/common/loadbalance/config/CustomLoadBalanceAutoConfiguration.java
new file mode 100644
index 0000000..a7d86c0
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-loadbalancer/src/main/java/org/dromara/common/loadbalance/config/CustomLoadBalanceAutoConfiguration.java
@@ -0,0 +1,13 @@
+package org.dromara.common.loadbalance.config;
+
+import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClients;
+
+/**
+ * 鑷畾涔夎礋杞藉潎琛¤嚜鍔ㄩ厤缃�
+ *
+ * @author Lion Li
+ */
+@LoadBalancerClients(defaultConfiguration = CustomLoadBalanceClientConfiguration.class)
+public class CustomLoadBalanceAutoConfiguration {
+
+}
diff --git a/ruoyi-common/ruoyi-common-loadbalancer/src/main/java/org/dromara/common/loadbalance/config/CustomLoadBalanceClientConfiguration.java b/ruoyi-common/ruoyi-common-loadbalancer/src/main/java/org/dromara/common/loadbalance/config/CustomLoadBalanceClientConfiguration.java
new file mode 100644
index 0000000..753a14a
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-loadbalancer/src/main/java/org/dromara/common/loadbalance/config/CustomLoadBalanceClientConfiguration.java
@@ -0,0 +1,30 @@
+package org.dromara.common.loadbalance.config;
+
+import org.dromara.common.loadbalance.core.CustomSpringCloudLoadBalancer;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.cloud.client.ServiceInstance;
+import org.springframework.cloud.loadbalancer.core.ReactorLoadBalancer;
+import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
+import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.env.Environment;
+
+/**
+ * 鑷畾涔夎礋杞藉潎琛″鎴风閰嶇疆
+ *
+ * @author LionLi
+ */
+@SuppressWarnings("all")
+@Configuration(proxyBeanMethods = false)
+public class CustomLoadBalanceClientConfiguration {
+
+ @Bean
+ @ConditionalOnBean(LoadBalancerClientFactory.class)
+ public ReactorLoadBalancer<ServiceInstance> customLoadBalancer(Environment environment,
+ LoadBalancerClientFactory loadBalancerClientFactory) {
+ String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
+ return new CustomSpringCloudLoadBalancer(name,
+ loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class));
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-loadbalancer/src/main/java/org/dromara/common/loadbalance/core/CustomDubboLoadBalancer.java b/ruoyi-common/ruoyi-common-loadbalancer/src/main/java/org/dromara/common/loadbalance/core/CustomDubboLoadBalancer.java
new file mode 100644
index 0000000..1d3337f
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-loadbalancer/src/main/java/org/dromara/common/loadbalance/core/CustomDubboLoadBalancer.java
@@ -0,0 +1,30 @@
+package org.dromara.common.loadbalance.core;
+
+import cn.hutool.core.net.NetUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.dubbo.common.URL;
+import org.apache.dubbo.rpc.Invocation;
+import org.apache.dubbo.rpc.Invoker;
+import org.apache.dubbo.rpc.cluster.loadbalance.AbstractLoadBalance;
+
+import java.util.List;
+import java.util.concurrent.ThreadLocalRandom;
+
+/**
+ * 鑷畾涔� Dubbo 璐熻浇鍧囪 绠楁硶
+ *
+ * @author Lion Li
+ */
+@Slf4j
+public class CustomDubboLoadBalancer extends AbstractLoadBalance {
+
+ @Override
+ protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
+ for (Invoker<T> invoker : invokers) {
+ if (NetUtil.localIpv4s().contains(invoker.getUrl().getHost())) {
+ return invoker;
+ }
+ }
+ return invokers.get(ThreadLocalRandom.current().nextInt(invokers.size()));
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-loadbalancer/src/main/java/org/dromara/common/loadbalance/core/CustomSpringCloudLoadBalancer.java b/ruoyi-common/ruoyi-common-loadbalancer/src/main/java/org/dromara/common/loadbalance/core/CustomSpringCloudLoadBalancer.java
new file mode 100644
index 0000000..4e3d16d
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-loadbalancer/src/main/java/org/dromara/common/loadbalance/core/CustomSpringCloudLoadBalancer.java
@@ -0,0 +1,64 @@
+package org.dromara.common.loadbalance.core;
+
+import cn.hutool.core.net.NetUtil;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.cloud.client.ServiceInstance;
+import org.springframework.cloud.client.loadbalancer.DefaultResponse;
+import org.springframework.cloud.client.loadbalancer.EmptyResponse;
+import org.springframework.cloud.client.loadbalancer.Request;
+import org.springframework.cloud.client.loadbalancer.Response;
+import org.springframework.cloud.loadbalancer.core.NoopServiceInstanceListSupplier;
+import org.springframework.cloud.loadbalancer.core.ReactorServiceInstanceLoadBalancer;
+import org.springframework.cloud.loadbalancer.core.SelectedInstanceCallback;
+import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
+import reactor.core.publisher.Mono;
+
+import java.util.List;
+import java.util.concurrent.ThreadLocalRandom;
+
+/**
+ * 鑷畾涔� SpringCloud 璐熻浇鍧囪 绠楁硶
+ *
+ * @author Lion Li
+ */
+@Slf4j
+@AllArgsConstructor
+public class CustomSpringCloudLoadBalancer implements ReactorServiceInstanceLoadBalancer {
+
+ private final String serviceId;
+
+ private final ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;
+
+ @Override
+ public Mono<Response<ServiceInstance>> choose(Request request) {
+ ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new);
+ return supplier.get(request).next().map(serviceInstances -> processInstanceResponse(supplier, serviceInstances));
+ }
+
+ private Response<ServiceInstance> processInstanceResponse(ServiceInstanceListSupplier supplier,
+ List<ServiceInstance> serviceInstances) {
+ Response<ServiceInstance> serviceInstanceResponse = getInstanceResponse(serviceInstances);
+ if (supplier instanceof SelectedInstanceCallback && serviceInstanceResponse.hasServer()) {
+ ((SelectedInstanceCallback) supplier).selectedServiceInstance(serviceInstanceResponse.getServer());
+ }
+ return serviceInstanceResponse;
+ }
+
+ private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances) {
+ if (instances.isEmpty()) {
+ if (log.isWarnEnabled()) {
+ log.warn("No servers available for service: " + serviceId);
+ }
+ return new EmptyResponse();
+ }
+ for (ServiceInstance instance : instances) {
+ if (NetUtil.localIpv4s().contains(instance.getHost())) {
+ return new DefaultResponse(instance);
+ }
+ }
+ return new DefaultResponse(instances.get(ThreadLocalRandom.current().nextInt(instances.size())));
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-loadbalancer/src/main/resources/META-INF/dubbo/org.apache.dubbo.rpc.cluster.LoadBalance b/ruoyi-common/ruoyi-common-loadbalancer/src/main/resources/META-INF/dubbo/org.apache.dubbo.rpc.cluster.LoadBalance
new file mode 100644
index 0000000..f40caf6
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-loadbalancer/src/main/resources/META-INF/dubbo/org.apache.dubbo.rpc.cluster.LoadBalance
@@ -0,0 +1 @@
+customDubboLoadBalancer=org.dromara.common.loadbalance.core.CustomDubboLoadBalancer
diff --git a/ruoyi-common/ruoyi-common-loadbalancer/src/main/resources/META-INF/spring.factories b/ruoyi-common/ruoyi-common-loadbalancer/src/main/resources/META-INF/spring.factories
new file mode 100644
index 0000000..18edf43
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-loadbalancer/src/main/resources/META-INF/spring.factories
@@ -0,0 +1,2 @@
+org.springframework.boot.env.EnvironmentPostProcessor=\
+ org.dromara.common.loadbalance.config.CustomEnvironmentPostProcessor
diff --git a/ruoyi-common/ruoyi-common-loadbalancer/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/ruoyi-common/ruoyi-common-loadbalancer/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..954b296
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-loadbalancer/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+org.dromara.common.loadbalance.config.CustomLoadBalanceAutoConfiguration
diff --git a/ruoyi-common/ruoyi-common-log/pom.xml b/ruoyi-common/ruoyi-common-log/pom.xml
new file mode 100644
index 0000000..cbda66f
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-log/pom.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common</artifactId>
+ <version>${revision}</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>ruoyi-common-log</artifactId>
+
+ <description>
+ ruoyi-common-log 鏃ュ織璁板綍
+ </description>
+
+ <dependencies>
+
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-satoken</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-json</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>com.alibaba</groupId>
+ <artifactId>transmittable-thread-local</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.dubbo</groupId>
+ <artifactId>dubbo-spring-boot-starter</artifactId>
+ <scope>provided</scope>
+ </dependency>
+
+ </dependencies>
+
+</project>
diff --git a/ruoyi-common/ruoyi-common-log/src/main/java/org/dromara/common/log/annotation/Log.java b/ruoyi-common/ruoyi-common-log/src/main/java/org/dromara/common/log/annotation/Log.java
new file mode 100644
index 0000000..2dced97
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-log/src/main/java/org/dromara/common/log/annotation/Log.java
@@ -0,0 +1,48 @@
+package org.dromara.common.log.annotation;
+
+import org.dromara.common.log.enums.BusinessType;
+import org.dromara.common.log.enums.OperatorType;
+
+import java.lang.annotation.*;
+
+/**
+ * 鑷畾涔夋搷浣滄棩蹇楄褰曟敞瑙�
+ *
+ * @author ruoyi
+ */
+@Target({ElementType.PARAMETER, ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface Log {
+ /**
+ * 妯″潡
+ */
+ String title() default "";
+
+ /**
+ * 鍔熻兘
+ */
+ BusinessType businessType() default BusinessType.OTHER;
+
+ /**
+ * 鎿嶄綔浜虹被鍒�
+ */
+ OperatorType operatorType() default OperatorType.MANAGE;
+
+ /**
+ * 鏄惁淇濆瓨璇锋眰鐨勫弬鏁�
+ */
+ boolean isSaveRequestData() default true;
+
+ /**
+ * 鏄惁淇濆瓨鍝嶅簲鐨勫弬鏁�
+ */
+ boolean isSaveResponseData() default true;
+
+
+ /**
+ * 鎺掗櫎鎸囧畾鐨勮姹傚弬鏁�
+ */
+ String[] excludeParamNames() default {};
+
+}
diff --git a/ruoyi-common/ruoyi-common-log/src/main/java/org/dromara/common/log/aspect/LogAspect.java b/ruoyi-common/ruoyi-common-log/src/main/java/org/dromara/common/log/aspect/LogAspect.java
new file mode 100644
index 0000000..06beca6
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-log/src/main/java/org/dromara/common/log/aspect/LogAspect.java
@@ -0,0 +1,222 @@
+package org.dromara.common.log.aspect;
+
+import cn.hutool.core.lang.Dict;
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.ArrayUtil;
+import cn.hutool.core.util.ObjectUtil;
+import com.alibaba.ttl.TransmittableThreadLocal;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.time.StopWatch;
+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.core.utils.ServletUtils;
+import org.dromara.common.core.utils.SpringUtils;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.json.utils.JsonUtils;
+import org.dromara.common.log.annotation.Log;
+import org.dromara.common.log.enums.BusinessStatus;
+import org.dromara.common.log.event.OperLogEvent;
+import org.dromara.common.satoken.utils.LoginHelper;
+import org.dromara.system.api.model.LoginUser;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+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;
+
+/**
+ * 鎿嶄綔鏃ュ織璁板綍澶勭悊
+ *
+ * @author Lion Li
+ */
+@Slf4j
+@Aspect
+@AutoConfiguration
+public class LogAspect {
+
+ /**
+ * 鎺掗櫎鏁忔劅灞炴�у瓧娈�
+ */
+ public static final String[] EXCLUDE_PROPERTIES = { "password", "oldPassword", "newPassword", "confirmPassword" };
+
+
+ /**
+ * 璁$畻鎿嶄綔娑堣�楁椂闂�
+ */
+ private static final ThreadLocal<StopWatch> TIME_THREADLOCAL = new TransmittableThreadLocal<>();
+
+ /**
+ * 澶勭悊璇锋眰鍓嶆墽琛�
+ */
+ @Before(value = "@annotation(controllerLog)")
+ public void boBefore(JoinPoint joinPoint, Log controllerLog) {
+ StopWatch stopWatch = new StopWatch();
+ TIME_THREADLOCAL.set(stopWatch);
+ stopWatch.start();
+ }
+
+ /**
+ * 澶勭悊瀹岃姹傚悗鎵ц
+ *
+ * @param joinPoint 鍒囩偣
+ */
+ @AfterReturning(pointcut = "@annotation(controllerLog)", returning = "jsonResult")
+ public void doAfterReturning(JoinPoint joinPoint, Log controllerLog, Object jsonResult) {
+ handleLog(joinPoint, controllerLog, null, jsonResult);
+ }
+
+ /**
+ * 鎷︽埅寮傚父鎿嶄綔
+ *
+ * @param joinPoint 鍒囩偣
+ * @param e 寮傚父
+ */
+ @AfterThrowing(value = "@annotation(controllerLog)", throwing = "e")
+ public void doAfterThrowing(JoinPoint joinPoint, Log controllerLog, Exception e) {
+ handleLog(joinPoint, controllerLog, e, null);
+ }
+
+ protected void handleLog(final JoinPoint joinPoint, Log controllerLog, final Exception e, Object jsonResult) {
+ try {
+
+ // *========鏁版嵁搴撴棩蹇�=========*//
+ OperLogEvent operLog = new OperLogEvent();
+ operLog.setTenantId(LoginHelper.getTenantId());
+ operLog.setStatus(BusinessStatus.SUCCESS.ordinal());
+ // 璇锋眰鐨勫湴鍧�
+ String ip = ServletUtils.getClientIP();
+ operLog.setOperIp(ip);
+ operLog.setOperUrl(StringUtils.substring(ServletUtils.getRequest().getRequestURI(), 0, 255));
+ LoginUser loginUser = LoginHelper.getLoginUser();
+ operLog.setOperName(loginUser.getUsername());
+ operLog.setDeptName(loginUser.getDeptName());
+
+ if (e != null) {
+ operLog.setStatus(BusinessStatus.FAIL.ordinal());
+ operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000));
+ }
+ // 璁剧疆鏂规硶鍚嶇О
+ String className = joinPoint.getTarget().getClass().getName();
+ String methodName = joinPoint.getSignature().getName();
+ operLog.setMethod(className + "." + methodName + "()");
+ // 璁剧疆璇锋眰鏂瑰紡
+ operLog.setRequestMethod(ServletUtils.getRequest().getMethod());
+ // 澶勭悊璁剧疆娉ㄨВ涓婄殑鍙傛暟
+ getControllerMethodDescription(joinPoint, controllerLog, operLog, jsonResult);
+ // 璁剧疆娑堣�楁椂闂�
+ StopWatch stopWatch = TIME_THREADLOCAL.get();
+ stopWatch.stop();
+ operLog.setCostTime(stopWatch.getTime());
+ // 鍙戝竷浜嬩欢淇濆瓨鏁版嵁搴�
+ SpringUtils.context().publishEvent(operLog);
+ } catch (Exception exp) {
+ // 璁板綍鏈湴寮傚父鏃ュ織
+ log.error("寮傚父淇℃伅:{}", exp.getMessage());
+ exp.printStackTrace();
+ } finally {
+ TIME_THREADLOCAL.remove();
+ }
+ }
+
+ /**
+ * 鑾峰彇娉ㄨВ涓鏂规硶鐨勬弿杩颁俊鎭� 鐢ㄤ簬Controller灞傛敞瑙�
+ *
+ * @param log 鏃ュ織
+ * @param operLog 鎿嶄綔鏃ュ織
+ * @throws Exception
+ */
+ public void getControllerMethodDescription(JoinPoint joinPoint, Log log, OperLogEvent operLog, Object jsonResult) throws Exception {
+ // 璁剧疆action鍔ㄤ綔
+ operLog.setBusinessType(log.businessType().ordinal());
+ // 璁剧疆鏍囬
+ operLog.setTitle(log.title());
+ // 璁剧疆鎿嶄綔浜虹被鍒�
+ operLog.setOperatorType(log.operatorType().ordinal());
+ // 鏄惁闇�瑕佷繚瀛榬equest锛屽弬鏁板拰鍊�
+ if (log.isSaveRequestData()) {
+ // 鑾峰彇鍙傛暟鐨勪俊鎭紝浼犲叆鍒版暟鎹簱涓��
+ setRequestValue(joinPoint, operLog, log.excludeParamNames());
+ }
+ // 鏄惁闇�瑕佷繚瀛榬esponse锛屽弬鏁板拰鍊�
+ if (log.isSaveResponseData() && ObjectUtil.isNotNull(jsonResult)) {
+ operLog.setJsonResult(StringUtils.substring(JsonUtils.toJsonString(jsonResult), 0, 2000));
+ }
+ }
+
+ /**
+ * 鑾峰彇璇锋眰鐨勫弬鏁帮紝鏀惧埌log涓�
+ *
+ * @param operLog 鎿嶄綔鏃ュ織
+ * @throws Exception 寮傚父
+ */
+ private void setRequestValue(JoinPoint joinPoint, OperLogEvent operLog, String[] excludeParamNames) throws Exception {
+ Map<String, String> paramsMap = ServletUtils.getParamMap(ServletUtils.getRequest());
+ String requestMethod = operLog.getRequestMethod();
+ if (MapUtil.isEmpty(paramsMap)
+ && HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod)) {
+ String params = argsArrayToString(joinPoint.getArgs(), excludeParamNames);
+ operLog.setOperParam(StringUtils.substring(params, 0, 2000));
+ } else {
+ MapUtil.removeAny(paramsMap, EXCLUDE_PROPERTIES);
+ MapUtil.removeAny(paramsMap, excludeParamNames);
+ operLog.setOperParam(StringUtils.substring(JsonUtils.toJsonString(paramsMap), 0, 2000));
+ }
+ }
+
+ /**
+ * 鍙傛暟鎷艰
+ */
+ private String argsArrayToString(Object[] paramsArray, String[] excludeParamNames) {
+ StringJoiner params = new StringJoiner(" ");
+ if (ArrayUtil.isEmpty(paramsArray)) {
+ return params.toString();
+ }
+ 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);
+ }
+ params.add(str);
+ }
+ }
+ return params.toString();
+ }
+
+ /**
+ * 鍒ゆ柇鏄惁闇�瑕佽繃婊ょ殑瀵硅薄銆�
+ *
+ * @param o 瀵硅薄淇℃伅銆�
+ * @return 濡傛灉鏄渶瑕佽繃婊ょ殑瀵硅薄锛屽垯杩斿洖true锛涘惁鍒欒繑鍥瀎alse銆�
+ */
+ @SuppressWarnings("rawtypes")
+ public boolean isFilterObject(final Object o) {
+ Class<?> clazz = o.getClass();
+ if (clazz.isArray()) {
+ return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
+ } else if (Collection.class.isAssignableFrom(clazz)) {
+ Collection collection = (Collection) o;
+ for (Object value : collection) {
+ return value instanceof MultipartFile;
+ }
+ } else if (Map.class.isAssignableFrom(clazz)) {
+ Map map = (Map) o;
+ for (Object value : map.values()) {
+ return value instanceof MultipartFile;
+ }
+ }
+ return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
+ || o instanceof BindingResult;
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-log/src/main/java/org/dromara/common/log/enums/BusinessStatus.java b/ruoyi-common/ruoyi-common-log/src/main/java/org/dromara/common/log/enums/BusinessStatus.java
new file mode 100644
index 0000000..d303dc3
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-log/src/main/java/org/dromara/common/log/enums/BusinessStatus.java
@@ -0,0 +1,18 @@
+package org.dromara.common.log.enums;
+
+/**
+ * 鎿嶄綔鐘舵��
+ *
+ * @author ruoyi
+ */
+public enum BusinessStatus {
+ /**
+ * 鎴愬姛
+ */
+ SUCCESS,
+
+ /**
+ * 澶辫触
+ */
+ FAIL,
+}
diff --git a/ruoyi-common/ruoyi-common-log/src/main/java/org/dromara/common/log/enums/BusinessType.java b/ruoyi-common/ruoyi-common-log/src/main/java/org/dromara/common/log/enums/BusinessType.java
new file mode 100644
index 0000000..2d25ebb
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-log/src/main/java/org/dromara/common/log/enums/BusinessType.java
@@ -0,0 +1,58 @@
+package org.dromara.common.log.enums;
+
+/**
+ * 涓氬姟鎿嶄綔绫诲瀷
+ *
+ * @author ruoyi
+ */
+public enum BusinessType {
+ /**
+ * 鍏跺畠
+ */
+ OTHER,
+
+ /**
+ * 鏂板
+ */
+ INSERT,
+
+ /**
+ * 淇敼
+ */
+ UPDATE,
+
+ /**
+ * 鍒犻櫎
+ */
+ DELETE,
+
+ /**
+ * 鎺堟潈
+ */
+ GRANT,
+
+ /**
+ * 瀵煎嚭
+ */
+ EXPORT,
+
+ /**
+ * 瀵煎叆
+ */
+ IMPORT,
+
+ /**
+ * 寮洪��
+ */
+ FORCE,
+
+ /**
+ * 鐢熸垚浠g爜
+ */
+ GENCODE,
+
+ /**
+ * 娓呯┖鏁版嵁
+ */
+ CLEAN,
+}
diff --git a/ruoyi-common/ruoyi-common-log/src/main/java/org/dromara/common/log/enums/OperatorType.java b/ruoyi-common/ruoyi-common-log/src/main/java/org/dromara/common/log/enums/OperatorType.java
new file mode 100644
index 0000000..de9328b
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-log/src/main/java/org/dromara/common/log/enums/OperatorType.java
@@ -0,0 +1,23 @@
+package org.dromara.common.log.enums;
+
+/**
+ * 鎿嶄綔浜虹被鍒�
+ *
+ * @author ruoyi
+ */
+public enum OperatorType {
+ /**
+ * 鍏跺畠
+ */
+ OTHER,
+
+ /**
+ * 鍚庡彴鐢ㄦ埛
+ */
+ MANAGE,
+
+ /**
+ * 鎵嬫満绔敤鎴�
+ */
+ MOBILE
+}
diff --git a/ruoyi-common/ruoyi-common-log/src/main/java/org/dromara/common/log/event/LogEventListener.java b/ruoyi-common/ruoyi-common-log/src/main/java/org/dromara/common/log/event/LogEventListener.java
new file mode 100644
index 0000000..3697e58
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-log/src/main/java/org/dromara/common/log/event/LogEventListener.java
@@ -0,0 +1,103 @@
+package org.dromara.common.log.event;
+
+import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.http.useragent.UserAgent;
+import cn.hutool.http.useragent.UserAgentUtil;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.dubbo.config.annotation.DubboReference;
+import org.dromara.common.core.constant.Constants;
+import org.dromara.common.core.utils.ServletUtils;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.core.utils.ip.AddressUtils;
+import org.dromara.common.satoken.utils.LoginHelper;
+import org.dromara.system.api.RemoteClientService;
+import org.dromara.system.api.RemoteLogService;
+import org.dromara.system.api.domain.bo.RemoteLogininforBo;
+import org.dromara.system.api.domain.bo.RemoteOperLogBo;
+import org.dromara.system.api.domain.vo.RemoteClientVo;
+import org.springframework.context.event.EventListener;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Component;
+
+/**
+ * 寮傛璋冪敤鏃ュ織鏈嶅姟
+ *
+ * @author ruoyi
+ */
+@Component
+@Slf4j
+public class LogEventListener {
+
+ @DubboReference
+ private RemoteLogService remoteLogService;
+ @DubboReference
+ private RemoteClientService remoteClientService;
+
+ /**
+ * 淇濆瓨绯荤粺鏃ュ織璁板綍
+ */
+ @Async
+ @EventListener
+ public void saveLog(OperLogEvent operLogEvent) {
+ RemoteOperLogBo sysOperLog = BeanUtil.toBean(operLogEvent, RemoteOperLogBo.class);
+ remoteLogService.saveLog(sysOperLog);
+ }
+
+ @Async
+ @EventListener
+ public void saveLogininfor(LogininforEvent logininforEvent) {
+ HttpServletRequest request = logininforEvent.getRequest();
+ final UserAgent userAgent = UserAgentUtil.parse(request.getHeader("User-Agent"));
+ final String ip = ServletUtils.getClientIP(request);
+ // 瀹㈡埛绔俊鎭�
+ String clientid = request.getHeader(LoginHelper.CLIENT_KEY);
+ RemoteClientVo clientVo = null;
+ if (StringUtils.isNotBlank(clientid)) {
+ clientVo = remoteClientService.queryByClientId(clientid);
+ }
+
+ String address = AddressUtils.getRealAddressByIP(ip);
+ StringBuilder s = new StringBuilder();
+ s.append(getBlock(ip));
+ s.append(address);
+ s.append(getBlock(logininforEvent.getUsername()));
+ s.append(getBlock(logininforEvent.getStatus()));
+ s.append(getBlock(logininforEvent.getMessage()));
+ // 鎵撳嵃淇℃伅鍒版棩蹇�
+ log.info(s.toString(), logininforEvent.getArgs());
+ // 鑾峰彇瀹㈡埛绔搷浣滅郴缁�
+ String os = userAgent.getOs().getName();
+ // 鑾峰彇瀹㈡埛绔祻瑙堝櫒
+ String browser = userAgent.getBrowser().getName();
+ // 灏佽瀵硅薄
+ RemoteLogininforBo logininfor = new RemoteLogininforBo();
+ logininfor.setTenantId(logininforEvent.getTenantId());
+ logininfor.setUserName(logininforEvent.getUsername());
+ if (ObjectUtil.isNotNull(clientVo)) {
+ logininfor.setClientKey(clientVo.getClientKey());
+ logininfor.setDeviceType(clientVo.getDeviceType());
+ }
+ logininfor.setIpaddr(ip);
+ logininfor.setLoginLocation(address);
+ logininfor.setBrowser(browser);
+ logininfor.setOs(os);
+ logininfor.setMsg(logininforEvent.getMessage());
+ // 鏃ュ織鐘舵��
+ if (StringUtils.equalsAny(logininforEvent.getStatus(), Constants.LOGIN_SUCCESS, Constants.LOGOUT, Constants.REGISTER)) {
+ logininfor.setStatus(Constants.SUCCESS);
+ } else if (Constants.LOGIN_FAIL.equals(logininforEvent.getStatus())) {
+ logininfor.setStatus(Constants.FAIL);
+ }
+ remoteLogService.saveLogininfor(logininfor);
+ }
+
+ private String getBlock(Object msg) {
+ if (msg == null) {
+ msg = "";
+ }
+ return "[" + msg + "]";
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-log/src/main/java/org/dromara/common/log/event/LogininforEvent.java b/ruoyi-common/ruoyi-common-log/src/main/java/org/dromara/common/log/event/LogininforEvent.java
new file mode 100644
index 0000000..938eaad
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-log/src/main/java/org/dromara/common/log/event/LogininforEvent.java
@@ -0,0 +1,52 @@
+package org.dromara.common.log.event;
+
+import lombok.Data;
+
+import jakarta.servlet.http.HttpServletRequest;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 鐧诲綍浜嬩欢
+ *
+ * @author Lion Li
+ */
+
+@Data
+public class LogininforEvent implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 绉熸埛ID
+ */
+ private String tenantId;
+
+ /**
+ * 鐢ㄦ埛璐﹀彿
+ */
+ private String username;
+
+ /**
+ * 鐧诲綍鐘舵�� 0鎴愬姛 1澶辫触
+ */
+ private String status;
+
+ /**
+ * 鎻愮ず娑堟伅
+ */
+ private String message;
+
+ /**
+ * 璇锋眰浣�
+ */
+ private HttpServletRequest request;
+
+ /**
+ * 鍏朵粬鍙傛暟
+ */
+ private Object[] args;
+
+}
diff --git a/ruoyi-common/ruoyi-common-log/src/main/java/org/dromara/common/log/event/OperLogEvent.java b/ruoyi-common/ruoyi-common-log/src/main/java/org/dromara/common/log/event/OperLogEvent.java
new file mode 100644
index 0000000..0386192
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-log/src/main/java/org/dromara/common/log/event/OperLogEvent.java
@@ -0,0 +1,115 @@
+package org.dromara.common.log.event;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 鎿嶄綔鏃ュ織浜嬩欢
+ *
+ * @author Lion Li
+ */
+
+@Data
+public class OperLogEvent implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 鏃ュ織涓婚敭
+ */
+ private Long operId;
+
+ /**
+ * 绉熸埛ID
+ */
+ private String tenantId;
+
+ /**
+ * 鎿嶄綔妯″潡
+ */
+ private String title;
+
+ /**
+ * 涓氬姟绫诲瀷锛�0鍏跺畠 1鏂板 2淇敼 3鍒犻櫎锛�
+ */
+ private Integer businessType;
+
+ /**
+ * 涓氬姟绫诲瀷鏁扮粍
+ */
+ private Integer[] businessTypes;
+
+ /**
+ * 璇锋眰鏂规硶
+ */
+ private String method;
+
+ /**
+ * 璇锋眰鏂瑰紡
+ */
+ private String requestMethod;
+
+ /**
+ * 鎿嶄綔绫诲埆锛�0鍏跺畠 1鍚庡彴鐢ㄦ埛 2鎵嬫満绔敤鎴凤級
+ */
+ private Integer operatorType;
+
+ /**
+ * 鎿嶄綔浜哄憳
+ */
+ private String operName;
+
+ /**
+ * 閮ㄩ棬鍚嶇О
+ */
+ private String deptName;
+
+ /**
+ * 璇锋眰url
+ */
+ private String operUrl;
+
+ /**
+ * 鎿嶄綔鍦板潃
+ */
+ private String operIp;
+
+ /**
+ * 鎿嶄綔鍦扮偣
+ */
+ private String operLocation;
+
+ /**
+ * 璇锋眰鍙傛暟
+ */
+ private String operParam;
+
+ /**
+ * 杩斿洖鍙傛暟
+ */
+ private String jsonResult;
+
+ /**
+ * 鎿嶄綔鐘舵�侊紙0姝e父 1寮傚父锛�
+ */
+ private Integer status;
+
+ /**
+ * 閿欒娑堟伅
+ */
+ private String errorMsg;
+
+ /**
+ * 鎿嶄綔鏃堕棿
+ */
+ private Date operTime;
+
+ /**
+ * 娑堣�楁椂闂�
+ */
+ private Long costTime;
+}
diff --git a/ruoyi-common/ruoyi-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/ruoyi-common/ruoyi-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..42b8812
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1,2 @@
+org.dromara.common.log.event.LogEventListener
+org.dromara.common.log.aspect.LogAspect
diff --git a/ruoyi-common/ruoyi-common-logstash/pom.xml b/ruoyi-common/ruoyi-common-logstash/pom.xml
new file mode 100644
index 0000000..36ad275
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-logstash/pom.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns="http://maven.apache.org/POM/4.0.0"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common</artifactId>
+ <version>${revision}</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>ruoyi-common-logstash</artifactId>
+
+ <description>
+ ruoyi-common-logstash logstash鏃ュ織鎺ㄩ�佹ā鍧�
+ </description>
+
+ <dependencies>
+ <dependency>
+ <groupId>net.logstash.logback</groupId>
+ <artifactId>logstash-logback-encoder</artifactId>
+ </dependency>
+ </dependencies>
+</project>
diff --git a/ruoyi-common/ruoyi-common-logstash/src/main/resources/logback-logstash.xml b/ruoyi-common/ruoyi-common-logstash/src/main/resources/logback-logstash.xml
new file mode 100644
index 0000000..d25f752
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-logstash/src/main/resources/logback-logstash.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<included>
+
+ <springProperty scope="context" name="appName" source="spring.application.name"/>
+
+ <!--杈撳嚭鍒發ogstash鐨刟ppender-->
+ <appender name="logstash" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
+ <!--鍙互璁块棶鐨刲ogstash鏃ュ織鏀堕泦绔彛-->
+ <destination>${logstash.address}</destination>
+ <encoder charset="UTF-8" class="net.logstash.logback.encoder.LogstashEncoder">
+ <customFields>{"spring.application.name":"${appName}"}</customFields>
+ </encoder>
+ </appender>
+
+ <root level="info">
+ <appender-ref ref="logstash"/>
+ </root>
+</included>
diff --git a/ruoyi-common/ruoyi-common-mail/pom.xml b/ruoyi-common/ruoyi-common-mail/pom.xml
new file mode 100644
index 0000000..c0e1b2e
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-mail/pom.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common</artifactId>
+ <version>${revision}</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>ruoyi-common-mail</artifactId>
+
+ <description>
+ ruoyi-common-mail 閭欢妯″潡
+ </description>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-core</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>jakarta.mail</groupId>
+ <artifactId>jakarta.mail-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.angus</groupId>
+ <artifactId>jakarta.mail</artifactId>
+ </dependency>
+ </dependencies>
+
+</project>
diff --git a/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/config/MailConfig.java b/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/config/MailConfig.java
new file mode 100644
index 0000000..1b51c27
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/config/MailConfig.java
@@ -0,0 +1,37 @@
+package org.dromara.common.mail.config;
+
+import org.dromara.common.mail.config.properties.MailProperties;
+import org.dromara.common.mail.utils.MailAccount;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+
+/**
+ * JavaMail 閰嶇疆
+ *
+ * @author Michelle.Chung
+ */
+@AutoConfiguration
+@EnableConfigurationProperties(MailProperties.class)
+public class MailConfig {
+
+ @Bean
+ @ConditionalOnProperty(value = "mail.enabled", havingValue = "true")
+ public MailAccount mailAccount(MailProperties mailProperties) {
+ MailAccount account = new MailAccount();
+ account.setHost(mailProperties.getHost());
+ account.setPort(mailProperties.getPort());
+ account.setAuth(mailProperties.getAuth());
+ account.setFrom(mailProperties.getFrom());
+ account.setUser(mailProperties.getUser());
+ account.setPass(mailProperties.getPass());
+ account.setSocketFactoryPort(mailProperties.getPort());
+ account.setStarttlsEnable(mailProperties.getStarttlsEnable());
+ account.setSslEnable(mailProperties.getSslEnable());
+ account.setTimeout(mailProperties.getTimeout());
+ account.setConnectionTimeout(mailProperties.getConnectionTimeout());
+ return account;
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/config/MailConfiguration.java b/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/config/MailConfiguration.java
new file mode 100644
index 0000000..6410122
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/config/MailConfiguration.java
@@ -0,0 +1,37 @@
+package org.dromara.common.mail.config;
+
+import org.dromara.common.mail.utils.MailAccount;
+import org.dromara.common.mail.config.properties.MailProperties;
+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;
+
+/**
+ * JavaMail 閰嶇疆
+ *
+ * @author Michelle.Chung
+ */
+@AutoConfiguration
+@EnableConfigurationProperties(MailProperties.class)
+public class MailConfiguration {
+
+ @Bean
+ @ConditionalOnProperty(value = "mail.enabled", havingValue = "true")
+ public MailAccount mailAccount(MailProperties mailProperties) {
+ MailAccount account = new MailAccount();
+ account.setHost(mailProperties.getHost());
+ account.setPort(mailProperties.getPort());
+ account.setAuth(mailProperties.getAuth());
+ account.setFrom(mailProperties.getFrom());
+ account.setUser(mailProperties.getUser());
+ account.setPass(mailProperties.getPass());
+ account.setSocketFactoryPort(mailProperties.getPort());
+ account.setStarttlsEnable(mailProperties.getStarttlsEnable());
+ account.setSslEnable(mailProperties.getSslEnable());
+ account.setTimeout(mailProperties.getTimeout());
+ account.setConnectionTimeout(mailProperties.getConnectionTimeout());
+ return account;
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/config/properties/MailProperties.java b/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/config/properties/MailProperties.java
new file mode 100644
index 0000000..d0e78a2
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/config/properties/MailProperties.java
@@ -0,0 +1,69 @@
+package org.dromara.common.mail.config.properties;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * JavaMail 閰嶇疆灞炴��
+ *
+ * @author Michelle.Chung
+ */
+@Data
+@ConfigurationProperties(prefix = "mail")
+public class MailProperties {
+
+ /**
+ * 杩囨护寮�鍏�
+ */
+ private Boolean enabled;
+
+ /**
+ * SMTP鏈嶅姟鍣ㄥ煙鍚�
+ */
+ private String host;
+
+ /**
+ * SMTP鏈嶅姟绔彛
+ */
+ private Integer port;
+
+ /**
+ * 鏄惁闇�瑕佺敤鎴峰悕瀵嗙爜楠岃瘉
+ */
+ private Boolean auth;
+
+ /**
+ * 鐢ㄦ埛鍚�
+ */
+ private String user;
+
+ /**
+ * 瀵嗙爜
+ */
+ private String pass;
+
+ /**
+ * 鍙戦�佹柟锛岄伒寰猂FC-822鏍囧噯
+ */
+ private String from;
+
+ /**
+ * 浣跨敤 STARTTLS瀹夊叏杩炴帴锛孲TARTTLS鏄绾枃鏈�氫俊鍗忚鐨勬墿灞曘�傚畠灏嗙函鏂囨湰杩炴帴鍗囩骇涓哄姞瀵嗚繛鎺ワ紙TLS鎴朣SL锛夛紝 鑰屼笉鏄娇鐢ㄤ竴涓崟鐙殑鍔犲瘑閫氫俊绔彛銆�
+ */
+ private Boolean starttlsEnable;
+
+ /**
+ * 浣跨敤 SSL瀹夊叏杩炴帴
+ */
+ private Boolean sslEnable;
+
+ /**
+ * SMTP瓒呮椂鏃堕暱锛屽崟浣嶆绉掞紝缂虹渷鍊间笉瓒呮椂
+ */
+ private Long timeout;
+
+ /**
+ * Socket杩炴帴瓒呮椂鍊硷紝鍗曚綅姣锛岀己鐪佸�间笉瓒呮椂
+ */
+ private Long connectionTimeout;
+}
diff --git a/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/GlobalMailAccount.java b/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/GlobalMailAccount.java
new file mode 100644
index 0000000..fdae869
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/GlobalMailAccount.java
@@ -0,0 +1,46 @@
+package org.dromara.common.mail.utils;
+
+import cn.hutool.core.io.IORuntimeException;
+
+/**
+ * 鍏ㄥ眬閭欢甯愭埛锛屼緷璧栦簬閭欢閰嶇疆鏂囦欢{@link MailAccount#MAIL_SETTING_PATHS}
+ *
+ * @author looly
+ */
+public enum GlobalMailAccount {
+ INSTANCE;
+
+ private final MailAccount mailAccount;
+
+ /**
+ * 鏋勯��
+ */
+ GlobalMailAccount() {
+ mailAccount = createDefaultAccount();
+ }
+
+ /**
+ * 鑾峰緱閭欢甯愭埛
+ *
+ * @return 閭欢甯愭埛
+ */
+ public MailAccount getAccount() {
+ return this.mailAccount;
+ }
+
+ /**
+ * 鍒涘缓榛樿甯愭埛
+ *
+ * @return MailAccount
+ */
+ private MailAccount createDefaultAccount() {
+ for (String mailSettingPath : MailAccount.MAIL_SETTING_PATHS) {
+ try {
+ return new MailAccount(mailSettingPath);
+ } catch (IORuntimeException ignore) {
+ //ignore
+ }
+ }
+ return null;
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/InternalMailUtil.java b/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/InternalMailUtil.java
new file mode 100644
index 0000000..b755e73
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/InternalMailUtil.java
@@ -0,0 +1,108 @@
+package org.dromara.common.mail.utils;
+
+import cn.hutool.core.util.ArrayUtil;
+import jakarta.mail.internet.AddressException;
+import jakarta.mail.internet.InternetAddress;
+import jakarta.mail.internet.MimeUtility;
+
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * 閭欢鍐呴儴宸ュ叿绫�
+ *
+ * @author looly
+ * @since 3.2.3
+ */
+public class InternalMailUtil {
+
+ /**
+ * 灏嗗涓瓧绗︿覆閭欢鍦板潃杞负{@link InternetAddress}鍒楄〃<br>
+ * 鍗曚釜瀛楃涓插湴鍧�鍙互鏄涓湴鍧�鍚堝苟鐨勫瓧绗︿覆
+ *
+ * @param addrStrs 鍦板潃鏁扮粍
+ * @param charset 缂栫爜锛堜富瑕佺敤浜庝腑鏂囩敤鎴峰悕鐨勭紪鐮侊級
+ * @return 鍦板潃鏁扮粍
+ * @since 4.0.3
+ */
+ public static InternetAddress[] parseAddressFromStrs(String[] addrStrs, Charset charset) {
+ final List<InternetAddress> resultList = new ArrayList<>(addrStrs.length);
+ InternetAddress[] addrs;
+ for (String addrStr : addrStrs) {
+ addrs = parseAddress(addrStr, charset);
+ if (ArrayUtil.isNotEmpty(addrs)) {
+ Collections.addAll(resultList, addrs);
+ }
+ }
+ return resultList.toArray(new InternetAddress[0]);
+ }
+
+ /**
+ * 瑙f瀽绗竴涓湴鍧�
+ *
+ * @param address 鍦板潃瀛楃涓�
+ * @param charset 缂栫爜锛寋@code null}琛ㄧず浣跨敤绯荤粺灞炴�у畾涔夌殑缂栫爜鎴栫郴缁熺紪鐮�
+ * @return 鍦板潃鍒楄〃
+ */
+ public static InternetAddress parseFirstAddress(String address, Charset charset) {
+ final InternetAddress[] internetAddresses = parseAddress(address, charset);
+ if (ArrayUtil.isEmpty(internetAddresses)) {
+ try {
+ return new InternetAddress(address);
+ } catch (AddressException e) {
+ throw new MailException(e);
+ }
+ }
+ return internetAddresses[0];
+ }
+
+ /**
+ * 灏嗕竴涓湴鍧�瀛楃涓茶В鏋愪负澶氫釜鍦板潃<br>
+ * 鍦板潃闂翠娇鐢�" "銆�","銆�";"鍒嗛殧
+ *
+ * @param address 鍦板潃瀛楃涓�
+ * @param charset 缂栫爜锛寋@code null}琛ㄧず浣跨敤绯荤粺灞炴�у畾涔夌殑缂栫爜鎴栫郴缁熺紪鐮�
+ * @return 鍦板潃鍒楄〃
+ */
+ public static InternetAddress[] parseAddress(String address, Charset charset) {
+ InternetAddress[] addresses;
+ try {
+ addresses = InternetAddress.parse(address);
+ } catch (AddressException e) {
+ throw new MailException(e);
+ }
+ //缂栫爜鐢ㄦ埛鍚�
+ if (ArrayUtil.isNotEmpty(addresses)) {
+ final String charsetStr = null == charset ? null : charset.name();
+ for (InternetAddress internetAddress : addresses) {
+ try {
+ internetAddress.setPersonal(internetAddress.getPersonal(), charsetStr);
+ } catch (UnsupportedEncodingException e) {
+ throw new MailException(e);
+ }
+ }
+ }
+
+ return addresses;
+ }
+
+ /**
+ * 缂栫爜涓枃瀛楃<br>
+ * 缂栫爜澶辫触杩斿洖鍘熷瓧绗︿覆
+ *
+ * @param text 琚紪鐮佺殑鏂囨湰
+ * @param charset 缂栫爜
+ * @return 缂栫爜鍚庣殑缁撴灉
+ */
+ public static String encodeText(String text, Charset charset) {
+ try {
+ return MimeUtility.encodeText(text, charset.name(), null);
+ } catch (UnsupportedEncodingException e) {
+ // ignore
+ }
+ return text;
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/Mail.java b/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/Mail.java
new file mode 100644
index 0000000..6ca4b69
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/Mail.java
@@ -0,0 +1,483 @@
+package org.dromara.common.mail.utils;
+
+import cn.hutool.core.builder.Builder;
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.io.IORuntimeException;
+import cn.hutool.core.io.IoUtil;
+import cn.hutool.core.util.ArrayUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
+import jakarta.activation.DataHandler;
+import jakarta.activation.DataSource;
+import jakarta.activation.FileDataSource;
+import jakarta.activation.FileTypeMap;
+import jakarta.mail.*;
+import jakarta.mail.internet.MimeBodyPart;
+import jakarta.mail.internet.MimeMessage;
+import jakarta.mail.internet.MimeMultipart;
+import jakarta.mail.internet.MimeUtility;
+import jakarta.mail.util.ByteArrayDataSource;
+
+import java.io.*;
+import java.nio.charset.Charset;
+import java.util.Date;
+
+/**
+ * 閭欢鍙戦�佸鎴风
+ *
+ * @author looly
+ * @since 3.2.0
+ */
+public class Mail implements Builder<MimeMessage> {
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 閭甯愭埛淇℃伅浠ュ強涓�浜涘鎴风閰嶇疆淇℃伅
+ */
+ private final MailAccount mailAccount;
+ /**
+ * 鏀朵欢浜哄垪琛�
+ */
+ private String[] tos;
+ /**
+ * 鎶勯�佷汉鍒楄〃锛坈arbon copy锛�
+ */
+ private String[] ccs;
+ /**
+ * 瀵嗛�佷汉鍒楄〃锛坆lind carbon copy锛�
+ */
+ private String[] bccs;
+ /**
+ * 鍥炲鍦板潃(reply-to)
+ */
+ private String[] reply;
+ /**
+ * 鏍囬
+ */
+ private String title;
+ /**
+ * 鍐呭
+ */
+ private String content;
+ /**
+ * 鏄惁涓篐TML
+ */
+ private boolean isHtml;
+ /**
+ * 姝f枃銆侀檮浠跺拰鍥剧墖鐨勬贩鍚堥儴鍒�
+ */
+ private final Multipart multipart = new MimeMultipart();
+ /**
+ * 鏄惁浣跨敤鍏ㄥ眬浼氳瘽锛岄粯璁や负false
+ */
+ private boolean useGlobalSession = false;
+
+ /**
+ * debug杈撳嚭浣嶇疆锛屽彲浠ヨ嚜瀹氫箟debug鏃ュ織
+ */
+ private PrintStream debugOutput;
+
+ /**
+ * 鍒涘缓閭欢瀹㈡埛绔�
+ *
+ * @param mailAccount 閭欢甯愬彿
+ * @return Mail
+ */
+ public static Mail create(MailAccount mailAccount) {
+ return new Mail(mailAccount);
+ }
+
+ /**
+ * 鍒涘缓閭欢瀹㈡埛绔紝浣跨敤鍏ㄥ眬閭欢甯愭埛
+ *
+ * @return Mail
+ */
+ public static Mail create() {
+ return new Mail();
+ }
+
+ // --------------------------------------------------------------- Constructor start
+
+ /**
+ * 鏋勯�狅紝浣跨敤鍏ㄥ眬閭欢甯愭埛
+ */
+ public Mail() {
+ this(GlobalMailAccount.INSTANCE.getAccount());
+ }
+
+ /**
+ * 鏋勯��
+ *
+ * @param mailAccount 閭欢甯愭埛锛屽鏋滀负null浣跨敤榛樿閰嶇疆鏂囦欢鐨勫叏灞�閭欢閰嶇疆
+ */
+ public Mail(MailAccount mailAccount) {
+ mailAccount = (null != mailAccount) ? mailAccount : GlobalMailAccount.INSTANCE.getAccount();
+ this.mailAccount = mailAccount.defaultIfEmpty();
+ }
+ // --------------------------------------------------------------- Constructor end
+
+ // --------------------------------------------------------------- Getters and Setters start
+
+ /**
+ * 璁剧疆鏀朵欢浜�
+ *
+ * @param tos 鏀朵欢浜哄垪琛�
+ * @return this
+ * @see #setTos(String...)
+ */
+ public Mail to(String... tos) {
+ return setTos(tos);
+ }
+
+ /**
+ * 璁剧疆澶氫釜鏀朵欢浜�
+ *
+ * @param tos 鏀朵欢浜哄垪琛�
+ * @return this
+ */
+ public Mail setTos(String... tos) {
+ this.tos = tos;
+ return this;
+ }
+
+ /**
+ * 璁剧疆澶氫釜鎶勯�佷汉锛坈arbon copy锛�
+ *
+ * @param ccs 鎶勯�佷汉鍒楄〃
+ * @return this
+ * @since 4.0.3
+ */
+ public Mail setCcs(String... ccs) {
+ this.ccs = ccs;
+ return this;
+ }
+
+ /**
+ * 璁剧疆澶氫釜瀵嗛�佷汉锛坆lind carbon copy锛�
+ *
+ * @param bccs 瀵嗛�佷汉鍒楄〃
+ * @return this
+ * @since 4.0.3
+ */
+ public Mail setBccs(String... bccs) {
+ this.bccs = bccs;
+ return this;
+ }
+
+ /**
+ * 璁剧疆澶氫釜鍥炲鍦板潃(reply-to)
+ *
+ * @param reply 鍥炲鍦板潃(reply-to)鍒楄〃
+ * @return this
+ * @since 4.6.0
+ */
+ public Mail setReply(String... reply) {
+ this.reply = reply;
+ return this;
+ }
+
+ /**
+ * 璁剧疆鏍囬
+ *
+ * @param title 鏍囬
+ * @return this
+ */
+ public Mail setTitle(String title) {
+ this.title = title;
+ return this;
+ }
+
+ /**
+ * 璁剧疆姝f枃<br>
+ * 姝f枃鍙互鏄櫘閫氭枃鏈篃鍙互鏄疕TML锛堥粯璁ゆ櫘閫氭枃鏈級锛屽彲浠ラ�氳繃璋冪敤{@link #setHtml(boolean)} 璁剧疆鏄惁涓篐TML
+ *
+ * @param content 姝f枃
+ * @return this
+ */
+ public Mail setContent(String content) {
+ this.content = content;
+ return this;
+ }
+
+ /**
+ * 璁剧疆鏄惁鏄疕TML
+ *
+ * @param isHtml 鏄惁涓篐TML
+ * @return this
+ */
+ public Mail setHtml(boolean isHtml) {
+ this.isHtml = isHtml;
+ return this;
+ }
+
+ /**
+ * 璁剧疆姝f枃
+ *
+ * @param content 姝f枃鍐呭
+ * @param isHtml 鏄惁涓篐TML
+ * @return this
+ */
+ public Mail setContent(String content, boolean isHtml) {
+ setContent(content);
+ return setHtml(isHtml);
+ }
+
+ /**
+ * 璁剧疆鏂囦欢绫诲瀷闄勪欢锛屾枃浠跺彲浠ユ槸鍥剧墖鏂囦欢锛屾鏃惰嚜鍔ㄨ缃甤id锛堟鏂囦腑寮曠敤鍥剧墖锛夛紝榛樿cid涓烘枃浠跺悕
+ *
+ * @param files 闄勪欢鏂囦欢鍒楄〃
+ * @return this
+ */
+ public Mail setFiles(File... files) {
+ if (ArrayUtil.isEmpty(files)) {
+ return this;
+ }
+
+ final DataSource[] attachments = new DataSource[files.length];
+ for (int i = 0; i < files.length; i++) {
+ attachments[i] = new FileDataSource(files[i]);
+ }
+ return setAttachments(attachments);
+ }
+
+ /**
+ * 澧炲姞闄勪欢鎴栧浘鐗囷紝闄勪欢浣跨敤{@link DataSource} 褰㈠紡琛ㄧず锛屽彲浠ヤ娇鐢▄@link FileDataSource}鍖呰鏂囦欢琛ㄧず鏂囦欢闄勪欢
+ *
+ * @param attachments 闄勪欢鍒楄〃
+ * @return this
+ * @since 4.0.9
+ */
+ public Mail setAttachments(DataSource... attachments) {
+ if (ArrayUtil.isNotEmpty(attachments)) {
+ final Charset charset = this.mailAccount.getCharset();
+ MimeBodyPart bodyPart;
+ String nameEncoded;
+ try {
+ for (DataSource attachment : attachments) {
+ bodyPart = new MimeBodyPart();
+ bodyPart.setDataHandler(new DataHandler(attachment));
+ nameEncoded = attachment.getName();
+ if (this.mailAccount.isEncodefilename()) {
+ nameEncoded = InternalMailUtil.encodeText(nameEncoded, charset);
+ }
+ // 鏅�氶檮浠舵枃浠跺悕
+ bodyPart.setFileName(nameEncoded);
+ if (StrUtil.startWith(attachment.getContentType(), "image/")) {
+ // 鍥剧墖闄勪欢锛岀敤浜庢鏂囦腑寮曠敤鍥剧墖
+ bodyPart.setContentID(nameEncoded);
+ }
+ this.multipart.addBodyPart(bodyPart);
+ }
+ } catch (MessagingException e) {
+ throw new MailException(e);
+ }
+ }
+ return this;
+ }
+
+ /**
+ * 澧炲姞鍥剧墖锛屽浘鐗囩殑閿搴斿埌閭欢妯℃澘涓殑鍗犱綅瀛楃涓诧紝鍥剧墖绫诲瀷榛樿涓�"image/jpeg"
+ *
+ * @param cid 鍥剧墖涓庡崰浣嶇锛屽崰浣嶇鏍煎紡涓篶id:${cid}
+ * @param imageStream 鍥剧墖鏂囦欢
+ * @return this
+ * @since 4.6.3
+ */
+ public Mail addImage(String cid, InputStream imageStream) {
+ return addImage(cid, imageStream, null);
+ }
+
+ /**
+ * 澧炲姞鍥剧墖锛屽浘鐗囩殑閿搴斿埌閭欢妯℃澘涓殑鍗犱綅瀛楃涓�
+ *
+ * @param cid 鍥剧墖涓庡崰浣嶇锛屽崰浣嶇鏍煎紡涓篶id:${cid}
+ * @param imageStream 鍥剧墖娴侊紝涓嶅叧闂�
+ * @param contentType 鍥剧墖绫诲瀷锛宯ull璧嬪�奸粯璁ょ殑"image/jpeg"
+ * @return this
+ * @since 4.6.3
+ */
+ public Mail addImage(String cid, InputStream imageStream, String contentType) {
+ ByteArrayDataSource imgSource;
+ try {
+ imgSource = new ByteArrayDataSource(imageStream, ObjectUtil.defaultIfNull(contentType, "image/jpeg"));
+ } catch (IOException e) {
+ throw new IORuntimeException(e);
+ }
+ imgSource.setName(cid);
+ return setAttachments(imgSource);
+ }
+
+ /**
+ * 澧炲姞鍥剧墖锛屽浘鐗囩殑閿搴斿埌閭欢妯℃澘涓殑鍗犱綅瀛楃涓�
+ *
+ * @param cid 鍥剧墖涓庡崰浣嶇锛屽崰浣嶇鏍煎紡涓篶id:${cid}
+ * @param imageFile 鍥剧墖鏂囦欢
+ * @return this
+ * @since 4.6.3
+ */
+ public Mail addImage(String cid, File imageFile) {
+ InputStream in = null;
+ try {
+ in = FileUtil.getInputStream(imageFile);
+ return addImage(cid, in, FileTypeMap.getDefaultFileTypeMap().getContentType(imageFile));
+ } finally {
+ IoUtil.close(in);
+ }
+ }
+
+ /**
+ * 璁剧疆瀛楃闆嗙紪鐮�
+ *
+ * @param charset 瀛楃闆嗙紪鐮�
+ * @return this
+ * @see MailAccount#setCharset(Charset)
+ */
+ public Mail setCharset(Charset charset) {
+ this.mailAccount.setCharset(charset);
+ return this;
+ }
+
+ /**
+ * 璁剧疆鏄惁浣跨敤鍏ㄥ眬浼氳瘽锛岄粯璁や负true
+ *
+ * @param isUseGlobalSession 鏄惁浣跨敤鍏ㄥ眬浼氳瘽锛岄粯璁や负true
+ * @return this
+ * @since 4.0.2
+ */
+ public Mail setUseGlobalSession(boolean isUseGlobalSession) {
+ this.useGlobalSession = isUseGlobalSession;
+ return this;
+ }
+
+ /**
+ * 璁剧疆debug杈撳嚭浣嶇疆锛屽彲浠ヨ嚜瀹氫箟debug鏃ュ織
+ *
+ * @param debugOutput debug杈撳嚭浣嶇疆
+ * @return this
+ * @since 5.5.6
+ */
+ public Mail setDebugOutput(PrintStream debugOutput) {
+ this.debugOutput = debugOutput;
+ return this;
+ }
+ // --------------------------------------------------------------- Getters and Setters end
+
+ @Override
+ public MimeMessage build() {
+ try {
+ return buildMsg();
+ } catch (MessagingException e) {
+ throw new MailException(e);
+ }
+ }
+
+ /**
+ * 鍙戦��
+ *
+ * @return message-id
+ * @throws MailException 閭欢鍙戦�佸紓甯�
+ */
+ public String send() throws MailException {
+ try {
+ return doSend();
+ } catch (MessagingException e) {
+ if (e instanceof SendFailedException) {
+ // 褰撳湴鍧�鏃犳晥鏃讹紝鏄剧ず鏇村姞璇︾粏鐨勬棤鏁堝湴鍧�淇℃伅
+ final Address[] invalidAddresses = ((SendFailedException) e).getInvalidAddresses();
+ final String msg = StrUtil.format("Invalid Addresses: {}", ArrayUtil.toString(invalidAddresses));
+ throw new MailException(msg, e);
+ }
+ throw new MailException(e);
+ }
+ }
+
+ // --------------------------------------------------------------- Private method start
+
+ /**
+ * 鎵ц鍙戦��
+ *
+ * @return message-id
+ * @throws MessagingException 鍙戦�佸紓甯�
+ */
+ private String doSend() throws MessagingException {
+ final MimeMessage mimeMessage = buildMsg();
+ Transport.send(mimeMessage);
+ return mimeMessage.getMessageID();
+ }
+
+ /**
+ * 鏋勫缓娑堟伅
+ *
+ * @return {@link MimeMessage}娑堟伅
+ * @throws MessagingException 娑堟伅寮傚父
+ */
+ private MimeMessage buildMsg() throws MessagingException {
+ final Charset charset = this.mailAccount.getCharset();
+ final MimeMessage msg = new MimeMessage(getSession());
+ // 鍙戜欢浜�
+ final String from = this.mailAccount.getFrom();
+ if (StrUtil.isEmpty(from)) {
+ // 鐢ㄦ埛鏈彁渚涘彂閫佹柟锛屽垯浠嶴ession涓嚜鍔ㄨ幏鍙�
+ msg.setFrom();
+ } else {
+ msg.setFrom(InternalMailUtil.parseFirstAddress(from, charset));
+ }
+ // 鏍囬
+ msg.setSubject(this.title, (null == charset) ? null : charset.name());
+ // 鍙戦�佹椂闂�
+ msg.setSentDate(new Date());
+ // 鍐呭鍜岄檮浠�
+ msg.setContent(buildContent(charset));
+ // 鏀朵欢浜�
+ msg.setRecipients(MimeMessage.RecipientType.TO, InternalMailUtil.parseAddressFromStrs(this.tos, charset));
+ // 鎶勯�佷汉
+ if (ArrayUtil.isNotEmpty(this.ccs)) {
+ msg.setRecipients(MimeMessage.RecipientType.CC, InternalMailUtil.parseAddressFromStrs(this.ccs, charset));
+ }
+ // 瀵嗛�佷汉
+ if (ArrayUtil.isNotEmpty(this.bccs)) {
+ msg.setRecipients(MimeMessage.RecipientType.BCC, InternalMailUtil.parseAddressFromStrs(this.bccs, charset));
+ }
+ // 鍥炲鍦板潃(reply-to)
+ if (ArrayUtil.isNotEmpty(this.reply)) {
+ msg.setReplyTo(InternalMailUtil.parseAddressFromStrs(this.reply, charset));
+ }
+
+ return msg;
+ }
+
+ /**
+ * 鏋勫缓閭欢淇℃伅涓讳綋
+ *
+ * @param charset 缂栫爜锛寋@code null}鍒欎娇鐢▄@link MimeUtility#getDefaultJavaCharset()}
+ * @return 閭欢淇℃伅涓讳綋
+ * @throws MessagingException 娑堟伅寮傚父
+ */
+ private Multipart buildContent(Charset charset) throws MessagingException {
+ final String charsetStr = null != charset ? charset.name() : MimeUtility.getDefaultJavaCharset();
+ // 姝f枃
+ final MimeBodyPart body = new MimeBodyPart();
+ body.setContent(content, StrUtil.format("text/{}; charset={}", isHtml ? "html" : "plain", charsetStr));
+ this.multipart.addBodyPart(body);
+
+ return this.multipart;
+ }
+
+ /**
+ * 鑾峰彇榛樿閭欢浼氳瘽<br>
+ * 濡傛灉涓哄叏灞�鍗曚緥鐨勪細璇濓紝鍒欏叏灞�鍙厑璁镐竴涓偖浠跺笎鍙凤紝鍚﹀垯姣忔鍙戦�侀偖浠朵細鏂板缓涓�涓柊鐨勪細璇�
+ *
+ * @return 閭欢浼氳瘽 {@link Session}
+ */
+ private Session getSession() {
+ final Session session = MailUtils.getSession(this.mailAccount, this.useGlobalSession);
+
+ if (null != this.debugOutput) {
+ session.setDebugOut(debugOutput);
+ }
+
+ return session;
+ }
+ // --------------------------------------------------------------- Private method end
+}
diff --git a/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/MailAccount.java b/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/MailAccount.java
new file mode 100644
index 0000000..2a732a1
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/MailAccount.java
@@ -0,0 +1,659 @@
+package org.dromara.common.mail.utils;
+
+import cn.hutool.core.util.CharsetUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.setting.Setting;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.nio.charset.Charset;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+
+/**
+ * 閭欢璐︽埛瀵硅薄
+ *
+ * @author Luxiaolei
+ */
+public class MailAccount implements Serializable {
+ @Serial
+ private static final long serialVersionUID = -6937313421815719204L;
+
+ private static final String MAIL_PROTOCOL = "mail.transport.protocol";
+ private static final String SMTP_HOST = "mail.smtp.host";
+ private static final String SMTP_PORT = "mail.smtp.port";
+ private static final String SMTP_AUTH = "mail.smtp.auth";
+ private static final String SMTP_TIMEOUT = "mail.smtp.timeout";
+ private static final String SMTP_CONNECTION_TIMEOUT = "mail.smtp.connectiontimeout";
+ private static final String SMTP_WRITE_TIMEOUT = "mail.smtp.writetimeout";
+
+ // SSL
+ private static final String STARTTLS_ENABLE = "mail.smtp.starttls.enable";
+ private static final String SSL_ENABLE = "mail.smtp.ssl.enable";
+ private static final String SSL_PROTOCOLS = "mail.smtp.ssl.protocols";
+ private static final String SOCKET_FACTORY = "mail.smtp.socketFactory.class";
+ private static final String SOCKET_FACTORY_FALLBACK = "mail.smtp.socketFactory.fallback";
+ private static final String SOCKET_FACTORY_PORT = "smtp.socketFactory.port";
+
+ // System Properties
+ private static final String SPLIT_LONG_PARAMS = "mail.mime.splitlongparameters";
+ //private static final String ENCODE_FILE_NAME = "mail.mime.encodefilename";
+ //private static final String CHARSET = "mail.mime.charset";
+
+ // 鍏朵粬
+ private static final String MAIL_DEBUG = "mail.debug";
+
+ public static final String[] MAIL_SETTING_PATHS = new String[]{"config/mail.setting", "config/mailAccount.setting", "mail.setting"};
+
+ /**
+ * SMTP鏈嶅姟鍣ㄥ煙鍚�
+ */
+ private String host;
+ /**
+ * SMTP鏈嶅姟绔彛
+ */
+ private Integer port;
+ /**
+ * 鏄惁闇�瑕佺敤鎴峰悕瀵嗙爜楠岃瘉
+ */
+ private Boolean auth;
+ /**
+ * 鐢ㄦ埛鍚�
+ */
+ private String user;
+ /**
+ * 瀵嗙爜
+ */
+ private String pass;
+ /**
+ * 鍙戦�佹柟锛岄伒寰猂FC-822鏍囧噯
+ */
+ private String from;
+
+ /**
+ * 鏄惁鎵撳紑璋冭瘯妯″紡锛岃皟璇曟ā寮忎細鏄剧ず涓庨偖浠舵湇鍔″櫒閫氫俊杩囩▼锛岄粯璁や笉寮�鍚�
+ */
+ private boolean debug;
+ /**
+ * 缂栫爜鐢ㄤ簬缂栫爜閭欢姝f枃鍜屽彂閫佷汉銆佹敹浠朵汉绛変腑鏂�
+ */
+ private Charset charset = CharsetUtil.CHARSET_UTF_8;
+ /**
+ * 瀵逛簬瓒呴暱鍙傛暟鏄惁鍒囧垎涓哄浠斤紝榛樿涓篺alse锛堝浗鍐呴偖绠遍檮浠朵笉鏀寔鍒囧垎鐨勯檮浠跺悕锛�
+ */
+ private boolean splitlongparameters = false;
+ /**
+ * 瀵逛簬鏂囦欢鍚嶆槸鍚︿娇鐢▄@link #charset}缂栫爜锛岄粯璁や负 {@code true}
+ */
+ private boolean encodefilename = true;
+
+ /**
+ * 浣跨敤 STARTTLS瀹夊叏杩炴帴锛孲TARTTLS鏄绾枃鏈�氫俊鍗忚鐨勬墿灞曘�傚畠灏嗙函鏂囨湰杩炴帴鍗囩骇涓哄姞瀵嗚繛鎺ワ紙TLS鎴朣SL锛夛紝 鑰屼笉鏄娇鐢ㄤ竴涓崟鐙殑鍔犲瘑閫氫俊绔彛銆�
+ */
+ private boolean starttlsEnable = false;
+ /**
+ * 浣跨敤 SSL瀹夊叏杩炴帴
+ */
+ private Boolean sslEnable;
+
+ /**
+ * SSL鍗忚锛屽涓崗璁敤绌烘牸鍒嗛殧
+ */
+ private String sslProtocols;
+
+ /**
+ * 鎸囧畾瀹炵幇javax.net.SocketFactory鎺ュ彛鐨勭被鐨勫悕绉�,杩欎釜绫诲皢琚敤浜庡垱寤篠MTP鐨勫鎺ュ瓧
+ */
+ private String socketFactoryClass = "javax.net.ssl.SSLSocketFactory";
+ /**
+ * 濡傛灉璁剧疆涓簍rue,鏈兘鍒涘缓涓�涓鎺ュ瓧浣跨敤鎸囧畾鐨勫鎺ュ瓧宸ュ巶绫诲皢瀵艰嚧浣跨敤java.net.Socket鍒涘缓鐨勫鎺ュ瓧绫�, 榛樿鍊间负true
+ */
+ private boolean socketFactoryFallback;
+ /**
+ * 鎸囧畾鐨勭鍙h繛鎺ュ埌鍦ㄤ娇鐢ㄦ寚瀹氱殑濂楁帴瀛楀伐鍘傘�傚鏋滄病鏈夎缃�,灏嗕娇鐢ㄩ粯璁ょ鍙�
+ */
+ private int socketFactoryPort = 465;
+
+ /**
+ * SMTP瓒呮椂鏃堕暱锛屽崟浣嶆绉掞紝缂虹渷鍊间笉瓒呮椂
+ */
+ private long timeout;
+ /**
+ * Socket杩炴帴瓒呮椂鍊硷紝鍗曚綅姣锛岀己鐪佸�间笉瓒呮椂
+ */
+ private long connectionTimeout;
+ /**
+ * Socket鍐欏嚭瓒呮椂鍊硷紝鍗曚綅姣锛岀己鐪佸�间笉瓒呮椂
+ */
+ private long writeTimeout;
+
+ /**
+ * 鑷畾涔夌殑鍏朵粬灞炴�э紝姝よ嚜瀹氫箟灞炴�т細瑕嗙洊榛樿灞炴��
+ */
+ private final Map<String, Object> customProperty = new HashMap<>();
+
+ // -------------------------------------------------------------- Constructor start
+
+ /**
+ * 鏋勯��,鎵�鏈夊弬鏁伴渶鑷瀹氫箟鎴栦繚鎸侀粯璁ゅ��
+ */
+ public MailAccount() {
+ }
+
+ /**
+ * 鏋勯��
+ *
+ * @param settingPath 閰嶇疆鏂囦欢璺緞
+ */
+ public MailAccount(String settingPath) {
+ this(new Setting(settingPath));
+ }
+
+ /**
+ * 鏋勯��
+ *
+ * @param setting 閰嶇疆鏂囦欢
+ */
+ public MailAccount(Setting setting) {
+ setting.toBean(this);
+ }
+
+ // -------------------------------------------------------------- Constructor end
+
+ /**
+ * 鑾峰緱SMTP鏈嶅姟鍣ㄥ煙鍚�
+ *
+ * @return SMTP鏈嶅姟鍣ㄥ煙鍚�
+ */
+ public String getHost() {
+ return host;
+ }
+
+ /**
+ * 璁剧疆SMTP鏈嶅姟鍣ㄥ煙鍚�
+ *
+ * @param host SMTP鏈嶅姟鍣ㄥ煙鍚�
+ * @return this
+ */
+ public MailAccount setHost(String host) {
+ this.host = host;
+ return this;
+ }
+
+ /**
+ * 鑾峰緱SMTP鏈嶅姟绔彛
+ *
+ * @return SMTP鏈嶅姟绔彛
+ */
+ public Integer getPort() {
+ return port;
+ }
+
+ /**
+ * 璁剧疆SMTP鏈嶅姟绔彛
+ *
+ * @param port SMTP鏈嶅姟绔彛
+ * @return this
+ */
+ public MailAccount setPort(Integer port) {
+ this.port = port;
+ return this;
+ }
+
+ /**
+ * 鏄惁闇�瑕佺敤鎴峰悕瀵嗙爜楠岃瘉
+ *
+ * @return 鏄惁闇�瑕佺敤鎴峰悕瀵嗙爜楠岃瘉
+ */
+ public Boolean isAuth() {
+ return auth;
+ }
+
+ /**
+ * 璁剧疆鏄惁闇�瑕佺敤鎴峰悕瀵嗙爜楠岃瘉
+ *
+ * @param isAuth 鏄惁闇�瑕佺敤鎴峰悕瀵嗙爜楠岃瘉
+ * @return this
+ */
+ public MailAccount setAuth(boolean isAuth) {
+ this.auth = isAuth;
+ return this;
+ }
+
+ /**
+ * 鑾峰彇鐢ㄦ埛鍚�
+ *
+ * @return 鐢ㄦ埛鍚�
+ */
+ public String getUser() {
+ return user;
+ }
+
+ /**
+ * 璁剧疆鐢ㄦ埛鍚�
+ *
+ * @param user 鐢ㄦ埛鍚�
+ * @return this
+ */
+ public MailAccount setUser(String user) {
+ this.user = user;
+ return this;
+ }
+
+ /**
+ * 鑾峰彇瀵嗙爜
+ *
+ * @return 瀵嗙爜
+ */
+ public String getPass() {
+ return pass;
+ }
+
+ /**
+ * 璁剧疆瀵嗙爜
+ *
+ * @param pass 瀵嗙爜
+ * @return this
+ */
+ public MailAccount setPass(String pass) {
+ this.pass = pass;
+ return this;
+ }
+
+ /**
+ * 鑾峰彇鍙戦�佹柟锛岄伒寰猂FC-822鏍囧噯
+ *
+ * @return 鍙戦�佹柟锛岄伒寰猂FC-822鏍囧噯
+ */
+ public String getFrom() {
+ return from;
+ }
+
+ /**
+ * 璁剧疆鍙戦�佹柟锛岄伒寰猂FC-822鏍囧噯<br>
+ * 鍙戜欢浜哄彲浠ユ槸浠ヤ笅褰㈠紡锛�
+ *
+ * <pre>
+ * 1. user@xxx.xx
+ * 2. name <user@xxx.xx>
+ * </pre>
+ *
+ * @param from 鍙戦�佹柟锛岄伒寰猂FC-822鏍囧噯
+ * @return this
+ */
+ public MailAccount setFrom(String from) {
+ this.from = from;
+ return this;
+ }
+
+ /**
+ * 鏄惁鎵撳紑璋冭瘯妯″紡锛岃皟璇曟ā寮忎細鏄剧ず涓庨偖浠舵湇鍔″櫒閫氫俊杩囩▼锛岄粯璁や笉寮�鍚�
+ *
+ * @return 鏄惁鎵撳紑璋冭瘯妯″紡锛岃皟璇曟ā寮忎細鏄剧ず涓庨偖浠舵湇鍔″櫒閫氫俊杩囩▼锛岄粯璁や笉寮�鍚�
+ * @since 4.0.2
+ */
+ public boolean isDebug() {
+ return debug;
+ }
+
+ /**
+ * 璁剧疆鏄惁鎵撳紑璋冭瘯妯″紡锛岃皟璇曟ā寮忎細鏄剧ず涓庨偖浠舵湇鍔″櫒閫氫俊杩囩▼锛岄粯璁や笉寮�鍚�
+ *
+ * @param debug 鏄惁鎵撳紑璋冭瘯妯″紡锛岃皟璇曟ā寮忎細鏄剧ず涓庨偖浠舵湇鍔″櫒閫氫俊杩囩▼锛岄粯璁や笉寮�鍚�
+ * @return this
+ * @since 4.0.2
+ */
+ public MailAccount setDebug(boolean debug) {
+ this.debug = debug;
+ return this;
+ }
+
+ /**
+ * 鑾峰彇瀛楃闆嗙紪鐮�
+ *
+ * @return 缂栫爜锛屽彲鑳戒负{@code null}
+ */
+ public Charset getCharset() {
+ return charset;
+ }
+
+ /**
+ * 璁剧疆瀛楃闆嗙紪鐮侊紝姝ら�夐」涓嶄細淇敼鍏ㄥ眬閰嶇疆锛岃嫢淇敼鍏ㄥ眬閰嶇疆锛岃璁剧疆姝ら」涓簕@code null}骞惰缃細
+ * <pre>
+ * System.setProperty("mail.mime.charset", charset);
+ * </pre>
+ *
+ * @param charset 瀛楃闆嗙紪鐮侊紝{@code null} 鍒欒〃绀轰娇鐢ㄥ叏灞�璁剧疆鐨勯粯璁ょ紪鐮侊紝鍏ㄥ眬缂栫爜涓簃ail.mime.charset绯荤粺灞炴��
+ * @return this
+ */
+ public MailAccount setCharset(Charset charset) {
+ this.charset = charset;
+ return this;
+ }
+
+ /**
+ * 瀵逛簬瓒呴暱鍙傛暟鏄惁鍒囧垎涓哄浠斤紝榛樿涓篺alse锛堝浗鍐呴偖绠遍檮浠朵笉鏀寔鍒囧垎鐨勯檮浠跺悕锛�
+ *
+ * @return 瀵逛簬瓒呴暱鍙傛暟鏄惁鍒囧垎涓哄浠�
+ */
+ public boolean isSplitlongparameters() {
+ return splitlongparameters;
+ }
+
+ /**
+ * 璁剧疆瀵逛簬瓒呴暱鍙傛暟鏄惁鍒囧垎涓哄浠斤紝榛樿涓篺alse锛堝浗鍐呴偖绠遍檮浠朵笉鏀寔鍒囧垎鐨勯檮浠跺悕锛�<br>
+ * 娉ㄦ剰姝ら」涓哄叏灞�璁剧疆锛屾椤逛細璋冪敤
+ * <pre>
+ * System.setProperty("mail.mime.splitlongparameters", true)
+ * </pre>
+ *
+ * @param splitlongparameters 瀵逛簬瓒呴暱鍙傛暟鏄惁鍒囧垎涓哄浠�
+ */
+ public void setSplitlongparameters(boolean splitlongparameters) {
+ this.splitlongparameters = splitlongparameters;
+ }
+
+ /**
+ * 瀵逛簬鏂囦欢鍚嶆槸鍚︿娇鐢▄@link #charset}缂栫爜锛岄粯璁や负 {@code true}
+ *
+ * @return 瀵逛簬鏂囦欢鍚嶆槸鍚︿娇鐢▄@link #charset}缂栫爜锛岄粯璁や负 {@code true}
+ * @since 5.7.16
+ */
+ public boolean isEncodefilename() {
+
+ return encodefilename;
+ }
+
+ /**
+ * 璁剧疆瀵逛簬鏂囦欢鍚嶆槸鍚︿娇鐢▄@link #charset}缂栫爜锛屾閫夐」涓嶄細淇敼鍏ㄥ眬閰嶇疆<br>
+ * 濡傛灉姝ら�夐」璁剧疆涓簕@code false}锛屽垯鏄惁缂栫爜鍙栧喅浜庝袱涓郴缁熷睘鎬э細
+ * <ul>
+ * <li>mail.mime.encodefilename 鏄惁缂栫爜闄勪欢鏂囦欢鍚�</li>
+ * <li>mail.mime.charset 缂栫爜鏂囦欢鍚嶇殑缂栫爜</li>
+ * </ul>
+ *
+ * @param encodefilename 瀵逛簬鏂囦欢鍚嶆槸鍚︿娇鐢▄@link #charset}缂栫爜
+ * @since 5.7.16
+ */
+ public void setEncodefilename(boolean encodefilename) {
+ this.encodefilename = encodefilename;
+ }
+
+ /**
+ * 鏄惁浣跨敤 STARTTLS瀹夊叏杩炴帴锛孲TARTTLS鏄绾枃鏈�氫俊鍗忚鐨勬墿灞曘�傚畠灏嗙函鏂囨湰杩炴帴鍗囩骇涓哄姞瀵嗚繛鎺ワ紙TLS鎴朣SL锛夛紝 鑰屼笉鏄娇鐢ㄤ竴涓崟鐙殑鍔犲瘑閫氫俊绔彛銆�
+ *
+ * @return 鏄惁浣跨敤 STARTTLS瀹夊叏杩炴帴
+ */
+ public boolean isStarttlsEnable() {
+ return this.starttlsEnable;
+ }
+
+ /**
+ * 璁剧疆鏄惁浣跨敤STARTTLS瀹夊叏杩炴帴锛孲TARTTLS鏄绾枃鏈�氫俊鍗忚鐨勬墿灞曘�傚畠灏嗙函鏂囨湰杩炴帴鍗囩骇涓哄姞瀵嗚繛鎺ワ紙TLS鎴朣SL锛夛紝 鑰屼笉鏄娇鐢ㄤ竴涓崟鐙殑鍔犲瘑閫氫俊绔彛銆�
+ *
+ * @param startttlsEnable 鏄惁浣跨敤STARTTLS瀹夊叏杩炴帴
+ * @return this
+ */
+ public MailAccount setStarttlsEnable(boolean startttlsEnable) {
+ this.starttlsEnable = startttlsEnable;
+ return this;
+ }
+
+ /**
+ * 鏄惁浣跨敤 SSL瀹夊叏杩炴帴
+ *
+ * @return 鏄惁浣跨敤 SSL瀹夊叏杩炴帴
+ */
+ public Boolean isSslEnable() {
+ return this.sslEnable;
+ }
+
+ /**
+ * 璁剧疆鏄惁浣跨敤SSL瀹夊叏杩炴帴
+ *
+ * @param sslEnable 鏄惁浣跨敤SSL瀹夊叏杩炴帴
+ * @return this
+ */
+ public MailAccount setSslEnable(Boolean sslEnable) {
+ this.sslEnable = sslEnable;
+ return this;
+ }
+
+ /**
+ * 鑾峰彇SSL鍗忚锛屽涓崗璁敤绌烘牸鍒嗛殧
+ *
+ * @return SSL鍗忚锛屽涓崗璁敤绌烘牸鍒嗛殧
+ * @since 5.5.7
+ */
+ public String getSslProtocols() {
+ return sslProtocols;
+ }
+
+ /**
+ * 璁剧疆SSL鍗忚锛屽涓崗璁敤绌烘牸鍒嗛殧
+ *
+ * @param sslProtocols SSL鍗忚锛屽涓崗璁敤绌烘牸鍒嗛殧
+ * @since 5.5.7
+ */
+ public void setSslProtocols(String sslProtocols) {
+ this.sslProtocols = sslProtocols;
+ }
+
+ /**
+ * 鑾峰彇鎸囧畾瀹炵幇javax.net.SocketFactory鎺ュ彛鐨勭被鐨勫悕绉�,杩欎釜绫诲皢琚敤浜庡垱寤篠MTP鐨勫鎺ュ瓧
+ *
+ * @return 鎸囧畾瀹炵幇javax.net.SocketFactory鎺ュ彛鐨勭被鐨勫悕绉�, 杩欎釜绫诲皢琚敤浜庡垱寤篠MTP鐨勫鎺ュ瓧
+ */
+ public String getSocketFactoryClass() {
+ return socketFactoryClass;
+ }
+
+ /**
+ * 璁剧疆鎸囧畾瀹炵幇javax.net.SocketFactory鎺ュ彛鐨勭被鐨勫悕绉�,杩欎釜绫诲皢琚敤浜庡垱寤篠MTP鐨勫鎺ュ瓧
+ *
+ * @param socketFactoryClass 鎸囧畾瀹炵幇javax.net.SocketFactory鎺ュ彛鐨勭被鐨勫悕绉�,杩欎釜绫诲皢琚敤浜庡垱寤篠MTP鐨勫鎺ュ瓧
+ * @return this
+ */
+ public MailAccount setSocketFactoryClass(String socketFactoryClass) {
+ this.socketFactoryClass = socketFactoryClass;
+ return this;
+ }
+
+ /**
+ * 濡傛灉璁剧疆涓簍rue,鏈兘鍒涘缓涓�涓鎺ュ瓧浣跨敤鎸囧畾鐨勫鎺ュ瓧宸ュ巶绫诲皢瀵艰嚧浣跨敤java.net.Socket鍒涘缓鐨勫鎺ュ瓧绫�, 榛樿鍊间负true
+ *
+ * @return 濡傛灉璁剧疆涓簍rue, 鏈兘鍒涘缓涓�涓鎺ュ瓧浣跨敤鎸囧畾鐨勫鎺ュ瓧宸ュ巶绫诲皢瀵艰嚧浣跨敤java.net.Socket鍒涘缓鐨勫鎺ュ瓧绫�, 榛樿鍊间负true
+ */
+ public boolean isSocketFactoryFallback() {
+ return socketFactoryFallback;
+ }
+
+ /**
+ * 濡傛灉璁剧疆涓簍rue,鏈兘鍒涘缓涓�涓鎺ュ瓧浣跨敤鎸囧畾鐨勫鎺ュ瓧宸ュ巶绫诲皢瀵艰嚧浣跨敤java.net.Socket鍒涘缓鐨勫鎺ュ瓧绫�, 榛樿鍊间负true
+ *
+ * @param socketFactoryFallback 濡傛灉璁剧疆涓簍rue,鏈兘鍒涘缓涓�涓鎺ュ瓧浣跨敤鎸囧畾鐨勫鎺ュ瓧宸ュ巶绫诲皢瀵艰嚧浣跨敤java.net.Socket鍒涘缓鐨勫鎺ュ瓧绫�, 榛樿鍊间负true
+ * @return this
+ */
+ public MailAccount setSocketFactoryFallback(boolean socketFactoryFallback) {
+ this.socketFactoryFallback = socketFactoryFallback;
+ return this;
+ }
+
+ /**
+ * 鑾峰彇鎸囧畾鐨勭鍙h繛鎺ュ埌鍦ㄤ娇鐢ㄦ寚瀹氱殑濂楁帴瀛楀伐鍘傘�傚鏋滄病鏈夎缃�,灏嗕娇鐢ㄩ粯璁ょ鍙�
+ *
+ * @return 鎸囧畾鐨勭鍙h繛鎺ュ埌鍦ㄤ娇鐢ㄦ寚瀹氱殑濂楁帴瀛楀伐鍘傘�傚鏋滄病鏈夎缃�,灏嗕娇鐢ㄩ粯璁ょ鍙�
+ */
+ public int getSocketFactoryPort() {
+ return socketFactoryPort;
+ }
+
+ /**
+ * 鎸囧畾鐨勭鍙h繛鎺ュ埌鍦ㄤ娇鐢ㄦ寚瀹氱殑濂楁帴瀛楀伐鍘傘�傚鏋滄病鏈夎缃�,灏嗕娇鐢ㄩ粯璁ょ鍙�
+ *
+ * @param socketFactoryPort 鎸囧畾鐨勭鍙h繛鎺ュ埌鍦ㄤ娇鐢ㄦ寚瀹氱殑濂楁帴瀛楀伐鍘傘�傚鏋滄病鏈夎缃�,灏嗕娇鐢ㄩ粯璁ょ鍙�
+ * @return this
+ */
+ public MailAccount setSocketFactoryPort(int socketFactoryPort) {
+ this.socketFactoryPort = socketFactoryPort;
+ return this;
+ }
+
+ /**
+ * 璁剧疆SMTP瓒呮椂鏃堕暱锛屽崟浣嶆绉掞紝缂虹渷鍊间笉瓒呮椂
+ *
+ * @param timeout SMTP瓒呮椂鏃堕暱锛屽崟浣嶆绉掞紝缂虹渷鍊间笉瓒呮椂
+ * @return this
+ * @since 4.1.17
+ */
+ public MailAccount setTimeout(long timeout) {
+ this.timeout = timeout;
+ return this;
+ }
+
+ /**
+ * 璁剧疆Socket杩炴帴瓒呮椂鍊硷紝鍗曚綅姣锛岀己鐪佸�间笉瓒呮椂
+ *
+ * @param connectionTimeout Socket杩炴帴瓒呮椂鍊硷紝鍗曚綅姣锛岀己鐪佸�间笉瓒呮椂
+ * @return this
+ * @since 4.1.17
+ */
+ public MailAccount setConnectionTimeout(long connectionTimeout) {
+ this.connectionTimeout = connectionTimeout;
+ return this;
+ }
+
+ /**
+ * 璁剧疆Socket鍐欏嚭瓒呮椂鍊硷紝鍗曚綅姣锛岀己鐪佸�间笉瓒呮椂
+ *
+ * @param writeTimeout Socket鍐欏嚭瓒呮椂鍊硷紝鍗曚綅姣锛岀己鐪佸�间笉瓒呮椂
+ * @return this
+ * @since 5.8.3
+ */
+ public MailAccount setWriteTimeout(long writeTimeout) {
+ this.writeTimeout = writeTimeout;
+ return this;
+ }
+
+ /**
+ * 鑾峰彇鑷畾涔夊睘鎬у垪琛�
+ *
+ * @return 鑷畾涔夊弬鏁板垪琛�
+ * @since 5.6.4
+ */
+ public Map<String, Object> getCustomProperty() {
+ return customProperty;
+ }
+
+ /**
+ * 璁剧疆鑷畾涔夊睘鎬э紝濡俶ail.smtp.ssl.socketFactory
+ *
+ * @param key 灞炴�у悕锛岀┖鐧借蹇界暐
+ * @param value 灞炴�у�硷紝 null琚拷鐣�
+ * @return this
+ * @since 5.6.4
+ */
+ public MailAccount setCustomProperty(String key, Object value) {
+ if (StrUtil.isNotBlank(key) && ObjectUtil.isNotNull(value)) {
+ this.customProperty.put(key, value);
+ }
+ return this;
+ }
+
+ /**
+ * 鑾峰緱SMTP鐩稿叧淇℃伅
+ *
+ * @return {@link Properties}
+ */
+ public Properties getSmtpProps() {
+ //鍏ㄥ眬绯荤粺鍙傛暟
+ System.setProperty(SPLIT_LONG_PARAMS, String.valueOf(this.splitlongparameters));
+
+ final Properties p = new Properties();
+ p.put(MAIL_PROTOCOL, "smtp");
+ p.put(SMTP_HOST, this.host);
+ p.put(SMTP_PORT, String.valueOf(this.port));
+ p.put(SMTP_AUTH, String.valueOf(this.auth));
+ if (this.timeout > 0) {
+ p.put(SMTP_TIMEOUT, String.valueOf(this.timeout));
+ }
+ if (this.connectionTimeout > 0) {
+ p.put(SMTP_CONNECTION_TIMEOUT, String.valueOf(this.connectionTimeout));
+ }
+ // issue#2355
+ if (this.writeTimeout > 0) {
+ p.put(SMTP_WRITE_TIMEOUT, String.valueOf(this.writeTimeout));
+ }
+
+ p.put(MAIL_DEBUG, String.valueOf(this.debug));
+
+ if (this.starttlsEnable) {
+ //STARTTLS鏄绾枃鏈�氫俊鍗忚鐨勬墿灞曘�傚畠灏嗙函鏂囨湰杩炴帴鍗囩骇涓哄姞瀵嗚繛鎺ワ紙TLS鎴朣SL锛夛紝 鑰屼笉鏄娇鐢ㄤ竴涓崟鐙殑鍔犲瘑閫氫俊绔彛銆�
+ p.put(STARTTLS_ENABLE, "true");
+
+ if (null == this.sslEnable) {
+ //涓轰簡鍏煎鏃х増鏈紝褰撶敤鎴锋病鏈夋椤归厤缃椂锛屾寜鐓tarttlsEnable寮�鍚姸鎬佹椂瀵瑰緟
+ this.sslEnable = true;
+ }
+ }
+
+ // SSL
+ if (null != this.sslEnable && this.sslEnable) {
+ p.put(SSL_ENABLE, "true");
+ p.put(SOCKET_FACTORY, socketFactoryClass);
+ p.put(SOCKET_FACTORY_FALLBACK, String.valueOf(this.socketFactoryFallback));
+ p.put(SOCKET_FACTORY_PORT, String.valueOf(this.socketFactoryPort));
+ // issue#IZN95@Gitee锛屽湪Linux涓嬮渶鑷畾涔塖SL鍗忚鐗堟湰
+ if (StrUtil.isNotBlank(this.sslProtocols)) {
+ p.put(SSL_PROTOCOLS, this.sslProtocols);
+ }
+ }
+
+ // 琛ュ厖鑷畾涔夊睘鎬э紝鍏佽鑷畾灞炴�ц鐩栧凡缁忚缃殑鍊�
+ p.putAll(this.customProperty);
+
+ return p;
+ }
+
+ /**
+ * 濡傛灉鏌愪簺鍊间负null锛屼娇鐢ㄩ粯璁ゅ��
+ *
+ * @return this
+ */
+ public MailAccount defaultIfEmpty() {
+ // 鍘绘帀鍙戜欢浜虹殑濮撳悕閮ㄥ垎
+ final String fromAddress = InternalMailUtil.parseFirstAddress(this.from, this.charset).getAddress();
+
+ if (StrUtil.isBlank(this.host)) {
+ // 濡傛灉SMTP鍦板潃涓虹┖锛岄粯璁や娇鐢╯mtp.<鍙戜欢浜洪偖绠卞悗缂�>
+ this.host = StrUtil.format("smtp.{}", StrUtil.subSuf(fromAddress, fromAddress.indexOf('@') + 1));
+ }
+ if (StrUtil.isBlank(user)) {
+ // 濡傛灉鐢ㄦ埛鍚嶄负绌猴紝榛樿涓哄彂浠朵汉锛坕ssue#I4FYVY@Gitee锛�
+ //this.user = StrUtil.subPre(fromAddress, fromAddress.indexOf('@'));
+ this.user = fromAddress;
+ }
+ if (null == this.auth) {
+ // 濡傛灉瀵嗙爜闈炵┖鐧斤紝鍒欎娇鐢ㄨ璇佹ā寮�
+ this.auth = (false == StrUtil.isBlank(this.pass));
+ }
+ if (null == this.port) {
+ // 绔彛鍦⊿SL鐘舵�佷笅榛樿涓巗ocketFactoryPort涓�鑷达紝闈濻SL鐘舵�佷笅榛樿涓�25
+ this.port = (null != this.sslEnable && this.sslEnable) ? this.socketFactoryPort : 25;
+ }
+ if (null == this.charset) {
+ // 榛樿UTF-8缂栫爜
+ this.charset = CharsetUtil.CHARSET_UTF_8;
+ }
+
+ return this;
+ }
+
+ @Override
+ public String toString() {
+ return "MailAccount [host=" + host + ", port=" + port + ", auth=" + auth + ", user=" + user + ", pass=" + (StrUtil.isEmpty(this.pass) ? "" : "******") + ", from=" + from + ", startttlsEnable="
+ + starttlsEnable + ", socketFactoryClass=" + socketFactoryClass + ", socketFactoryFallback=" + socketFactoryFallback + ", socketFactoryPort=" + socketFactoryPort + "]";
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/MailException.java b/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/MailException.java
new file mode 100644
index 0000000..cc199d4
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/MailException.java
@@ -0,0 +1,40 @@
+package org.dromara.common.mail.utils;
+
+import cn.hutool.core.exceptions.ExceptionUtil;
+import cn.hutool.core.util.StrUtil;
+
+import java.io.Serial;
+
+/**
+ * 閭欢寮傚父
+ *
+ * @author xiaoleilu
+ */
+public class MailException extends RuntimeException {
+ @Serial
+ private static final long serialVersionUID = 8247610319171014183L;
+
+ public MailException(Throwable e) {
+ super(ExceptionUtil.getMessage(e), e);
+ }
+
+ public MailException(String message) {
+ super(message);
+ }
+
+ public MailException(String messageTemplate, Object... params) {
+ super(StrUtil.format(messageTemplate, params));
+ }
+
+ public MailException(String message, Throwable throwable) {
+ super(message, throwable);
+ }
+
+ public MailException(String message, Throwable throwable, boolean enableSuppression, boolean writableStackTrace) {
+ super(message, throwable, enableSuppression, writableStackTrace);
+ }
+
+ public MailException(Throwable throwable, String messageTemplate, Object... params) {
+ super(StrUtil.format(messageTemplate, params), throwable);
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/MailUtils.java b/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/MailUtils.java
new file mode 100644
index 0000000..040cc57
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/MailUtils.java
@@ -0,0 +1,467 @@
+package org.dromara.common.mail.utils;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.io.IoUtil;
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.CharUtil;
+import cn.hutool.core.util.StrUtil;
+import jakarta.mail.Authenticator;
+import jakarta.mail.Session;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import org.dromara.common.core.utils.SpringUtils;
+import org.dromara.common.core.utils.StringUtils;
+
+import java.io.File;
+import java.io.InputStream;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+
+/**
+ * 閭欢宸ュ叿绫�
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class MailUtils {
+
+ private static final MailAccount ACCOUNT = SpringUtils.getBean(MailAccount.class);
+
+ /**
+ * 鑾峰彇閭欢鍙戦�佸疄渚�
+ */
+ public static MailAccount getMailAccount() {
+ return ACCOUNT;
+ }
+
+ /**
+ * 鑾峰彇閭欢鍙戦�佸疄渚� (鑷畾涔夊彂閫佷汉浠ュ強鎺堟潈鐮�)
+ *
+ * @param user 鍙戦�佷汉
+ * @param pass 鎺堟潈鐮�
+ */
+ public static MailAccount getMailAccount(String from, String user, String pass) {
+ ACCOUNT.setFrom(StringUtils.blankToDefault(from, ACCOUNT.getFrom()));
+ ACCOUNT.setUser(StringUtils.blankToDefault(user, ACCOUNT.getUser()));
+ ACCOUNT.setPass(StringUtils.blankToDefault(pass, ACCOUNT.getPass()));
+ return ACCOUNT;
+ }
+
+ /**
+ * 浣跨敤閰嶇疆鏂囦欢涓缃殑璐︽埛鍙戦�佹枃鏈偖浠讹紝鍙戦�佺粰鍗曚釜鎴栧涓敹浠朵汉<br>
+ * 澶氫釜鏀朵欢浜哄彲浠ヤ娇鐢ㄩ�楀彿鈥�,鈥濆垎闅旓紝涔熷彲浠ラ�氳繃鍒嗗彿鈥�;鈥濆垎闅�
+ *
+ * @param to 鏀朵欢浜�
+ * @param subject 鏍囬
+ * @param content 姝f枃
+ * @param files 闄勪欢鍒楄〃
+ * @return message-id
+ * @since 3.2.0
+ */
+ public static String sendText(String to, String subject, String content, File... files) {
+ return send(to, subject, content, false, files);
+ }
+
+ /**
+ * 浣跨敤閰嶇疆鏂囦欢涓缃殑璐︽埛鍙戦�丠TML閭欢锛屽彂閫佺粰鍗曚釜鎴栧涓敹浠朵汉<br>
+ * 澶氫釜鏀朵欢浜哄彲浠ヤ娇鐢ㄩ�楀彿鈥�,鈥濆垎闅旓紝涔熷彲浠ラ�氳繃鍒嗗彿鈥�;鈥濆垎闅�
+ *
+ * @param to 鏀朵欢浜�
+ * @param subject 鏍囬
+ * @param content 姝f枃
+ * @param files 闄勪欢鍒楄〃
+ * @return message-id
+ * @since 3.2.0
+ */
+ public static String sendHtml(String to, String subject, String content, File... files) {
+ return send(to, subject, content, true, files);
+ }
+
+ /**
+ * 浣跨敤閰嶇疆鏂囦欢涓缃殑璐︽埛鍙戦�侀偖浠讹紝鍙戦�佸崟涓垨澶氫釜鏀朵欢浜�<br>
+ * 澶氫釜鏀朵欢浜哄彲浠ヤ娇鐢ㄩ�楀彿鈥�,鈥濆垎闅旓紝涔熷彲浠ラ�氳繃鍒嗗彿鈥�;鈥濆垎闅�
+ *
+ * @param to 鏀朵欢浜�
+ * @param subject 鏍囬
+ * @param content 姝f枃
+ * @param isHtml 鏄惁涓篐TML
+ * @param files 闄勪欢鍒楄〃
+ * @return message-id
+ */
+ public static String send(String to, String subject, String content, boolean isHtml, File... files) {
+ return send(splitAddress(to), subject, content, isHtml, files);
+ }
+
+ /**
+ * 浣跨敤閰嶇疆鏂囦欢涓缃殑璐︽埛鍙戦�侀偖浠讹紝鍙戦�佸崟涓垨澶氫釜鏀朵欢浜�<br>
+ * 澶氫釜鏀朵欢浜恒�佹妱閫佷汉銆佸瘑閫佷汉鍙互浣跨敤閫楀彿鈥�,鈥濆垎闅旓紝涔熷彲浠ラ�氳繃鍒嗗彿鈥�;鈥濆垎闅�
+ *
+ * @param to 鏀朵欢浜猴紝鍙互浣跨敤閫楀彿鈥�,鈥濆垎闅旓紝涔熷彲浠ラ�氳繃鍒嗗彿鈥�;鈥濆垎闅�
+ * @param cc 鎶勯�佷汉锛屽彲浠ヤ娇鐢ㄩ�楀彿鈥�,鈥濆垎闅旓紝涔熷彲浠ラ�氳繃鍒嗗彿鈥�;鈥濆垎闅�
+ * @param bcc 瀵嗛�佷汉锛屽彲浠ヤ娇鐢ㄩ�楀彿鈥�,鈥濆垎闅旓紝涔熷彲浠ラ�氳繃鍒嗗彿鈥�;鈥濆垎闅�
+ * @param subject 鏍囬
+ * @param content 姝f枃
+ * @param isHtml 鏄惁涓篐TML
+ * @param files 闄勪欢鍒楄〃
+ * @return message-id
+ * @since 4.0.3
+ */
+ public static String send(String to, String cc, String bcc, String subject, String content, boolean isHtml, File... files) {
+ return send(splitAddress(to), splitAddress(cc), splitAddress(bcc), subject, content, isHtml, files);
+ }
+
+ /**
+ * 浣跨敤閰嶇疆鏂囦欢涓缃殑璐︽埛鍙戦�佹枃鏈偖浠讹紝鍙戦�佺粰澶氫汉
+ *
+ * @param tos 鏀朵欢浜哄垪琛�
+ * @param subject 鏍囬
+ * @param content 姝f枃
+ * @param files 闄勪欢鍒楄〃
+ * @return message-id
+ */
+ public static String sendText(Collection<String> tos, String subject, String content, File... files) {
+ return send(tos, subject, content, false, files);
+ }
+
+ /**
+ * 浣跨敤閰嶇疆鏂囦欢涓缃殑璐︽埛鍙戦�丠TML閭欢锛屽彂閫佺粰澶氫汉
+ *
+ * @param tos 鏀朵欢浜哄垪琛�
+ * @param subject 鏍囬
+ * @param content 姝f枃
+ * @param files 闄勪欢鍒楄〃
+ * @return message-id
+ * @since 3.2.0
+ */
+ public static String sendHtml(Collection<String> tos, String subject, String content, File... files) {
+ return send(tos, subject, content, true, files);
+ }
+
+ /**
+ * 浣跨敤閰嶇疆鏂囦欢涓缃殑璐︽埛鍙戦�侀偖浠讹紝鍙戦�佺粰澶氫汉
+ *
+ * @param tos 鏀朵欢浜哄垪琛�
+ * @param subject 鏍囬
+ * @param content 姝f枃
+ * @param isHtml 鏄惁涓篐TML
+ * @param files 闄勪欢鍒楄〃
+ * @return message-id
+ */
+ public static String send(Collection<String> tos, String subject, String content, boolean isHtml, File... files) {
+ return send(tos, null, null, subject, content, isHtml, files);
+ }
+
+ /**
+ * 浣跨敤閰嶇疆鏂囦欢涓缃殑璐︽埛鍙戦�侀偖浠讹紝鍙戦�佺粰澶氫汉
+ *
+ * @param tos 鏀朵欢浜哄垪琛�
+ * @param ccs 鎶勯�佷汉鍒楄〃锛屽彲浠ヤ负null鎴栫┖
+ * @param bccs 瀵嗛�佷汉鍒楄〃锛屽彲浠ヤ负null鎴栫┖
+ * @param subject 鏍囬
+ * @param content 姝f枃
+ * @param isHtml 鏄惁涓篐TML
+ * @param files 闄勪欢鍒楄〃
+ * @return message-id
+ * @since 4.0.3
+ */
+ public static String send(Collection<String> tos, Collection<String> ccs, Collection<String> bccs, String subject, String content, boolean isHtml, File... files) {
+ return send(getMailAccount(), true, tos, ccs, bccs, subject, content, null, isHtml, files);
+ }
+
+ // ------------------------------------------------------------------------------------------------------------------------------- Custom MailAccount
+
+ /**
+ * 鍙戦�侀偖浠剁粰澶氫汉
+ *
+ * @param mailAccount 閭欢璁よ瘉瀵硅薄
+ * @param to 鏀朵欢浜猴紝澶氫釜鏀朵欢浜洪�楀彿鎴栬�呭垎鍙烽殧寮�
+ * @param subject 鏍囬
+ * @param content 姝f枃
+ * @param isHtml 鏄惁涓篐TML鏍煎紡
+ * @param files 闄勪欢鍒楄〃
+ * @return message-id
+ * @since 3.2.0
+ */
+ public static String send(MailAccount mailAccount, String to, String subject, String content, boolean isHtml, File... files) {
+ return send(mailAccount, splitAddress(to), subject, content, isHtml, files);
+ }
+
+ /**
+ * 鍙戦�侀偖浠剁粰澶氫汉
+ *
+ * @param mailAccount 閭欢甯愭埛淇℃伅
+ * @param tos 鏀朵欢浜哄垪琛�
+ * @param subject 鏍囬
+ * @param content 姝f枃
+ * @param isHtml 鏄惁涓篐TML鏍煎紡
+ * @param files 闄勪欢鍒楄〃
+ * @return message-id
+ */
+ public static String send(MailAccount mailAccount, Collection<String> tos, String subject, String content, boolean isHtml, File... files) {
+ return send(mailAccount, tos, null, null, subject, content, isHtml, files);
+ }
+
+ /**
+ * 鍙戦�侀偖浠剁粰澶氫汉
+ *
+ * @param mailAccount 閭欢甯愭埛淇℃伅
+ * @param tos 鏀朵欢浜哄垪琛�
+ * @param ccs 鎶勯�佷汉鍒楄〃锛屽彲浠ヤ负null鎴栫┖
+ * @param bccs 瀵嗛�佷汉鍒楄〃锛屽彲浠ヤ负null鎴栫┖
+ * @param subject 鏍囬
+ * @param content 姝f枃
+ * @param isHtml 鏄惁涓篐TML鏍煎紡
+ * @param files 闄勪欢鍒楄〃
+ * @return message-id
+ * @since 4.0.3
+ */
+ public static String send(MailAccount mailAccount, Collection<String> tos, Collection<String> ccs, Collection<String> bccs, String subject, String content, boolean isHtml, File... files) {
+ return send(mailAccount, false, tos, ccs, bccs, subject, content, null, isHtml, files);
+ }
+
+ /**
+ * 浣跨敤閰嶇疆鏂囦欢涓缃殑璐︽埛鍙戦�丠TML閭欢锛屽彂閫佺粰鍗曚釜鎴栧涓敹浠朵汉<br>
+ * 澶氫釜鏀朵欢浜哄彲浠ヤ娇鐢ㄩ�楀彿鈥�,鈥濆垎闅旓紝涔熷彲浠ラ�氳繃鍒嗗彿鈥�;鈥濆垎闅�
+ *
+ * @param to 鏀朵欢浜�
+ * @param subject 鏍囬
+ * @param content 姝f枃
+ * @param imageMap 鍥剧墖涓庡崰浣嶇锛屽崰浣嶇鏍煎紡涓篶id:$IMAGE_PLACEHOLDER
+ * @param files 闄勪欢鍒楄〃
+ * @return message-id
+ * @since 3.2.0
+ */
+ public static String sendHtml(String to, String subject, String content, Map<String, InputStream> imageMap, File... files) {
+ return send(to, subject, content, imageMap, true, files);
+ }
+
+ /**
+ * 浣跨敤閰嶇疆鏂囦欢涓缃殑璐︽埛鍙戦�侀偖浠讹紝鍙戦�佸崟涓垨澶氫釜鏀朵欢浜�<br>
+ * 澶氫釜鏀朵欢浜哄彲浠ヤ娇鐢ㄩ�楀彿鈥�,鈥濆垎闅旓紝涔熷彲浠ラ�氳繃鍒嗗彿鈥�;鈥濆垎闅�
+ *
+ * @param to 鏀朵欢浜�
+ * @param subject 鏍囬
+ * @param content 姝f枃
+ * @param imageMap 鍥剧墖涓庡崰浣嶇锛屽崰浣嶇鏍煎紡涓篶id:$IMAGE_PLACEHOLDER
+ * @param isHtml 鏄惁涓篐TML
+ * @param files 闄勪欢鍒楄〃
+ * @return message-id
+ */
+ public static String send(String to, String subject, String content, Map<String, InputStream> imageMap, boolean isHtml, File... files) {
+ return send(splitAddress(to), subject, content, imageMap, isHtml, files);
+ }
+
+ /**
+ * 浣跨敤閰嶇疆鏂囦欢涓缃殑璐︽埛鍙戦�侀偖浠讹紝鍙戦�佸崟涓垨澶氫釜鏀朵欢浜�<br>
+ * 澶氫釜鏀朵欢浜恒�佹妱閫佷汉銆佸瘑閫佷汉鍙互浣跨敤閫楀彿鈥�,鈥濆垎闅旓紝涔熷彲浠ラ�氳繃鍒嗗彿鈥�;鈥濆垎闅�
+ *
+ * @param to 鏀朵欢浜猴紝鍙互浣跨敤閫楀彿鈥�,鈥濆垎闅旓紝涔熷彲浠ラ�氳繃鍒嗗彿鈥�;鈥濆垎闅�
+ * @param cc 鎶勯�佷汉锛屽彲浠ヤ娇鐢ㄩ�楀彿鈥�,鈥濆垎闅旓紝涔熷彲浠ラ�氳繃鍒嗗彿鈥�;鈥濆垎闅�
+ * @param bcc 瀵嗛�佷汉锛屽彲浠ヤ娇鐢ㄩ�楀彿鈥�,鈥濆垎闅旓紝涔熷彲浠ラ�氳繃鍒嗗彿鈥�;鈥濆垎闅�
+ * @param subject 鏍囬
+ * @param content 姝f枃
+ * @param imageMap 鍥剧墖涓庡崰浣嶇锛屽崰浣嶇鏍煎紡涓篶id:$IMAGE_PLACEHOLDER
+ * @param isHtml 鏄惁涓篐TML
+ * @param files 闄勪欢鍒楄〃
+ * @return message-id
+ * @since 4.0.3
+ */
+ public static String send(String to, String cc, String bcc, String subject, String content, Map<String, InputStream> imageMap, boolean isHtml, File... files) {
+ return send(splitAddress(to), splitAddress(cc), splitAddress(bcc), subject, content, imageMap, isHtml, files);
+ }
+
+ /**
+ * 浣跨敤閰嶇疆鏂囦欢涓缃殑璐︽埛鍙戦�丠TML閭欢锛屽彂閫佺粰澶氫汉
+ *
+ * @param tos 鏀朵欢浜哄垪琛�
+ * @param subject 鏍囬
+ * @param content 姝f枃
+ * @param imageMap 鍥剧墖涓庡崰浣嶇锛屽崰浣嶇鏍煎紡涓篶id:$IMAGE_PLACEHOLDER
+ * @param files 闄勪欢鍒楄〃
+ * @return message-id
+ * @since 3.2.0
+ */
+ public static String sendHtml(Collection<String> tos, String subject, String content, Map<String, InputStream> imageMap, File... files) {
+ return send(tos, subject, content, imageMap, true, files);
+ }
+
+ /**
+ * 浣跨敤閰嶇疆鏂囦欢涓缃殑璐︽埛鍙戦�侀偖浠讹紝鍙戦�佺粰澶氫汉
+ *
+ * @param tos 鏀朵欢浜哄垪琛�
+ * @param subject 鏍囬
+ * @param content 姝f枃
+ * @param imageMap 鍥剧墖涓庡崰浣嶇锛屽崰浣嶇鏍煎紡涓篶id:$IMAGE_PLACEHOLDER
+ * @param isHtml 鏄惁涓篐TML
+ * @param files 闄勪欢鍒楄〃
+ * @return message-id
+ */
+ public static String send(Collection<String> tos, String subject, String content, Map<String, InputStream> imageMap, boolean isHtml, File... files) {
+ return send(tos, null, null, subject, content, imageMap, isHtml, files);
+ }
+
+ /**
+ * 浣跨敤閰嶇疆鏂囦欢涓缃殑璐︽埛鍙戦�侀偖浠讹紝鍙戦�佺粰澶氫汉
+ *
+ * @param tos 鏀朵欢浜哄垪琛�
+ * @param ccs 鎶勯�佷汉鍒楄〃锛屽彲浠ヤ负null鎴栫┖
+ * @param bccs 瀵嗛�佷汉鍒楄〃锛屽彲浠ヤ负null鎴栫┖
+ * @param subject 鏍囬
+ * @param content 姝f枃
+ * @param imageMap 鍥剧墖涓庡崰浣嶇锛屽崰浣嶇鏍煎紡涓篶id:$IMAGE_PLACEHOLDER
+ * @param isHtml 鏄惁涓篐TML
+ * @param files 闄勪欢鍒楄〃
+ * @return message-id
+ * @since 4.0.3
+ */
+ public static String send(Collection<String> tos, Collection<String> ccs, Collection<String> bccs, String subject, String content, Map<String, InputStream> imageMap, boolean isHtml, File... files) {
+ return send(getMailAccount(), true, tos, ccs, bccs, subject, content, imageMap, isHtml, files);
+ }
+
+ // ------------------------------------------------------------------------------------------------------------------------------- Custom MailAccount
+
+ /**
+ * 鍙戦�侀偖浠剁粰澶氫汉
+ *
+ * @param mailAccount 閭欢璁よ瘉瀵硅薄
+ * @param to 鏀朵欢浜猴紝澶氫釜鏀朵欢浜洪�楀彿鎴栬�呭垎鍙烽殧寮�
+ * @param subject 鏍囬
+ * @param content 姝f枃
+ * @param imageMap 鍥剧墖涓庡崰浣嶇锛屽崰浣嶇鏍煎紡涓篶id:$IMAGE_PLACEHOLDER
+ * @param isHtml 鏄惁涓篐TML鏍煎紡
+ * @param files 闄勪欢鍒楄〃
+ * @return message-id
+ * @since 3.2.0
+ */
+ public static String send(MailAccount mailAccount, String to, String subject, String content, Map<String, InputStream> imageMap, boolean isHtml, File... files) {
+ return send(mailAccount, splitAddress(to), subject, content, imageMap, isHtml, files);
+ }
+
+ /**
+ * 鍙戦�侀偖浠剁粰澶氫汉
+ *
+ * @param mailAccount 閭欢甯愭埛淇℃伅
+ * @param tos 鏀朵欢浜哄垪琛�
+ * @param subject 鏍囬
+ * @param content 姝f枃
+ * @param imageMap 鍥剧墖涓庡崰浣嶇锛屽崰浣嶇鏍煎紡涓篶id:$IMAGE_PLACEHOLDER
+ * @param isHtml 鏄惁涓篐TML鏍煎紡
+ * @param files 闄勪欢鍒楄〃
+ * @return message-id
+ * @since 4.6.3
+ */
+ public static String send(MailAccount mailAccount, Collection<String> tos, String subject, String content, Map<String, InputStream> imageMap, boolean isHtml, File... files) {
+ return send(mailAccount, tos, null, null, subject, content, imageMap, isHtml, files);
+ }
+
+ /**
+ * 鍙戦�侀偖浠剁粰澶氫汉
+ *
+ * @param mailAccount 閭欢甯愭埛淇℃伅
+ * @param tos 鏀朵欢浜哄垪琛�
+ * @param ccs 鎶勯�佷汉鍒楄〃锛屽彲浠ヤ负null鎴栫┖
+ * @param bccs 瀵嗛�佷汉鍒楄〃锛屽彲浠ヤ负null鎴栫┖
+ * @param subject 鏍囬
+ * @param content 姝f枃
+ * @param imageMap 鍥剧墖涓庡崰浣嶇锛屽崰浣嶇鏍煎紡涓篶id:$IMAGE_PLACEHOLDER
+ * @param isHtml 鏄惁涓篐TML鏍煎紡
+ * @param files 闄勪欢鍒楄〃
+ * @return message-id
+ * @since 4.6.3
+ */
+ public static String send(MailAccount mailAccount, Collection<String> tos, Collection<String> ccs, Collection<String> bccs, String subject, String content, Map<String, InputStream> imageMap,
+ boolean isHtml, File... files) {
+ return send(mailAccount, false, tos, ccs, bccs, subject, content, imageMap, isHtml, files);
+ }
+
+ /**
+ * 鏍规嵁閰嶇疆鏂囦欢锛岃幏鍙栭偖浠跺鎴风浼氳瘽
+ *
+ * @param mailAccount 閭欢璐︽埛閰嶇疆
+ * @param isSingleton 鏄惁鍗曚緥锛堝叏灞�鍏变韩浼氳瘽锛�
+ * @return {@link Session}
+ * @since 5.5.7
+ */
+ public static Session getSession(MailAccount mailAccount, boolean isSingleton) {
+ Authenticator authenticator = null;
+ if (mailAccount.isAuth()) {
+ authenticator = new UserPassAuthenticator(mailAccount.getUser(), mailAccount.getPass());
+ }
+
+ return isSingleton ? Session.getDefaultInstance(mailAccount.getSmtpProps(), authenticator) //
+ : Session.getInstance(mailAccount.getSmtpProps(), authenticator);
+ }
+
+ // ------------------------------------------------------------------------------------------------------------------------ Private method start
+
+ /**
+ * 鍙戦�侀偖浠剁粰澶氫汉
+ *
+ * @param mailAccount 閭欢甯愭埛淇℃伅
+ * @param useGlobalSession 鏄惁鍏ㄥ眬鍏变韩Session
+ * @param tos 鏀朵欢浜哄垪琛�
+ * @param ccs 鎶勯�佷汉鍒楄〃锛屽彲浠ヤ负null鎴栫┖
+ * @param bccs 瀵嗛�佷汉鍒楄〃锛屽彲浠ヤ负null鎴栫┖
+ * @param subject 鏍囬
+ * @param content 姝f枃
+ * @param imageMap 鍥剧墖涓庡崰浣嶇锛屽崰浣嶇鏍煎紡涓篶id:${cid}
+ * @param isHtml 鏄惁涓篐TML鏍煎紡
+ * @param files 闄勪欢鍒楄〃
+ * @return message-id
+ * @since 4.6.3
+ */
+ private static String send(MailAccount mailAccount, boolean useGlobalSession, Collection<String> tos, Collection<String> ccs, Collection<String> bccs, String subject, String content,
+ Map<String, InputStream> imageMap, boolean isHtml, File... files) {
+ final Mail mail = Mail.create(mailAccount).setUseGlobalSession(useGlobalSession);
+
+ // 鍙�夋妱閫佷汉
+ if (CollUtil.isNotEmpty(ccs)) {
+ mail.setCcs(ccs.toArray(new String[0]));
+ }
+ // 鍙�夊瘑閫佷汉
+ if (CollUtil.isNotEmpty(bccs)) {
+ mail.setBccs(bccs.toArray(new String[0]));
+ }
+
+ mail.setTos(tos.toArray(new String[0]));
+ mail.setTitle(subject);
+ mail.setContent(content);
+ mail.setHtml(isHtml);
+ mail.setFiles(files);
+
+ // 鍥剧墖
+ if (MapUtil.isNotEmpty(imageMap)) {
+ for (Map.Entry<String, InputStream> entry : imageMap.entrySet()) {
+ mail.addImage(entry.getKey(), entry.getValue());
+ // 鍏抽棴娴�
+ IoUtil.close(entry.getValue());
+ }
+ }
+
+ return mail.send();
+ }
+
+ /**
+ * 灏嗗涓仈绯讳汉杞负鍒楄〃锛屽垎闅旂涓洪�楀彿鎴栬�呭垎鍙�
+ *
+ * @param addresses 澶氫釜鑱旂郴浜猴紝濡傛灉涓虹┖杩斿洖null
+ * @return 鑱旂郴浜哄垪琛�
+ */
+ private static List<String> splitAddress(String addresses) {
+ if (StrUtil.isBlank(addresses)) {
+ return null;
+ }
+
+ List<String> result;
+ if (StrUtil.contains(addresses, CharUtil.COMMA)) {
+ result = StrUtil.splitTrim(addresses, CharUtil.COMMA);
+ } else if (StrUtil.contains(addresses, ';')) {
+ result = StrUtil.splitTrim(addresses, ';');
+ } else {
+ result = CollUtil.newArrayList(addresses);
+ }
+ return result;
+ }
+ // ------------------------------------------------------------------------------------------------------------------------ Private method end
+
+}
diff --git a/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/UserPassAuthenticator.java b/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/UserPassAuthenticator.java
new file mode 100644
index 0000000..fbbe5e3
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/UserPassAuthenticator.java
@@ -0,0 +1,33 @@
+package org.dromara.common.mail.utils;
+
+import jakarta.mail.Authenticator;
+import jakarta.mail.PasswordAuthentication;
+
+/**
+ * 鐢ㄦ埛鍚嶅瘑鐮侀獙璇佸櫒
+ *
+ * @author looly
+ * @since 3.1.2
+ */
+public class UserPassAuthenticator extends Authenticator {
+
+ private final String user;
+ private final String pass;
+
+ /**
+ * 鏋勯��
+ *
+ * @param user 鐢ㄦ埛鍚�
+ * @param pass 瀵嗙爜
+ */
+ public UserPassAuthenticator(String user, String pass) {
+ this.user = user;
+ this.pass = pass;
+ }
+
+ @Override
+ protected PasswordAuthentication getPasswordAuthentication() {
+ return new PasswordAuthentication(this.user, this.pass);
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-mail/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/ruoyi-common/ruoyi-common-mail/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..aa1474e
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-mail/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+org.dromara.common.mail.config.MailConfiguration
diff --git a/ruoyi-common/ruoyi-common-mybatis/pom.xml b/ruoyi-common/ruoyi-common-mybatis/pom.xml
new file mode 100644
index 0000000..64876fb
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-mybatis/pom.xml
@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common</artifactId>
+ <version>${revision}</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>ruoyi-common-mybatis</artifactId>
+
+ <description>
+ ruoyi-common-mybatis 鏁版嵁搴撴湇鍔�
+ </description>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-satoken</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-dubbo</artifactId>
+ <optional>true</optional>
+ </dependency>
+
+ <dependency>
+ <groupId>org.mybatis.spring.boot</groupId>
+ <artifactId>mybatis-spring-boot-starter</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>com.baomidou</groupId>
+ <artifactId>mybatis-plus-boot-starter</artifactId>
+ <exclusions>
+ <exclusion>
+ <groupId>org.mybatis</groupId>
+ <artifactId>mybatis-spring</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+
+ <!-- sql鎬ц兘鍒嗘瀽鎻掍欢 -->
+ <dependency>
+ <groupId>p6spy</groupId>
+ <artifactId>p6spy</artifactId>
+ </dependency>
+
+ <!-- Dynamic DataSource -->
+ <dependency>
+ <groupId>com.baomidou</groupId>
+ <artifactId>dynamic-datasource-spring-boot3-starter</artifactId>
+ <version>${dynamic-ds.version}</version>
+ </dependency>
+
+ <!-- Mysql Connector -->
+ <dependency>
+ <groupId>com.mysql</groupId>
+ <artifactId>mysql-connector-j</artifactId>
+ </dependency>
+ <!-- Oracle -->
+ <dependency>
+ <groupId>com.oracle.database.jdbc</groupId>
+ <artifactId>ojdbc8</artifactId>
+ </dependency>
+ <!-- PostgreSql -->
+ <dependency>
+ <groupId>org.postgresql</groupId>
+ <artifactId>postgresql</artifactId>
+ </dependency>
+ <!-- SqlServer -->
+ <dependency>
+ <groupId>com.microsoft.sqlserver</groupId>
+ <artifactId>mssql-jdbc</artifactId>
+ </dependency>
+
+ </dependencies>
+
+</project>
diff --git a/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/annotation/DataColumn.java b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/annotation/DataColumn.java
new file mode 100644
index 0000000..aca470f
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/annotation/DataColumn.java
@@ -0,0 +1,28 @@
+package org.dromara.common.mybatis.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 鏁版嵁鏉冮檺
+ *
+ * 涓�涓敞瑙e彧鑳藉搴斾竴涓ā鏉�
+ *
+ * @author Lion Li
+ * @version 3.5.0
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface DataColumn {
+
+ /**
+ * 鍗犱綅绗﹀叧閿瓧
+ */
+ String[] key() default "deptName";
+
+ /**
+ * 鍗犱綅绗︽浛鎹㈠��
+ */
+ String[] value() default "dept_id";
+
+}
diff --git a/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/annotation/DataPermission.java b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/annotation/DataPermission.java
new file mode 100644
index 0000000..f4351e3
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/annotation/DataPermission.java
@@ -0,0 +1,18 @@
+package org.dromara.common.mybatis.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 鏁版嵁鏉冮檺缁�
+ *
+ * @author Lion Li
+ * @version 3.5.0
+ */
+@Target({ElementType.METHOD, ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface DataPermission {
+
+ DataColumn[] value();
+
+}
diff --git a/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/config/MybatisPlusConfiguration.java b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/config/MybatisPlusConfiguration.java
new file mode 100644
index 0000000..e0ec34b
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/config/MybatisPlusConfiguration.java
@@ -0,0 +1,116 @@
+package org.dromara.common.mybatis.config;
+
+import cn.hutool.core.net.NetUtil;
+import com.baomidou.mybatisplus.autoconfigure.DdlApplicationRunner;
+import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration;
+import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
+import com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator;
+import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
+import com.baomidou.mybatisplus.extension.ddl.IDdl;
+import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
+import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
+import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
+import org.dromara.common.core.factory.YmlPropertySourceFactory;
+import org.dromara.common.mybatis.handler.InjectionMetaObjectHandler;
+import org.dromara.common.mybatis.interceptor.PlusDataPermissionInterceptor;
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.PropertySource;
+import org.springframework.transaction.annotation.EnableTransactionManagement;
+
+import java.util.List;
+
+/**
+ * mybatis-plus閰嶇疆绫�(涓嬫柟娉ㄩ噴鏈夋彃浠朵粙缁�)
+ *
+ * @author Lion Li
+ */
+@EnableTransactionManagement(proxyTargetClass = true)
+@AutoConfiguration(before = MybatisPlusAutoConfiguration.class)
+@MapperScan("${mybatis-plus.mapperPackage}")
+@PropertySource(value = "classpath:common-mybatis.yml", factory = YmlPropertySourceFactory.class)
+public class MybatisPlusConfiguration {
+
+ @Bean
+ public MybatisPlusInterceptor mybatisPlusInterceptor() {
+ MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
+ // 鏁版嵁鏉冮檺澶勭悊
+ interceptor.addInnerInterceptor(dataPermissionInterceptor());
+ // 鍒嗛〉鎻掍欢
+ interceptor.addInnerInterceptor(paginationInnerInterceptor());
+ // 涔愯閿佹彃浠�
+ interceptor.addInnerInterceptor(optimisticLockerInnerInterceptor());
+ return interceptor;
+ }
+
+ /**
+ * 鏁版嵁鏉冮檺鎷︽埅鍣�
+ */
+ public PlusDataPermissionInterceptor dataPermissionInterceptor() {
+ return new PlusDataPermissionInterceptor();
+ }
+
+ /**
+ * 鍒嗛〉鎻掍欢锛岃嚜鍔ㄨ瘑鍒暟鎹簱绫诲瀷
+ */
+ public PaginationInnerInterceptor paginationInnerInterceptor() {
+ PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
+ // 璁剧疆鏈�澶у崟椤甸檺鍒舵暟閲忥紝榛樿 500 鏉★紝-1 涓嶅彈闄愬埗
+ paginationInnerInterceptor.setMaxLimit(-1L);
+ // 鍒嗛〉鍚堢悊鍖�
+ paginationInnerInterceptor.setOverflow(true);
+ return paginationInnerInterceptor;
+ }
+
+ /**
+ * 涔愯閿佹彃浠�
+ */
+ public OptimisticLockerInnerInterceptor optimisticLockerInnerInterceptor() {
+ return new OptimisticLockerInnerInterceptor();
+ }
+
+ /**
+ * 鍏冨璞″瓧娈靛~鍏呮帶鍒跺櫒
+ */
+ @Bean
+ public MetaObjectHandler metaObjectHandler() {
+ return new InjectionMetaObjectHandler();
+ }
+
+ /**
+ * 浣跨敤缃戝崱淇℃伅缁戝畾闆姳鐢熸垚鍣�
+ * 闃叉闆嗙兢闆姳ID閲嶅
+ */
+ @Bean
+ public IdentifierGenerator idGenerator() {
+ return new DefaultIdentifierGenerator(NetUtil.getLocalhost());
+ }
+
+ /**
+ * PaginationInnerInterceptor 鍒嗛〉鎻掍欢锛岃嚜鍔ㄨ瘑鍒暟鎹簱绫诲瀷
+ * https://baomidou.com/pages/97710a/
+ * OptimisticLockerInnerInterceptor 涔愯閿佹彃浠�
+ * https://baomidou.com/pages/0d93c0/
+ * MetaObjectHandler 鍏冨璞″瓧娈靛~鍏呮帶鍒跺櫒
+ * https://baomidou.com/pages/4c6bcf/
+ * ISqlInjector sql娉ㄥ叆鍣�
+ * https://baomidou.com/pages/42ea4a/
+ * BlockAttackInnerInterceptor 濡傛灉鏄鍏ㄨ〃鐨勫垹闄ゆ垨鏇存柊鎿嶄綔锛屽氨浼氱粓姝㈣鎿嶄綔
+ * https://baomidou.com/pages/f9a237/
+ * IllegalSQLInnerInterceptor sql鎬ц兘瑙勮寖鎻掍欢(鍨冨溇SQL鎷︽埅)
+ * IdentifierGenerator 鑷畾涔変富閿瓥鐣�
+ * https://baomidou.com/pages/568eb2/
+ * TenantLineInnerInterceptor 澶氱鎴锋彃浠�
+ * https://baomidou.com/pages/aef2f2/
+ * DynamicTableNameInnerInterceptor 鍔ㄦ�佽〃鍚嶆彃浠�
+ * https://baomidou.com/pages/2a45ff/
+ */
+
+ @Bean
+ public DdlApplicationRunner ddlApplicationRunner(@Autowired(required = false) List<IDdl> ddlList) {
+ return new DdlApplicationRunner(ddlList);
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/core/domain/BaseEntity.java b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/core/domain/BaseEntity.java
new file mode 100644
index 0000000..820b49a
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/core/domain/BaseEntity.java
@@ -0,0 +1,71 @@
+package org.dromara.common.mybatis.core.domain;
+
+import com.baomidou.mybatisplus.annotation.FieldFill;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Entity鍩虹被
+ *
+ * @author Lion Li
+ */
+
+@Data
+public class BaseEntity implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 鎼滅储鍊�
+ */
+ @JsonIgnore
+ @TableField(exist = false)
+ private String searchValue;
+
+ /**
+ * 鍒涘缓閮ㄩ棬
+ */
+ @TableField(fill = FieldFill.INSERT)
+ private Long createDept;
+
+ /**
+ * 鍒涘缓鑰�
+ */
+ @TableField(fill = FieldFill.INSERT)
+ private Long createBy;
+
+ /**
+ * 鍒涘缓鏃堕棿
+ */
+ @TableField(fill = FieldFill.INSERT)
+ private Date createTime;
+
+ /**
+ * 鏇存柊鑰�
+ */
+ @TableField(fill = FieldFill.INSERT_UPDATE)
+ private Long updateBy;
+
+ /**
+ * 鏇存柊鏃堕棿
+ */
+ @TableField(fill = FieldFill.INSERT_UPDATE)
+ private Date updateTime;
+
+ /**
+ * 璇锋眰鍙傛暟
+ */
+ @JsonInclude(JsonInclude.Include.NON_EMPTY)
+ @TableField(exist = false)
+ private Map<String, Object> params = new HashMap<>();
+
+}
diff --git a/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/core/mapper/BaseMapperPlus.java b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/core/mapper/BaseMapperPlus.java
new file mode 100644
index 0000000..b14e2d2
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/core/mapper/BaseMapperPlus.java
@@ -0,0 +1,198 @@
+package org.dromara.common.mybatis.core.mapper;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjectUtil;
+import com.baomidou.mybatisplus.core.conditions.Wrapper;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.core.toolkit.ReflectionKit;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.toolkit.Db;
+import org.apache.ibatis.logging.Log;
+import org.apache.ibatis.logging.LogFactory;
+import org.dromara.common.core.utils.MapstructUtils;
+
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * 鑷畾涔� Mapper 鎺ュ彛, 瀹炵幇 鑷畾涔夋墿灞�
+ *
+ * @param <T> table 娉涘瀷
+ * @param <V> vo 娉涘瀷
+ * @author Lion Li
+ * @since 2021-05-13
+ */
+@SuppressWarnings("unchecked")
+public interface BaseMapperPlus<T, V> extends BaseMapper<T> {
+
+ Log log = LogFactory.getLog(BaseMapperPlus.class);
+
+ default Class<V> currentVoClass() {
+ return (Class<V>) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseMapperPlus.class, 1);
+ }
+
+ default Class<T> currentModelClass() {
+ return (Class<T>) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseMapperPlus.class, 0);
+ }
+
+ default List<T> selectList() {
+ return this.selectList(new QueryWrapper<>());
+ }
+
+ /**
+ * 鎵归噺鎻掑叆
+ */
+ default boolean insertBatch(Collection<T> entityList) {
+ return Db.saveBatch(entityList);
+ }
+
+ /**
+ * 鎵归噺鏇存柊
+ */
+ default boolean updateBatchById(Collection<T> entityList) {
+ return Db.updateBatchById(entityList);
+ }
+
+ /**
+ * 鎵归噺鎻掑叆鎴栨洿鏂�
+ */
+ default boolean insertOrUpdateBatch(Collection<T> entityList) {
+ return Db.saveOrUpdateBatch(entityList);
+ }
+
+ /**
+ * 鎵归噺鎻掑叆(鍖呭惈闄愬埗鏉℃暟)
+ */
+ default boolean insertBatch(Collection<T> entityList, int batchSize) {
+ return Db.saveBatch(entityList, batchSize);
+ }
+
+ /**
+ * 鎵归噺鏇存柊(鍖呭惈闄愬埗鏉℃暟)
+ */
+ default boolean updateBatchById(Collection<T> entityList, int batchSize) {
+ return Db.updateBatchById(entityList, batchSize);
+ }
+
+ /**
+ * 鎵归噺鎻掑叆鎴栨洿鏂�(鍖呭惈闄愬埗鏉℃暟)
+ */
+ default boolean insertOrUpdateBatch(Collection<T> entityList, int batchSize) {
+ return Db.saveOrUpdateBatch(entityList, batchSize);
+ }
+
+ /**
+ * 鎻掑叆鎴栨洿鏂�(鍖呭惈闄愬埗鏉℃暟)
+ */
+ default boolean insertOrUpdate(T entity) {
+ return Db.saveOrUpdate(entity);
+ }
+
+ default V selectVoById(Serializable id) {
+ return selectVoById(id, this.currentVoClass());
+ }
+
+ /**
+ * 鏍规嵁 ID 鏌ヨ
+ */
+ default <C> C selectVoById(Serializable id, Class<C> voClass) {
+ T obj = this.selectById(id);
+ if (ObjectUtil.isNull(obj)) {
+ return null;
+ }
+ return MapstructUtils.convert(obj, voClass);
+ }
+
+ default List<V> selectVoBatchIds(Collection<? extends Serializable> idList) {
+ return selectVoBatchIds(idList, this.currentVoClass());
+ }
+
+ /**
+ * 鏌ヨ锛堟牴鎹甀D 鎵归噺鏌ヨ锛�
+ */
+ default <C> List<C> selectVoBatchIds(Collection<? extends Serializable> idList, Class<C> voClass) {
+ List<T> list = this.selectBatchIds(idList);
+ if (CollUtil.isEmpty(list)) {
+ return CollUtil.newArrayList();
+ }
+ return MapstructUtils.convert(list, voClass);
+ }
+
+ default List<V> selectVoByMap(Map<String, Object> map) {
+ return selectVoByMap(map, this.currentVoClass());
+ }
+
+ /**
+ * 鏌ヨ锛堟牴鎹� columnMap 鏉′欢锛�
+ */
+ default <C> List<C> selectVoByMap(Map<String, Object> map, Class<C> voClass) {
+ List<T> list = this.selectByMap(map);
+ if (CollUtil.isEmpty(list)) {
+ return CollUtil.newArrayList();
+ }
+ return MapstructUtils.convert(list, voClass);
+ }
+
+ default V selectVoOne(Wrapper<T> wrapper) {
+ return selectVoOne(wrapper, this.currentVoClass());
+ }
+
+ /**
+ * 鏍规嵁 entity 鏉′欢锛屾煡璇竴鏉¤褰�
+ */
+ default <C> C selectVoOne(Wrapper<T> wrapper, Class<C> voClass) {
+ T obj = this.selectOne(wrapper);
+ if (ObjectUtil.isNull(obj)) {
+ return null;
+ }
+ return MapstructUtils.convert(obj, voClass);
+ }
+
+ default List<V> selectVoList() {
+ return selectVoList(new QueryWrapper<>(), this.currentVoClass());
+ }
+
+ default List<V> selectVoList(Wrapper<T> wrapper) {
+ return selectVoList(wrapper, this.currentVoClass());
+ }
+
+ /**
+ * 鏍规嵁 entity 鏉′欢锛屾煡璇㈠叏閮ㄨ褰�
+ */
+ default <C> List<C> selectVoList(Wrapper<T> wrapper, Class<C> voClass) {
+ List<T> list = this.selectList(wrapper);
+ if (CollUtil.isEmpty(list)) {
+ return CollUtil.newArrayList();
+ }
+ return MapstructUtils.convert(list, voClass);
+ }
+
+ default <P extends IPage<V>> P selectVoPage(IPage<T> page, Wrapper<T> wrapper) {
+ return selectVoPage(page, wrapper, this.currentVoClass());
+ }
+
+ /**
+ * 鍒嗛〉鏌ヨVO
+ */
+ default <C, P extends IPage<C>> P selectVoPage(IPage<T> page, Wrapper<T> wrapper, Class<C> voClass) {
+ List<T> list = this.selectList(page, wrapper);
+ IPage<C> voPage = new Page<>(page.getCurrent(), page.getSize(), page.getTotal());
+ if (CollUtil.isEmpty(list)) {
+ return (P) voPage;
+ }
+ voPage.setRecords(MapstructUtils.convert(list, voClass));
+ return (P) voPage;
+ }
+
+ default <C> List<C> selectObjs(Wrapper<T> wrapper, Function<? super Object, C> mapper) {
+ return this.selectObjs(wrapper).stream().filter(Objects::nonNull).map(mapper).collect(Collectors.toList());
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/core/page/PageQuery.java b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/core/page/PageQuery.java
new file mode 100644
index 0000000..8ef4a57
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/core/page/PageQuery.java
@@ -0,0 +1,114 @@
+package org.dromara.common.mybatis.core.page;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjectUtil;
+import com.baomidou.mybatisplus.core.metadata.OrderItem;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import org.dromara.common.core.exception.ServiceException;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.core.utils.sql.SqlUtil;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 鍒嗛〉鏌ヨ瀹炰綋绫�
+ *
+ * @author Lion Li
+ */
+
+@Data
+public class PageQuery implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 鍒嗛〉澶у皬
+ */
+ private Integer pageSize;
+
+ /**
+ * 褰撳墠椤垫暟
+ */
+ private Integer pageNum;
+
+ /**
+ * 鎺掑簭鍒�
+ */
+ private String orderByColumn;
+
+ /**
+ * 鎺掑簭鐨勬柟鍚慸esc鎴栬�卆sc
+ */
+ private String isAsc;
+
+ /**
+ * 褰撳墠璁板綍璧峰绱㈠紩 榛樿鍊�
+ */
+ public static final int DEFAULT_PAGE_NUM = 1;
+
+ /**
+ * 姣忛〉鏄剧ず璁板綍鏁� 榛樿鍊� 榛樿鏌ュ叏閮�
+ */
+ public static final int DEFAULT_PAGE_SIZE = Integer.MAX_VALUE;
+
+ public <T> Page<T> build() {
+ Integer pageNum = ObjectUtil.defaultIfNull(getPageNum(), DEFAULT_PAGE_NUM);
+ Integer pageSize = ObjectUtil.defaultIfNull(getPageSize(), DEFAULT_PAGE_SIZE);
+ if (pageNum <= 0) {
+ pageNum = DEFAULT_PAGE_NUM;
+ }
+ Page<T> page = new Page<>(pageNum, pageSize);
+ List<OrderItem> orderItems = buildOrderItem();
+ if (CollUtil.isNotEmpty(orderItems)) {
+ page.addOrder(orderItems);
+ }
+ return page;
+ }
+
+ /**
+ * 鏋勫缓鎺掑簭
+ *
+ * 鏀寔鐨勭敤娉曞涓�:
+ * {isAsc:"asc",orderByColumn:"id"} order by id asc
+ * {isAsc:"asc",orderByColumn:"id,createTime"} order by id asc,create_time asc
+ * {isAsc:"desc",orderByColumn:"id,createTime"} order by id desc,create_time desc
+ * {isAsc:"asc,desc",orderByColumn:"id,createTime"} order by id asc,create_time desc
+ */
+ private List<OrderItem> buildOrderItem() {
+ if (StringUtils.isBlank(orderByColumn) || StringUtils.isBlank(isAsc)) {
+ return null;
+ }
+ String orderBy = SqlUtil.escapeOrderBySql(orderByColumn);
+ orderBy = StringUtils.toUnderScoreCase(orderBy);
+
+ // 鍏煎鍓嶇鎺掑簭绫诲瀷
+ isAsc = StringUtils.replaceEach(isAsc, new String[]{"ascending", "descending"}, new String[]{"asc", "desc"});
+
+ String[] orderByArr = orderBy.split(StringUtils.SEPARATOR);
+ String[] isAscArr = isAsc.split(StringUtils.SEPARATOR);
+ if (isAscArr.length != 1 && isAscArr.length != orderByArr.length) {
+ throw new ServiceException("鎺掑簭鍙傛暟鏈夎");
+ }
+
+ List<OrderItem> list = new ArrayList<>();
+ // 姣忎釜瀛楁鍚勮嚜鎺掑簭
+ for (int i = 0; i < orderByArr.length; i++) {
+ String orderByStr = orderByArr[i];
+ String isAscStr = isAscArr.length == 1 ? isAscArr[0] : isAscArr[i];
+ if ("asc".equals(isAscStr)) {
+ list.add(OrderItem.asc(orderByStr));
+ } else if ("desc".equals(isAscStr)) {
+ list.add(OrderItem.desc(orderByStr));
+ } else {
+ throw new ServiceException("鎺掑簭鍙傛暟鏈夎");
+ }
+ }
+ return list;
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/core/page/TableDataInfo.java b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/core/page/TableDataInfo.java
new file mode 100644
index 0000000..a4b6799
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/core/page/TableDataInfo.java
@@ -0,0 +1,81 @@
+package org.dromara.common.mybatis.core.page;
+
+import cn.hutool.http.HttpStatus;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 琛ㄦ牸鍒嗛〉鏁版嵁瀵硅薄
+ *
+ * @author Lion Li
+ */
+
+@Data
+@NoArgsConstructor
+public class TableDataInfo<T> implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 鎬昏褰曟暟
+ */
+ private long total;
+
+ /**
+ * 鍒楄〃鏁版嵁
+ */
+ private List<T> rows;
+
+ /**
+ * 娑堟伅鐘舵�佺爜
+ */
+ private int code;
+
+ /**
+ * 娑堟伅鍐呭
+ */
+ private String msg;
+
+ /**
+ * 鍒嗛〉
+ *
+ * @param list 鍒楄〃鏁版嵁
+ * @param total 鎬昏褰曟暟
+ */
+ public TableDataInfo(List<T> list, long total) {
+ this.rows = list;
+ this.total = total;
+ }
+
+ public static <T> TableDataInfo<T> build(IPage<T> page) {
+ TableDataInfo<T> rspData = new TableDataInfo<>();
+ rspData.setCode(HttpStatus.HTTP_OK);
+ rspData.setMsg("鏌ヨ鎴愬姛");
+ rspData.setRows(page.getRecords());
+ rspData.setTotal(page.getTotal());
+ return rspData;
+ }
+
+ public static <T> TableDataInfo<T> build(List<T> list) {
+ TableDataInfo<T> rspData = new TableDataInfo<>();
+ rspData.setCode(HttpStatus.HTTP_OK);
+ rspData.setMsg("鏌ヨ鎴愬姛");
+ rspData.setRows(list);
+ rspData.setTotal(list.size());
+ return rspData;
+ }
+
+ public static <T> TableDataInfo<T> build() {
+ TableDataInfo<T> rspData = new TableDataInfo<>();
+ rspData.setCode(HttpStatus.HTTP_OK);
+ rspData.setMsg("鏌ヨ鎴愬姛");
+ return rspData;
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/enums/DataBaseType.java b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/enums/DataBaseType.java
new file mode 100644
index 0000000..93487e9
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/enums/DataBaseType.java
@@ -0,0 +1,49 @@
+package org.dromara.common.mybatis.enums;
+
+import org.dromara.common.core.utils.StringUtils;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 鏁版嵁搴撶被鍨�
+ *
+ * @author Lion Li
+ */
+@Getter
+@AllArgsConstructor
+public enum DataBaseType {
+
+ /**
+ * MySQL
+ */
+ MY_SQL("MySQL"),
+
+ /**
+ * Oracle
+ */
+ ORACLE("Oracle"),
+
+ /**
+ * PostgreSQL
+ */
+ POSTGRE_SQL("PostgreSQL"),
+
+ /**
+ * SQL Server
+ */
+ SQL_SERVER("Microsoft SQL Server");
+
+ private final String type;
+
+ public static DataBaseType find(String databaseProductName) {
+ if (StringUtils.isBlank(databaseProductName)) {
+ return null;
+ }
+ for (DataBaseType type : values()) {
+ if (type.getType().equals(databaseProductName)) {
+ return type;
+ }
+ }
+ return null;
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/enums/DataScopeType.java b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/enums/DataScopeType.java
new file mode 100644
index 0000000..9ea66b0
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/enums/DataScopeType.java
@@ -0,0 +1,73 @@
+package org.dromara.common.mybatis.enums;
+
+import org.dromara.common.core.utils.StringUtils;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.dromara.common.mybatis.helper.DataPermissionHelper;
+
+/**
+ * 鏁版嵁鏉冮檺绫诲瀷
+ * <p>
+ * 璇硶鏀寔 spel 妯℃澘琛ㄨ揪寮�
+ * <p>
+ * 鍐呯疆鏁版嵁 user 褰撳墠鐢ㄦ埛 鍐呭鍙傝�� LoginUser
+ * 濡傞渶鎵╁睍鏁版嵁 鍙娇鐢� {@link DataPermissionHelper} 鎿嶄綔
+ * 鍐呯疆鏈嶅姟 sdss 绯荤粺鏁版嵁鏉冮檺鏈嶅姟 鍐呭鍙傝�� SysDataScopeService
+ * 濡傞渶鎵╁睍鏇村鑷畾涔夋湇鍔� 鍙互鍙傝�� sdss 鑷缂栧啓
+ *
+ * @author Lion Li
+ * @version 3.5.0
+ */
+@Getter
+@AllArgsConstructor
+public enum DataScopeType {
+
+ /**
+ * 鍏ㄩ儴鏁版嵁鏉冮檺
+ */
+ ALL("1", "", ""),
+
+ /**
+ * 鑷畾鏁版嵁鏉冮檺
+ */
+ CUSTOM("2", " #{#deptName} IN ( #{@sdss.getRoleCustom( #user.roleId )} ) ", " 1 = 0 "),
+
+ /**
+ * 閮ㄩ棬鏁版嵁鏉冮檺
+ */
+ DEPT("3", " #{#deptName} = #{#user.deptId} ", " 1 = 0 "),
+
+ /**
+ * 閮ㄩ棬鍙婁互涓嬫暟鎹潈闄�
+ */
+ DEPT_AND_CHILD("4", " #{#deptName} IN ( #{@sdss.getDeptAndChild( #user.deptId )} )", " 1 = 0 "),
+
+ /**
+ * 浠呮湰浜烘暟鎹潈闄�
+ */
+ SELF("5", " #{#userName} = #{#user.userId} ", " 1 = 0 ");
+
+ private final String code;
+
+ /**
+ * 璇硶 閲囩敤 spel 妯℃澘琛ㄨ揪寮�
+ */
+ private final String sqlTemplate;
+
+ /**
+ * 涓嶆弧瓒� sqlTemplate 鍒欏~鍏�
+ */
+ private final String elseSql;
+
+ public static DataScopeType findCode(String code) {
+ if (StringUtils.isBlank(code)) {
+ return null;
+ }
+ for (DataScopeType type : values()) {
+ if (type.getCode().equals(code)) {
+ return type;
+ }
+ }
+ return null;
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/filter/DubboDataPermissionFilter.java b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/filter/DubboDataPermissionFilter.java
new file mode 100644
index 0000000..61e549d
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/filter/DubboDataPermissionFilter.java
@@ -0,0 +1,28 @@
+package org.dromara.common.mybatis.filter;
+
+import org.dromara.common.mybatis.helper.DataPermissionHelper;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.dubbo.common.constants.CommonConstants;
+import org.apache.dubbo.common.extension.Activate;
+import org.apache.dubbo.rpc.*;
+
+import java.util.Map;
+
+/**
+ * dubbo 鏁版嵁鏉冮檺鍙傛暟浼犻��
+ *
+ * @author Lion Li
+ */
+@Slf4j
+@Activate(group = {CommonConstants.CONSUMER})
+public class DubboDataPermissionFilter implements Filter {
+
+ @Override
+ public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
+ RpcServiceContext context = RpcContext.getServiceContext();
+ Map<String, Object> dataPermissionContext = DataPermissionHelper.getContext();
+ context.setObjectAttachment(DataPermissionHelper.DATA_PERMISSION_KEY, dataPermissionContext);
+ return invoker.invoke(invocation);
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/handler/InjectionMetaObjectHandler.java b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/handler/InjectionMetaObjectHandler.java
new file mode 100644
index 0000000..c4ff3bf
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/handler/InjectionMetaObjectHandler.java
@@ -0,0 +1,82 @@
+package org.dromara.common.mybatis.handler;
+
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.http.HttpStatus;
+import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.ibatis.reflection.MetaObject;
+import org.dromara.common.core.exception.ServiceException;
+import org.dromara.common.mybatis.core.domain.BaseEntity;
+import org.dromara.common.satoken.utils.LoginHelper;
+import org.dromara.system.api.model.LoginUser;
+
+import java.util.Date;
+
+/**
+ * MP娉ㄥ叆澶勭悊鍣�
+ *
+ * @author Lion Li
+ */
+@Slf4j
+public class InjectionMetaObjectHandler implements MetaObjectHandler {
+
+ @Override
+ public void insertFill(MetaObject metaObject) {
+ try {
+ if (ObjectUtil.isNotNull(metaObject)
+ && metaObject.getOriginalObject() instanceof BaseEntity baseEntity) {
+ Date current = ObjectUtil.isNotNull(baseEntity.getCreateTime())
+ ? baseEntity.getCreateTime() : new Date();
+ baseEntity.setCreateTime(current);
+ baseEntity.setUpdateTime(current);
+ LoginUser loginUser = getLoginUser();
+ if (ObjectUtil.isNotNull(loginUser)) {
+ Long userId = ObjectUtil.isNotNull(baseEntity.getCreateBy())
+ ? baseEntity.getCreateBy() : loginUser.getUserId();
+ // 褰撳墠宸茬櫥褰� 涓� 鍒涘缓浜轰负绌� 鍒欏~鍏�
+ baseEntity.setCreateBy(userId);
+ // 褰撳墠宸茬櫥褰� 涓� 鏇存柊浜轰负绌� 鍒欏~鍏�
+ baseEntity.setUpdateBy(userId);
+ baseEntity.setCreateDept(ObjectUtil.isNotNull(baseEntity.getCreateDept())
+ ? baseEntity.getCreateDept() : loginUser.getDeptId());
+ }
+ }
+ } catch (Exception e) {
+ throw new ServiceException("鑷姩娉ㄥ叆寮傚父 => " + e.getMessage(), HttpStatus.HTTP_UNAUTHORIZED);
+ }
+ }
+
+ @Override
+ public void updateFill(MetaObject metaObject) {
+ try {
+ if (ObjectUtil.isNotNull(metaObject)
+ && metaObject.getOriginalObject() instanceof BaseEntity baseEntity) {
+ Date current = new Date();
+ // 鏇存柊鏃堕棿濉厖(涓嶇涓轰笉涓虹┖)
+ baseEntity.setUpdateTime(current);
+ LoginUser loginUser = getLoginUser();
+ // 褰撳墠宸茬櫥褰� 鏇存柊浜哄~鍏�(涓嶇涓轰笉涓虹┖)
+ if (ObjectUtil.isNotNull(loginUser)) {
+ baseEntity.setUpdateBy(loginUser.getUserId());
+ }
+ }
+ } catch (Exception e) {
+ throw new ServiceException("鑷姩娉ㄥ叆寮傚父 => " + e.getMessage(), HttpStatus.HTTP_UNAUTHORIZED);
+ }
+ }
+
+ /**
+ * 鑾峰彇鐧诲綍鐢ㄦ埛
+ */
+ private LoginUser getLoginUser() {
+ LoginUser loginUser;
+ try {
+ loginUser = LoginHelper.getLoginUser();
+ } catch (Exception e) {
+ log.warn("鑷姩娉ㄥ叆璀﹀憡 => 鐢ㄦ埛鏈櫥褰�");
+ return null;
+ }
+ return loginUser;
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/handler/MybatisExceptionHandler.java b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/handler/MybatisExceptionHandler.java
new file mode 100644
index 0000000..c517fa7
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/handler/MybatisExceptionHandler.java
@@ -0,0 +1,46 @@
+package org.dromara.common.mybatis.handler;
+
+import org.dromara.common.core.domain.R;
+import lombok.extern.slf4j.Slf4j;
+import org.mybatis.spring.MyBatisSystemException;
+import org.springframework.dao.DuplicateKeyException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+import jakarta.servlet.http.HttpServletRequest;
+
+/**
+ * Mybatis寮傚父澶勭悊鍣�
+ *
+ * @author Lion Li
+ */
+@Slf4j
+@RestControllerAdvice
+public class MybatisExceptionHandler {
+
+ /**
+ * 涓婚敭鎴朥NIQUE绱㈠紩锛屾暟鎹噸澶嶅紓甯�
+ */
+ @ExceptionHandler(DuplicateKeyException.class)
+ public R<Void> handleDuplicateKeyException(DuplicateKeyException e, HttpServletRequest request) {
+ String requestURI = request.getRequestURI();
+ log.error("璇锋眰鍦板潃'{}',鏁版嵁搴撲腑宸插瓨鍦ㄨ褰�'{}'", requestURI, e.getMessage());
+ return R.fail("鏁版嵁搴撲腑宸插瓨鍦ㄨ璁板綍锛岃鑱旂郴绠$悊鍛樼‘璁�");
+ }
+
+ /**
+ * Mybatis绯荤粺寮傚父 閫氱敤澶勭悊
+ */
+ @ExceptionHandler(MyBatisSystemException.class)
+ public R<Void> handleCannotFindDataSourceException(MyBatisSystemException e, HttpServletRequest request) {
+ String requestURI = request.getRequestURI();
+ String message = e.getMessage();
+ if ("CannotFindDataSourceException".contains(message)) {
+ log.error("璇锋眰鍦板潃'{}', 鏈壘鍒版暟鎹簮", requestURI);
+ return R.fail("鏈壘鍒版暟鎹簮锛岃鑱旂郴绠$悊鍛樼‘璁�");
+ }
+ log.error("璇锋眰鍦板潃'{}', Mybatis绯荤粺寮傚父", requestURI, e);
+ return R.fail(message);
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/handler/PlusDataPermissionHandler.java b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/handler/PlusDataPermissionHandler.java
new file mode 100644
index 0000000..dc4f9cb
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/handler/PlusDataPermissionHandler.java
@@ -0,0 +1,186 @@
+package org.dromara.common.mybatis.handler;
+
+import cn.hutool.core.annotation.AnnotationUtil;
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ClassUtil;
+import cn.hutool.core.util.ObjectUtil;
+import lombok.extern.slf4j.Slf4j;
+import net.sf.jsqlparser.JSQLParserException;
+import net.sf.jsqlparser.expression.Expression;
+import net.sf.jsqlparser.expression.Parenthesis;
+import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
+import net.sf.jsqlparser.parser.CCJSqlParserUtil;
+import org.dromara.common.core.exception.ServiceException;
+import org.dromara.common.core.utils.SpringUtils;
+import org.dromara.common.core.utils.StreamUtils;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.mybatis.annotation.DataColumn;
+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.dromara.system.api.model.LoginUser;
+import org.dromara.system.api.model.RoleDTO;
+import org.springframework.context.expression.BeanFactoryResolver;
+import org.springframework.expression.BeanResolver;
+import org.springframework.expression.ExpressionParser;
+import org.springframework.expression.ParserContext;
+import org.springframework.expression.common.TemplateParserContext;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
+import org.springframework.expression.spel.support.StandardEvaluationContext;
+
+import java.lang.reflect.Method;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Function;
+
+/**
+ * 鏁版嵁鏉冮檺杩囨护
+ *
+ * @author Lion Li
+ * @version 3.5.0
+ */
+@Slf4j
+public class PlusDataPermissionHandler {
+
+ /**
+ * 鏂规硶鎴栫被(鍚嶇О) 涓� 娉ㄨВ鐨勬槧灏勫叧绯荤紦瀛�
+ */
+ private final Map<String, DataPermission> dataPermissionCacheMap = new ConcurrentHashMap<>();
+
+ /**
+ * spel 瑙f瀽鍣�
+ */
+ private final ExpressionParser parser = new SpelExpressionParser();
+ private final ParserContext parserContext = new TemplateParserContext();
+ /**
+ * bean瑙f瀽鍣� 鐢ㄤ簬澶勭悊 spel 琛ㄨ揪寮忎腑瀵� bean 鐨勮皟鐢�
+ */
+ private final BeanResolver beanResolver = new BeanFactoryResolver(SpringUtils.getBeanFactory());
+
+
+ public Expression getSqlSegment(Expression where, String mappedStatementId, boolean isSelect) {
+ DataColumn[] dataColumns = findAnnotation(mappedStatementId);
+ LoginUser currentUser = DataPermissionHelper.getVariable("user");
+ if (ObjectUtil.isNull(currentUser)) {
+ currentUser = LoginHelper.getLoginUser();
+ DataPermissionHelper.setVariable("user", currentUser);
+ }
+ // 濡傛灉鏄秴绾х鐞嗗憳鎴栫鎴风鐞嗗憳锛屽垯涓嶈繃婊ゆ暟鎹�
+ if (LoginHelper.isSuperAdmin() || LoginHelper.isTenantAdmin()) {
+ return where;
+ }
+ String dataFilterSql = buildDataFilter(dataColumns, isSelect);
+ if (StringUtils.isBlank(dataFilterSql)) {
+ return where;
+ }
+ try {
+ Expression expression = CCJSqlParserUtil.parseExpression(dataFilterSql);
+ // 鏁版嵁鏉冮檺浣跨敤鍗曠嫭鐨勬嫭鍙� 闃叉涓庡叾浠栨潯浠跺啿绐�
+ Parenthesis parenthesis = new Parenthesis(expression);
+ if (ObjectUtil.isNotNull(where)) {
+ return new AndExpression(where, parenthesis);
+ } else {
+ return parenthesis;
+ }
+ } catch (JSQLParserException e) {
+ throw new ServiceException("鏁版嵁鏉冮檺瑙f瀽寮傚父 => " + e.getMessage());
+ }
+ }
+
+ /**
+ * 鏋勯�犳暟鎹繃婊ql
+ */
+ private String buildDataFilter(DataColumn[] dataColumns, boolean isSelect) {
+ // 鏇存柊鎴栧垹闄ら渶婊¤冻鎵�鏈夋潯浠�
+ String joinStr = isSelect ? " OR " : " AND ";
+ LoginUser user = DataPermissionHelper.getVariable("user");
+ StandardEvaluationContext context = new StandardEvaluationContext();
+ context.setBeanResolver(beanResolver);
+ DataPermissionHelper.getContext().forEach(context::setVariable);
+ Set<String> conditions = new HashSet<>();
+ for (RoleDTO role : user.getRoles()) {
+ user.setRoleId(role.getRoleId());
+ // 鑾峰彇瑙掕壊鏉冮檺娉涘瀷
+ DataScopeType type = DataScopeType.findCode(role.getDataScope());
+ if (ObjectUtil.isNull(type)) {
+ throw new ServiceException("瑙掕壊鏁版嵁鑼冨洿寮傚父 => " + role.getDataScope());
+ }
+ // 鍏ㄩ儴鏁版嵁鏉冮檺鐩存帴杩斿洖
+ if (type == DataScopeType.ALL) {
+ return "";
+ }
+ boolean isSuccess = false;
+ for (DataColumn dataColumn : dataColumns) {
+ if (dataColumn.key().length != dataColumn.value().length) {
+ throw new ServiceException("瑙掕壊鏁版嵁鑼冨洿寮傚父 => key涓巚alue闀垮害涓嶅尮閰�");
+ }
+ // 涓嶅寘鍚� key 鍙橀噺 鍒欎笉澶勭悊
+ if (!StringUtils.containsAny(type.getSqlTemplate(),
+ Arrays.stream(dataColumn.key()).map(key -> "#" + key).toArray(String[]::new)
+ )) {
+ continue;
+ }
+ // 璁剧疆娉ㄨВ鍙橀噺 key 涓鸿〃杈惧紡鍙橀噺 value 涓哄彉閲忓��
+ for (int i = 0; i < dataColumn.key().length; i++) {
+ context.setVariable(dataColumn.key()[i], dataColumn.value()[i]);
+ }
+
+ // 瑙f瀽sql妯℃澘骞跺~鍏�
+ String sql = parser.parseExpression(type.getSqlTemplate(), parserContext).getValue(context, String.class);
+ conditions.add(joinStr + sql);
+ isSuccess = true;
+ }
+ // 鏈鐞嗘垚鍔熷垯濉厖鍏滃簳鏂规
+ if (!isSuccess && StringUtils.isNotBlank(type.getElseSql())) {
+ conditions.add(joinStr + type.getElseSql());
+ }
+ }
+
+ if (CollUtil.isNotEmpty(conditions)) {
+ String sql = StreamUtils.join(conditions, Function.identity(), "");
+ return sql.substring(joinStr.length());
+ }
+ return "";
+ }
+
+ public DataColumn[] findAnnotation(String mappedStatementId) {
+ StringBuilder sb = new StringBuilder(mappedStatementId);
+ int index = sb.lastIndexOf(".");
+ String clazzName = sb.substring(0, index);
+ String methodName = sb.substring(index + 1, sb.length());
+ Class<?> clazz;
+ try {
+ clazz = ClassUtil.loadClass(clazzName);
+ } catch (Exception e) {
+ return null;
+ }
+ List<Method> methods = Arrays.stream(ClassUtil.getDeclaredMethods(clazz))
+ .filter(method -> method.getName().equals(methodName)).toList();
+ DataPermission dataPermission;
+ // 鑾峰彇鏂规硶娉ㄨВ
+ for (Method method : methods) {
+ dataPermission = dataPermissionCacheMap.get(mappedStatementId);
+ if (ObjectUtil.isNotNull(dataPermission)) {
+ return dataPermission.value();
+ }
+ if (AnnotationUtil.hasAnnotation(method, DataPermission.class)) {
+ dataPermission = AnnotationUtil.getAnnotation(method, DataPermission.class);
+ dataPermissionCacheMap.put(mappedStatementId, dataPermission);
+ return dataPermission.value();
+ }
+ }
+ dataPermission = dataPermissionCacheMap.get(clazz.getName());
+ if (ObjectUtil.isNotNull(dataPermission)) {
+ return dataPermission.value();
+ }
+ // 鑾峰彇绫绘敞瑙�
+ if (AnnotationUtil.hasAnnotation(clazz, DataPermission.class)) {
+ dataPermission = AnnotationUtil.getAnnotation(clazz, DataPermission.class);
+ dataPermissionCacheMap.put(clazz.getName(), dataPermission);
+ return dataPermission.value();
+ }
+ return null;
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/helper/DataBaseHelper.java b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/helper/DataBaseHelper.java
new file mode 100644
index 0000000..4a29383
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/helper/DataBaseHelper.java
@@ -0,0 +1,82 @@
+package org.dromara.common.mybatis.helper;
+
+import cn.hutool.core.convert.Convert;
+import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
+import org.dromara.common.core.exception.ServiceException;
+import org.dromara.common.core.utils.SpringUtils;
+import org.dromara.common.mybatis.enums.DataBaseType;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+
+import javax.sql.DataSource;
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * 鏁版嵁搴撳姪鎵�
+ *
+ * @author Lion Li
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class DataBaseHelper {
+
+ private static final DynamicRoutingDataSource DS = SpringUtils.getBean(DynamicRoutingDataSource.class);
+
+ /**
+ * 鑾峰彇褰撳墠鏁版嵁搴撶被鍨�
+ */
+ public static DataBaseType getDataBaseType() {
+ DataSource dataSource = DS.determineDataSource();
+ try (Connection conn = dataSource.getConnection()) {
+ DatabaseMetaData metaData = conn.getMetaData();
+ String databaseProductName = metaData.getDatabaseProductName();
+ return DataBaseType.find(databaseProductName);
+ } catch (SQLException e) {
+ throw new ServiceException(e.getMessage());
+ }
+ }
+
+ 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();
+ }
+
+ 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 position(',100,' in ',0,100,101,')) <> 0
+ return "(select position(',%s,' in ','||%s||',')) <> 0".formatted(var, var2);
+ } else if (dataBasyType == DataBaseType.ORACLE) {
+ // 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);
+ }
+
+ /**
+ * 鑾峰彇褰撳墠鍔犺浇鐨勬暟鎹簱鍚�
+ */
+ public static List<String> getDataSourceNameList() {
+ return new ArrayList<>(DS.getDataSources().keySet());
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/helper/DataPermissionHelper.java b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/helper/DataPermissionHelper.java
new file mode 100644
index 0000000..0beae4d
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/helper/DataPermissionHelper.java
@@ -0,0 +1,93 @@
+package org.dromara.common.mybatis.helper;
+
+import cn.dev33.satoken.context.SaHolder;
+import cn.dev33.satoken.context.model.SaStorage;
+import cn.hutool.core.util.ObjectUtil;
+import com.baomidou.mybatisplus.core.plugins.IgnoreStrategy;
+import com.baomidou.mybatisplus.core.plugins.InterceptorIgnoreHelper;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Supplier;
+
+/**
+ * 鏁版嵁鏉冮檺鍔╂墜
+ *
+ * @author Lion Li
+ * @version 3.5.0
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+@SuppressWarnings("unchecked cast")
+public class DataPermissionHelper {
+
+ public static final String DATA_PERMISSION_KEY = "data:permission";
+
+ public static <T> T getVariable(String key) {
+ Map<String, Object> context = getContext();
+ return (T) context.get(key);
+ }
+
+
+ public static void setVariable(String key, Object value) {
+ Map<String, Object> context = getContext();
+ context.put(key, value);
+ }
+
+ public static Map<String, Object> getContext() {
+ SaStorage saStorage = SaHolder.getStorage();
+ Object attribute = saStorage.get(DATA_PERMISSION_KEY);
+ if (ObjectUtil.isNull(attribute)) {
+ saStorage.set(DATA_PERMISSION_KEY, new HashMap<>());
+ attribute = saStorage.get(DATA_PERMISSION_KEY);
+ }
+ if (attribute instanceof Map map) {
+ return map;
+ }
+ throw new NullPointerException("data permission context type exception");
+ }
+
+ /**
+ * 寮�鍚拷鐣ユ暟鎹潈闄�(寮�鍚悗闇�鎵嬪姩璋冪敤 {@link #disableIgnore()} 鍏抽棴)
+ */
+ public static void enableIgnore() {
+ InterceptorIgnoreHelper.handle(IgnoreStrategy.builder().dataPermission(true).build());
+ }
+
+ /**
+ * 鍏抽棴蹇界暐鏁版嵁鏉冮檺
+ */
+ public static void disableIgnore() {
+ InterceptorIgnoreHelper.clearIgnoreStrategy();
+ }
+
+ /**
+ * 鍦ㄥ拷鐣ユ暟鎹潈闄愪腑鎵ц
+ *
+ * @param handle 澶勭悊鎵ц鏂规硶
+ */
+ public static void ignore(Runnable handle) {
+ enableIgnore();
+ try {
+ handle.run();
+ } finally {
+ disableIgnore();
+ }
+ }
+
+ /**
+ * 鍦ㄥ拷鐣ユ暟鎹潈闄愪腑鎵ц
+ *
+ * @param handle 澶勭悊鎵ц鏂规硶
+ */
+ public static <T> T ignore(Supplier<T> handle) {
+ enableIgnore();
+ try {
+ return handle.get();
+ } finally {
+ disableIgnore();
+ }
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/interceptor/PlusDataPermissionInterceptor.java b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/interceptor/PlusDataPermissionInterceptor.java
new file mode 100644
index 0000000..0ab0c11
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/interceptor/PlusDataPermissionInterceptor.java
@@ -0,0 +1,129 @@
+package org.dromara.common.mybatis.interceptor;
+
+import cn.hutool.core.collection.ConcurrentHashSet;
+import cn.hutool.core.util.ArrayUtil;
+import com.baomidou.mybatisplus.core.plugins.InterceptorIgnoreHelper;
+import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
+import com.baomidou.mybatisplus.extension.parser.JsqlParserSupport;
+import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
+import org.dromara.common.mybatis.annotation.DataColumn;
+import org.dromara.common.mybatis.handler.PlusDataPermissionHandler;
+import net.sf.jsqlparser.expression.Expression;
+import net.sf.jsqlparser.statement.delete.Delete;
+import net.sf.jsqlparser.statement.select.PlainSelect;
+import net.sf.jsqlparser.statement.select.Select;
+import net.sf.jsqlparser.statement.select.SelectBody;
+import net.sf.jsqlparser.statement.select.SetOperationList;
+import net.sf.jsqlparser.statement.update.Update;
+import org.apache.ibatis.executor.Executor;
+import org.apache.ibatis.executor.statement.StatementHandler;
+import org.apache.ibatis.mapping.BoundSql;
+import org.apache.ibatis.mapping.MappedStatement;
+import org.apache.ibatis.mapping.SqlCommandType;
+import org.apache.ibatis.session.ResultHandler;
+import org.apache.ibatis.session.RowBounds;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * 鏁版嵁鏉冮檺鎷︽埅鍣�
+ *
+ * @author Lion Li
+ * @version 3.5.0
+ */
+public class PlusDataPermissionInterceptor extends JsqlParserSupport implements InnerInterceptor {
+
+ private final PlusDataPermissionHandler dataPermissionHandler = new PlusDataPermissionHandler();
+ /**
+ * 鏃犳晥娉ㄨВ鏂规硶缂撳瓨鐢ㄤ簬蹇�熻繑鍥�
+ */
+ private final Set<String> invalidCacheSet = new ConcurrentHashSet<>();
+
+ @Override
+ public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
+ // 妫�鏌ュ拷鐣ユ敞瑙�
+ if (InterceptorIgnoreHelper.willIgnoreDataPermission(ms.getId())) {
+ return;
+ }
+ // 妫�鏌ユ槸鍚︽棤鏁� 鏃犳暟鎹潈闄愭敞瑙�
+ if (invalidCacheSet.contains(ms.getId())) {
+ return;
+ }
+ DataColumn[] dataColumns = dataPermissionHandler.findAnnotation(ms.getId());
+ if (ArrayUtil.isEmpty(dataColumns)) {
+ invalidCacheSet.add(ms.getId());
+ return;
+ }
+ // 瑙f瀽 sql 鍒嗛厤瀵瑰簲鏂规硶
+ PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);
+ mpBs.sql(parserSingle(mpBs.sql(), ms.getId()));
+ }
+
+ @Override
+ public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) {
+ PluginUtils.MPStatementHandler mpSh = PluginUtils.mpStatementHandler(sh);
+ MappedStatement ms = mpSh.mappedStatement();
+ SqlCommandType sct = ms.getSqlCommandType();
+ if (sct == SqlCommandType.UPDATE || sct == SqlCommandType.DELETE) {
+ if (InterceptorIgnoreHelper.willIgnoreDataPermission(ms.getId())) {
+ return;
+ }
+ // 妫�鏌ユ槸鍚︽棤鏁� 鏃犳暟鎹潈闄愭敞瑙�
+ if (invalidCacheSet.contains(ms.getId())) {
+ return;
+ }
+ DataColumn[] dataColumns = dataPermissionHandler.findAnnotation(ms.getId());
+ if (ArrayUtil.isEmpty(dataColumns)) {
+ invalidCacheSet.add(ms.getId());
+ return;
+ }
+ PluginUtils.MPBoundSql mpBs = mpSh.mPBoundSql();
+ mpBs.sql(parserMulti(mpBs.sql(), ms.getId()));
+ }
+ }
+
+ @Override
+ protected void processSelect(Select select, int index, String sql, Object obj) {
+ SelectBody selectBody = select.getSelectBody();
+ if (selectBody instanceof PlainSelect plainSelect) {
+ this.setWhere(plainSelect, (String) obj);
+ } else if (selectBody instanceof SetOperationList setOperationList) {
+ List<SelectBody> selectBodyList = setOperationList.getSelects();
+ selectBodyList.forEach(s -> this.setWhere((PlainSelect) s, (String) obj));
+ }
+ }
+
+ @Override
+ protected void processUpdate(Update update, int index, String sql, Object obj) {
+ Expression sqlSegment = dataPermissionHandler.getSqlSegment(update.getWhere(), (String) obj, false);
+ if (null != sqlSegment) {
+ update.setWhere(sqlSegment);
+ }
+ }
+
+ @Override
+ protected void processDelete(Delete delete, int index, String sql, Object obj) {
+ Expression sqlSegment = dataPermissionHandler.getSqlSegment(delete.getWhere(), (String) obj, false);
+ if (null != sqlSegment) {
+ delete.setWhere(sqlSegment);
+ }
+ }
+
+ /**
+ * 璁剧疆 where 鏉′欢
+ *
+ * @param plainSelect 鏌ヨ瀵硅薄
+ * @param mappedStatementId 鎵ц鏂规硶id
+ */
+ protected void setWhere(PlainSelect plainSelect, String mappedStatementId) {
+ Expression sqlSegment = dataPermissionHandler.getSqlSegment(plainSelect.getWhere(), mappedStatementId, true);
+ if (null != sqlSegment) {
+ plainSelect.setWhere(sqlSegment);
+ }
+ }
+
+}
+
diff --git a/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/service/SysDataScopeService.java b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/service/SysDataScopeService.java
new file mode 100644
index 0000000..0c187f6
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/service/SysDataScopeService.java
@@ -0,0 +1,28 @@
+package org.dromara.common.mybatis.service;
+
+import org.dromara.system.api.RemoteDataScopeService;
+import org.apache.dubbo.config.annotation.DubboReference;
+import org.springframework.stereotype.Service;
+
+/**
+ * 鏁版嵁鏉冮檺 瀹炵幇
+ * <p>
+ * 娉ㄦ剰: 姝ervice鍐呬笉鍏佽璋冪敤鏍囨敞`鏁版嵁鏉冮檺`娉ㄨВ鐨勬柟娉�
+ * 渚嬪: deptMapper.selectList 姝� selectList 鏂规硶鏍囨敞浜哷鏁版嵁鏉冮檺`娉ㄨВ 浼氬嚭鐜板惊鐜В鏋愮殑闂
+ *
+ * @author Lion Li
+ */
+@Service("sdss")
+public class SysDataScopeService {
+
+ @DubboReference
+ private RemoteDataScopeService remoteDataScopeService;
+
+ public String getRoleCustom(Long roleId) {
+ return remoteDataScopeService.getRoleCustom(roleId);
+ }
+
+ public String getDeptAndChild(Long deptId) {
+ return remoteDataScopeService.getDeptAndChild(deptId);
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-mybatis/src/main/resources/META-INF/dubbo/org.apache.dubbo.rpc.Filter b/ruoyi-common/ruoyi-common-mybatis/src/main/resources/META-INF/dubbo/org.apache.dubbo.rpc.Filter
new file mode 100644
index 0000000..6d8ff88
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-mybatis/src/main/resources/META-INF/dubbo/org.apache.dubbo.rpc.Filter
@@ -0,0 +1 @@
+dubboDataPermissionFilter=org.dromara.common.mybatis.filter.DubboDataPermissionFilter
diff --git a/ruoyi-common/ruoyi-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/ruoyi-common/ruoyi-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..3a20a02
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1,3 @@
+org.dromara.common.mybatis.config.MybatisPlusConfiguration
+org.dromara.common.mybatis.handler.MybatisExceptionHandler
+org.dromara.common.mybatis.service.SysDataScopeService
diff --git a/ruoyi-common/ruoyi-common-mybatis/src/main/resources/common-mybatis.yml b/ruoyi-common/ruoyi-common-mybatis/src/main/resources/common-mybatis.yml
new file mode 100644
index 0000000..f5dc637
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-mybatis/src/main/resources/common-mybatis.yml
@@ -0,0 +1,33 @@
+# 鍐呯疆閰嶇疆 涓嶅厑璁镐慨鏀� 濡傞渶淇敼璇峰湪 nacos 涓婂啓鐩稿悓閰嶇疆瑕嗙洊
+# MyBatisPlus閰嶇疆
+# https://baomidou.com/config/
+mybatis-plus:
+ # 鍚姩鏃舵槸鍚︽鏌� MyBatis XML 鏂囦欢鐨勫瓨鍦紝榛樿涓嶆鏌�
+ checkConfigLocation: false
+ configuration:
+ # 鑷姩椹煎嘲鍛藉悕瑙勫垯锛坈amel case锛夋槧灏�
+ mapUnderscoreToCamelCase: true
+ # MyBatis 鑷姩鏄犲皠绛栫暐
+ # NONE锛氫笉鍚敤 PARTIAL锛氬彧瀵归潪宓屽 resultMap 鑷姩鏄犲皠 FULL锛氬鎵�鏈� resultMap 鑷姩鏄犲皠
+ autoMappingBehavior: FULL
+ # MyBatis 鑷姩鏄犲皠鏃舵湭鐭ュ垪鎴栨湭鐭ュ睘鎬у鐞嗙瓥
+ # NONE锛氫笉鍋氬鐞� WARNING锛氭墦鍗扮浉鍏宠鍛� FAILING锛氭姏鍑哄紓甯稿拰璇︾粏淇℃伅
+ autoMappingUnknownColumnBehavior: NONE
+ # 鏇磋缁嗙殑鏃ュ織杈撳嚭 浼氭湁鎬ц兘鎹熻�� org.apache.ibatis.logging.stdout.StdOutImpl
+ # 鍏抽棴鏃ュ織璁板綍 (鍙崟绾娇鐢� p6spy 鍒嗘瀽) org.apache.ibatis.logging.nologging.NoLoggingImpl
+ # 榛樿鏃ュ織杈撳嚭 org.apache.ibatis.logging.slf4j.Slf4jImpl
+ logImpl: org.apache.ibatis.logging.nologging.NoLoggingImpl
+ global-config:
+ # 鏄惁鎵撳嵃 Logo banner
+ banner: true
+ dbConfig:
+ # 涓婚敭绫诲瀷
+ # AUTO 鑷 NONE 绌� INPUT 鐢ㄦ埛杈撳叆 ASSIGN_ID 闆姳 ASSIGN_UUID 鍞竴 UUID
+ idType: ASSIGN_ID
+ # 閫昏緫宸插垹闄ゅ��(妗嗘灦琛ㄥ潎浣跨敤姝ゅ�� 绂佹闅忔剰淇敼)
+ logicDeleteValue: 2
+ # 閫昏緫鏈垹闄ゅ��
+ logicNotDeleteValue: 0
+ insertStrategy: NOT_NULL
+ updateStrategy: NOT_NULL
+ whereStrategy: NOT_NULL
diff --git a/ruoyi-common/ruoyi-common-oss/pom.xml b/ruoyi-common/ruoyi-common-oss/pom.xml
new file mode 100644
index 0000000..8e7afff
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-oss/pom.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common</artifactId>
+ <version>${revision}</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>ruoyi-common-oss</artifactId>
+
+ <description>
+ ruoyi-common-oss oss鏈嶅姟
+ </description>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-json</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-redis</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>com.amazonaws</groupId>
+ <artifactId>aws-java-sdk-s3</artifactId>
+ </dependency>
+ </dependencies>
+
+</project>
diff --git a/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/constant/OssConstant.java b/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/constant/OssConstant.java
new file mode 100644
index 0000000..9d8db93
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/constant/OssConstant.java
@@ -0,0 +1,40 @@
+package org.dromara.common.oss.constant;
+
+import org.dromara.common.core.constant.GlobalConstants;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * 瀵硅薄瀛樺偍甯搁噺
+ *
+ * @author Lion Li
+ */
+public interface OssConstant {
+
+ /**
+ * 榛樿閰嶇疆KEY
+ */
+ String DEFAULT_CONFIG_KEY = GlobalConstants.GLOBAL_REDIS_KEY + "sys_oss:default_config";
+
+ /**
+ * 棰勮鍒楄〃璧勬簮寮�鍏矺ey
+ */
+ String PEREVIEW_LIST_RESOURCE_KEY = "sys.oss.previewListResource";
+
+ /**
+ * 绯荤粺鏁版嵁ids
+ */
+ List<Long> SYSTEM_DATA_IDS = Arrays.asList(1L, 2L, 3L, 4L);
+
+ /**
+ * 浜戞湇鍔″晢
+ */
+ String[] CLOUD_SERVICE = new String[] {"aliyun", "qcloud", "qiniu", "obs"};
+
+ /**
+ * https 鐘舵��
+ */
+ String IS_HTTPS = "Y";
+
+}
diff --git a/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/core/OssClient.java b/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/core/OssClient.java
new file mode 100644
index 0000000..53e05c9
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/core/OssClient.java
@@ -0,0 +1,262 @@
+package org.dromara.common.oss.core;
+
+import cn.hutool.core.io.IoUtil;
+import cn.hutool.core.util.IdUtil;
+import com.amazonaws.ClientConfiguration;
+import com.amazonaws.HttpMethod;
+import com.amazonaws.Protocol;
+import com.amazonaws.auth.AWSCredentials;
+import com.amazonaws.auth.AWSCredentialsProvider;
+import com.amazonaws.auth.AWSStaticCredentialsProvider;
+import com.amazonaws.auth.BasicAWSCredentials;
+import com.amazonaws.client.builder.AwsClientBuilder;
+import com.amazonaws.services.s3.AmazonS3;
+import com.amazonaws.services.s3.AmazonS3Client;
+import com.amazonaws.services.s3.AmazonS3ClientBuilder;
+import com.amazonaws.services.s3.model.*;
+import org.dromara.common.core.utils.DateUtils;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.oss.constant.OssConstant;
+import org.dromara.common.oss.entity.UploadResult;
+import org.dromara.common.oss.enumd.AccessPolicyType;
+import org.dromara.common.oss.enumd.PolicyType;
+import org.dromara.common.oss.exception.OssException;
+import org.dromara.common.oss.properties.OssProperties;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.Date;
+
+/**
+ * S3 瀛樺偍鍗忚 鎵�鏈夊吋瀹筍3鍗忚鐨勪簯鍘傚晢鍧囨敮鎸�
+ * 闃块噷浜� 鑵捐浜� 涓冪墰浜� minio
+ *
+ * @author Lion Li
+ */
+public class OssClient {
+
+ private final String configKey;
+
+ private final OssProperties properties;
+
+ private final AmazonS3 client;
+
+ public OssClient(String configKey, OssProperties ossProperties) {
+ this.configKey = configKey;
+ this.properties = ossProperties;
+ try {
+ AwsClientBuilder.EndpointConfiguration endpointConfig =
+ new AwsClientBuilder.EndpointConfiguration(properties.getEndpoint(), properties.getRegion());
+
+ AWSCredentials credentials = new BasicAWSCredentials(properties.getAccessKey(), properties.getSecretKey());
+ AWSCredentialsProvider credentialsProvider = new AWSStaticCredentialsProvider(credentials);
+ ClientConfiguration clientConfig = new ClientConfiguration();
+ if (OssConstant.IS_HTTPS.equals(properties.getIsHttps())) {
+ clientConfig.setProtocol(Protocol.HTTPS);
+ } else {
+ clientConfig.setProtocol(Protocol.HTTP);
+ }
+ AmazonS3ClientBuilder build = AmazonS3Client.builder()
+ .withEndpointConfiguration(endpointConfig)
+ .withClientConfiguration(clientConfig)
+ .withCredentials(credentialsProvider)
+ .disableChunkedEncoding();
+ if (!StringUtils.containsAny(properties.getEndpoint(), OssConstant.CLOUD_SERVICE)) {
+ // minio 浣跨敤https闄愬埗浣跨敤鍩熷悕璁块棶 闇�瑕佹閰嶇疆 绔欑偣濉煙鍚�
+ build.enablePathStyleAccess();
+ }
+ this.client = build.build();
+
+ createBucket();
+ } catch (Exception e) {
+ if (e instanceof OssException) {
+ throw e;
+ }
+ throw new OssException("閰嶇疆閿欒! 璇锋鏌ョ郴缁熼厤缃�:[" + e.getMessage() + "]");
+ }
+ }
+
+ public void createBucket() {
+ try {
+ String bucketName = properties.getBucketName();
+ if (client.doesBucketExistV2(bucketName)) {
+ return;
+ }
+ CreateBucketRequest createBucketRequest = new CreateBucketRequest(bucketName);
+ AccessPolicyType accessPolicy = getAccessPolicy();
+ createBucketRequest.setCannedAcl(accessPolicy.getAcl());
+ client.createBucket(createBucketRequest);
+ client.setBucketPolicy(bucketName, getPolicy(bucketName, accessPolicy.getPolicyType()));
+ } catch (Exception e) {
+ throw new OssException("鍒涘缓Bucket澶辫触, 璇锋牳瀵归厤缃俊鎭�:[" + e.getMessage() + "]");
+ }
+ }
+
+ public UploadResult upload(byte[] data, String path, String contentType) {
+ return upload(new ByteArrayInputStream(data), path, contentType);
+ }
+
+ public UploadResult upload(InputStream inputStream, String path, String contentType) {
+ if (!(inputStream instanceof ByteArrayInputStream)) {
+ inputStream = new ByteArrayInputStream(IoUtil.readBytes(inputStream));
+ }
+ try {
+ ObjectMetadata metadata = new ObjectMetadata();
+ metadata.setContentType(contentType);
+ metadata.setContentLength(inputStream.available());
+ PutObjectRequest putObjectRequest = new PutObjectRequest(properties.getBucketName(), path, inputStream, metadata);
+ // 璁剧疆涓婁紶瀵硅薄鐨� Acl 涓哄叕鍏辫
+ putObjectRequest.setCannedAcl(getAccessPolicy().getAcl());
+ client.putObject(putObjectRequest);
+ } catch (Exception e) {
+ throw new OssException("涓婁紶鏂囦欢澶辫触锛岃妫�鏌ラ厤缃俊鎭�:[" + e.getMessage() + "]");
+ }
+ return UploadResult.builder().url(getUrl() + "/" + path).filename(path).build();
+ }
+
+ public UploadResult upload(File file, String path) {
+ try {
+ PutObjectRequest putObjectRequest = new PutObjectRequest(properties.getBucketName(), path, file);
+ // 璁剧疆涓婁紶瀵硅薄鐨� Acl 涓哄叕鍏辫
+ putObjectRequest.setCannedAcl(getAccessPolicy().getAcl());
+ client.putObject(putObjectRequest);
+ } catch (Exception e) {
+ throw new OssException("涓婁紶鏂囦欢澶辫触锛岃妫�鏌ラ厤缃俊鎭�:[" + e.getMessage() + "]");
+ }
+ return UploadResult.builder().url(getUrl() + "/" + path).filename(path).build();
+ }
+
+ public void delete(String path) {
+ path = path.replace(getUrl() + "/", "");
+ try {
+ client.deleteObject(properties.getBucketName(), path);
+ } catch (Exception e) {
+ throw new OssException("鍒犻櫎鏂囦欢澶辫触锛岃妫�鏌ラ厤缃俊鎭�:[" + e.getMessage() + "]");
+ }
+ }
+
+ public UploadResult uploadSuffix(byte[] data, String suffix, String contentType) {
+ return upload(data, getPath(properties.getPrefix(), suffix), contentType);
+ }
+
+ public UploadResult uploadSuffix(InputStream inputStream, String suffix, String contentType) {
+ return upload(inputStream, getPath(properties.getPrefix(), suffix), contentType);
+ }
+
+ public UploadResult uploadSuffix(File file, String suffix) {
+ return upload(file, getPath(properties.getPrefix(), suffix));
+ }
+
+ /**
+ * 鑾峰彇鏂囦欢鍏冩暟鎹�
+ *
+ * @param path 瀹屾暣鏂囦欢璺緞
+ */
+ public ObjectMetadata getObjectMetadata(String path) {
+ path = path.replace(getUrl() + "/", "");
+ S3Object object = client.getObject(properties.getBucketName(), path);
+ return object.getObjectMetadata();
+ }
+
+ public InputStream getObjectContent(String path) {
+ path = path.replace(getUrl() + "/", "");
+ S3Object object = client.getObject(properties.getBucketName(), path);
+ return object.getObjectContent();
+ }
+
+ public String getUrl() {
+ String domain = properties.getDomain();
+ String endpoint = properties.getEndpoint();
+ String header = OssConstant.IS_HTTPS.equals(properties.getIsHttps()) ? "https://" : "http://";
+ // 浜戞湇鍔″晢鐩存帴杩斿洖
+ if (StringUtils.containsAny(endpoint, OssConstant.CLOUD_SERVICE)) {
+ if (StringUtils.isNotBlank(domain)) {
+ return header + domain;
+ }
+ return header + properties.getBucketName() + "." + endpoint;
+ }
+ // minio 鍗曠嫭澶勭悊
+ if (StringUtils.isNotBlank(domain)) {
+ return header + domain + "/" + properties.getBucketName();
+ }
+ return header + endpoint + "/" + properties.getBucketName();
+ }
+
+ public String getPath(String prefix, String suffix) {
+ // 鐢熸垚uuid
+ String uuid = IdUtil.fastSimpleUUID();
+ // 鏂囦欢璺緞
+ String path = DateUtils.datePath() + "/" + uuid;
+ if (StringUtils.isNotBlank(prefix)) {
+ path = prefix + "/" + path;
+ }
+ return path + suffix;
+ }
+
+
+ public String getConfigKey() {
+ return configKey;
+ }
+
+ /**
+ * 鑾峰彇绉佹湁URL閾炬帴
+ *
+ * @param objectKey 瀵硅薄KEY
+ * @param second 鎺堟潈鏃堕棿
+ */
+ public String getPrivateUrl(String objectKey, Integer second) {
+ GeneratePresignedUrlRequest generatePresignedUrlRequest =
+ new GeneratePresignedUrlRequest(properties.getBucketName(), objectKey)
+ .withMethod(HttpMethod.GET)
+ .withExpiration(new Date(System.currentTimeMillis() + 1000L * second));
+ URL url = client.generatePresignedUrl(generatePresignedUrlRequest);
+ return url.toString();
+ }
+
+ /**
+ * 妫�鏌ラ厤缃槸鍚︾浉鍚�
+ */
+ public boolean checkPropertiesSame(OssProperties properties) {
+ return this.properties.equals(properties);
+ }
+
+ /**
+ * 鑾峰彇褰撳墠妗舵潈闄愮被鍨�
+ *
+ * @return 褰撳墠妗舵潈闄愮被鍨媍ode
+ */
+ public AccessPolicyType getAccessPolicy() {
+ return AccessPolicyType.getByType(properties.getAccessPolicy());
+ }
+
+ private static String getPolicy(String bucketName, PolicyType policyType) {
+ StringBuilder builder = new StringBuilder();
+ builder.append("{\n\"Statement\": [\n{\n\"Action\": [\n");
+ builder.append(switch (policyType) {
+ case WRITE -> "\"s3:GetBucketLocation\",\n\"s3:ListBucketMultipartUploads\"\n";
+ case READ_WRITE -> "\"s3:GetBucketLocation\",\n\"s3:ListBucket\",\n\"s3:ListBucketMultipartUploads\"\n";
+ default -> "\"s3:GetBucketLocation\"\n";
+ });
+ builder.append("],\n\"Effect\": \"Allow\",\n\"Principal\": \"*\",\n\"Resource\": \"arn:aws:s3:::");
+ builder.append(bucketName);
+ builder.append("\"\n},\n");
+ if (policyType == PolicyType.READ) {
+ builder.append("{\n\"Action\": [\n\"s3:ListBucket\"\n],\n\"Effect\": \"Deny\",\n\"Principal\": \"*\",\n\"Resource\": \"arn:aws:s3:::");
+ builder.append(bucketName);
+ builder.append("\"\n},\n");
+ }
+ builder.append("{\n\"Action\": ");
+ builder.append(switch (policyType) {
+ case WRITE -> "[\n\"s3:AbortMultipartUpload\",\n\"s3:DeleteObject\",\n\"s3:ListMultipartUploadParts\",\n\"s3:PutObject\"\n],\n";
+ case READ_WRITE -> "[\n\"s3:AbortMultipartUpload\",\n\"s3:DeleteObject\",\n\"s3:GetObject\",\n\"s3:ListMultipartUploadParts\",\n\"s3:PutObject\"\n],\n";
+ default -> "\"s3:GetObject\",\n";
+ });
+ builder.append("\"Effect\": \"Allow\",\n\"Principal\": \"*\",\n\"Resource\": \"arn:aws:s3:::");
+ builder.append(bucketName);
+ builder.append("/*\"\n}\n],\n\"Version\": \"2012-10-17\"\n}\n");
+ return builder.toString();
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/entity/UploadResult.java b/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/entity/UploadResult.java
new file mode 100644
index 0000000..a6f57e5
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/entity/UploadResult.java
@@ -0,0 +1,24 @@
+package org.dromara.common.oss.entity;
+
+import lombok.Builder;
+import lombok.Data;
+
+/**
+ * 涓婁紶杩斿洖浣�
+ *
+ * @author Lion Li
+ */
+@Data
+@Builder
+public class UploadResult {
+
+ /**
+ * 鏂囦欢璺緞
+ */
+ private String url;
+
+ /**
+ * 鏂囦欢鍚�
+ */
+ private String filename;
+}
diff --git a/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/enumd/AccessPolicyType.java b/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/enumd/AccessPolicyType.java
new file mode 100644
index 0000000..9074d72
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/enumd/AccessPolicyType.java
@@ -0,0 +1,55 @@
+package org.dromara.common.oss.enumd;
+
+import com.amazonaws.services.s3.model.CannedAccessControlList;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 妗惰闂瓥鐣ラ厤缃�
+ *
+ * @author 闄堣碀
+ */
+@Getter
+@AllArgsConstructor
+public enum AccessPolicyType {
+
+ /**
+ * private
+ */
+ PRIVATE("0", CannedAccessControlList.Private, PolicyType.WRITE),
+
+ /**
+ * public
+ */
+ PUBLIC("1", CannedAccessControlList.PublicRead, PolicyType.READ),
+
+ /**
+ * custom
+ */
+ CUSTOM("2",CannedAccessControlList.PublicRead, PolicyType.READ);
+
+ /**
+ * 妗� 鏉冮檺绫诲瀷
+ */
+ private final String type;
+
+ /**
+ * 鏂囦欢瀵硅薄 鏉冮檺绫诲瀷
+ */
+ private final CannedAccessControlList acl;
+
+ /**
+ * 妗剁瓥鐣ョ被鍨�
+ */
+ private final PolicyType policyType;
+
+ public static AccessPolicyType getByType(String type) {
+ for (AccessPolicyType value : values()) {
+ if (value.getType().equals(type)) {
+ return value;
+ }
+ }
+ throw new RuntimeException("'type' not found By " + type);
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/enumd/PolicyType.java b/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/enumd/PolicyType.java
new file mode 100644
index 0000000..fe96341
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/enumd/PolicyType.java
@@ -0,0 +1,35 @@
+package org.dromara.common.oss.enumd;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * minio绛栫暐閰嶇疆
+ *
+ * @author Lion Li
+ */
+@Getter
+@AllArgsConstructor
+public enum PolicyType {
+
+ /**
+ * 鍙
+ */
+ READ("read-only"),
+
+ /**
+ * 鍙啓
+ */
+ WRITE("write-only"),
+
+ /**
+ * 璇诲啓
+ */
+ READ_WRITE("read-write");
+
+ /**
+ * 绫诲瀷
+ */
+ private final String type;
+
+}
diff --git a/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/exception/OssException.java b/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/exception/OssException.java
new file mode 100644
index 0000000..52e9623
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/exception/OssException.java
@@ -0,0 +1,19 @@
+package org.dromara.common.oss.exception;
+
+import java.io.Serial;
+
+/**
+ * OSS寮傚父绫�
+ *
+ * @author Lion Li
+ */
+public class OssException extends RuntimeException {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ public OssException(String msg) {
+ super(msg);
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/factory/OssFactory.java b/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/factory/OssFactory.java
new file mode 100644
index 0000000..763b090
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/factory/OssFactory.java
@@ -0,0 +1,65 @@
+package org.dromara.common.oss.factory;
+
+import org.dromara.common.core.constant.CacheNames;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.json.utils.JsonUtils;
+import org.dromara.common.oss.constant.OssConstant;
+import org.dromara.common.oss.core.OssClient;
+import org.dromara.common.oss.exception.OssException;
+import org.dromara.common.oss.properties.OssProperties;
+import org.dromara.common.redis.utils.CacheUtils;
+import org.dromara.common.redis.utils.RedisUtils;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 鏂囦欢涓婁紶Factory
+ *
+ * @author Lion Li
+ */
+@Slf4j
+public class OssFactory {
+
+ private static final Map<String, OssClient> CLIENT_CACHE = new ConcurrentHashMap<>();
+
+ /**
+ * 鑾峰彇榛樿瀹炰緥
+ */
+ public static OssClient instance() {
+ // 鑾峰彇redis 榛樿绫诲瀷
+ String configKey = RedisUtils.getCacheObject(OssConstant.DEFAULT_CONFIG_KEY);
+ if (StringUtils.isEmpty(configKey)) {
+ throw new OssException("鏂囦欢瀛樺偍鏈嶅姟绫诲瀷鏃犳硶鎵惧埌!");
+ }
+ return instance(configKey);
+ }
+
+ /**
+ * 鏍规嵁绫诲瀷鑾峰彇瀹炰緥
+ */
+ public static synchronized OssClient instance(String configKey) {
+ String json = CacheUtils.get(CacheNames.SYS_OSS_CONFIG, configKey);
+ if (json == null) {
+ throw new OssException("绯荤粺寮傚父, '" + configKey + "'閰嶇疆淇℃伅涓嶅瓨鍦�!");
+ }
+ OssProperties properties = JsonUtils.parseObject(json, OssProperties.class);
+ // 浣跨敤绉熸埛鏍囪瘑閬垮厤澶氫釜绉熸埛鐩稿悓key瀹炰緥瑕嗙洊
+ String key = properties.getTenantId() + ":" + configKey;
+ OssClient client = CLIENT_CACHE.get(key);
+ if (client == null) {
+ CLIENT_CACHE.put(key, new OssClient(configKey, properties));
+ log.info("鍒涘缓OSS瀹炰緥 key => {}", configKey);
+ return CLIENT_CACHE.get(key);
+ }
+ // 閰嶇疆涓嶇浉鍚屽垯閲嶆柊鏋勫缓
+ if (!client.checkPropertiesSame(properties)) {
+ CLIENT_CACHE.put(key, new OssClient(configKey, properties));
+ log.info("閲嶈浇OSS瀹炰緥 key => {}", configKey);
+ return CLIENT_CACHE.get(key);
+ }
+ return client;
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/properties/OssProperties.java b/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/properties/OssProperties.java
new file mode 100644
index 0000000..cb37206
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/properties/OssProperties.java
@@ -0,0 +1,63 @@
+package org.dromara.common.oss.properties;
+
+import lombok.Data;
+
+/**
+ * OSS瀵硅薄瀛樺偍 閰嶇疆灞炴��
+ *
+ * @author Lion Li
+ */
+@Data
+public class OssProperties {
+
+ /**
+ * 绉熸埛id
+ */
+ private String tenantId;
+
+ /**
+ * 璁块棶绔欑偣
+ */
+ private String endpoint;
+
+ /**
+ * 鑷畾涔夊煙鍚�
+ */
+ private String domain;
+
+ /**
+ * 鍓嶇紑
+ */
+ private String prefix;
+
+ /**
+ * ACCESS_KEY
+ */
+ private String accessKey;
+
+ /**
+ * SECRET_KEY
+ */
+ private String secretKey;
+
+ /**
+ * 瀛樺偍绌洪棿鍚�
+ */
+ private String bucketName;
+
+ /**
+ * 瀛樺偍鍖哄煙
+ */
+ private String region;
+
+ /**
+ * 鏄惁https锛圷=鏄�,N=鍚︼級
+ */
+ private String isHttps;
+
+ /**
+ * 妗舵潈闄愮被鍨�(0private 1public 2custom)
+ */
+ private String accessPolicy;
+
+}
diff --git a/ruoyi-common/ruoyi-common-prometheus/pom.xml b/ruoyi-common/ruoyi-common-prometheus/pom.xml
new file mode 100644
index 0000000..319bed4
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-prometheus/pom.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns="http://maven.apache.org/POM/4.0.0"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common</artifactId>
+ <version>${revision}</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>ruoyi-common-prometheus</artifactId>
+
+ <description>
+ ruoyi-common-prometheus prometheus鐩戞帶
+ </description>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-actuator</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>io.micrometer</groupId>
+ <artifactId>micrometer-registry-prometheus</artifactId>
+ </dependency>
+ </dependencies>
+</project>
diff --git a/ruoyi-common/ruoyi-common-prometheus/src/main/java/org/dromara/common/prometheus/config/PrometheusConfiguration.java b/ruoyi-common/ruoyi-common-prometheus/src/main/java/org/dromara/common/prometheus/config/PrometheusConfiguration.java
new file mode 100644
index 0000000..c5cfbf5
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-prometheus/src/main/java/org/dromara/common/prometheus/config/PrometheusConfiguration.java
@@ -0,0 +1,22 @@
+package org.dromara.common.prometheus.config;
+
+import io.micrometer.core.instrument.MeterRegistry;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.context.annotation.Bean;
+
+/**
+ * prometheus 閰嶇疆
+ *
+ * @author Lion Li
+ */
+@AutoConfiguration
+public class PrometheusConfiguration {
+
+ @Bean
+ public MeterRegistryCustomizer<MeterRegistry> configurer(@Value("${spring.application.name}") String applicationName) {
+ return (registry) -> registry.config().commonTags("application", applicationName);
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-prometheus/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/ruoyi-common/ruoyi-common-prometheus/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..d05b042
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-prometheus/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+org.dromara.common.prometheus.config.PrometheusConfiguration
diff --git a/ruoyi-common/ruoyi-common-ratelimiter/pom.xml b/ruoyi-common/ruoyi-common-ratelimiter/pom.xml
new file mode 100644
index 0000000..bbde940
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-ratelimiter/pom.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common</artifactId>
+ <version>${revision}</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>ruoyi-common-ratelimiter</artifactId>
+
+ <description>
+ ruoyi-common-ratelimiter 闄愭祦鍔熻兘
+ </description>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-core</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-redis</artifactId>
+ </dependency>
+ </dependencies>
+
+</project>
diff --git a/ruoyi-common/ruoyi-common-ratelimiter/src/main/java/org/dromara/common/ratelimiter/annotation/RateLimiter.java b/ruoyi-common/ruoyi-common-ratelimiter/src/main/java/org/dromara/common/ratelimiter/annotation/RateLimiter.java
new file mode 100644
index 0000000..de09752
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-ratelimiter/src/main/java/org/dromara/common/ratelimiter/annotation/RateLimiter.java
@@ -0,0 +1,41 @@
+package org.dromara.common.ratelimiter.annotation;
+
+import org.dromara.common.ratelimiter.enums.LimitType;
+
+import java.lang.annotation.*;
+
+/**
+ * 闄愭祦娉ㄨВ
+ *
+ * @author Lion Li
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface RateLimiter {
+ /**
+ * 闄愭祦key,鏀寔浣跨敤Spring el琛ㄨ揪寮忔潵鍔ㄦ�佽幏鍙栨柟娉曚笂鐨勫弬鏁板��
+ * 鏍煎紡绫讳技浜� #code.id #{#code}
+ */
+ String key() default "";
+
+ /**
+ * 闄愭祦鏃堕棿,鍗曚綅绉�
+ */
+ int time() default 60;
+
+ /**
+ * 闄愭祦娆℃暟
+ */
+ int count() default 100;
+
+ /**
+ * 闄愭祦绫诲瀷
+ */
+ LimitType limitType() default LimitType.DEFAULT;
+
+ /**
+ * 鎻愮ず娑堟伅 鏀寔鍥介檯鍖� 鏍煎紡涓� {code}
+ */
+ String message() default "{rate.limiter.message}";
+}
diff --git a/ruoyi-common/ruoyi-common-ratelimiter/src/main/java/org/dromara/common/ratelimiter/aspectj/RateLimiterAspect.java b/ruoyi-common/ruoyi-common-ratelimiter/src/main/java/org/dromara/common/ratelimiter/aspectj/RateLimiterAspect.java
new file mode 100644
index 0000000..8f3a5ca
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-ratelimiter/src/main/java/org/dromara/common/ratelimiter/aspectj/RateLimiterAspect.java
@@ -0,0 +1,127 @@
+package org.dromara.common.ratelimiter.aspectj;
+
+import cn.hutool.core.util.ArrayUtil;
+import org.dromara.common.core.constant.GlobalConstants;
+import org.dromara.common.core.exception.ServiceException;
+import org.dromara.common.core.utils.MessageUtils;
+import org.dromara.common.core.utils.ServletUtils;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.ratelimiter.annotation.RateLimiter;
+import org.dromara.common.ratelimiter.enums.LimitType;
+import org.dromara.common.redis.utils.RedisUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Before;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.redisson.api.RateType;
+import org.springframework.core.DefaultParameterNameDiscoverer;
+import org.springframework.core.ParameterNameDiscoverer;
+import org.springframework.expression.EvaluationContext;
+import org.springframework.expression.Expression;
+import org.springframework.expression.ExpressionParser;
+import org.springframework.expression.ParserContext;
+import org.springframework.expression.common.TemplateParserContext;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
+import org.springframework.expression.spel.support.StandardEvaluationContext;
+
+import java.lang.reflect.Method;
+
+/**
+ * 闄愭祦澶勭悊
+ *
+ * @author Lion Li
+ */
+@Slf4j
+@Aspect
+public class RateLimiterAspect {
+
+ /**
+ * 瀹氫箟spel琛ㄨ揪寮忚В鏋愬櫒
+ */
+ private final ExpressionParser parser = new SpelExpressionParser();
+ /**
+ * 瀹氫箟spel瑙f瀽妯$増
+ */
+ private final ParserContext parserContext = new TemplateParserContext();
+ /**
+ * 瀹氫箟spel涓婁笅鏂囧璞¤繘琛岃В鏋�
+ */
+ private final EvaluationContext context = new StandardEvaluationContext();
+ /**
+ * 鏂规硶鍙傛暟瑙f瀽鍣�
+ */
+ private final ParameterNameDiscoverer pnd = new DefaultParameterNameDiscoverer();
+
+ @Before("@annotation(rateLimiter)")
+ public void doBefore(JoinPoint point, RateLimiter rateLimiter) throws Throwable {
+ int time = rateLimiter.time();
+ int count = rateLimiter.count();
+ String combineKey = getCombineKey(rateLimiter, point);
+ try {
+ RateType rateType = RateType.OVERALL;
+ if (rateLimiter.limitType() == LimitType.CLUSTER) {
+ rateType = RateType.PER_CLIENT;
+ }
+ long number = RedisUtils.rateLimiter(combineKey, rateType, count, time);
+ if (number == -1) {
+ String message = rateLimiter.message();
+ if (StringUtils.startsWith(message, "{") && StringUtils.endsWith(message, "}")) {
+ message = MessageUtils.message(StringUtils.substring(message, 1, message.length() - 1));
+ }
+ throw new ServiceException(message);
+ }
+ log.info("闄愬埗浠ょ墝 => {}, 鍓╀綑浠ょ墝 => {}, 缂撳瓨key => '{}'", count, number, combineKey);
+ } catch (Exception e) {
+ if (e instanceof ServiceException) {
+ throw e;
+ } else {
+ throw new RuntimeException("鏈嶅姟鍣ㄩ檺娴佸紓甯革紝璇风◢鍊欏啀璇�");
+ }
+ }
+ }
+
+ public String getCombineKey(RateLimiter rateLimiter, JoinPoint point) {
+ String key = rateLimiter.key();
+ // 鑾峰彇鏂规硶(閫氳繃鏂规硶绛惧悕鏉ヨ幏鍙�)
+ MethodSignature signature = (MethodSignature) point.getSignature();
+ Method method = signature.getMethod();
+ Class<?> targetClass = method.getDeclaringClass();
+ // 鍒ゆ柇鏄惁鏄痵pel鏍煎紡
+ if (StringUtils.containsAny(key, "#")) {
+ // 鑾峰彇鍙傛暟鍊�
+ Object[] args = point.getArgs();
+ // 鑾峰彇鏂规硶涓婂弬鏁扮殑鍚嶇О
+ String[] parameterNames = pnd.getParameterNames(method);
+ if (ArrayUtil.isEmpty(parameterNames)) {
+ throw new ServiceException("闄愭祦key瑙f瀽寮傚父!璇疯仈绯荤鐞嗗憳!");
+ }
+ for (int i = 0; i < parameterNames.length; i++) {
+ context.setVariable(parameterNames[i], args[i]);
+ }
+ // 瑙f瀽杩斿洖缁檏ey
+ try {
+ Expression expression;
+ if (StringUtils.startsWith(key, parserContext.getExpressionPrefix())
+ && StringUtils.endsWith(key, parserContext.getExpressionSuffix())) {
+ expression = parser.parseExpression(key, parserContext);
+ } else {
+ expression = parser.parseExpression(key);
+ }
+ key = expression.getValue(context, String.class) + ":";
+ } catch (Exception e) {
+ throw new ServiceException("闄愭祦key瑙f瀽寮傚父!璇疯仈绯荤鐞嗗憳!");
+ }
+ }
+ StringBuilder stringBuffer = new StringBuilder(GlobalConstants.RATE_LIMIT_KEY);
+ stringBuffer.append(ServletUtils.getRequest().getRequestURI()).append(":");
+ if (rateLimiter.limitType() == LimitType.IP) {
+ // 鑾峰彇璇锋眰ip
+ stringBuffer.append(ServletUtils.getClientIP()).append(":");
+ } else if (rateLimiter.limitType() == LimitType.CLUSTER) {
+ // 鑾峰彇瀹㈡埛绔疄渚媔d
+ stringBuffer.append(RedisUtils.getClient().getId()).append(":");
+ }
+ return stringBuffer.append(key).toString();
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-ratelimiter/src/main/java/org/dromara/common/ratelimiter/config/RateLimiterConfig.java b/ruoyi-common/ruoyi-common-ratelimiter/src/main/java/org/dromara/common/ratelimiter/config/RateLimiterConfig.java
new file mode 100644
index 0000000..4b7e5b7
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-ratelimiter/src/main/java/org/dromara/common/ratelimiter/config/RateLimiterConfig.java
@@ -0,0 +1,20 @@
+package org.dromara.common.ratelimiter.config;
+
+import org.dromara.common.ratelimiter.aspectj.RateLimiterAspect;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.data.redis.connection.RedisConfiguration;
+
+/**
+ * @author guangxin
+ * @date 2023/1/18
+ */
+@AutoConfiguration(after = RedisConfiguration.class)
+public class RateLimiterConfig {
+
+ @Bean
+ public RateLimiterAspect rateLimiterAspect() {
+ return new RateLimiterAspect();
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-ratelimiter/src/main/java/org/dromara/common/ratelimiter/enums/LimitType.java b/ruoyi-common/ruoyi-common-ratelimiter/src/main/java/org/dromara/common/ratelimiter/enums/LimitType.java
new file mode 100644
index 0000000..b7f059f
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-ratelimiter/src/main/java/org/dromara/common/ratelimiter/enums/LimitType.java
@@ -0,0 +1,24 @@
+package org.dromara.common.ratelimiter.enums;
+
+/**
+ * 闄愭祦绫诲瀷
+ *
+ * @author ruoyi
+ */
+
+public enum LimitType {
+ /**
+ * 榛樿绛栫暐鍏ㄥ眬闄愭祦
+ */
+ DEFAULT,
+
+ /**
+ * 鏍规嵁璇锋眰鑰匢P杩涜闄愭祦
+ */
+ IP,
+
+ /**
+ * 瀹炰緥闄愭祦(闆嗙兢澶氬悗绔疄渚�)
+ */
+ CLUSTER
+}
diff --git a/ruoyi-common/ruoyi-common-ratelimiter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/ruoyi-common/ruoyi-common-ratelimiter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..3b95432
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-ratelimiter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+org.dromara.common.ratelimiter.config.RateLimiterConfig
diff --git a/ruoyi-common/ruoyi-common-redis/pom.xml b/ruoyi-common/ruoyi-common-redis/pom.xml
new file mode 100644
index 0000000..5e5c277
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-redis/pom.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common</artifactId>
+ <version>${revision}</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>ruoyi-common-redis</artifactId>
+
+ <description>
+ ruoyi-common-redis 缂撳瓨鏈嶅姟
+ </description>
+
+ <dependencies>
+ <!-- RuoYi Common Core-->
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-core</artifactId>
+ </dependency>
+
+ <!--redisson-->
+ <dependency>
+ <groupId>org.redisson</groupId>
+ <artifactId>redisson-spring-boot-starter</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>com.baomidou</groupId>
+ <artifactId>lock4j-redisson-spring-boot-starter</artifactId>
+ </dependency>
+ </dependencies>
+
+</project>
diff --git a/ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/config/RedisConfiguration.java b/ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/config/RedisConfiguration.java
new file mode 100644
index 0000000..8264423
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/config/RedisConfiguration.java
@@ -0,0 +1,144 @@
+package org.dromara.common.redis.config;
+
+import cn.hutool.core.util.ObjectUtil;
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.PropertyAccessor;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.redis.config.properties.RedissonProperties;
+import org.dromara.common.redis.handler.KeyPrefixHandler;
+import org.dromara.common.redis.manager.PlusSpringCacheManager;
+import org.redisson.client.codec.StringCodec;
+import org.redisson.codec.CompositeCodec;
+import org.redisson.codec.TypedJsonJacksonCodec;
+import org.redisson.spring.starter.RedissonAutoConfigurationCustomizer;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.cache.CacheManager;
+import org.springframework.cache.annotation.EnableCaching;
+import org.springframework.context.annotation.Bean;
+
+/**
+ * redis閰嶇疆
+ *
+ * @author Lion Li
+ */
+@Slf4j
+@AutoConfiguration
+@EnableCaching
+@EnableConfigurationProperties(RedissonProperties.class)
+public class RedisConfiguration {
+
+ @Autowired
+ private RedissonProperties redissonProperties;
+
+ @Autowired
+ private ObjectMapper objectMapper;
+
+ @Bean
+ public RedissonAutoConfigurationCustomizer redissonCustomizer() {
+ return config -> {
+ ObjectMapper om = objectMapper.copy();
+ om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
+ // 鎸囧畾搴忓垪鍖栬緭鍏ョ殑绫诲瀷锛岀被蹇呴』鏄潪final淇グ鐨勩�傚簭鍒楀寲鏃跺皢瀵硅薄鍏ㄧ被鍚嶄竴璧蜂繚瀛樹笅鏉�
+ om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
+ TypedJsonJacksonCodec jsonCodec = new TypedJsonJacksonCodec(Object.class, om);
+ // 缁勫悎搴忓垪鍖� key 浣跨敤 String 鍐呭浣跨敤閫氱敤 json 鏍煎紡
+ CompositeCodec codec = new CompositeCodec(StringCodec.INSTANCE, jsonCodec, jsonCodec);
+ config.setThreads(redissonProperties.getThreads())
+ .setNettyThreads(redissonProperties.getNettyThreads())
+ // 缂撳瓨 Lua 鑴氭湰 鍑忓皯缃戠粶浼犺緭(redisson 澶ч儴鍒嗙殑鍔熻兘閮芥槸鍩轰簬 Lua 鑴氭湰瀹炵幇)
+ .setUseScriptCache(true)
+ .setCodec(codec);
+ RedissonProperties.SingleServerConfig singleServerConfig = redissonProperties.getSingleServerConfig();
+ if (ObjectUtil.isNotNull(singleServerConfig)) {
+ // 浣跨敤鍗曟満妯″紡
+ config.useSingleServer()
+ //璁剧疆redis key鍓嶇紑
+ .setNameMapper(new KeyPrefixHandler(redissonProperties.getKeyPrefix()))
+ .setTimeout(singleServerConfig.getTimeout())
+ .setClientName(singleServerConfig.getClientName())
+ .setIdleConnectionTimeout(singleServerConfig.getIdleConnectionTimeout())
+ .setSubscriptionConnectionPoolSize(singleServerConfig.getSubscriptionConnectionPoolSize())
+ .setConnectionMinimumIdleSize(singleServerConfig.getConnectionMinimumIdleSize())
+ .setConnectionPoolSize(singleServerConfig.getConnectionPoolSize());
+ }
+ // 闆嗙兢閰嶇疆鏂瑰紡 鍙傝�冧笅鏂规敞閲�
+ RedissonProperties.ClusterServersConfig clusterServersConfig = redissonProperties.getClusterServersConfig();
+ if (ObjectUtil.isNotNull(clusterServersConfig)) {
+ config.useClusterServers()
+ //璁剧疆redis key鍓嶇紑
+ .setNameMapper(new KeyPrefixHandler(redissonProperties.getKeyPrefix()))
+ .setTimeout(clusterServersConfig.getTimeout())
+ .setClientName(clusterServersConfig.getClientName())
+ .setIdleConnectionTimeout(clusterServersConfig.getIdleConnectionTimeout())
+ .setSubscriptionConnectionPoolSize(clusterServersConfig.getSubscriptionConnectionPoolSize())
+ .setMasterConnectionMinimumIdleSize(clusterServersConfig.getMasterConnectionMinimumIdleSize())
+ .setMasterConnectionPoolSize(clusterServersConfig.getMasterConnectionPoolSize())
+ .setSlaveConnectionMinimumIdleSize(clusterServersConfig.getSlaveConnectionMinimumIdleSize())
+ .setSlaveConnectionPoolSize(clusterServersConfig.getSlaveConnectionPoolSize())
+ .setReadMode(clusterServersConfig.getReadMode())
+ .setSubscriptionMode(clusterServersConfig.getSubscriptionMode());
+ }
+ log.info("鍒濆鍖� redis 閰嶇疆");
+ };
+ }
+
+ /**
+ * 鑷畾涔夌紦瀛樼鐞嗗櫒 鏁村悎spring-cache
+ */
+ @Bean
+ public CacheManager cacheManager() {
+ return new PlusSpringCacheManager();
+ }
+
+ /**
+ * redis闆嗙兢閰嶇疆 yml
+ *
+ * --- # redis 闆嗙兢閰嶇疆(鍗曟満涓庨泦缇ゅ彧鑳藉紑鍚竴涓彟涓�涓渶瑕佹敞閲婃帀)
+ * spring.data:
+ * redis:
+ * cluster:
+ * nodes:
+ * - 192.168.0.100:6379
+ * - 192.168.0.101:6379
+ * - 192.168.0.102:6379
+ * # 瀵嗙爜
+ * password:
+ * # 杩炴帴瓒呮椂鏃堕棿
+ * timeout: 10s
+ * # 鏄惁寮�鍚痵sl
+ * ssl.enabled: false
+ *
+ * redisson:
+ * # 绾跨▼姹犳暟閲�
+ * threads: 16
+ * # Netty绾跨▼姹犳暟閲�
+ * nettyThreads: 32
+ * # 闆嗙兢閰嶇疆
+ * clusterServersConfig:
+ * # 瀹㈡埛绔悕绉�
+ * clientName: ${ruoyi.name}
+ * # master鏈�灏忕┖闂茶繛鎺ユ暟
+ * masterConnectionMinimumIdleSize: 32
+ * # master杩炴帴姹犲ぇ灏�
+ * masterConnectionPoolSize: 64
+ * # slave鏈�灏忕┖闂茶繛鎺ユ暟
+ * slaveConnectionMinimumIdleSize: 32
+ * # slave杩炴帴姹犲ぇ灏�
+ * slaveConnectionPoolSize: 64
+ * # 杩炴帴绌洪棽瓒呮椂锛屽崟浣嶏細姣
+ * idleConnectionTimeout: 10000
+ * # 鍛戒护绛夊緟瓒呮椂锛屽崟浣嶏細姣
+ * timeout: 3000
+ * # 鍙戝竷鍜岃闃呰繛鎺ユ睜澶у皬
+ * subscriptionConnectionPoolSize: 50
+ * # 璇诲彇妯″紡
+ * readMode: "SLAVE"
+ * # 璁㈤槄妯″紡
+ * subscriptionMode: "MASTER"
+ */
+
+}
diff --git a/ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/config/properties/RedissonProperties.java b/ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/config/properties/RedissonProperties.java
new file mode 100644
index 0000000..ebec786
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/config/properties/RedissonProperties.java
@@ -0,0 +1,135 @@
+package org.dromara.common.redis.config.properties;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.redisson.config.ReadMode;
+import org.redisson.config.SubscriptionMode;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * Redisson 閰嶇疆灞炴��
+ *
+ * @author Lion Li
+ */
+@Data
+@ConfigurationProperties(prefix = "redisson")
+public class RedissonProperties {
+
+ /**
+ * redis缂撳瓨key鍓嶇紑
+ */
+ private String keyPrefix;
+
+ /**
+ * 绾跨▼姹犳暟閲�,榛樿鍊� = 褰撳墠澶勭悊鏍告暟閲� * 2
+ */
+ private int threads;
+
+ /**
+ * Netty绾跨▼姹犳暟閲�,榛樿鍊� = 褰撳墠澶勭悊鏍告暟閲� * 2
+ */
+ private int nettyThreads;
+
+ /**
+ * 鍗曟満鏈嶅姟閰嶇疆
+ */
+ private SingleServerConfig singleServerConfig;
+
+ /**
+ * 闆嗙兢鏈嶅姟閰嶇疆
+ */
+ private ClusterServersConfig clusterServersConfig;
+
+ @Data
+ @NoArgsConstructor
+ public static class SingleServerConfig {
+
+ /**
+ * 瀹㈡埛绔悕绉�
+ */
+ private String clientName;
+
+ /**
+ * 鏈�灏忕┖闂茶繛鎺ユ暟
+ */
+ private int connectionMinimumIdleSize;
+
+ /**
+ * 杩炴帴姹犲ぇ灏�
+ */
+ private int connectionPoolSize;
+
+ /**
+ * 杩炴帴绌洪棽瓒呮椂锛屽崟浣嶏細姣
+ */
+ private int idleConnectionTimeout;
+
+ /**
+ * 鍛戒护绛夊緟瓒呮椂锛屽崟浣嶏細姣
+ */
+ private int timeout;
+
+ /**
+ * 鍙戝竷鍜岃闃呰繛鎺ユ睜澶у皬
+ */
+ private int subscriptionConnectionPoolSize;
+
+ }
+
+ @Data
+ @NoArgsConstructor
+ public static class ClusterServersConfig {
+
+ /**
+ * 瀹㈡埛绔悕绉�
+ */
+ private String clientName;
+
+ /**
+ * master鏈�灏忕┖闂茶繛鎺ユ暟
+ */
+ private int masterConnectionMinimumIdleSize;
+
+ /**
+ * master杩炴帴姹犲ぇ灏�
+ */
+ private int masterConnectionPoolSize;
+
+ /**
+ * slave鏈�灏忕┖闂茶繛鎺ユ暟
+ */
+ private int slaveConnectionMinimumIdleSize;
+
+ /**
+ * slave杩炴帴姹犲ぇ灏�
+ */
+ private int slaveConnectionPoolSize;
+
+ /**
+ * 杩炴帴绌洪棽瓒呮椂锛屽崟浣嶏細姣
+ */
+ private int idleConnectionTimeout;
+
+ /**
+ * 鍛戒护绛夊緟瓒呮椂锛屽崟浣嶏細姣
+ */
+ private int timeout;
+
+ /**
+ * 鍙戝竷鍜岃闃呰繛鎺ユ睜澶у皬
+ */
+ private int subscriptionConnectionPoolSize;
+
+ /**
+ * 璇诲彇妯″紡
+ */
+ private ReadMode readMode;
+
+ /**
+ * 璁㈤槄妯″紡
+ */
+ private SubscriptionMode subscriptionMode;
+
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/handler/KeyPrefixHandler.java b/ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/handler/KeyPrefixHandler.java
new file mode 100644
index 0000000..3bf3e34
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/handler/KeyPrefixHandler.java
@@ -0,0 +1,50 @@
+package org.dromara.common.redis.handler;
+
+import org.dromara.common.core.utils.StringUtils;
+import org.redisson.api.NameMapper;
+
+/**
+ * redis缂撳瓨key鍓嶇紑澶勭悊
+ *
+ * @author ye
+ * @date 2022/7/14 17:44
+ * @since 4.3.0
+ */
+public class KeyPrefixHandler implements NameMapper {
+
+ private final String keyPrefix;
+
+ public KeyPrefixHandler(String keyPrefix) {
+ //鍓嶇紑涓虹┖ 鍒欒繑鍥炵┖鍓嶇紑
+ this.keyPrefix = StringUtils.isBlank(keyPrefix) ? "" : keyPrefix + ":";
+ }
+
+ /**
+ * 澧炲姞鍓嶇紑
+ */
+ @Override
+ public String map(String name) {
+ if (StringUtils.isBlank(name)) {
+ return null;
+ }
+ if (StringUtils.isNotBlank(keyPrefix) && !name.startsWith(keyPrefix)) {
+ return keyPrefix + name;
+ }
+ return name;
+ }
+
+ /**
+ * 鍘婚櫎鍓嶇紑
+ */
+ @Override
+ public String unmap(String name) {
+ if (StringUtils.isBlank(name)) {
+ return null;
+ }
+ if (StringUtils.isNotBlank(keyPrefix) && name.startsWith(keyPrefix)) {
+ return name.substring(keyPrefix.length());
+ }
+ return name;
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/manager/PlusSpringCacheManager.java b/ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/manager/PlusSpringCacheManager.java
new file mode 100644
index 0000000..8c84889
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/manager/PlusSpringCacheManager.java
@@ -0,0 +1,192 @@
+/**
+ * Copyright (c) 2013-2021 Nikita Koksharov
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.dromara.common.redis.manager;
+
+import org.dromara.common.redis.utils.RedisUtils;
+import org.redisson.api.RMap;
+import org.redisson.api.RMapCache;
+import org.redisson.spring.cache.CacheConfig;
+import org.redisson.spring.cache.RedissonCache;
+import org.springframework.boot.convert.DurationStyle;
+import org.springframework.cache.Cache;
+import org.springframework.cache.CacheManager;
+import org.springframework.cache.transaction.TransactionAwareCacheDecorator;
+import org.springframework.util.StringUtils;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+/**
+ * A {@link CacheManager} implementation
+ * backed by Redisson instance.
+ * <p>
+ * 淇敼 RedissonSpringCacheManager 婧愮爜
+ * 閲嶅啓 cacheName 澶勭悊鏂规硶 鏀寔澶氬弬鏁�
+ *
+ * @author Nikita Koksharov
+ *
+ */
+@SuppressWarnings("unchecked")
+public class PlusSpringCacheManager implements CacheManager {
+
+ private boolean dynamic = true;
+
+ private boolean allowNullValues = true;
+
+ private boolean transactionAware = true;
+
+ Map<String, CacheConfig> configMap = new ConcurrentHashMap<>();
+ ConcurrentMap<String, Cache> instanceMap = new ConcurrentHashMap<>();
+
+ /**
+ * Creates CacheManager supplied by Redisson instance
+ */
+ public PlusSpringCacheManager() {
+ }
+
+
+ /**
+ * Defines possibility of storing {@code null} values.
+ * <p>
+ * Default is <code>true</code>
+ *
+ * @param allowNullValues stores if <code>true</code>
+ */
+ public void setAllowNullValues(boolean allowNullValues) {
+ this.allowNullValues = allowNullValues;
+ }
+
+ /**
+ * Defines if cache aware of Spring-managed transactions.
+ * If {@code true} put/evict operations are executed only for successful transaction in after-commit phase.
+ * <p>
+ * Default is <code>false</code>
+ *
+ * @param transactionAware cache is transaction aware if <code>true</code>
+ */
+ public void setTransactionAware(boolean transactionAware) {
+ this.transactionAware = transactionAware;
+ }
+
+ /**
+ * Defines 'fixed' cache names.
+ * A new cache instance will not be created in dynamic for non-defined names.
+ * <p>
+ * `null` parameter setups dynamic mode
+ *
+ * @param names of caches
+ */
+ public void setCacheNames(Collection<String> names) {
+ if (names != null) {
+ for (String name : names) {
+ getCache(name);
+ }
+ dynamic = false;
+ } else {
+ dynamic = true;
+ }
+ }
+
+ /**
+ * Set cache config mapped by cache name
+ *
+ * @param config object
+ */
+ public void setConfig(Map<String, ? extends CacheConfig> config) {
+ this.configMap = (Map<String, CacheConfig>) config;
+ }
+
+ protected CacheConfig createDefaultConfig() {
+ return new CacheConfig();
+ }
+
+ @Override
+ public Cache getCache(String name) {
+ // 閲嶅啓 cacheName 鏀寔澶氬弬鏁�
+ String[] array = StringUtils.delimitedListToStringArray(name, "#");
+ name = array[0];
+
+ Cache cache = instanceMap.get(name);
+ if (cache != null) {
+ return cache;
+ }
+ if (!dynamic) {
+ return cache;
+ }
+
+ CacheConfig config = configMap.get(name);
+ if (config == null) {
+ config = createDefaultConfig();
+ configMap.put(name, config);
+ }
+
+ if (array.length > 1) {
+ config.setTTL(DurationStyle.detectAndParse(array[1]).toMillis());
+ }
+ if (array.length > 2) {
+ config.setMaxIdleTime(DurationStyle.detectAndParse(array[2]).toMillis());
+ }
+ if (array.length > 3) {
+ config.setMaxSize(Integer.parseInt(array[3]));
+ }
+
+ if (config.getMaxIdleTime() == 0 && config.getTTL() == 0 && config.getMaxSize() == 0) {
+ return createMap(name, config);
+ }
+
+ return createMapCache(name, config);
+ }
+
+ private Cache createMap(String name, CacheConfig config) {
+ RMap<Object, Object> map = RedisUtils.getClient().getMap(name);
+
+ Cache cache = new RedissonCache(map, allowNullValues);
+ if (transactionAware) {
+ cache = new TransactionAwareCacheDecorator(cache);
+ }
+ Cache oldCache = instanceMap.putIfAbsent(name, cache);
+ if (oldCache != null) {
+ cache = oldCache;
+ }
+ return cache;
+ }
+
+ private Cache createMapCache(String name, CacheConfig config) {
+ RMapCache<Object, Object> map = RedisUtils.getClient().getMapCache(name);
+
+ Cache cache = new RedissonCache(map, config, allowNullValues);
+ if (transactionAware) {
+ cache = new TransactionAwareCacheDecorator(cache);
+ }
+ Cache oldCache = instanceMap.putIfAbsent(name, cache);
+ if (oldCache != null) {
+ cache = oldCache;
+ } else {
+ map.setMaxSize(config.getMaxSize());
+ }
+ return cache;
+ }
+
+ @Override
+ public Collection<String> getCacheNames() {
+ return Collections.unmodifiableSet(configMap.keySet());
+ }
+
+
+}
diff --git a/ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/utils/CacheUtils.java b/ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/utils/CacheUtils.java
new file mode 100644
index 0000000..42a88d6
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/utils/CacheUtils.java
@@ -0,0 +1,75 @@
+package org.dromara.common.redis.utils;
+
+import org.dromara.common.core.utils.SpringUtils;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import org.redisson.api.RMap;
+import org.springframework.cache.Cache;
+import org.springframework.cache.CacheManager;
+
+import java.util.Set;
+
+/**
+ * 缂撳瓨鎿嶄綔宸ュ叿绫� {@link }
+ *
+ * @author Michelle.Chung
+ * @date 2022/8/13
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+@SuppressWarnings(value = {"unchecked"})
+public class CacheUtils {
+
+ private static final CacheManager CACHE_MANAGER = SpringUtils.getBean(CacheManager.class);
+
+ /**
+ * 鑾峰彇缂撳瓨缁勫唴鎵�鏈夌殑KEY
+ *
+ * @param cacheNames 缂撳瓨缁勫悕绉�
+ */
+ public static Set<Object> keys(String cacheNames) {
+ RMap<Object, Object> rmap = (RMap<Object, Object>) CACHE_MANAGER.getCache(cacheNames).getNativeCache();
+ return rmap.keySet();
+ }
+
+ /**
+ * 鑾峰彇缂撳瓨鍊�
+ *
+ * @param cacheNames 缂撳瓨缁勫悕绉�
+ * @param key 缂撳瓨key
+ */
+ public static <T> T get(String cacheNames, Object key) {
+ Cache.ValueWrapper wrapper = CACHE_MANAGER.getCache(cacheNames).get(key);
+ return wrapper != null ? (T) wrapper.get() : null;
+ }
+
+ /**
+ * 淇濆瓨缂撳瓨鍊�
+ *
+ * @param cacheNames 缂撳瓨缁勫悕绉�
+ * @param key 缂撳瓨key
+ * @param value 缂撳瓨鍊�
+ */
+ public static void put(String cacheNames, Object key, Object value) {
+ CACHE_MANAGER.getCache(cacheNames).put(key, value);
+ }
+
+ /**
+ * 鍒犻櫎缂撳瓨鍊�
+ *
+ * @param cacheNames 缂撳瓨缁勫悕绉�
+ * @param key 缂撳瓨key
+ */
+ public static void evict(String cacheNames, Object key) {
+ CACHE_MANAGER.getCache(cacheNames).evict(key);
+ }
+
+ /**
+ * 娓呯┖缂撳瓨鍊�
+ *
+ * @param cacheNames 缂撳瓨缁勫悕绉�
+ */
+ public static void clear(String cacheNames) {
+ CACHE_MANAGER.getCache(cacheNames).clear();
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/utils/RedisUtils.java b/ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/utils/RedisUtils.java
new file mode 100644
index 0000000..ce32055
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/utils/RedisUtils.java
@@ -0,0 +1,538 @@
+package org.dromara.common.redis.utils;
+
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import org.dromara.common.core.utils.SpringUtils;
+import org.redisson.api.*;
+
+import java.time.Duration;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * redis 宸ュ叿绫�
+ *
+ * @author Lion Li
+ * @version 3.1.0 鏂板
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+@SuppressWarnings(value = {"unchecked", "rawtypes"})
+public class RedisUtils {
+
+ private static final RedissonClient CLIENT = SpringUtils.getBean(RedissonClient.class);
+
+ /**
+ * 闄愭祦
+ *
+ * @param key 闄愭祦key
+ * @param rateType 闄愭祦绫诲瀷
+ * @param rate 閫熺巼
+ * @param rateInterval 閫熺巼闂撮殧
+ * @return -1 琛ㄧず澶辫触
+ */
+ public static long rateLimiter(String key, RateType rateType, int rate, int rateInterval) {
+ RRateLimiter rateLimiter = CLIENT.getRateLimiter(key);
+ rateLimiter.trySetRate(rateType, rate, rateInterval, RateIntervalUnit.SECONDS);
+ if (rateLimiter.tryAcquire()) {
+ return rateLimiter.availablePermits();
+ } else {
+ return -1L;
+ }
+ }
+
+ /**
+ * 鑾峰彇瀹㈡埛绔疄渚�
+ */
+ public static RedissonClient getClient() {
+ return CLIENT;
+ }
+
+ /**
+ * 鍙戝竷閫氶亾娑堟伅
+ *
+ * @param channelKey 閫氶亾key
+ * @param msg 鍙戦�佹暟鎹�
+ * @param consumer 鑷畾涔夊鐞�
+ */
+ public static <T> void publish(String channelKey, T msg, Consumer<T> consumer) {
+ RTopic topic = CLIENT.getTopic(channelKey);
+ topic.publish(msg);
+ consumer.accept(msg);
+ }
+
+ public static <T> void publish(String channelKey, T msg) {
+ RTopic topic = CLIENT.getTopic(channelKey);
+ topic.publish(msg);
+ }
+
+ /**
+ * 璁㈤槄閫氶亾鎺ユ敹娑堟伅
+ *
+ * @param channelKey 閫氶亾key
+ * @param clazz 娑堟伅绫诲瀷
+ * @param consumer 鑷畾涔夊鐞�
+ */
+ public static <T> void subscribe(String channelKey, Class<T> clazz, Consumer<T> consumer) {
+ RTopic topic = CLIENT.getTopic(channelKey);
+ topic.addListener(clazz, (channel, msg) -> consumer.accept(msg));
+ }
+
+ /**
+ * 缂撳瓨鍩烘湰鐨勫璞★紝Integer銆丼tring銆佸疄浣撶被绛�
+ *
+ * @param key 缂撳瓨鐨勯敭鍊�
+ * @param value 缂撳瓨鐨勫��
+ */
+ public static <T> void setCacheObject(final String key, final T value) {
+ setCacheObject(key, value, false);
+ }
+
+ /**
+ * 缂撳瓨鍩烘湰鐨勫璞★紝淇濈暀褰撳墠瀵硅薄 TTL 鏈夋晥鏈�
+ *
+ * @param key 缂撳瓨鐨勯敭鍊�
+ * @param value 缂撳瓨鐨勫��
+ * @param isSaveTtl 鏄惁淇濈暀TTL鏈夋晥鏈�(渚嬪: set涔嬪墠ttl鍓╀綑90 set涔嬪悗杩樻槸涓�90)
+ * @since Redis 6.X 浠ヤ笂浣跨敤 setAndKeepTTL 鍏煎 5.X 鏂规
+ */
+ public static <T> void setCacheObject(final String key, final T value, final boolean isSaveTtl) {
+ RBucket<T> bucket = CLIENT.getBucket(key);
+ if (isSaveTtl) {
+ try {
+ bucket.setAndKeepTTL(value);
+ } catch (Exception e) {
+ long timeToLive = bucket.remainTimeToLive();
+ setCacheObject(key, value, Duration.ofMillis(timeToLive));
+ }
+ } else {
+ bucket.set(value);
+ }
+ }
+
+ /**
+ * 缂撳瓨鍩烘湰鐨勫璞★紝Integer銆丼tring銆佸疄浣撶被绛�
+ *
+ * @param key 缂撳瓨鐨勯敭鍊�
+ * @param value 缂撳瓨鐨勫��
+ * @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();
+ }
+
+ /**
+ * 濡傛灉涓嶅瓨鍦ㄥ垯璁剧疆 骞惰繑鍥� true 濡傛灉瀛樺湪鍒欒繑鍥� false
+ *
+ * @param key 缂撳瓨鐨勯敭鍊�
+ * @param value 缂撳瓨鐨勫��
+ * @return set鎴愬姛鎴栧け璐�
+ */
+ public static <T> boolean setObjectIfAbsent(final String key, final T value, final Duration duration) {
+ RBucket<T> bucket = CLIENT.getBucket(key);
+ return bucket.setIfAbsent(value, duration);
+ }
+
+ /**
+ * 濡傛灉瀛樺湪鍒欒缃� 骞惰繑鍥� true 濡傛灉瀛樺湪鍒欒繑鍥� false
+ *
+ * @param key 缂撳瓨鐨勯敭鍊�
+ * @param value 缂撳瓨鐨勫��
+ * @return set鎴愬姛鎴栧け璐�
+ */
+ public static <T> boolean setObjectIfExists(final String key, final T value, final Duration duration) {
+ RBucket<T> bucket = CLIENT.getBucket(key);
+ return bucket.setIfExists(value, duration);
+ }
+
+ /**
+ * 娉ㄥ唽瀵硅薄鐩戝惉鍣�
+ * <p>
+ * key 鐩戝惉鍣ㄩ渶寮�鍚� `notify-keyspace-events` 绛� redis 鐩稿叧閰嶇疆
+ *
+ * @param key 缂撳瓨鐨勯敭鍊�
+ * @param listener 鐩戝惉鍣ㄩ厤缃�
+ */
+ public static <T> void addObjectListener(final String key, final ObjectListener listener) {
+ RBucket<T> result = CLIENT.getBucket(key);
+ result.addListener(listener);
+ }
+
+ /**
+ * 璁剧疆鏈夋晥鏃堕棿
+ *
+ * @param key Redis閿�
+ * @param timeout 瓒呮椂鏃堕棿
+ * @return true=璁剧疆鎴愬姛锛沠alse=璁剧疆澶辫触
+ */
+ public static boolean expire(final String key, final long timeout) {
+ return expire(key, Duration.ofSeconds(timeout));
+ }
+
+ /**
+ * 璁剧疆鏈夋晥鏃堕棿
+ *
+ * @param key Redis閿�
+ * @param duration 瓒呮椂鏃堕棿
+ * @return true=璁剧疆鎴愬姛锛沠alse=璁剧疆澶辫触
+ */
+ public static boolean expire(final String key, final Duration duration) {
+ RBucket rBucket = CLIENT.getBucket(key);
+ return rBucket.expire(duration);
+ }
+
+ /**
+ * 鑾峰緱缂撳瓨鐨勫熀鏈璞°��
+ *
+ * @param key 缂撳瓨閿��
+ * @return 缂撳瓨閿�煎搴旂殑鏁版嵁
+ */
+ public static <T> T getCacheObject(final String key) {
+ RBucket<T> rBucket = CLIENT.getBucket(key);
+ return rBucket.get();
+ }
+
+ /**
+ * 鑾峰緱key鍓╀綑瀛樻椿鏃堕棿
+ *
+ * @param key 缂撳瓨閿��
+ * @return 鍓╀綑瀛樻椿鏃堕棿
+ */
+ public static <T> long getTimeToLive(final String key) {
+ RBucket<T> rBucket = CLIENT.getBucket(key);
+ return rBucket.remainTimeToLive();
+ }
+
+ /**
+ * 鍒犻櫎鍗曚釜瀵硅薄
+ *
+ * @param key 缂撳瓨鐨勯敭鍊�
+ */
+ public static boolean deleteObject(final String key) {
+ return CLIENT.getBucket(key).delete();
+ }
+
+ /**
+ * 鍒犻櫎闆嗗悎瀵硅薄
+ *
+ * @param collection 澶氫釜瀵硅薄
+ */
+ public static void deleteObject(final Collection collection) {
+ RBatch batch = CLIENT.createBatch();
+ collection.forEach(t -> {
+ batch.getBucket(t.toString()).deleteAsync();
+ });
+ batch.execute();
+ }
+
+ /**
+ * 妫�鏌ョ紦瀛樺璞℃槸鍚﹀瓨鍦�
+ *
+ * @param key 缂撳瓨鐨勯敭鍊�
+ */
+ public static boolean isExistsObject(final String key) {
+ return CLIENT.getBucket(key).isExists();
+ }
+
+ /**
+ * 缂撳瓨List鏁版嵁
+ *
+ * @param key 缂撳瓨鐨勯敭鍊�
+ * @param dataList 寰呯紦瀛樼殑List鏁版嵁
+ * @return 缂撳瓨鐨勫璞�
+ */
+ public static <T> boolean setCacheList(final String key, final List<T> dataList) {
+ RList<T> rList = CLIENT.getList(key);
+ return rList.addAll(dataList);
+ }
+
+ /**
+ * 杩藉姞缂撳瓨List鏁版嵁
+ *
+ * @param key 缂撳瓨鐨勯敭鍊�
+ * @param data 寰呯紦瀛樼殑鏁版嵁
+ * @return 缂撳瓨鐨勫璞�
+ */
+ public static <T> boolean addCacheList(final String key, final T data) {
+ RList<T> rList = CLIENT.getList(key);
+ return rList.add(data);
+ }
+
+ /**
+ * 娉ㄥ唽List鐩戝惉鍣�
+ * <p>
+ * key 鐩戝惉鍣ㄩ渶寮�鍚� `notify-keyspace-events` 绛� redis 鐩稿叧閰嶇疆
+ *
+ * @param key 缂撳瓨鐨勯敭鍊�
+ * @param listener 鐩戝惉鍣ㄩ厤缃�
+ */
+ public static <T> void addListListener(final String key, final ObjectListener listener) {
+ RList<T> rList = CLIENT.getList(key);
+ rList.addListener(listener);
+ }
+
+ /**
+ * 鑾峰緱缂撳瓨鐨刲ist瀵硅薄
+ *
+ * @param key 缂撳瓨鐨勯敭鍊�
+ * @return 缂撳瓨閿�煎搴旂殑鏁版嵁
+ */
+ public static <T> List<T> getCacheList(final String key) {
+ RList<T> rList = CLIENT.getList(key);
+ return rList.readAll();
+ }
+
+ /**
+ * 鑾峰緱缂撳瓨鐨刲ist瀵硅薄(鑼冨洿)
+ *
+ * @param key 缂撳瓨鐨勯敭鍊�
+ * @param form 璧峰涓嬫爣
+ * @param to 鎴涓嬫爣
+ * @return 缂撳瓨閿�煎搴旂殑鏁版嵁
+ */
+ public static <T> List<T> getCacheListRange(final String key, int form, int to) {
+ RList<T> rList = CLIENT.getList(key);
+ return rList.range(form, to);
+ }
+
+ /**
+ * 缂撳瓨Set
+ *
+ * @param key 缂撳瓨閿��
+ * @param dataSet 缂撳瓨鐨勬暟鎹�
+ * @return 缂撳瓨鏁版嵁鐨勫璞�
+ */
+ public static <T> boolean setCacheSet(final String key, final Set<T> dataSet) {
+ RSet<T> rSet = CLIENT.getSet(key);
+ return rSet.addAll(dataSet);
+ }
+
+ /**
+ * 杩藉姞缂撳瓨Set鏁版嵁
+ *
+ * @param key 缂撳瓨鐨勯敭鍊�
+ * @param data 寰呯紦瀛樼殑鏁版嵁
+ * @return 缂撳瓨鐨勫璞�
+ */
+ public static <T> boolean addCacheSet(final String key, final T data) {
+ RSet<T> rSet = CLIENT.getSet(key);
+ return rSet.add(data);
+ }
+
+ /**
+ * 娉ㄥ唽Set鐩戝惉鍣�
+ * <p>
+ * key 鐩戝惉鍣ㄩ渶寮�鍚� `notify-keyspace-events` 绛� redis 鐩稿叧閰嶇疆
+ *
+ * @param key 缂撳瓨鐨勯敭鍊�
+ * @param listener 鐩戝惉鍣ㄩ厤缃�
+ */
+ public static <T> void addSetListener(final String key, final ObjectListener listener) {
+ RSet<T> rSet = CLIENT.getSet(key);
+ rSet.addListener(listener);
+ }
+
+ /**
+ * 鑾峰緱缂撳瓨鐨剆et
+ *
+ * @param key 缂撳瓨鐨刱ey
+ * @return set瀵硅薄
+ */
+ public static <T> Set<T> getCacheSet(final String key) {
+ RSet<T> rSet = CLIENT.getSet(key);
+ return rSet.readAll();
+ }
+
+ /**
+ * 缂撳瓨Map
+ *
+ * @param key 缂撳瓨鐨勯敭鍊�
+ * @param dataMap 缂撳瓨鐨勬暟鎹�
+ */
+ public static <T> void setCacheMap(final String key, final Map<String, T> dataMap) {
+ if (dataMap != null) {
+ RMap<String, T> rMap = CLIENT.getMap(key);
+ rMap.putAll(dataMap);
+ }
+ }
+
+ /**
+ * 娉ㄥ唽Map鐩戝惉鍣�
+ * <p>
+ * key 鐩戝惉鍣ㄩ渶寮�鍚� `notify-keyspace-events` 绛� redis 鐩稿叧閰嶇疆
+ *
+ * @param key 缂撳瓨鐨勯敭鍊�
+ * @param listener 鐩戝惉鍣ㄩ厤缃�
+ */
+ public static <T> void addMapListener(final String key, final ObjectListener listener) {
+ RMap<String, T> rMap = CLIENT.getMap(key);
+ rMap.addListener(listener);
+ }
+
+ /**
+ * 鑾峰緱缂撳瓨鐨凪ap
+ *
+ * @param key 缂撳瓨鐨勯敭鍊�
+ * @return map瀵硅薄
+ */
+ public static <T> Map<String, T> getCacheMap(final String key) {
+ RMap<String, T> rMap = CLIENT.getMap(key);
+ return rMap.getAll(rMap.keySet());
+ }
+
+ /**
+ * 鑾峰緱缂撳瓨Map鐨刱ey鍒楄〃
+ *
+ * @param key 缂撳瓨鐨勯敭鍊�
+ * @return key鍒楄〃
+ */
+ public static <T> Set<String> getCacheMapKeySet(final String key) {
+ RMap<String, T> rMap = CLIENT.getMap(key);
+ return rMap.keySet();
+ }
+
+ /**
+ * 寰�Hash涓瓨鍏ユ暟鎹�
+ *
+ * @param key Redis閿�
+ * @param hKey Hash閿�
+ * @param value 鍊�
+ */
+ public static <T> void setCacheMapValue(final String key, final String hKey, final T value) {
+ RMap<String, T> rMap = CLIENT.getMap(key);
+ rMap.put(hKey, value);
+ }
+
+ /**
+ * 鑾峰彇Hash涓殑鏁版嵁
+ *
+ * @param key Redis閿�
+ * @param hKey Hash閿�
+ * @return Hash涓殑瀵硅薄
+ */
+ public static <T> T getCacheMapValue(final String key, final String hKey) {
+ RMap<String, T> rMap = CLIENT.getMap(key);
+ return rMap.get(hKey);
+ }
+
+ /**
+ * 鍒犻櫎Hash涓殑鏁版嵁
+ *
+ * @param key Redis閿�
+ * @param hKey Hash閿�
+ * @return Hash涓殑瀵硅薄
+ */
+ public static <T> T delCacheMapValue(final String key, final String hKey) {
+ RMap<String, T> rMap = CLIENT.getMap(key);
+ return rMap.remove(hKey);
+ }
+
+ /**
+ * 鍒犻櫎Hash涓殑鏁版嵁
+ *
+ * @param key Redis閿�
+ * @param hKeys Hash閿�
+ */
+ public static <T> void delMultiCacheMapValue(final String key, final Set<String> hKeys) {
+ RBatch batch = CLIENT.createBatch();
+ RMapAsync<String, T> rMap = batch.getMap(key);
+ for (String hKey : hKeys) {
+ rMap.removeAsync(hKey);
+ }
+ batch.execute();
+ }
+
+ /**
+ * 鑾峰彇澶氫釜Hash涓殑鏁版嵁
+ *
+ * @param key Redis閿�
+ * @param hKeys Hash閿泦鍚�
+ * @return Hash瀵硅薄闆嗗悎
+ */
+ public static <K, V> Map<K, V> getMultiCacheMapValue(final String key, final Set<K> hKeys) {
+ RMap<K, V> rMap = CLIENT.getMap(key);
+ return rMap.getAll(hKeys);
+ }
+
+ /**
+ * 璁剧疆鍘熷瓙鍊�
+ *
+ * @param key Redis閿�
+ * @param value 鍊�
+ */
+ public static void setAtomicValue(String key, long value) {
+ RAtomicLong atomic = CLIENT.getAtomicLong(key);
+ atomic.set(value);
+ }
+
+ /**
+ * 鑾峰彇鍘熷瓙鍊�
+ *
+ * @param key Redis閿�
+ * @return 褰撳墠鍊�
+ */
+ public static long getAtomicValue(String key) {
+ RAtomicLong atomic = CLIENT.getAtomicLong(key);
+ return atomic.get();
+ }
+
+ /**
+ * 閫掑鍘熷瓙鍊�
+ *
+ * @param key Redis閿�
+ * @return 褰撳墠鍊�
+ */
+ public static long incrAtomicValue(String key) {
+ RAtomicLong atomic = CLIENT.getAtomicLong(key);
+ return atomic.incrementAndGet();
+ }
+
+ /**
+ * 閫掑噺鍘熷瓙鍊�
+ *
+ * @param key Redis閿�
+ * @return 褰撳墠鍊�
+ */
+ public static long decrAtomicValue(String key) {
+ RAtomicLong atomic = CLIENT.getAtomicLong(key);
+ return atomic.decrementAndGet();
+ }
+
+ /**
+ * 鑾峰緱缂撳瓨鐨勫熀鏈璞″垪琛�
+ *
+ * @param pattern 瀛楃涓插墠缂�
+ * @return 瀵硅薄鍒楄〃
+ */
+ public static Collection<String> keys(final String pattern) {
+ Stream<String> stream = CLIENT.getKeys().getKeysStreamByPattern(pattern);
+ return stream.collect(Collectors.toList());
+ }
+
+ /**
+ * 鍒犻櫎缂撳瓨鐨勫熀鏈璞″垪琛�
+ *
+ * @param pattern 瀛楃涓插墠缂�
+ */
+ public static void deleteKeys(final String pattern) {
+ CLIENT.getKeys().deleteByPattern(pattern);
+ }
+
+ /**
+ * 妫�鏌edis涓槸鍚﹀瓨鍦╧ey
+ *
+ * @param key 閿�
+ */
+ public static Boolean hasKey(String key) {
+ RKeys rKeys = CLIENT.getKeys();
+ return rKeys.countExists(key) > 0;
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-redis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/ruoyi-common/ruoyi-common-redis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..934b585
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-redis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+org.dromara.common.redis.config.RedisConfiguration
diff --git a/ruoyi-common/ruoyi-common-satoken/pom.xml b/ruoyi-common/ruoyi-common-satoken/pom.xml
new file mode 100644
index 0000000..93d46eb
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-satoken/pom.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common</artifactId>
+ <version>${revision}</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>ruoyi-common-satoken</artifactId>
+
+ <description>
+ ruoyi-common-satoken
+ </description>
+
+ <dependencies>
+
+ <dependency>
+ <groupId>cn.dev33</groupId>
+ <artifactId>sa-token-core</artifactId>
+ </dependency>
+
+ <!-- Sa-Token 鏁村悎 jwt -->
+ <dependency>
+ <groupId>cn.dev33</groupId>
+ <artifactId>sa-token-jwt</artifactId>
+ <version>${satoken.version}</version>
+ <exclusions>
+ <exclusion>
+ <groupId>cn.hutool</groupId>
+ <artifactId>hutool-all</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+
+ <dependency>
+ <groupId>cn.hutool</groupId>
+ <artifactId>hutool-jwt</artifactId>
+ </dependency>
+
+ <!-- RuoYi Api System -->
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-api-system</artifactId>
+ </dependency>
+
+ <!-- RuoYi Common Redis-->
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-redis</artifactId>
+ </dependency>
+
+ </dependencies>
+
+</project>
diff --git a/ruoyi-common/ruoyi-common-satoken/src/main/java/org/dromara/common/satoken/config/SaTokenConfiguration.java b/ruoyi-common/ruoyi-common-satoken/src/main/java/org/dromara/common/satoken/config/SaTokenConfiguration.java
new file mode 100644
index 0000000..981cada
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-satoken/src/main/java/org/dromara/common/satoken/config/SaTokenConfiguration.java
@@ -0,0 +1,44 @@
+package org.dromara.common.satoken.config;
+
+import cn.dev33.satoken.dao.SaTokenDao;
+import cn.dev33.satoken.jwt.StpLogicJwtForSimple;
+import cn.dev33.satoken.stp.StpInterface;
+import cn.dev33.satoken.stp.StpLogic;
+import org.dromara.common.core.factory.YmlPropertySourceFactory;
+import org.dromara.common.satoken.core.dao.PlusSaTokenDao;
+import org.dromara.common.satoken.core.service.SaPermissionImpl;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.PropertySource;
+
+/**
+ * Sa-Token 閰嶇疆
+ *
+ * @author Lion Li
+ */
+@AutoConfiguration
+@PropertySource(value = "classpath:common-satoken.yml", factory = YmlPropertySourceFactory.class)
+public class SaTokenConfiguration {
+
+ @Bean
+ public StpLogic getStpLogicJwt() {
+ return new StpLogicJwtForSimple();
+ }
+
+ /**
+ * 鏉冮檺鎺ュ彛瀹炵幇(浣跨敤bean娉ㄥ叆鏂逛究鐢ㄦ埛鏇挎崲)
+ */
+ @Bean
+ public StpInterface stpInterface() {
+ return new SaPermissionImpl();
+ }
+
+ /**
+ * 鑷畾涔塪ao灞傚瓨鍌�
+ */
+ @Bean
+ public SaTokenDao saTokenDao() {
+ return new PlusSaTokenDao();
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-satoken/src/main/java/org/dromara/common/satoken/core/dao/PlusSaTokenDao.java b/ruoyi-common/ruoyi-common-satoken/src/main/java/org/dromara/common/satoken/core/dao/PlusSaTokenDao.java
new file mode 100644
index 0000000..d885962
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-satoken/src/main/java/org/dromara/common/satoken/core/dao/PlusSaTokenDao.java
@@ -0,0 +1,148 @@
+package org.dromara.common.satoken.core.dao;
+
+import cn.dev33.satoken.dao.SaTokenDao;
+import cn.dev33.satoken.util.SaFoxUtil;
+import org.dromara.common.redis.utils.RedisUtils;
+
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Sa-Token鎸佷箙灞傛帴鍙�(浣跨敤妗嗘灦鑷甫RedisUtils瀹炵幇 鍗忚缁熶竴)
+ *
+ * @author Lion Li
+ */
+public class PlusSaTokenDao implements SaTokenDao {
+
+ /**
+ * 鑾峰彇Value锛屽鏃犺繑绌�
+ */
+ @Override
+ public String get(String key) {
+ return RedisUtils.getCacheObject(key);
+ }
+
+ /**
+ * 鍐欏叆Value锛屽苟璁惧畾瀛樻椿鏃堕棿 (鍗曚綅: 绉�)
+ */
+ @Override
+ public void set(String key, String value, long timeout) {
+ if (timeout == 0 || timeout <= NOT_VALUE_EXPIRE) {
+ return;
+ }
+ // 鍒ゆ柇鏄惁涓烘案涓嶈繃鏈�
+ if (timeout == NEVER_EXPIRE) {
+ RedisUtils.setCacheObject(key, value);
+ } else {
+ RedisUtils.setCacheObject(key, value, Duration.ofSeconds(timeout));
+ }
+ }
+
+ /**
+ * 淇慨鏀规寚瀹歬ey-value閿�煎 (杩囨湡鏃堕棿涓嶅彉)
+ */
+ @Override
+ public void update(String key, String value) {
+ if (RedisUtils.hasKey(key)) {
+ RedisUtils.setCacheObject(key, value, true);
+ }
+ }
+
+ /**
+ * 鍒犻櫎Value
+ */
+ @Override
+ public void delete(String key) {
+ RedisUtils.deleteObject(key);
+ }
+
+ /**
+ * 鑾峰彇Value鐨勫墿浣欏瓨娲绘椂闂� (鍗曚綅: 绉�)
+ */
+ @Override
+ public long getTimeout(String key) {
+ long timeout = RedisUtils.getTimeToLive(key);
+ return timeout < 0 ? timeout : timeout / 1000;
+ }
+
+ /**
+ * 淇敼Value鐨勫墿浣欏瓨娲绘椂闂� (鍗曚綅: 绉�)
+ */
+ @Override
+ public void updateTimeout(String key, long timeout) {
+ RedisUtils.expire(key, Duration.ofSeconds(timeout));
+ }
+
+
+ /**
+ * 鑾峰彇Object锛屽鏃犺繑绌�
+ */
+ @Override
+ public Object getObject(String key) {
+ return RedisUtils.getCacheObject(key);
+ }
+
+ /**
+ * 鍐欏叆Object锛屽苟璁惧畾瀛樻椿鏃堕棿 (鍗曚綅: 绉�)
+ */
+ @Override
+ public void setObject(String key, Object object, long timeout) {
+ if (timeout == 0 || timeout <= NOT_VALUE_EXPIRE) {
+ return;
+ }
+ // 鍒ゆ柇鏄惁涓烘案涓嶈繃鏈�
+ if (timeout == NEVER_EXPIRE) {
+ RedisUtils.setCacheObject(key, object);
+ } else {
+ RedisUtils.setCacheObject(key, object, Duration.ofSeconds(timeout));
+ }
+ }
+
+ /**
+ * 鏇存柊Object (杩囨湡鏃堕棿涓嶅彉)
+ */
+ @Override
+ public void updateObject(String key, Object object) {
+ if (RedisUtils.hasKey(key)) {
+ RedisUtils.setCacheObject(key, object, true);
+ }
+ }
+
+ /**
+ * 鍒犻櫎Object
+ */
+ @Override
+ public void deleteObject(String key) {
+ RedisUtils.deleteObject(key);
+ }
+
+ /**
+ * 鑾峰彇Object鐨勫墿浣欏瓨娲绘椂闂� (鍗曚綅: 绉�)
+ */
+ @Override
+ public long getObjectTimeout(String key) {
+ long timeout = RedisUtils.getTimeToLive(key);
+ return timeout < 0 ? timeout : timeout / 1000;
+ }
+
+ /**
+ * 淇敼Object鐨勫墿浣欏瓨娲绘椂闂� (鍗曚綅: 绉�)
+ */
+ @Override
+ public void updateObjectTimeout(String key, long timeout) {
+ RedisUtils.expire(key, Duration.ofSeconds(timeout));
+ }
+
+
+ /**
+ * 鎼滅储鏁版嵁
+ */
+ @Override
+ public List<String> searchData(String prefix, String keyword, int start, int size, boolean sortType) {
+ Collection<String> keys = RedisUtils.keys(prefix + "*" + keyword + "*");
+ List<String> list = new ArrayList<>(keys);
+ return SaFoxUtil.searchList(list, start, size, sortType);
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-satoken/src/main/java/org/dromara/common/satoken/core/service/SaPermissionImpl.java b/ruoyi-common/ruoyi-common-satoken/src/main/java/org/dromara/common/satoken/core/service/SaPermissionImpl.java
new file mode 100644
index 0000000..257922b
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-satoken/src/main/java/org/dromara/common/satoken/core/service/SaPermissionImpl.java
@@ -0,0 +1,47 @@
+package org.dromara.common.satoken.core.service;
+
+import cn.dev33.satoken.stp.StpInterface;
+import org.dromara.common.core.enums.UserType;
+import org.dromara.common.satoken.utils.LoginHelper;
+import org.dromara.system.api.model.LoginUser;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * sa-token 鏉冮檺绠$悊瀹炵幇绫�
+ *
+ * @author Lion Li
+ */
+public class SaPermissionImpl implements StpInterface {
+
+ /**
+ * 鑾峰彇鑿滃崟鏉冮檺鍒楄〃
+ */
+ @Override
+ public List<String> getPermissionList(Object loginId, String loginType) {
+ LoginUser loginUser = LoginHelper.getLoginUser();
+ UserType userType = UserType.getUserType(loginUser.getUserType());
+ if (userType == UserType.SYS_USER) {
+ return new ArrayList<>(loginUser.getMenuPermission());
+ } else if (userType == UserType.APP_USER) {
+ // 鍏朵粬绔� 鑷鏍规嵁涓氬姟缂栧啓
+ }
+ return new ArrayList<>();
+ }
+
+ /**
+ * 鑾峰彇瑙掕壊鏉冮檺鍒楄〃
+ */
+ @Override
+ public List<String> getRoleList(Object loginId, String loginType) {
+ LoginUser loginUser = LoginHelper.getLoginUser();
+ UserType userType = UserType.getUserType(loginUser.getUserType());
+ if (userType == UserType.SYS_USER) {
+ return new ArrayList<>(loginUser.getRolePermission());
+ } else if (userType == UserType.APP_USER) {
+ // 鍏朵粬绔� 鑷鏍规嵁涓氬姟缂栧啓
+ }
+ return new ArrayList<>();
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-satoken/src/main/java/org/dromara/common/satoken/utils/LoginHelper.java b/ruoyi-common/ruoyi-common-satoken/src/main/java/org/dromara/common/satoken/utils/LoginHelper.java
new file mode 100644
index 0000000..7310f27
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-satoken/src/main/java/org/dromara/common/satoken/utils/LoginHelper.java
@@ -0,0 +1,176 @@
+package org.dromara.common.satoken.utils;
+
+import cn.dev33.satoken.context.SaHolder;
+import cn.dev33.satoken.context.model.SaStorage;
+import cn.dev33.satoken.session.SaSession;
+import cn.dev33.satoken.stp.SaLoginModel;
+import cn.dev33.satoken.stp.StpUtil;
+import cn.hutool.core.convert.Convert;
+import cn.hutool.core.util.ObjectUtil;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import org.dromara.common.core.constant.TenantConstants;
+import org.dromara.common.core.constant.UserConstants;
+import org.dromara.common.core.enums.UserType;
+import org.dromara.system.api.model.LoginUser;
+
+import java.util.Set;
+import java.util.function.Supplier;
+
+/**
+ * 鐧诲綍閴存潈鍔╂墜
+ * <p>
+ * user_type 涓� 鐢ㄦ埛绫诲瀷 鍚屼竴涓敤鎴疯〃 鍙互鏈夊绉嶇敤鎴风被鍨� 渚嬪 pc,app
+ * deivce 涓� 璁惧绫诲瀷 鍚屼竴涓敤鎴风被鍨� 鍙互鏈� 澶氱璁惧绫诲瀷 渚嬪 web,ios
+ * 鍙互缁勬垚 鐢ㄦ埛绫诲瀷涓庤澶囩被鍨嬪瀵瑰鐨� 鏉冮檺鐏垫椿鎺у埗
+ * <p>
+ * 澶氱敤鎴蜂綋绯� 閽堝 澶氱鐢ㄦ埛绫诲瀷 浣嗘潈闄愭帶鍒朵笉涓�鑷�
+ * 鍙互缁勬垚 澶氱敤鎴风被鍨嬭〃涓庡璁惧绫诲瀷 鍒嗗埆鎺у埗鏉冮檺
+ *
+ * @author Lion Li
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class LoginHelper {
+
+ public static final String LOGIN_USER_KEY = "loginUser";
+ public static final String TENANT_KEY = "tenantId";
+ public static final String USER_KEY = "userId";
+ public static final String DEPT_KEY = "deptId";
+ public static final String CLIENT_KEY = "clientid";
+ public static final String TENANT_ADMIN_KEY = "isTenantAdmin";
+
+ /**
+ * 鐧诲綍绯荤粺 鍩轰簬 璁惧绫诲瀷
+ * 閽堝鐩稿悓鐢ㄦ埛浣撶郴涓嶅悓璁惧
+ *
+ * @param loginUser 鐧诲綍鐢ㄦ埛淇℃伅
+ * @param model 閰嶇疆鍙傛暟
+ */
+ public static void login(LoginUser loginUser, SaLoginModel model) {
+ SaStorage storage = SaHolder.getStorage();
+ storage.set(LOGIN_USER_KEY, loginUser);
+ storage.set(TENANT_KEY, loginUser.getTenantId());
+ storage.set(USER_KEY, loginUser.getUserId());
+ storage.set(DEPT_KEY, loginUser.getDeptId());
+ model = ObjectUtil.defaultIfNull(model, new SaLoginModel());
+ StpUtil.login(loginUser.getLoginId(),
+ model.setExtra(TENANT_KEY, loginUser.getTenantId())
+ .setExtra(USER_KEY, loginUser.getUserId())
+ .setExtra(DEPT_KEY, loginUser.getDeptId()));
+ SaSession tokenSession = StpUtil.getTokenSession();
+ tokenSession.updateTimeout(model.getTimeout());
+ tokenSession.set(LOGIN_USER_KEY, loginUser);
+ }
+
+ /**
+ * 鑾峰彇鐢ㄦ埛(澶氱骇缂撳瓨)
+ */
+ public static LoginUser getLoginUser() {
+ return (LoginUser) getStorageIfAbsentSet(LOGIN_USER_KEY, () -> {
+ SaSession session = StpUtil.getTokenSession();
+ if (ObjectUtil.isNull(session)) {
+ return null;
+ }
+ return session.get(LOGIN_USER_KEY);
+ });
+ }
+
+ /**
+ * 鑾峰彇鐢ㄦ埛鍩轰簬token
+ */
+ public static LoginUser getLoginUser(String token) {
+ SaSession session = StpUtil.getTokenSessionByToken(token);
+ if (ObjectUtil.isNull(session)) {
+ return null;
+ }
+ return (LoginUser) session.get(LOGIN_USER_KEY);
+ }
+
+ /**
+ * 鑾峰彇鐢ㄦ埛id
+ */
+ public static Long getUserId() {
+ return Convert.toLong(getExtra(USER_KEY));
+ }
+
+ /**
+ * 鑾峰彇绉熸埛ID
+ */
+ public static String getTenantId() {
+ return Convert.toStr(getExtra(TENANT_KEY));
+ }
+
+ /**
+ * 鑾峰彇閮ㄩ棬ID
+ */
+ public static Long getDeptId() {
+ return Convert.toLong(getExtra(DEPT_KEY));
+ }
+
+ private static Object getExtra(String key) {
+ return getStorageIfAbsentSet(key, () -> StpUtil.getExtra(key));
+ }
+
+ /**
+ * 鑾峰彇鐢ㄦ埛璐︽埛
+ */
+ public static String getUsername() {
+ return getLoginUser().getUsername();
+ }
+
+ /**
+ * 鑾峰彇鐢ㄦ埛绫诲瀷
+ */
+ public static UserType getUserType() {
+ String loginId = StpUtil.getLoginIdAsString();
+ return UserType.getUserType(loginId);
+ }
+
+ /**
+ * 鏄惁涓鸿秴绾х鐞嗗憳
+ *
+ * @param userId 鐢ㄦ埛ID
+ * @return 缁撴灉
+ */
+ public static boolean isSuperAdmin(Long userId) {
+ return UserConstants.SUPER_ADMIN_ID.equals(userId);
+ }
+
+ public static boolean isSuperAdmin() {
+ return isSuperAdmin(getUserId());
+ }
+
+ /**
+ * 鏄惁涓鸿秴绾х鐞嗗憳
+ *
+ * @param rolePermission 瑙掕壊鏉冮檺鏍囪瘑缁�
+ * @return 缁撴灉
+ */
+ public static boolean isTenantAdmin(Set<String> rolePermission) {
+ return rolePermission.contains(TenantConstants.TENANT_ADMIN_ROLE_KEY);
+ }
+
+ public static boolean isTenantAdmin() {
+ Object value = getStorageIfAbsentSet(TENANT_ADMIN_KEY, () -> {
+ return isTenantAdmin(getLoginUser().getRolePermission());
+ });
+ return Convert.toBool(value);
+ }
+
+ public static boolean isLogin() {
+ return getLoginUser() != null;
+ }
+
+ public static Object getStorageIfAbsentSet(String key, Supplier<Object> handle) {
+ try {
+ Object obj = SaHolder.getStorage().get(key);
+ if (ObjectUtil.isNull(obj)) {
+ obj = handle.get();
+ SaHolder.getStorage().set(key, obj);
+ }
+ return obj;
+ } catch (Exception e) {
+ return null;
+ }
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-satoken/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/ruoyi-common/ruoyi-common-satoken/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..9103401
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-satoken/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+org.dromara.common.satoken.config.SaTokenConfiguration
diff --git a/ruoyi-common/ruoyi-common-satoken/src/main/resources/common-satoken.yml b/ruoyi-common/ruoyi-common-satoken/src/main/resources/common-satoken.yml
new file mode 100644
index 0000000..95e1b41
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-satoken/src/main/resources/common-satoken.yml
@@ -0,0 +1,13 @@
+# 鍐呯疆閰嶇疆 涓嶅厑璁镐慨鏀� 濡傞渶淇敼璇峰湪 nacos 涓婂啓鐩稿悓閰嶇疆瑕嗙洊
+# Sa-Token閰嶇疆
+sa-token:
+ # 鍏佽鍔ㄦ�佽缃� token 鏈夋晥鏈�
+ dynamic-active-timeout: true
+ # 鍏佽浠� 璇锋眰鍙傛暟 璇诲彇 token
+ is-read-body: true
+ # 鍏佽浠� header 璇诲彇 token
+ is-read-header: true
+ # 鍏抽棴 cookie 閴存潈 浠庢牴婧愭潨缁� csrf 婕忔礊椋庨櫓
+ is-read-cookie: false
+ # token鍓嶇紑
+ token-prefix: "Bearer"
diff --git a/ruoyi-common/ruoyi-common-seata/pom.xml b/ruoyi-common/ruoyi-common-seata/pom.xml
new file mode 100644
index 0000000..b2b51f8
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-seata/pom.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns="http://maven.apache.org/POM/4.0.0"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common</artifactId>
+ <version>${revision}</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>ruoyi-common-seata</artifactId>
+
+ <description>
+ ruoyi-common-seata 鍒嗗竷寮忎簨鍔�
+ </description>
+
+ <dependencies>
+
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-core</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.dubbo.extensions</groupId>
+ <artifactId>dubbo-filter-seata</artifactId>
+ <version>1.0.1</version>
+ <exclusions>
+ <exclusion>
+ <groupId>io.seata</groupId>
+ <artifactId>seata-core</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+
+ <!-- SpringBoot Seata -->
+ <dependency>
+ <groupId>com.alibaba.cloud</groupId>
+ <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
+ <exclusions>
+ <exclusion>
+ <groupId>org.apache.logging.log4j</groupId>
+ <artifactId>*</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.apache.dubbo.extensions</groupId>
+ <artifactId>dubbo-filter-seata</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ </dependencies>
+</project>
diff --git a/ruoyi-common/ruoyi-common-seata/src/main/java/org/dromara/common/seata/config/SeataConfiguration.java b/ruoyi-common/ruoyi-common-seata/src/main/java/org/dromara/common/seata/config/SeataConfiguration.java
new file mode 100644
index 0000000..6b4ddb2
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-seata/src/main/java/org/dromara/common/seata/config/SeataConfiguration.java
@@ -0,0 +1,16 @@
+package org.dromara.common.seata.config;
+
+import org.dromara.common.core.factory.YmlPropertySourceFactory;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.context.annotation.PropertySource;
+
+/**
+ * seata 閰嶇疆
+ *
+ * @author Lion Li
+ */
+@AutoConfiguration
+@PropertySource(value = "classpath:common-seata.yml", factory = YmlPropertySourceFactory.class)
+public class SeataConfiguration {
+
+}
diff --git a/ruoyi-common/ruoyi-common-seata/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/ruoyi-common/ruoyi-common-seata/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..c51e1d0
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-seata/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+org.dromara.common.seata.config.SeataConfiguration
diff --git a/ruoyi-common/ruoyi-common-seata/src/main/resources/common-seata.yml b/ruoyi-common/ruoyi-common-seata/src/main/resources/common-seata.yml
new file mode 100644
index 0000000..698f2dd
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-seata/src/main/resources/common-seata.yml
@@ -0,0 +1,19 @@
+# 鍐呯疆閰嶇疆 涓嶅厑璁镐慨鏀� 濡傞渶淇敼璇峰湪 nacos 涓婂啓鐩稿悓閰嶇疆瑕嗙洊
+# seata閰嶇疆
+seata:
+ config:
+ type: nacos
+ nacos:
+ server-addr: ${spring.cloud.nacos.server-addr}
+ group: ${spring.cloud.nacos.config.group}
+ namespace: ${spring.profiles.active}
+ data-id: seata-server.properties
+ registry:
+ type: nacos
+ nacos:
+ application: ruoyi-seata-server
+ server-addr: ${spring.cloud.nacos.server-addr}
+ group: ${spring.cloud.nacos.discovery.group}
+ namespace: ${spring.profiles.active}
+ # 鍏抽棴鑷姩浠g悊
+ enable-auto-data-source-proxy: false
diff --git a/ruoyi-common/ruoyi-common-security/pom.xml b/ruoyi-common/ruoyi-common-security/pom.xml
new file mode 100644
index 0000000..ee499f1
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-security/pom.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common</artifactId>
+ <version>${revision}</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>ruoyi-common-security</artifactId>
+
+ <description>
+ ruoyi-common-security 瀹夊叏妯″潡
+ </description>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-satoken</artifactId>
+ </dependency>
+
+ <!-- Sa-Token 鏉冮檺璁よ瘉, 鍦ㄧ嚎鏂囨。锛歨ttp://sa-token.dev33.cn/ -->
+ <dependency>
+ <groupId>cn.dev33</groupId>
+ <artifactId>sa-token-spring-boot3-starter</artifactId>
+ </dependency>
+
+ </dependencies>
+
+</project>
diff --git a/ruoyi-common/ruoyi-common-security/src/main/java/org/dromara/common/security/config/SecurityConfiguration.java b/ruoyi-common/ruoyi-common-security/src/main/java/org/dromara/common/security/config/SecurityConfiguration.java
new file mode 100644
index 0000000..efd34fc
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-security/src/main/java/org/dromara/common/security/config/SecurityConfiguration.java
@@ -0,0 +1,47 @@
+package org.dromara.common.security.config;
+
+import cn.dev33.satoken.SaManager;
+import cn.dev33.satoken.filter.SaServletFilter;
+import cn.dev33.satoken.interceptor.SaInterceptor;
+import cn.dev33.satoken.same.SaSameUtil;
+import cn.dev33.satoken.util.SaResult;
+import org.dromara.common.core.constant.HttpStatus;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+/**
+ * 鏉冮檺瀹夊叏閰嶇疆
+ *
+ * @author Lion Li
+ */
+@AutoConfiguration
+public class SecurityConfiguration implements WebMvcConfigurer {
+
+ /**
+ * 娉ㄥ唽sa-token鐨勬嫤鎴櫒
+ */
+ @Override
+ public void addInterceptors(InterceptorRegistry registry) {
+ // 娉ㄥ唽璺敱鎷︽埅鍣紝鑷畾涔夐獙璇佽鍒�
+ registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**");
+ }
+
+ /**
+ * 鏍¢獙鏄惁浠庣綉鍏宠浆鍙�
+ */
+ @Bean
+ public SaServletFilter getSaServletFilter() {
+ return new SaServletFilter()
+ .addInclude("/**")
+ .addExclude("/actuator/**")
+ .setAuth(obj -> {
+ if (SaManager.getConfig().getCheckSameToken()) {
+ SaSameUtil.checkCurrentRequestToken();
+ }
+ })
+ .setError(e -> SaResult.error("璁よ瘉澶辫触锛屾棤娉曡闂郴缁熻祫婧�").setCode(HttpStatus.UNAUTHORIZED));
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-security/src/main/java/org/dromara/common/security/handler/GlobalExceptionHandler.java b/ruoyi-common/ruoyi-common-security/src/main/java/org/dromara/common/security/handler/GlobalExceptionHandler.java
new file mode 100644
index 0000000..8fb1f76
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-security/src/main/java/org/dromara/common/security/handler/GlobalExceptionHandler.java
@@ -0,0 +1,175 @@
+package org.dromara.common.security.handler;
+
+import cn.dev33.satoken.exception.NotLoginException;
+import cn.dev33.satoken.exception.NotPermissionException;
+import cn.dev33.satoken.exception.NotRoleException;
+import cn.dev33.satoken.exception.SameTokenInvalidException;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.http.HttpStatus;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.validation.ConstraintViolation;
+import jakarta.validation.ConstraintViolationException;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.core.exception.ServiceException;
+import org.dromara.common.core.exception.base.BaseException;
+import org.dromara.common.core.utils.StreamUtils;
+import org.springframework.context.support.DefaultMessageSourceResolvable;
+import org.springframework.validation.BindException;
+import org.springframework.web.HttpRequestMethodNotSupportedException;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.MissingPathVariableException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
+
+/**
+ * 鍏ㄥ眬寮傚父澶勭悊鍣�
+ *
+ * @author Lion Li
+ */
+@Slf4j
+@RestControllerAdvice
+public class GlobalExceptionHandler {
+
+ /**
+ * 鏉冮檺鐮佸紓甯�
+ */
+ @ExceptionHandler(NotPermissionException.class)
+ public R<Void> handleNotPermissionException(NotPermissionException e, HttpServletRequest request) {
+ String requestURI = request.getRequestURI();
+ log.error("璇锋眰鍦板潃'{}',鏉冮檺鐮佹牎楠屽け璐�'{}'", requestURI, e.getMessage());
+ return R.fail(HttpStatus.HTTP_FORBIDDEN, "娌℃湁璁块棶鏉冮檺锛岃鑱旂郴绠$悊鍛樻巿鏉�");
+ }
+
+ /**
+ * 瑙掕壊鏉冮檺寮傚父
+ */
+ @ExceptionHandler(NotRoleException.class)
+ public R<Void> handleNotRoleException(NotRoleException e, HttpServletRequest request) {
+ String requestURI = request.getRequestURI();
+ log.error("璇锋眰鍦板潃'{}',瑙掕壊鏉冮檺鏍¢獙澶辫触'{}'", requestURI, e.getMessage());
+ return R.fail(HttpStatus.HTTP_FORBIDDEN, "娌℃湁璁块棶鏉冮檺锛岃鑱旂郴绠$悊鍛樻巿鏉�");
+ }
+
+ /**
+ * 璁よ瘉澶辫触
+ */
+ @ExceptionHandler(NotLoginException.class)
+ public R<Void> handleNotLoginException(NotLoginException e, HttpServletRequest request) {
+ String requestURI = request.getRequestURI();
+ log.error("璇锋眰鍦板潃'{}',璁よ瘉澶辫触'{}',鏃犳硶璁块棶绯荤粺璧勬簮", requestURI, e.getMessage());
+ return R.fail(HttpStatus.HTTP_UNAUTHORIZED, "璁よ瘉澶辫触锛屾棤娉曡闂郴缁熻祫婧�");
+ }
+
+ /**
+ * 鏃犳晥璁よ瘉
+ */
+ @ExceptionHandler(SameTokenInvalidException.class)
+ public R<Void> handleSameTokenInvalidException(SameTokenInvalidException e, HttpServletRequest request) {
+ String requestURI = request.getRequestURI();
+ log.error("璇锋眰鍦板潃'{}',鍐呯綉璁よ瘉澶辫触'{}',鏃犳硶璁块棶绯荤粺璧勬簮", requestURI, e.getMessage());
+ return R.fail(HttpStatus.HTTP_UNAUTHORIZED, "璁よ瘉澶辫触锛屾棤娉曡闂郴缁熻祫婧�");
+ }
+
+ /**
+ * 璇锋眰鏂瑰紡涓嶆敮鎸�
+ */
+ @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
+ public R<Void> handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException e,
+ HttpServletRequest request) {
+ String requestURI = request.getRequestURI();
+ log.error("璇锋眰鍦板潃'{}',涓嶆敮鎸�'{}'璇锋眰", requestURI, e.getMethod());
+ return R.fail(e.getMessage());
+ }
+
+ /**
+ * 涓氬姟寮傚父
+ */
+ @ExceptionHandler(ServiceException.class)
+ public R<Void> handleServiceException(ServiceException e, HttpServletRequest request) {
+ log.error(e.getMessage());
+ Integer code = e.getCode();
+ return ObjectUtil.isNotNull(code) ? R.fail(code, e.getMessage()) : R.fail(e.getMessage());
+ }
+
+ /**
+ * 涓氬姟寮傚父
+ */
+ @ExceptionHandler(BaseException.class)
+ public R<Void> handleBaseException(BaseException e, HttpServletRequest request) {
+ log.error(e.getMessage());
+ return R.fail(e.getMessage());
+ }
+
+ /**
+ * 璇锋眰璺緞涓己灏戝繀闇�鐨勮矾寰勫彉閲�
+ */
+ @ExceptionHandler(MissingPathVariableException.class)
+ public R<Void> handleMissingPathVariableException(MissingPathVariableException e, HttpServletRequest request) {
+ String requestURI = request.getRequestURI();
+ log.error("璇锋眰璺緞涓己灏戝繀闇�鐨勮矾寰勫彉閲�'{}',鍙戠敓绯荤粺寮傚父.", requestURI);
+ return R.fail(String.format("璇锋眰璺緞涓己灏戝繀闇�鐨勮矾寰勫彉閲廩%s]", e.getVariableName()));
+ }
+
+ /**
+ * 璇锋眰鍙傛暟绫诲瀷涓嶅尮閰�
+ */
+ @ExceptionHandler(MethodArgumentTypeMismatchException.class)
+ public R<Void> handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e, HttpServletRequest request) {
+ String requestURI = request.getRequestURI();
+ log.error("璇锋眰鍙傛暟绫诲瀷涓嶅尮閰�'{}',鍙戠敓绯荤粺寮傚父.", requestURI);
+ return R.fail(String.format("璇锋眰鍙傛暟绫诲瀷涓嶅尮閰嶏紝鍙傛暟[%s]瑕佹眰绫诲瀷涓猴細'%s'锛屼絾杈撳叆鍊间负锛�'%s'", e.getName(), e.getRequiredType().getName(), e.getValue()));
+ }
+
+ /**
+ * 鎷︽埅鏈煡鐨勮繍琛屾椂寮傚父
+ */
+ @ExceptionHandler(RuntimeException.class)
+ public R<Void> handleRuntimeException(RuntimeException e, HttpServletRequest request) {
+ String requestURI = request.getRequestURI();
+ log.error("璇锋眰鍦板潃'{}',鍙戠敓鏈煡寮傚父.", requestURI, e);
+ return R.fail(e.getMessage());
+ }
+
+ /**
+ * 绯荤粺寮傚父
+ */
+ @ExceptionHandler(Exception.class)
+ public R<Void> handleException(Exception e, HttpServletRequest request) {
+ String requestURI = request.getRequestURI();
+ log.error("璇锋眰鍦板潃'{}',鍙戠敓绯荤粺寮傚父.", requestURI, e);
+ return R.fail(e.getMessage());
+ }
+
+ /**
+ * 鑷畾涔夐獙璇佸紓甯�
+ */
+ @ExceptionHandler(BindException.class)
+ public R<Void> handleBindException(BindException e) {
+ log.error(e.getMessage());
+ String message = StreamUtils.join(e.getAllErrors(), DefaultMessageSourceResolvable::getDefaultMessage, ", ");
+ return R.fail(message);
+ }
+
+ /**
+ * 鑷畾涔夐獙璇佸紓甯�
+ */
+ @ExceptionHandler(ConstraintViolationException.class)
+ public R<Void> constraintViolationException(ConstraintViolationException e) {
+ log.error(e.getMessage());
+ String message = StreamUtils.join(e.getConstraintViolations(), ConstraintViolation::getMessage, ", ");
+ return R.fail(message);
+ }
+
+ /**
+ * 鑷畾涔夐獙璇佸紓甯�
+ */
+ @ExceptionHandler(MethodArgumentNotValidException.class)
+ public R<Void> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
+ log.error(e.getMessage());
+ String message = e.getBindingResult().getFieldError().getDefaultMessage();
+ return R.fail(message);
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/ruoyi-common/ruoyi-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..4c96c4e
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1,2 @@
+org.dromara.common.security.handler.GlobalExceptionHandler
+org.dromara.common.security.config.SecurityConfiguration
diff --git a/ruoyi-common/ruoyi-common-sensitive/pom.xml b/ruoyi-common/ruoyi-common-sensitive/pom.xml
new file mode 100644
index 0000000..fecdf09
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-sensitive/pom.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common</artifactId>
+ <version>${revision}</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>ruoyi-common-sensitive</artifactId>
+
+ <description>
+ ruoyi-common-sensitive 鑴辨晱妯″潡
+ </description>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-json</artifactId>
+ </dependency>
+ </dependencies>
+
+</project>
diff --git a/ruoyi-common/ruoyi-common-sensitive/src/main/java/org/dromara/common/sensitive/annotation/Sensitive.java b/ruoyi-common/ruoyi-common-sensitive/src/main/java/org/dromara/common/sensitive/annotation/Sensitive.java
new file mode 100644
index 0000000..1dfc896
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-sensitive/src/main/java/org/dromara/common/sensitive/annotation/Sensitive.java
@@ -0,0 +1,28 @@
+package org.dromara.common.sensitive.annotation;
+
+import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import org.dromara.common.sensitive.core.SensitiveStrategy;
+import org.dromara.common.sensitive.handler.SensitiveHandler;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 鏁版嵁鑴辨晱娉ㄨВ
+ *
+ * @author zhujie
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.FIELD)
+@JacksonAnnotationsInside
+@JsonSerialize(using = SensitiveHandler.class)
+public @interface Sensitive {
+ SensitiveStrategy strategy();
+
+ String roleKey() default "";
+
+ String perms() default "";
+}
diff --git a/ruoyi-common/ruoyi-common-sensitive/src/main/java/org/dromara/common/sensitive/core/SensitiveService.java b/ruoyi-common/ruoyi-common-sensitive/src/main/java/org/dromara/common/sensitive/core/SensitiveService.java
new file mode 100644
index 0000000..7b5264b
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-sensitive/src/main/java/org/dromara/common/sensitive/core/SensitiveService.java
@@ -0,0 +1,18 @@
+package org.dromara.common.sensitive.core;
+
+/**
+ * 鑴辨晱鏈嶅姟
+ * 榛樿绠$悊鍛樹笉杩囨护
+ * 闇�鑷鏍规嵁涓氬姟閲嶅啓瀹炵幇
+ *
+ * @author Lion Li
+ * @version 3.6.0
+ */
+public interface SensitiveService {
+
+ /**
+ * 鏄惁鑴辨晱
+ */
+ boolean isSensitive(String roleKey, String perms);
+
+}
diff --git a/ruoyi-common/ruoyi-common-sensitive/src/main/java/org/dromara/common/sensitive/core/SensitiveStrategy.java b/ruoyi-common/ruoyi-common-sensitive/src/main/java/org/dromara/common/sensitive/core/SensitiveStrategy.java
new file mode 100644
index 0000000..9d1978a
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-sensitive/src/main/java/org/dromara/common/sensitive/core/SensitiveStrategy.java
@@ -0,0 +1,49 @@
+package org.dromara.common.sensitive.core;
+
+import cn.hutool.core.util.DesensitizedUtil;
+import lombok.AllArgsConstructor;
+
+import java.util.function.Function;
+
+/**
+ * 鑴辨晱绛栫暐
+ *
+ * @author Yjoioooo
+ * @version 3.6.0
+ */
+@AllArgsConstructor
+public enum SensitiveStrategy {
+
+ /**
+ * 韬唤璇佽劚鏁�
+ */
+ ID_CARD(s -> DesensitizedUtil.idCardNum(s, 3, 4)),
+
+ /**
+ * 鎵嬫満鍙疯劚鏁�
+ */
+ PHONE(DesensitizedUtil::mobilePhone),
+
+ /**
+ * 鍦板潃鑴辨晱
+ */
+ ADDRESS(s -> DesensitizedUtil.address(s, 8)),
+
+ /**
+ * 閭鑴辨晱
+ */
+ EMAIL(DesensitizedUtil::email),
+
+ /**
+ * 閾惰鍗�
+ */
+ BANK_CARD(DesensitizedUtil::bankCard);
+
+ //鍙嚜琛屾坊鍔犲叾浠栬劚鏁忕瓥鐣�
+
+ private final Function<String, String> desensitizer;
+
+ public Function<String, String> desensitizer() {
+ return desensitizer;
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-sensitive/src/main/java/org/dromara/common/sensitive/handler/SensitiveHandler.java b/ruoyi-common/ruoyi-common-sensitive/src/main/java/org/dromara/common/sensitive/handler/SensitiveHandler.java
new file mode 100644
index 0000000..c76c83a
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-sensitive/src/main/java/org/dromara/common/sensitive/handler/SensitiveHandler.java
@@ -0,0 +1,58 @@
+package org.dromara.common.sensitive.handler;
+
+import cn.hutool.core.util.ObjectUtil;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.BeanProperty;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.ser.ContextualSerializer;
+import org.dromara.common.core.utils.SpringUtils;
+import org.dromara.common.sensitive.annotation.Sensitive;
+import org.dromara.common.sensitive.core.SensitiveService;
+import org.dromara.common.sensitive.core.SensitiveStrategy;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.BeansException;
+
+import java.io.IOException;
+import java.util.Objects;
+
+/**
+ * 鏁版嵁鑴辨晱json搴忓垪鍖栧伐鍏�
+ *
+ * @author Yjoioooo
+ */
+@Slf4j
+public class SensitiveHandler extends JsonSerializer<String> implements ContextualSerializer {
+
+ private SensitiveStrategy strategy;
+ private String roleKey;
+ private String perms;
+
+ @Override
+ public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
+ try {
+ SensitiveService sensitiveService = SpringUtils.getBean(SensitiveService.class);
+ if (ObjectUtil.isNotNull(sensitiveService) && sensitiveService.isSensitive(roleKey, perms)) {
+ gen.writeString(strategy.desensitizer().apply(value));
+ } else {
+ gen.writeString(value);
+ }
+ } catch (BeansException e) {
+ log.error("鑴辨晱瀹炵幇涓嶅瓨鍦�, 閲囩敤榛樿澶勭悊 => {}", e.getMessage());
+ gen.writeString(value);
+ }
+ }
+
+ @Override
+ public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {
+ Sensitive annotation = property.getAnnotation(Sensitive.class);
+ if (Objects.nonNull(annotation) && Objects.equals(String.class, property.getType().getRawClass())) {
+ this.strategy = annotation.strategy();
+ this.roleKey = annotation.roleKey();
+ this.perms = annotation.perms();
+ return this;
+ }
+ return prov.findValueSerializer(property.getType(), property);
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-sentinel/pom.xml b/ruoyi-common/ruoyi-common-sentinel/pom.xml
new file mode 100644
index 0000000..3185baf
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-sentinel/pom.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common</artifactId>
+ <version>${revision}</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>ruoyi-common-sentinel</artifactId>
+
+ <description>
+ ruoyi-common-sentinel 闄愭祦妯″潡
+ </description>
+
+ <dependencies>
+ <!-- SpringCloud Alibaba Sentinel -->
+ <dependency>
+ <groupId>com.alibaba.cloud</groupId>
+ <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
+ </dependency>
+
+ <!-- Sentinel Datasource Nacos -->
+ <dependency>
+ <groupId>com.alibaba.csp</groupId>
+ <artifactId>sentinel-datasource-nacos</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.alibaba.csp</groupId>
+ <artifactId>sentinel-apache-dubbo3-adapter</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>com.fasterxml.jackson.dataformat</groupId>
+ <artifactId>jackson-dataformat-xml</artifactId>
+ <optional>true</optional>
+ </dependency>
+
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-core</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-autoconfigure</artifactId>
+ </dependency>
+ </dependencies>
+
+</project>
diff --git a/ruoyi-common/ruoyi-common-sentinel/src/main/java/com/alibaba/cloud/sentinel/custom/SentinelAutoConfiguration.java b/ruoyi-common/ruoyi-common-sentinel/src/main/java/com/alibaba/cloud/sentinel/custom/SentinelAutoConfiguration.java
new file mode 100644
index 0000000..df23c49
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-sentinel/src/main/java/com/alibaba/cloud/sentinel/custom/SentinelAutoConfiguration.java
@@ -0,0 +1,265 @@
+/*
+ * Copyright 2013-2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.alibaba.cloud.sentinel.custom;
+
+import com.alibaba.cloud.commons.lang.StringUtils;
+import com.alibaba.cloud.sentinel.SentinelProperties;
+import com.alibaba.cloud.sentinel.datasource.converter.JsonConverter;
+import com.alibaba.cloud.sentinel.datasource.converter.XmlConverter;
+import com.alibaba.csp.sentinel.annotation.aspectj.SentinelResourceAspect;
+import com.alibaba.csp.sentinel.config.SentinelConfig;
+import com.alibaba.csp.sentinel.init.InitExecutor;
+import com.alibaba.csp.sentinel.log.LogBase;
+import com.alibaba.csp.sentinel.slots.block.authority.AuthorityRule;
+import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule;
+import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
+import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRule;
+import com.alibaba.csp.sentinel.slots.system.SystemRule;
+import com.alibaba.csp.sentinel.transport.config.TransportConfig;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.dataformat.xml.XmlMapper;
+import jakarta.annotation.PostConstruct;
+import org.dromara.common.core.utils.StreamUtils;
+import org.dromara.common.sentinel.config.properties.SentinelCustomProperties;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.beans.factory.support.DefaultListableBeanFactory;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.cloud.client.ServiceInstance;
+import org.springframework.cloud.client.discovery.DiscoveryClient;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.env.Environment;
+
+import java.util.List;
+
+import static com.alibaba.cloud.sentinel.SentinelConstants.BLOCK_PAGE_URL_CONF_KEY;
+import static com.alibaba.csp.sentinel.config.SentinelConfig.setConfig;
+
+/**
+ * 鏀归�爏entinel鑷姩閰嶇疆 鏀寔鏈嶅姟鍚嶆敞鍐�
+ *
+ * @author Lion Li
+ *
+ * @author xiaojing
+ * @author jiashuai.xie
+ * @author <a href="mailto:fangjian0423@gmail.com">Jim</a>
+ */
+@Configuration(proxyBeanMethods = false)
+@ConditionalOnProperty(name = "spring.cloud.sentinel.enabled", matchIfMissing = true)
+@EnableConfigurationProperties({SentinelProperties.class, SentinelCustomProperties.class})
+public class SentinelAutoConfiguration {
+
+ @Value("${project.name:${spring.application.name:}}")
+ private String projectName;
+
+ @Autowired
+ private SentinelProperties properties;
+ @Autowired
+ private SentinelCustomProperties customProperties;
+ @Autowired
+ private DiscoveryClient discoveryClient;
+
+ @PostConstruct
+ private void init() {
+ if (StringUtils.isEmpty(System.getProperty(LogBase.LOG_DIR))
+ && StringUtils.isNotBlank(properties.getLog().getDir())) {
+ System.setProperty(LogBase.LOG_DIR, properties.getLog().getDir());
+ }
+ if (StringUtils.isEmpty(System.getProperty(LogBase.LOG_NAME_USE_PID))
+ && properties.getLog().isSwitchPid()) {
+ System.setProperty(LogBase.LOG_NAME_USE_PID,
+ String.valueOf(properties.getLog().isSwitchPid()));
+ }
+ if (StringUtils.isEmpty(System.getProperty(SentinelConfig.APP_NAME_PROP_KEY))
+ && StringUtils.isNotBlank(projectName)) {
+ System.setProperty(SentinelConfig.APP_NAME_PROP_KEY, projectName);
+ }
+ if (StringUtils.isEmpty(System.getProperty(TransportConfig.SERVER_PORT))
+ && StringUtils.isNotBlank(properties.getTransport().getPort())) {
+ System.setProperty(TransportConfig.SERVER_PORT,
+ properties.getTransport().getPort());
+ }
+
+ if (StringUtils.isNotBlank(customProperties.getServerName())) {
+ List<ServiceInstance> instances = discoveryClient.getInstances(customProperties.getServerName());
+ String serverList = StreamUtils.join(instances, instance ->
+ String.format("http://%s:%s", instance.getHost(), instance.getPort()));
+ System.setProperty(TransportConfig.CONSOLE_SERVER, serverList);
+ } else {
+ if (StringUtils.isEmpty(System.getProperty(TransportConfig.CONSOLE_SERVER))
+ && StringUtils.isNotBlank(properties.getTransport().getDashboard())) {
+ System.setProperty(TransportConfig.CONSOLE_SERVER,
+ properties.getTransport().getDashboard());
+ }
+ }
+
+ if (StringUtils.isEmpty(System.getProperty(TransportConfig.HEARTBEAT_INTERVAL_MS))
+ && StringUtils
+ .isNotBlank(properties.getTransport().getHeartbeatIntervalMs())) {
+ System.setProperty(TransportConfig.HEARTBEAT_INTERVAL_MS,
+ properties.getTransport().getHeartbeatIntervalMs());
+ }
+ if (StringUtils.isEmpty(System.getProperty(TransportConfig.HEARTBEAT_CLIENT_IP))
+ && StringUtils.isNotBlank(properties.getTransport().getClientIp())) {
+ System.setProperty(TransportConfig.HEARTBEAT_CLIENT_IP,
+ properties.getTransport().getClientIp());
+ }
+ if (StringUtils.isEmpty(System.getProperty(SentinelConfig.CHARSET))
+ && StringUtils.isNotBlank(properties.getMetric().getCharset())) {
+ System.setProperty(SentinelConfig.CHARSET,
+ properties.getMetric().getCharset());
+ }
+ if (StringUtils
+ .isEmpty(System.getProperty(SentinelConfig.SINGLE_METRIC_FILE_SIZE))
+ && StringUtils.isNotBlank(properties.getMetric().getFileSingleSize())) {
+ System.setProperty(SentinelConfig.SINGLE_METRIC_FILE_SIZE,
+ properties.getMetric().getFileSingleSize());
+ }
+ if (StringUtils
+ .isEmpty(System.getProperty(SentinelConfig.TOTAL_METRIC_FILE_COUNT))
+ && StringUtils.isNotBlank(properties.getMetric().getFileTotalCount())) {
+ System.setProperty(SentinelConfig.TOTAL_METRIC_FILE_COUNT,
+ properties.getMetric().getFileTotalCount());
+ }
+ if (StringUtils.isEmpty(System.getProperty(SentinelConfig.COLD_FACTOR))
+ && StringUtils.isNotBlank(properties.getFlow().getColdFactor())) {
+ System.setProperty(SentinelConfig.COLD_FACTOR,
+ properties.getFlow().getColdFactor());
+ }
+ if (StringUtils.isNotBlank(properties.getBlockPage())) {
+ setConfig(BLOCK_PAGE_URL_CONF_KEY, properties.getBlockPage());
+ }
+
+ // earlier initialize
+ if (properties.isEager()) {
+ InitExecutor.doInit();
+ }
+
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ public SentinelResourceAspect sentinelResourceAspect() {
+ return new SentinelResourceAspect();
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ @ConditionalOnClass(name = "org.springframework.web.client.RestTemplate")
+ @ConditionalOnProperty(name = "resttemplate.sentinel.enabled", havingValue = "true",
+ matchIfMissing = true)
+ public SentinelBeanPostProcessor sentinelBeanPostProcessor(
+ ApplicationContext applicationContext) {
+ return new SentinelBeanPostProcessor(applicationContext);
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ public SentinelDataSourceHandler sentinelDataSourceHandler(
+ DefaultListableBeanFactory beanFactory, SentinelProperties sentinelProperties,
+ Environment env) {
+ return new SentinelDataSourceHandler(beanFactory, sentinelProperties, env);
+ }
+
+ @ConditionalOnClass(ObjectMapper.class)
+ @Configuration(proxyBeanMethods = false)
+ protected static class SentinelConverterConfiguration {
+
+ @Configuration(proxyBeanMethods = false)
+ protected static class SentinelJsonConfiguration {
+
+ private ObjectMapper objectMapper = new ObjectMapper();
+
+ public SentinelJsonConfiguration() {
+ objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,
+ false);
+ }
+
+ @Bean("sentinel-json-flow-converter")
+ public JsonConverter jsonFlowConverter() {
+ return new JsonConverter(objectMapper, FlowRule.class);
+ }
+
+ @Bean("sentinel-json-degrade-converter")
+ public JsonConverter jsonDegradeConverter() {
+ return new JsonConverter(objectMapper, DegradeRule.class);
+ }
+
+ @Bean("sentinel-json-system-converter")
+ public JsonConverter jsonSystemConverter() {
+ return new JsonConverter(objectMapper, SystemRule.class);
+ }
+
+ @Bean("sentinel-json-authority-converter")
+ public JsonConverter jsonAuthorityConverter() {
+ return new JsonConverter(objectMapper, AuthorityRule.class);
+ }
+
+ @Bean("sentinel-json-param-flow-converter")
+ public JsonConverter jsonParamFlowConverter() {
+ return new JsonConverter(objectMapper, ParamFlowRule.class);
+ }
+
+ }
+
+ @ConditionalOnClass(XmlMapper.class)
+ @Configuration(proxyBeanMethods = false)
+ protected static class SentinelXmlConfiguration {
+
+ private XmlMapper xmlMapper = new XmlMapper();
+
+ public SentinelXmlConfiguration() {
+ xmlMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,
+ false);
+ }
+
+ @Bean("sentinel-xml-flow-converter")
+ public XmlConverter xmlFlowConverter() {
+ return new XmlConverter(xmlMapper, FlowRule.class);
+ }
+
+ @Bean("sentinel-xml-degrade-converter")
+ public XmlConverter xmlDegradeConverter() {
+ return new XmlConverter(xmlMapper, DegradeRule.class);
+ }
+
+ @Bean("sentinel-xml-system-converter")
+ public XmlConverter xmlSystemConverter() {
+ return new XmlConverter(xmlMapper, SystemRule.class);
+ }
+
+ @Bean("sentinel-xml-authority-converter")
+ public XmlConverter xmlAuthorityConverter() {
+ return new XmlConverter(xmlMapper, AuthorityRule.class);
+ }
+
+ @Bean("sentinel-xml-param-flow-converter")
+ public XmlConverter xmlParamFlowConverter() {
+ return new XmlConverter(xmlMapper, ParamFlowRule.class);
+ }
+
+ }
+
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-sentinel/src/main/java/org/dromara/common/sentinel/config/properties/SentinelCustomProperties.java b/ruoyi-common/ruoyi-common-sentinel/src/main/java/org/dromara/common/sentinel/config/properties/SentinelCustomProperties.java
new file mode 100644
index 0000000..934cd36
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-sentinel/src/main/java/org/dromara/common/sentinel/config/properties/SentinelCustomProperties.java
@@ -0,0 +1,17 @@
+package org.dromara.common.sentinel.config.properties;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * sentinel鑷畾涔夐厤缃被
+ *
+ * @author Lion Li
+ */
+@Data
+@ConfigurationProperties(prefix = "spring.cloud.sentinel.transport")
+public class SentinelCustomProperties {
+
+ private String serverName;
+
+}
diff --git a/ruoyi-common/ruoyi-common-sentinel/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/ruoyi-common/ruoyi-common-sentinel/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-sentinel/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+
diff --git a/ruoyi-common/ruoyi-common-skylog/pom.xml b/ruoyi-common/ruoyi-common-skylog/pom.xml
new file mode 100644
index 0000000..212bcf0
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-skylog/pom.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns="http://maven.apache.org/POM/4.0.0"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common</artifactId>
+ <version>${revision}</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>ruoyi-common-skylog</artifactId>
+
+ <description>
+ ruoyi-common-skylog skywalking鏃ュ織鏀堕泦妯″潡
+ </description>
+
+ <dependencies>
+ <!-- skywalking 鏁村悎 logback -->
+ <dependency>
+ <groupId>org.apache.skywalking</groupId>
+ <artifactId>apm-toolkit-logback-1.x</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.skywalking</groupId>
+ <artifactId>apm-toolkit-trace</artifactId>
+ </dependency>
+ </dependencies>
+</project>
diff --git a/ruoyi-common/ruoyi-common-skylog/src/main/resources/logback-skylog.xml b/ruoyi-common/ruoyi-common-skylog/src/main/resources/logback-skylog.xml
new file mode 100644
index 0000000..7e64cc5
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-skylog/src/main/resources/logback-skylog.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<included>
+
+ <!-- 鎺у埗鍙拌緭鍑� tid -->
+ <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
+ <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
+ <layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">
+ <pattern>[%tid] ${console.log.pattern}</pattern>
+ </layout>
+ <charset>utf-8</charset>
+ </encoder>
+ </appender>
+
+ <!-- skywalking 閲囬泦鏃ュ織 -->
+ <appender name="sky_log" class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.log.GRPCLogClientAppender">
+ <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
+ <layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">
+ <pattern>[%tid] ${console.log.pattern}</pattern>
+ </layout>
+ <charset>utf-8</charset>
+ </encoder>
+ </appender>
+
+ <root level="info">
+ <appender-ref ref="console"/>
+ <appender-ref ref="sky_log"/>
+ </root>
+</included>
diff --git a/ruoyi-common/ruoyi-common-sms/pom.xml b/ruoyi-common/ruoyi-common-sms/pom.xml
new file mode 100644
index 0000000..64e5242
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-sms/pom.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns="http://maven.apache.org/POM/4.0.0"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common</artifactId>
+ <version>${revision}</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>ruoyi-common-sms</artifactId>
+
+ <description>
+ ruoyi-common-sms 鐭俊妯″潡
+ </description>
+
+ <dependencies>
+
+ <dependency>
+ <groupId>org.dromara.sms4j</groupId>
+ <artifactId>sms4j-spring-boot-starter</artifactId>
+ <exclusions>
+ <!-- 鎺掗櫎浜笢鐭俊鍐呭瓨鍦ㄧ殑fastjson绛夊緟浣滆�呭悗缁慨澶� -->
+ <exclusion>
+ <groupId>com.alibaba</groupId>
+ <artifactId>fastjson</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+
+ </dependencies>
+</project>
diff --git a/ruoyi-common/ruoyi-common-sms/src/main/java/org/dromara/common/sms/config/SmsAutoConfiguration.java b/ruoyi-common/ruoyi-common-sms/src/main/java/org/dromara/common/sms/config/SmsAutoConfiguration.java
new file mode 100644
index 0000000..29f60fc
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-sms/src/main/java/org/dromara/common/sms/config/SmsAutoConfiguration.java
@@ -0,0 +1,14 @@
+package org.dromara.common.sms.config;
+
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+
+/**
+ * 鐭俊閰嶇疆绫�(鏆傛椂娌$敤 棰勭暀鎵╁睍)
+ *
+ * @author Lion Li
+ * @version 4.2.0
+ */
+@AutoConfiguration
+public class SmsAutoConfiguration {
+
+}
diff --git a/ruoyi-common/ruoyi-common-sms/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/ruoyi-common/ruoyi-common-sms/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..5919ce3
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-sms/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+org.dromara.common.sms.config.SmsAutoConfiguration
diff --git a/ruoyi-common/ruoyi-common-social/pom.xml b/ruoyi-common/ruoyi-common-social/pom.xml
new file mode 100644
index 0000000..887a8e0
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-social/pom.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common</artifactId>
+ <version>${revision}</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>ruoyi-common-social</artifactId>
+
+ <description>
+ ruoyi-common-social 鎺堟潈璁よ瘉
+ </description>
+
+ <dependencies>
+ <dependency>
+ <groupId>me.zhyd.oauth</groupId>
+ <artifactId>JustAuth</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-json</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-redis</artifactId>
+ </dependency>
+ </dependencies>
+
+</project>
diff --git a/ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/config/SocialAutoConfiguration.java b/ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/config/SocialAutoConfiguration.java
new file mode 100644
index 0000000..19b39d8
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/config/SocialAutoConfiguration.java
@@ -0,0 +1,23 @@
+package org.dromara.common.social.config;
+
+import me.zhyd.oauth.cache.AuthStateCache;
+import org.dromara.common.social.config.properties.SocialProperties;
+import org.dromara.common.social.utils.AuthRedisStateCache;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+
+/**
+ * Social 閰嶇疆灞炴��
+ * @author thiszhc
+ */
+@AutoConfiguration
+@EnableConfigurationProperties(SocialProperties.class)
+public class SocialAutoConfiguration {
+
+ @Bean
+ public AuthStateCache authStateCache() {
+ return new AuthRedisStateCache();
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/config/properties/SocialLoginConfigProperties.java b/ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/config/properties/SocialLoginConfigProperties.java
new file mode 100644
index 0000000..76be234
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/config/properties/SocialLoginConfigProperties.java
@@ -0,0 +1,68 @@
+package org.dromara.common.social.config.properties;
+
+import lombok.Data;
+
+/**
+ * 绀句氦鐧诲綍閰嶇疆
+ *
+ * @author thiszhc
+ */
+@Data
+public class SocialLoginConfigProperties {
+
+ /**
+ * 搴旂敤 ID
+ */
+ private String clientId;
+
+ /**
+ * 搴旂敤瀵嗛挜
+ */
+ private String clientSecret;
+
+ /**
+ * 鍥炶皟鍦板潃
+ */
+ private String redirectUri;
+
+ /**
+ * 鏄惁鑾峰彇unionId
+ */
+ private boolean unionId;
+
+ /**
+ * Coding 浼佷笟鍚嶇О
+ */
+ private String codingGroupName;
+
+ /**
+ * 鏀粯瀹濆叕閽�
+ */
+ private String alipayPublicKey;
+
+ /**
+ * 浼佷笟寰俊搴旂敤ID
+ */
+ private String agentId;
+
+ /**
+ * stackoverflow api key
+ */
+ private String stackOverflowKey;
+
+ /**
+ * 璁惧ID
+ */
+ private String deviceId;
+
+ /**
+ * 瀹㈡埛绔郴缁熺被鍨�
+ */
+ private String clientOsType;
+
+ /**
+ * maxkey 鏈嶅姟鍣ㄥ湴鍧�
+ */
+ private String serverUrl;
+
+}
diff --git a/ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/config/properties/SocialProperties.java b/ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/config/properties/SocialProperties.java
new file mode 100644
index 0000000..1beb7d0
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/config/properties/SocialProperties.java
@@ -0,0 +1,29 @@
+package org.dromara.common.social.config.properties;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+
+/**
+ * Social 閰嶇疆灞炴��
+ *
+ * @author thiszhc
+ */
+@Data
+@Component
+@ConfigurationProperties(prefix = "justauth")
+public class SocialProperties {
+
+ /**
+ * 鏄惁鍚敤
+ */
+ private Boolean enabled;
+
+ /**
+ * 鎺堟潈绫诲瀷
+ */
+ private Map<String, SocialLoginConfigProperties> type;
+
+}
diff --git a/ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/maxkey/AuthMaxKeyRequest.java b/ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/maxkey/AuthMaxKeyRequest.java
new file mode 100644
index 0000000..b95c19e
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/maxkey/AuthMaxKeyRequest.java
@@ -0,0 +1,80 @@
+package org.dromara.common.social.maxkey;
+
+import cn.hutool.core.lang.Dict;
+import me.zhyd.oauth.cache.AuthStateCache;
+import me.zhyd.oauth.config.AuthConfig;
+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.request.AuthDefaultRequest;
+import org.dromara.common.core.utils.SpringUtils;
+import org.dromara.common.json.utils.JsonUtils;
+
+/**
+ * @author 闀挎槬鍙摜 2023骞�03鏈�26鏃�
+ */
+public class AuthMaxKeyRequest extends AuthDefaultRequest {
+
+ public static final String SERVER_URL = SpringUtils.getProperty("justauth.type.maxkey.server-url");
+
+ /**
+ * 璁惧畾褰掑睘鍩�
+ */
+ public AuthMaxKeyRequest(AuthConfig config) {
+ super(config, AuthMaxKeySource.MAXKEY);
+ }
+
+ public AuthMaxKeyRequest(AuthConfig config, AuthStateCache authStateCache) {
+ super(config, AuthMaxKeySource.MAXKEY, authStateCache);
+ }
+
+ @Override
+ protected AuthToken getAccessToken(AuthCallback authCallback) {
+ String body = doPostAuthorizationCode(authCallback.getCode());
+ Dict object = JsonUtils.parseMap(body);
+ // oauth/token 楠岃瘉寮傚父
+ if (object.containsKey("error")) {
+ throw new AuthException(object.getStr("error_description"));
+ }
+ // user 楠岃瘉寮傚父
+ if (object.containsKey("message")) {
+ throw new AuthException(object.getStr("message"));
+ }
+ return AuthToken.builder()
+ .accessToken(object.getStr("access_token"))
+ .refreshToken(object.getStr("refresh_token"))
+ .idToken(object.getStr("id_token"))
+ .tokenType(object.getStr("token_type"))
+ .scope(object.getStr("scope"))
+ .build();
+ }
+
+ @Override
+ protected AuthUser getUserInfo(AuthToken authToken) {
+ String body = doGetUserInfo(authToken);
+ Dict object = JsonUtils.parseMap(body);
+ // oauth/token 楠岃瘉寮傚父
+ if (object.containsKey("error")) {
+ throw new AuthException(object.getStr("error_description"));
+ }
+ // user 楠岃瘉寮傚父
+ if (object.containsKey("message")) {
+ throw new AuthException(object.getStr("message"));
+ }
+ return AuthUser.builder()
+ .uuid(object.getStr("userId"))
+ .username(object.getStr("username"))
+ .nickname(object.getStr("displayName"))
+ .avatar(object.getStr("avatar_url"))
+ .blog(object.getStr("web_url"))
+ .company(object.getStr("organization"))
+ .location(object.getStr("location"))
+ .email(object.getStr("email"))
+ .remark(object.getStr("bio"))
+ .token(authToken)
+ .source(source.toString())
+ .build();
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/maxkey/AuthMaxKeySource.java b/ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/maxkey/AuthMaxKeySource.java
new file mode 100644
index 0000000..1ff57f7
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/maxkey/AuthMaxKeySource.java
@@ -0,0 +1,52 @@
+package org.dromara.common.social.maxkey;
+
+import me.zhyd.oauth.config.AuthSource;
+import me.zhyd.oauth.request.AuthDefaultRequest;
+
+/**
+ * Oauth2 榛樿鎺ュ彛璇存槑
+ *
+ * @author 闀挎槬鍙摜 2023骞�03鏈�26鏃�
+ *
+ */
+public enum AuthMaxKeySource implements AuthSource {
+
+ /**
+ * 鑷繁鎼缓鐨� maxkey 绉佹湇
+ */
+ MAXKEY {
+
+ /**
+ * 鎺堟潈鐨刟pi
+ */
+ @Override
+ public String authorize() {
+ return AuthMaxKeyRequest.SERVER_URL + "/sign/authz/oauth/v20/authorize";
+ }
+
+ /**
+ * 鑾峰彇accessToken鐨刟pi
+ */
+ @Override
+ public String accessToken() {
+ return AuthMaxKeyRequest.SERVER_URL + "/sign/authz/oauth/v20/token";
+ }
+
+ /**
+ * 鑾峰彇鐢ㄦ埛淇℃伅鐨刟pi
+ */
+ @Override
+ public String userInfo() {
+ return AuthMaxKeyRequest.SERVER_URL + "/sign/api/oauth/v20/me";
+ }
+
+ /**
+ * 骞冲彴瀵瑰簲鐨� AuthRequest 瀹炵幇绫伙紝蹇呴』缁ф壙鑷� {@link AuthDefaultRequest}
+ */
+ @Override
+ public Class<? extends AuthDefaultRequest> getTargetClass() {
+ return AuthMaxKeyRequest.class;
+ }
+
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/utils/AuthRedisStateCache.java b/ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/utils/AuthRedisStateCache.java
new file mode 100644
index 0000000..0b6ec20
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/utils/AuthRedisStateCache.java
@@ -0,0 +1,61 @@
+package org.dromara.common.social.utils;
+
+import lombok.AllArgsConstructor;
+import me.zhyd.oauth.cache.AuthStateCache;
+import org.dromara.common.core.constant.GlobalConstants;
+import org.dromara.common.redis.utils.RedisUtils;
+
+import java.time.Duration;
+
+/**
+ * 鎺堟潈鐘舵�佺紦瀛�
+ */
+@AllArgsConstructor
+public class AuthRedisStateCache implements AuthStateCache {
+
+ /**
+ * 瀛樺叆缂撳瓨
+ *
+ * @param key 缂撳瓨key
+ * @param value 缂撳瓨鍐呭
+ */
+ @Override
+ public void cache(String key, String value) {
+ // 鎺堟潈瓒呮椂鏃堕棿 榛樿涓夊垎閽�
+ RedisUtils.setCacheObject(GlobalConstants.SOCIAL_AUTH_CODE_KEY + key, value, Duration.ofMinutes(3));
+ }
+
+ /**
+ * 瀛樺叆缂撳瓨
+ *
+ * @param key 缂撳瓨key
+ * @param value 缂撳瓨鍐呭
+ * @param timeout 鎸囧畾缂撳瓨杩囨湡鏃堕棿(姣)
+ */
+ @Override
+ public void cache(String key, String value, long timeout) {
+ RedisUtils.setCacheObject(GlobalConstants.SOCIAL_AUTH_CODE_KEY + key, value, Duration.ofMillis(timeout));
+ }
+
+ /**
+ * 鑾峰彇缂撳瓨鍐呭
+ *
+ * @param key 缂撳瓨key
+ * @return 缂撳瓨鍐呭
+ */
+ @Override
+ public String get(String key) {
+ return RedisUtils.getCacheObject(GlobalConstants.SOCIAL_AUTH_CODE_KEY + key);
+ }
+
+ /**
+ * 鏄惁瀛樺湪key锛屽鏋滃搴攌ey鐨剉alue鍊煎凡杩囨湡锛屼篃杩斿洖false
+ *
+ * @param key 缂撳瓨key
+ * @return true锛氬瓨鍦╧ey锛屽苟涓攙alue娌¤繃鏈燂紱false锛歬ey涓嶅瓨鍦ㄦ垨鑰呭凡杩囨湡
+ */
+ @Override
+ public boolean containsKey(String key) {
+ return RedisUtils.hasKey(GlobalConstants.SOCIAL_AUTH_CODE_KEY + key);
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/utils/SocialUtils.java b/ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/utils/SocialUtils.java
new file mode 100644
index 0000000..a5ed6f1
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/utils/SocialUtils.java
@@ -0,0 +1,70 @@
+package org.dromara.common.social.utils;
+
+import cn.hutool.core.util.ObjectUtil;
+import me.zhyd.oauth.config.AuthConfig;
+import me.zhyd.oauth.exception.AuthException;
+import me.zhyd.oauth.model.AuthCallback;
+import me.zhyd.oauth.model.AuthResponse;
+import me.zhyd.oauth.model.AuthUser;
+import me.zhyd.oauth.request.*;
+import org.dromara.common.core.utils.SpringUtils;
+import org.dromara.common.social.config.properties.SocialLoginConfigProperties;
+import org.dromara.common.social.config.properties.SocialProperties;
+import org.dromara.common.social.maxkey.AuthMaxKeyRequest;
+
+/**
+ * 璁よ瘉鎺堟潈宸ュ叿绫�
+ *
+ * @author thiszhc
+ */
+public class SocialUtils {
+
+ private static final AuthRedisStateCache STATE_CACHE = SpringUtils.getBean(AuthRedisStateCache.class);
+
+ @SuppressWarnings("unchecked")
+ public static AuthResponse<AuthUser> loginAuth(String source, String code, String state, SocialProperties socialProperties) throws AuthException {
+ AuthRequest authRequest = getAuthRequest(source, socialProperties);
+ AuthCallback callback = new AuthCallback();
+ callback.setCode(code);
+ callback.setState(state);
+ return authRequest.login(callback);
+ }
+
+ public static AuthRequest getAuthRequest(String source, SocialProperties socialProperties) throws AuthException {
+ SocialLoginConfigProperties obj = socialProperties.getType().get(source);
+ if (ObjectUtil.isNull(obj)) {
+ throw new AuthException("涓嶆敮鎸佺殑绗笁鏂圭櫥褰曠被鍨�");
+ }
+ final AuthConfig.AuthConfigBuilder builder = AuthConfig.builder()
+ .clientId(obj.getClientId())
+ .clientSecret(obj.getClientSecret())
+ .redirectUri(obj.getRedirectUri());
+ return switch (source.toLowerCase()) {
+ case "dingtalk" -> new AuthDingTalkRequest(builder.build(), STATE_CACHE);
+ case "baidu" -> new AuthBaiduRequest(builder.build(), STATE_CACHE);
+ case "github" -> new AuthGithubRequest(builder.build(), STATE_CACHE);
+ case "gitee" -> new AuthGiteeRequest(builder.build(), STATE_CACHE);
+ case "weibo" -> new AuthWeiboRequest(builder.build(), STATE_CACHE);
+ case "coding" -> new AuthCodingRequest(builder.build(), STATE_CACHE);
+ case "oschina" -> new AuthOschinaRequest(builder.build(), STATE_CACHE);
+ // 鏀粯瀹濆湪鍒涘缓鍥炶皟鍦板潃鏃讹紝涓嶅厑璁镐娇鐢╨ocalhost鎴栬��127.0.0.1锛屾墍浠ヨ繖鍎跨殑鍥炶皟鍦板潃浣跨敤鐨勫眬鍩熺綉鍐呯殑ip
+ case "alipay_wallet" -> new AuthAlipayRequest(builder.build(), socialProperties.getType().get("alipay_wallet").getAlipayPublicKey(), STATE_CACHE);
+ case "qq" -> new AuthQqRequest(builder.build(), STATE_CACHE);
+ case "wechat_open" -> new AuthWeChatOpenRequest(builder.build(), STATE_CACHE);
+ case "taobao" -> new AuthTaobaoRequest(builder.build(), STATE_CACHE);
+ case "douyin" -> new AuthDouyinRequest(builder.build(), STATE_CACHE);
+ case "linkedin" -> new AuthLinkedinRequest(builder.build(), STATE_CACHE);
+ case "microsoft" -> new AuthMicrosoftRequest(builder.build(), STATE_CACHE);
+ case "renren" -> new AuthRenrenRequest(builder.build(), STATE_CACHE);
+ case "stack_overflow" -> new AuthStackOverflowRequest(builder.stackOverflowKey("").build(), STATE_CACHE);
+ case "huawei" -> new AuthHuaweiRequest(builder.build(), STATE_CACHE);
+ case "wechat_enterprise" -> new AuthWeChatEnterpriseQrcodeRequest(builder.agentId("").build(), STATE_CACHE);
+ case "gitlab" -> new AuthGitlabRequest(builder.build(), STATE_CACHE);
+ case "wechat_mp" -> new AuthWeChatMpRequest(builder.build(), STATE_CACHE);
+ case "aliyun" -> new AuthAliyunRequest(builder.build(), STATE_CACHE);
+ case "maxkey" -> new AuthMaxKeyRequest(builder.build(), STATE_CACHE);
+ default -> throw new AuthException("鏈幏鍙栧埌鏈夋晥鐨凙uth閰嶇疆");
+ };
+ }
+}
+
diff --git a/ruoyi-common/ruoyi-common-social/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/ruoyi-common/ruoyi-common-social/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..fc544a0
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-social/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+org.dromara.common.social.config.SocialAutoConfiguration
diff --git a/ruoyi-common/ruoyi-common-tenant/pom.xml b/ruoyi-common/ruoyi-common-tenant/pom.xml
new file mode 100644
index 0000000..2b87bf9
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-tenant/pom.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common</artifactId>
+ <version>${revision}</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>ruoyi-common-tenant</artifactId>
+
+ <description>
+ ruoyi-common-tenant 绉熸埛妯″潡
+ </description>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-mybatis</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-redis</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>com.alibaba</groupId>
+ <artifactId>transmittable-thread-local</artifactId>
+ </dependency>
+
+ </dependencies>
+
+</project>
diff --git a/ruoyi-common/ruoyi-common-tenant/src/main/java/org/dromara/common/tenant/config/TenantConfiguration.java b/ruoyi-common/ruoyi-common-tenant/src/main/java/org/dromara/common/tenant/config/TenantConfiguration.java
new file mode 100644
index 0000000..f2c708a
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-tenant/src/main/java/org/dromara/common/tenant/config/TenantConfiguration.java
@@ -0,0 +1,106 @@
+package org.dromara.common.tenant.config;
+
+import cn.dev33.satoken.dao.SaTokenDao;
+import cn.hutool.core.util.ObjectUtil;
+import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
+import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
+import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
+import org.dromara.common.core.utils.reflect.ReflectUtils;
+import org.dromara.common.mybatis.config.MybatisPlusConfiguration;
+import org.dromara.common.redis.config.RedisConfiguration;
+import org.dromara.common.redis.config.properties.RedissonProperties;
+import org.dromara.common.tenant.core.TenantSaTokenDao;
+import org.dromara.common.tenant.handle.PlusTenantLineHandler;
+import org.dromara.common.tenant.handle.TenantKeyPrefixHandler;
+import org.dromara.common.tenant.manager.TenantSpringCacheManager;
+import org.dromara.common.tenant.properties.TenantProperties;
+import org.redisson.config.ClusterServersConfig;
+import org.redisson.config.SingleServerConfig;
+import org.redisson.spring.starter.RedissonAutoConfigurationCustomizer;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.cache.CacheManager;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Primary;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 绉熸埛閰嶇疆绫�
+ *
+ * @author Lion Li
+ */
+@EnableConfigurationProperties(TenantProperties.class)
+@AutoConfiguration(after = {RedisConfiguration.class})
+@ConditionalOnProperty(value = "tenant.enable", havingValue = "true")
+public class TenantConfiguration {
+
+ @ConditionalOnBean(MybatisPlusConfiguration.class)
+ @AutoConfiguration(after = {MybatisPlusConfiguration.class})
+ static class MybatisPlusConfig {
+
+ /**
+ * 鍒濆鍖栫鎴烽厤缃�
+ */
+ @Bean
+ public boolean tenantInit(MybatisPlusInterceptor mybatisPlusInterceptor,
+ TenantProperties tenantProperties) {
+ List<InnerInterceptor> interceptors = new ArrayList<>();
+ // 澶氱鎴锋彃浠� 蹇呴』鏀惧埌绗竴浣�
+ interceptors.add(tenantLineInnerInterceptor(tenantProperties));
+ interceptors.addAll(mybatisPlusInterceptor.getInterceptors());
+ mybatisPlusInterceptor.setInterceptors(interceptors);
+ return true;
+ }
+
+ /**
+ * 澶氱鎴锋彃浠�
+ */
+ public TenantLineInnerInterceptor tenantLineInnerInterceptor(TenantProperties tenantProperties) {
+ return new TenantLineInnerInterceptor(new PlusTenantLineHandler(tenantProperties));
+ }
+ }
+
+ @Bean
+ public RedissonAutoConfigurationCustomizer tenantRedissonCustomizer(RedissonProperties redissonProperties) {
+ return config -> {
+ TenantKeyPrefixHandler nameMapper = new TenantKeyPrefixHandler(redissonProperties.getKeyPrefix());
+ SingleServerConfig singleServerConfig = ReflectUtils.invokeGetter(config, "singleServerConfig");
+ if (ObjectUtil.isNotNull(singleServerConfig)) {
+ // 浣跨敤鍗曟満妯″紡
+ // 璁剧疆澶氱鎴� redis key鍓嶇紑
+ singleServerConfig.setNameMapper(nameMapper);
+ ReflectUtils.invokeSetter(config, "singleServerConfig", singleServerConfig);
+ }
+ ClusterServersConfig clusterServersConfig = ReflectUtils.invokeGetter(config, "clusterServersConfig");
+ // 闆嗙兢閰嶇疆鏂瑰紡 鍙傝�冧笅鏂规敞閲�
+ if (ObjectUtil.isNotNull(clusterServersConfig)) {
+ // 璁剧疆澶氱鎴� redis key鍓嶇紑
+ clusterServersConfig.setNameMapper(nameMapper);
+ ReflectUtils.invokeSetter(config, "clusterServersConfig", clusterServersConfig);
+ }
+ };
+ }
+
+ /**
+ * 澶氱鎴风紦瀛樼鐞嗗櫒
+ */
+ @Primary
+ @Bean
+ public CacheManager tenantCacheManager() {
+ return new TenantSpringCacheManager();
+ }
+
+ /**
+ * 澶氱鎴烽壌鏉僤ao瀹炵幇
+ */
+ @Primary
+ @Bean
+ public SaTokenDao tenantSaTokenDao() {
+ return new TenantSaTokenDao();
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-tenant/src/main/java/org/dromara/common/tenant/core/TenantEntity.java b/ruoyi-common/ruoyi-common-tenant/src/main/java/org/dromara/common/tenant/core/TenantEntity.java
new file mode 100644
index 0000000..8ad0d2c
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-tenant/src/main/java/org/dromara/common/tenant/core/TenantEntity.java
@@ -0,0 +1,21 @@
+package org.dromara.common.tenant.core;
+
+import org.dromara.common.mybatis.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 绉熸埛鍩虹被
+ *
+ * @author Michelle.Chung
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class TenantEntity extends BaseEntity {
+
+ /**
+ * 绉熸埛缂栧彿
+ */
+ private String tenantId;
+
+}
diff --git a/ruoyi-common/ruoyi-common-tenant/src/main/java/org/dromara/common/tenant/core/TenantSaTokenDao.java b/ruoyi-common/ruoyi-common-tenant/src/main/java/org/dromara/common/tenant/core/TenantSaTokenDao.java
new file mode 100644
index 0000000..b8da28e
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-tenant/src/main/java/org/dromara/common/tenant/core/TenantSaTokenDao.java
@@ -0,0 +1,148 @@
+package org.dromara.common.tenant.core;
+
+import org.dromara.common.core.constant.GlobalConstants;
+import org.dromara.common.redis.utils.RedisUtils;
+import org.dromara.common.satoken.core.dao.PlusSaTokenDao;
+
+import java.time.Duration;
+import java.util.List;
+
+/**
+ * SaToken 璁よ瘉鏁版嵁鎸佷箙灞� 閫傞厤澶氱鎴�
+ *
+ * @author Lion Li
+ */
+public class TenantSaTokenDao extends PlusSaTokenDao {
+
+ @Override
+ public String get(String key) {
+ return super.get(GlobalConstants.GLOBAL_REDIS_KEY + key);
+ }
+
+ @Override
+ public void set(String key, String value, long timeout) {
+ super.set(GlobalConstants.GLOBAL_REDIS_KEY + key, value, timeout);
+ }
+
+ /**
+ * 淇慨鏀规寚瀹歬ey-value閿�煎 (杩囨湡鏃堕棿涓嶅彉)
+ */
+ @Override
+ public void update(String key, String value) {
+ long expire = getTimeout(key);
+ // -2 = 鏃犳閿�
+ if (expire == NOT_VALUE_EXPIRE) {
+ return;
+ }
+ this.set(key, value, expire);
+ }
+
+ /**
+ * 鍒犻櫎Value
+ */
+ @Override
+ public void delete(String key) {
+ super.delete(GlobalConstants.GLOBAL_REDIS_KEY + key);
+ }
+
+ /**
+ * 鑾峰彇Value鐨勫墿浣欏瓨娲绘椂闂� (鍗曚綅: 绉�)
+ */
+ @Override
+ public long getTimeout(String key) {
+ return super.getTimeout(GlobalConstants.GLOBAL_REDIS_KEY + key);
+ }
+
+ /**
+ * 淇敼Value鐨勫墿浣欏瓨娲绘椂闂� (鍗曚綅: 绉�)
+ */
+ @Override
+ public void updateTimeout(String key, long timeout) {
+ // 鍒ゆ柇鏄惁鎯宠璁剧疆涓烘案涔�
+ if (timeout == NEVER_EXPIRE) {
+ long expire = getTimeout(key);
+ if (expire == NEVER_EXPIRE) {
+ // 濡傛灉鍏跺凡缁忚璁剧疆涓烘案涔咃紝鍒欎笉浣滀换浣曞鐞�
+ } else {
+ // 濡傛灉灏氭湭琚缃负姘镐箙锛岄偅涔堝啀娆et涓�娆�
+ this.set(key, this.get(key), timeout);
+ }
+ return;
+ }
+ RedisUtils.expire(GlobalConstants.GLOBAL_REDIS_KEY + key, Duration.ofSeconds(timeout));
+ }
+
+
+ /**
+ * 鑾峰彇Object锛屽鏃犺繑绌�
+ */
+ @Override
+ public Object getObject(String key) {
+ return super.getObject(GlobalConstants.GLOBAL_REDIS_KEY + key);
+ }
+
+ /**
+ * 鍐欏叆Object锛屽苟璁惧畾瀛樻椿鏃堕棿 (鍗曚綅: 绉�)
+ */
+ @Override
+ public void setObject(String key, Object object, long timeout) {
+ super.setObject(GlobalConstants.GLOBAL_REDIS_KEY + key, object, timeout);
+ }
+
+ /**
+ * 鏇存柊Object (杩囨湡鏃堕棿涓嶅彉)
+ */
+ @Override
+ public void updateObject(String key, Object object) {
+ long expire = getObjectTimeout(key);
+ // -2 = 鏃犳閿�
+ if (expire == NOT_VALUE_EXPIRE) {
+ return;
+ }
+ this.setObject(key, object, expire);
+ }
+
+ /**
+ * 鍒犻櫎Object
+ */
+ @Override
+ public void deleteObject(String key) {
+ super.deleteObject(GlobalConstants.GLOBAL_REDIS_KEY + key);
+ }
+
+ /**
+ * 鑾峰彇Object鐨勫墿浣欏瓨娲绘椂闂� (鍗曚綅: 绉�)
+ */
+ @Override
+ public long getObjectTimeout(String key) {
+ return super.getObjectTimeout(GlobalConstants.GLOBAL_REDIS_KEY + key);
+ }
+
+ /**
+ * 淇敼Object鐨勫墿浣欏瓨娲绘椂闂� (鍗曚綅: 绉�)
+ */
+ @Override
+ public void updateObjectTimeout(String key, long timeout) {
+ // 鍒ゆ柇鏄惁鎯宠璁剧疆涓烘案涔�
+ if (timeout == NEVER_EXPIRE) {
+ long expire = getObjectTimeout(key);
+ if (expire == NEVER_EXPIRE) {
+ // 濡傛灉鍏跺凡缁忚璁剧疆涓烘案涔咃紝鍒欎笉浣滀换浣曞鐞�
+ } else {
+ // 濡傛灉灏氭湭琚缃负姘镐箙锛岄偅涔堝啀娆et涓�娆�
+ this.setObject(key, this.getObject(key), timeout);
+ }
+ return;
+ }
+ RedisUtils.expire(GlobalConstants.GLOBAL_REDIS_KEY + key, Duration.ofSeconds(timeout));
+ }
+
+
+ /**
+ * 鎼滅储鏁版嵁
+ */
+ @Override
+ public List<String> searchData(String prefix, String keyword, int start, int size, boolean sortType) {
+ return super.searchData(GlobalConstants.GLOBAL_REDIS_KEY + prefix, keyword, start, size, sortType);
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-tenant/src/main/java/org/dromara/common/tenant/exception/TenantException.java b/ruoyi-common/ruoyi-common-tenant/src/main/java/org/dromara/common/tenant/exception/TenantException.java
new file mode 100644
index 0000000..ee2bc97
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-tenant/src/main/java/org/dromara/common/tenant/exception/TenantException.java
@@ -0,0 +1,20 @@
+package org.dromara.common.tenant.exception;
+
+import org.dromara.common.core.exception.base.BaseException;
+
+import java.io.Serial;
+
+/**
+ * 绉熸埛寮傚父绫�
+ *
+ * @author Lion Li
+ */
+public class TenantException extends BaseException {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ public TenantException(String code, Object... args) {
+ super("tenant", code, args, null);
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-tenant/src/main/java/org/dromara/common/tenant/handle/PlusTenantLineHandler.java b/ruoyi-common/ruoyi-common-tenant/src/main/java/org/dromara/common/tenant/handle/PlusTenantLineHandler.java
new file mode 100644
index 0000000..6c93ee5
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-tenant/src/main/java/org/dromara/common/tenant/handle/PlusTenantLineHandler.java
@@ -0,0 +1,56 @@
+package org.dromara.common.tenant.handle;
+
+import cn.hutool.core.collection.ListUtil;
+import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import net.sf.jsqlparser.expression.Expression;
+import net.sf.jsqlparser.expression.NullValue;
+import net.sf.jsqlparser.expression.StringValue;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.tenant.helper.TenantHelper;
+import org.dromara.common.tenant.properties.TenantProperties;
+
+import java.util.List;
+
+/**
+ * 鑷畾涔夌鎴峰鐞嗗櫒
+ *
+ * @author Lion Li
+ */
+@Slf4j
+@AllArgsConstructor
+public class PlusTenantLineHandler implements TenantLineHandler {
+
+ private final TenantProperties tenantProperties;
+
+ @Override
+ public Expression getTenantId() {
+ String tenantId = TenantHelper.getTenantId();
+ if (StringUtils.isBlank(tenantId)) {
+ log.error("鏃犳硶鑾峰彇鏈夋晥鐨勭鎴穒d -> Null");
+ return new NullValue();
+ }
+ // 杩斿洖鍥哄畾绉熸埛
+ return new StringValue(tenantId);
+ }
+
+ @Override
+ public boolean ignoreTable(String tableName) {
+ String tenantId = TenantHelper.getTenantId();
+ // 鍒ゆ柇鏄惁鏈夌鎴�
+ if (StringUtils.isNotBlank(tenantId)) {
+ // 涓嶉渶瑕佽繃婊ょ鎴风殑琛�
+ List<String> excludes = tenantProperties.getExcludes();
+ // 闈炰笟鍔¤〃
+ List<String> tables = ListUtil.toList(
+ "gen_table",
+ "gen_table_column"
+ );
+ tables.addAll(excludes);
+ return tables.contains(tableName);
+ }
+ return true;
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-tenant/src/main/java/org/dromara/common/tenant/handle/TenantKeyPrefixHandler.java b/ruoyi-common/ruoyi-common-tenant/src/main/java/org/dromara/common/tenant/handle/TenantKeyPrefixHandler.java
new file mode 100644
index 0000000..14dad1a
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-tenant/src/main/java/org/dromara/common/tenant/handle/TenantKeyPrefixHandler.java
@@ -0,0 +1,66 @@
+package org.dromara.common.tenant.handle;
+
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.core.constant.GlobalConstants;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.redis.handler.KeyPrefixHandler;
+import org.dromara.common.tenant.helper.TenantHelper;
+
+/**
+ * 澶氱鎴穜edis缂撳瓨key鍓嶇紑澶勭悊
+ *
+ * @author Lion Li
+ */
+@Slf4j
+public class TenantKeyPrefixHandler extends KeyPrefixHandler {
+
+ public TenantKeyPrefixHandler(String keyPrefix) {
+ super(keyPrefix);
+ }
+
+ /**
+ * 澧炲姞鍓嶇紑
+ */
+ @Override
+ public String map(String name) {
+ if (StringUtils.isBlank(name)) {
+ return null;
+ }
+ if (StringUtils.contains(name, GlobalConstants.GLOBAL_REDIS_KEY)) {
+ return super.map(name);
+ }
+ String tenantId = TenantHelper.getTenantId();
+ if (StringUtils.isBlank(tenantId)) {
+ log.error("鏃犳硶鑾峰彇鏈夋晥鐨勭鎴穒d -> Null");
+ }
+ if (StringUtils.startsWith(name, tenantId + "")) {
+ // 濡傛灉瀛樺湪鍒欑洿鎺ヨ繑鍥�
+ return super.map(name);
+ }
+ return super.map(tenantId + ":" + name);
+ }
+
+ /**
+ * 鍘婚櫎鍓嶇紑
+ */
+ @Override
+ public String unmap(String name) {
+ String unmap = super.unmap(name);
+ if (StringUtils.isBlank(unmap)) {
+ return null;
+ }
+ if (StringUtils.contains(name, GlobalConstants.GLOBAL_REDIS_KEY)) {
+ return super.unmap(name);
+ }
+ String tenantId = TenantHelper.getTenantId();
+ if (StringUtils.isBlank(tenantId)) {
+ log.error("鏃犳硶鑾峰彇鏈夋晥鐨勭鎴穒d -> Null");
+ }
+ if (StringUtils.startsWith(unmap, tenantId + "")) {
+ // 濡傛灉瀛樺湪鍒欏垹闄�
+ return unmap.substring((tenantId + ":").length());
+ }
+ return unmap;
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-tenant/src/main/java/org/dromara/common/tenant/helper/TenantHelper.java b/ruoyi-common/ruoyi-common-tenant/src/main/java/org/dromara/common/tenant/helper/TenantHelper.java
new file mode 100644
index 0000000..e830c19
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-tenant/src/main/java/org/dromara/common/tenant/helper/TenantHelper.java
@@ -0,0 +1,189 @@
+package org.dromara.common.tenant.helper;
+
+import cn.dev33.satoken.context.SaHolder;
+import cn.dev33.satoken.stp.StpUtil;
+import cn.hutool.core.convert.Convert;
+import com.alibaba.ttl.TransmittableThreadLocal;
+import com.baomidou.mybatisplus.core.plugins.IgnoreStrategy;
+import com.baomidou.mybatisplus.core.plugins.InterceptorIgnoreHelper;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.core.constant.GlobalConstants;
+import org.dromara.common.core.utils.SpringUtils;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.redis.utils.RedisUtils;
+import org.dromara.common.satoken.utils.LoginHelper;
+
+import java.util.function.Supplier;
+
+/**
+ * 绉熸埛鍔╂墜
+ *
+ * @author Lion Li
+ */
+@Slf4j
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class TenantHelper {
+
+ private static final String DYNAMIC_TENANT_KEY = GlobalConstants.GLOBAL_REDIS_KEY + "dynamicTenant";
+
+ private static final ThreadLocal<String> TEMP_DYNAMIC_TENANT = new TransmittableThreadLocal<>();
+
+ /**
+ * 绉熸埛鍔熻兘鏄惁鍚敤
+ */
+ public static boolean isEnable() {
+ return Convert.toBool(SpringUtils.getProperty("tenant.enable"), false);
+ }
+
+ /**
+ * 寮�鍚拷鐣ョ鎴�(寮�鍚悗闇�鎵嬪姩璋冪敤 {@link #disableIgnore()} 鍏抽棴)
+ */
+ public static void enableIgnore() {
+ InterceptorIgnoreHelper.handle(IgnoreStrategy.builder().tenantLine(true).build());
+ }
+
+ /**
+ * 鍏抽棴蹇界暐绉熸埛
+ */
+ public static void disableIgnore() {
+ InterceptorIgnoreHelper.clearIgnoreStrategy();
+ }
+
+ /**
+ * 鍦ㄥ拷鐣ョ鎴蜂腑鎵ц
+ *
+ * @param handle 澶勭悊鎵ц鏂规硶
+ */
+ public static void ignore(Runnable handle) {
+ enableIgnore();
+ try {
+ handle.run();
+ } finally {
+ disableIgnore();
+ }
+ }
+
+ /**
+ * 鍦ㄥ拷鐣ョ鎴蜂腑鎵ц
+ *
+ * @param handle 澶勭悊鎵ц鏂规硶
+ */
+ public static <T> T ignore(Supplier<T> handle) {
+ enableIgnore();
+ try {
+ return handle.get();
+ } finally {
+ disableIgnore();
+ }
+ }
+
+ /**
+ * 璁剧疆鍔ㄦ�佺鎴�(涓�鐩存湁鏁� 闇�瑕佹墜鍔ㄦ竻鐞�)
+ * <p>
+ * 濡傛灉涓烘湭鐧诲綍鐘舵�佷笅 閭d箞鍙湪褰撳墠绾跨▼鍐呯敓鏁�
+ */
+ public static void setDynamic(String tenantId) {
+ if (!isEnable()) {
+ return;
+ }
+ if (!isLogin()) {
+ TEMP_DYNAMIC_TENANT.set(tenantId);
+ return;
+ }
+ String cacheKey = DYNAMIC_TENANT_KEY + ":" + LoginHelper.getUserId();
+ RedisUtils.setCacheObject(cacheKey, tenantId);
+ SaHolder.getStorage().set(cacheKey, tenantId);
+ }
+
+ /**
+ * 鑾峰彇鍔ㄦ�佺鎴�(涓�鐩存湁鏁� 闇�瑕佹墜鍔ㄦ竻鐞�)
+ * <p>
+ * 濡傛灉涓烘湭鐧诲綍鐘舵�佷笅 閭d箞鍙湪褰撳墠绾跨▼鍐呯敓鏁�
+ */
+ public static String getDynamic() {
+ if (!isEnable()) {
+ return null;
+ }
+ if (!isLogin()) {
+ return TEMP_DYNAMIC_TENANT.get();
+ }
+ String cacheKey = DYNAMIC_TENANT_KEY + ":" + LoginHelper.getUserId();
+ String tenantId = (String) SaHolder.getStorage().get(cacheKey);
+ if (StringUtils.isNotBlank(tenantId)) {
+ return tenantId;
+ }
+ tenantId = RedisUtils.getCacheObject(cacheKey);
+ SaHolder.getStorage().set(cacheKey, tenantId);
+ return tenantId;
+ }
+
+ /**
+ * 娓呴櫎鍔ㄦ�佺鎴�
+ */
+ public static void clearDynamic() {
+ if (!isEnable()) {
+ return;
+ }
+ if (!isLogin()) {
+ TEMP_DYNAMIC_TENANT.remove();
+ return;
+ }
+ String cacheKey = DYNAMIC_TENANT_KEY + ":" + LoginHelper.getUserId();
+ RedisUtils.deleteObject(cacheKey);
+ SaHolder.getStorage().delete(cacheKey);
+ }
+
+ /**
+ * 鍦ㄥ姩鎬佺鎴蜂腑鎵ц
+ *
+ * @param handle 澶勭悊鎵ц鏂规硶
+ */
+ public static void dynamic(String tenantId, Runnable handle) {
+ setDynamic(tenantId);
+ try {
+ handle.run();
+ } finally {
+ clearDynamic();
+ }
+ }
+
+ /**
+ * 鍦ㄥ姩鎬佺鎴蜂腑鎵ц
+ *
+ * @param handle 澶勭悊鎵ц鏂规硶
+ */
+ public static <T> T dynamic(String tenantId, Supplier<T> handle) {
+ setDynamic(tenantId);
+ try {
+ return handle.get();
+ } finally {
+ clearDynamic();
+ }
+ }
+
+ /**
+ * 鑾峰彇褰撳墠绉熸埛id(鍔ㄦ�佺鎴蜂紭鍏�)
+ */
+ public static String getTenantId() {
+ if (!isEnable()) {
+ return null;
+ }
+ String tenantId = TenantHelper.getDynamic();
+ if (StringUtils.isBlank(tenantId)) {
+ tenantId = LoginHelper.getTenantId();
+ }
+ return tenantId;
+ }
+
+ private static boolean isLogin() {
+ try {
+ StpUtil.checkLogin();
+ return true;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-tenant/src/main/java/org/dromara/common/tenant/manager/TenantSpringCacheManager.java b/ruoyi-common/ruoyi-common-tenant/src/main/java/org/dromara/common/tenant/manager/TenantSpringCacheManager.java
new file mode 100644
index 0000000..d230afc
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-tenant/src/main/java/org/dromara/common/tenant/manager/TenantSpringCacheManager.java
@@ -0,0 +1,32 @@
+package org.dromara.common.tenant.manager;
+
+import org.dromara.common.core.constant.GlobalConstants;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.redis.manager.PlusSpringCacheManager;
+import org.dromara.common.tenant.helper.TenantHelper;
+import org.springframework.cache.Cache;
+
+/**
+ * 閲嶅啓 cacheName 澶勭悊鏂规硶 鏀寔澶氱鎴�
+ *
+ * @author Lion Li
+ */
+public class TenantSpringCacheManager extends PlusSpringCacheManager {
+
+ public TenantSpringCacheManager() {
+ }
+
+ @Override
+ public Cache getCache(String name) {
+ if (StringUtils.contains(name, GlobalConstants.GLOBAL_REDIS_KEY)) {
+ return super.getCache(name);
+ }
+ String tenantId = TenantHelper.getTenantId();
+ if (StringUtils.startsWith(name, tenantId)) {
+ // 濡傛灉瀛樺湪鍒欑洿鎺ヨ繑鍥�
+ return super.getCache(name);
+ }
+ return super.getCache(tenantId + ":" + name);
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-tenant/src/main/java/org/dromara/common/tenant/properties/TenantProperties.java b/ruoyi-common/ruoyi-common-tenant/src/main/java/org/dromara/common/tenant/properties/TenantProperties.java
new file mode 100644
index 0000000..1675ccf
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-tenant/src/main/java/org/dromara/common/tenant/properties/TenantProperties.java
@@ -0,0 +1,27 @@
+package org.dromara.common.tenant.properties;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+import java.util.List;
+
+/**
+ * 绉熸埛 閰嶇疆灞炴��
+ *
+ * @author Lion Li
+ */
+@Data
+@ConfigurationProperties(prefix = "tenant")
+public class TenantProperties {
+
+ /**
+ * 鏄惁鍚敤
+ */
+ private Boolean enable;
+
+ /**
+ * 鎺掗櫎琛�
+ */
+ private List<String> excludes;
+
+}
diff --git a/ruoyi-common/ruoyi-common-tenant/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/ruoyi-common/ruoyi-common-tenant/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..c34f2c5
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-tenant/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+org.dromara.common.tenant.config.TenantConfiguration
diff --git a/ruoyi-common/ruoyi-common-translation/pom.xml b/ruoyi-common/ruoyi-common-translation/pom.xml
new file mode 100644
index 0000000..030dd7d
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-translation/pom.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common</artifactId>
+ <version>${revision}</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>ruoyi-common-translation</artifactId>
+
+ <description>
+ ruoyi-common-translation 閫氱敤缈昏瘧鍔熻兘
+ </description>
+
+ <dependencies>
+
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-json</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-dict</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-dubbo</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-api-resource</artifactId>
+ </dependency>
+
+ </dependencies>
+
+</project>
diff --git a/ruoyi-common/ruoyi-common-translation/src/main/java/org/dromara/common/translation/annotation/Translation.java b/ruoyi-common/ruoyi-common-translation/src/main/java/org/dromara/common/translation/annotation/Translation.java
new file mode 100644
index 0000000..6c1227f
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-translation/src/main/java/org/dromara/common/translation/annotation/Translation.java
@@ -0,0 +1,39 @@
+package org.dromara.common.translation.annotation;
+
+import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import org.dromara.common.translation.core.handler.TranslationHandler;
+
+import java.lang.annotation.*;
+
+/**
+ * 閫氱敤缈昏瘧娉ㄨВ
+ *
+ * @author Lion Li
+ */
+@Inherited
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.FIELD, ElementType.METHOD})
+@Documented
+@JacksonAnnotationsInside
+@JsonSerialize(using = TranslationHandler.class)
+public @interface Translation {
+
+ /**
+ * 绫诲瀷 (闇�涓庡疄鐜扮被涓婄殑 {@link org.dromara.common.translation.annotation.TranslationType} 娉ㄨВtype瀵瑰簲)
+ * <p>
+ * 榛樿鍙栧綋鍓嶅瓧娈电殑鍊� 濡傛灉璁剧疆浜� @{@link Translation#mapper()} 鍒欏彇鏄犲皠瀛楁鐨勫��
+ */
+ String type();
+
+ /**
+ * 鏄犲皠瀛楁 (濡傛灉涓嶄负绌哄垯鍙栨瀛楁鐨勫��)
+ */
+ String mapper() default "";
+
+ /**
+ * 鍏朵粬鏉′欢 渚嬪: 瀛楀吀type(sys_user_sex)
+ */
+ String other() default "";
+
+}
diff --git a/ruoyi-common/ruoyi-common-translation/src/main/java/org/dromara/common/translation/annotation/TranslationType.java b/ruoyi-common/ruoyi-common-translation/src/main/java/org/dromara/common/translation/annotation/TranslationType.java
new file mode 100644
index 0000000..43bfab0
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-translation/src/main/java/org/dromara/common/translation/annotation/TranslationType.java
@@ -0,0 +1,23 @@
+package org.dromara.common.translation.annotation;
+
+import org.dromara.common.translation.core.TranslationInterface;
+
+import java.lang.annotation.*;
+
+/**
+ * 缈昏瘧绫诲瀷娉ㄨВ (鏍囨敞鍒皗@link TranslationInterface} 鐨勫疄鐜扮被)
+ *
+ * @author Lion Li
+ */
+@Inherited
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE})
+@Documented
+public @interface TranslationType {
+
+ /**
+ * 绫诲瀷
+ */
+ String type();
+
+}
diff --git a/ruoyi-common/ruoyi-common-translation/src/main/java/org/dromara/common/translation/config/TranslationConfig.java b/ruoyi-common/ruoyi-common-translation/src/main/java/org/dromara/common/translation/config/TranslationConfig.java
new file mode 100644
index 0000000..5dcd0c1
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-translation/src/main/java/org/dromara/common/translation/config/TranslationConfig.java
@@ -0,0 +1,50 @@
+package org.dromara.common.translation.config;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.dromara.common.translation.annotation.TranslationType;
+import org.dromara.common.translation.core.TranslationInterface;
+import org.dromara.common.translation.core.handler.TranslationBeanSerializerModifier;
+import org.dromara.common.translation.core.handler.TranslationHandler;
+import jakarta.annotation.PostConstruct;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 缈昏瘧妯″潡閰嶇疆绫�
+ *
+ * @author Lion Li
+ */
+@Slf4j
+@AutoConfiguration
+public class TranslationConfig {
+
+ @Autowired
+ private List<TranslationInterface<?>> list;
+
+ @Autowired
+ private ObjectMapper objectMapper;
+
+ @PostConstruct
+ public void init() {
+ Map<String, TranslationInterface<?>> map = new HashMap<>(list.size());
+ for (TranslationInterface<?> trans : list) {
+ if (trans.getClass().isAnnotationPresent(TranslationType.class)) {
+ TranslationType annotation = trans.getClass().getAnnotation(TranslationType.class);
+ map.put(annotation.type(), trans);
+ } else {
+ log.warn(trans.getClass().getName() + " 缈昏瘧瀹炵幇绫绘湭鏍囨敞 TranslationType 娉ㄨВ!");
+ }
+ }
+ TranslationHandler.TRANSLATION_MAPPER.putAll(map);
+ // 璁剧疆 Bean 搴忓垪鍖栦慨鏀瑰櫒
+ objectMapper.setSerializerFactory(
+ objectMapper.getSerializerFactory()
+ .withSerializerModifier(new TranslationBeanSerializerModifier()));
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-translation/src/main/java/org/dromara/common/translation/constant/TransConstant.java b/ruoyi-common/ruoyi-common-translation/src/main/java/org/dromara/common/translation/constant/TransConstant.java
new file mode 100644
index 0000000..a6ecb2d
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-translation/src/main/java/org/dromara/common/translation/constant/TransConstant.java
@@ -0,0 +1,36 @@
+package org.dromara.common.translation.constant;
+
+/**
+ * 缈昏瘧甯搁噺
+ *
+ * @author Lion Li
+ */
+public interface TransConstant {
+
+ /**
+ * 鐢ㄦ埛id杞处鍙�
+ */
+ String USER_ID_TO_NAME = "user_id_to_name";
+
+ /**
+ * 鐢ㄦ埛id杞敤鎴锋樀绉�
+ */
+ String USER_ID_TO_NICKNAME = "user_id_to_nickname";
+
+
+ /**
+ * 閮ㄩ棬id杞悕绉�
+ */
+ String DEPT_ID_TO_NAME = "dept_id_to_name";
+
+ /**
+ * 瀛楀吀type杞琹abel
+ */
+ String DICT_TYPE_TO_LABEL = "dict_type_to_label";
+
+ /**
+ * ossId杞瑄rl
+ */
+ String OSS_ID_TO_URL = "oss_id_to_url";
+
+}
diff --git a/ruoyi-common/ruoyi-common-translation/src/main/java/org/dromara/common/translation/core/TranslationInterface.java b/ruoyi-common/ruoyi-common-translation/src/main/java/org/dromara/common/translation/core/TranslationInterface.java
new file mode 100644
index 0000000..e4d6dd3
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-translation/src/main/java/org/dromara/common/translation/core/TranslationInterface.java
@@ -0,0 +1,20 @@
+package org.dromara.common.translation.core;
+
+import org.dromara.common.translation.annotation.TranslationType;
+
+/**
+ * 缈昏瘧鎺ュ彛 (瀹炵幇绫婚渶鏍囨敞 {@link TranslationType} 娉ㄨВ鏍囨槑缈昏瘧绫诲瀷)
+ *
+ * @author Lion Li
+ */
+public interface TranslationInterface<T> {
+
+ /**
+ * 缈昏瘧
+ *
+ * @param key 闇�瑕佽缈昏瘧鐨勯敭(涓嶄负绌�)
+ * @param other 鍏朵粬鍙傛暟
+ * @return 杩斿洖閿搴旂殑鍊�
+ */
+ T translation(Object key, String other);
+}
diff --git a/ruoyi-common/ruoyi-common-translation/src/main/java/org/dromara/common/translation/core/handler/TranslationBeanSerializerModifier.java b/ruoyi-common/ruoyi-common-translation/src/main/java/org/dromara/common/translation/core/handler/TranslationBeanSerializerModifier.java
new file mode 100644
index 0000000..727672f
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-translation/src/main/java/org/dromara/common/translation/core/handler/TranslationBeanSerializerModifier.java
@@ -0,0 +1,29 @@
+package org.dromara.common.translation.core.handler;
+
+import com.fasterxml.jackson.databind.BeanDescription;
+import com.fasterxml.jackson.databind.SerializationConfig;
+import com.fasterxml.jackson.databind.ser.BeanPropertyWriter;
+import com.fasterxml.jackson.databind.ser.BeanSerializerModifier;
+
+import java.util.List;
+
+/**
+ * Bean 搴忓垪鍖栦慨鏀瑰櫒 瑙e喅 Null 琚崟鐙鐞嗛棶棰�
+ *
+ * @author Lion Li
+ */
+public class TranslationBeanSerializerModifier extends BeanSerializerModifier {
+
+ @Override
+ public List<BeanPropertyWriter> changeProperties(SerializationConfig config, BeanDescription beanDesc,
+ List<BeanPropertyWriter> beanProperties) {
+ for (BeanPropertyWriter writer : beanProperties) {
+ // 濡傛灉搴忓垪鍖栧櫒涓� TranslationHandler 鐨勮瘽 灏� Null 鍊间篃浜ょ粰浠栧鐞�
+ if (writer.getSerializer() instanceof TranslationHandler serializer) {
+ writer.assignNullSerializer(serializer);
+ }
+ }
+ return beanProperties;
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-translation/src/main/java/org/dromara/common/translation/core/handler/TranslationHandler.java b/ruoyi-common/ruoyi-common-translation/src/main/java/org/dromara/common/translation/core/handler/TranslationHandler.java
new file mode 100644
index 0000000..bb9615b
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-translation/src/main/java/org/dromara/common/translation/core/handler/TranslationHandler.java
@@ -0,0 +1,65 @@
+package org.dromara.common.translation.core.handler;
+
+import cn.hutool.core.util.ObjectUtil;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.BeanProperty;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.ser.ContextualSerializer;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.core.utils.reflect.ReflectUtils;
+import org.dromara.common.translation.annotation.Translation;
+import org.dromara.common.translation.core.TranslationInterface;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 缈昏瘧澶勭悊鍣�
+ *
+ * @author Lion Li
+ */
+@Slf4j
+public class TranslationHandler extends JsonSerializer<Object> implements ContextualSerializer {
+
+ /**
+ * 鍏ㄥ眬缈昏瘧瀹炵幇绫绘槧灏勫櫒
+ */
+ public static final Map<String, TranslationInterface<?>> TRANSLATION_MAPPER = new ConcurrentHashMap<>();
+
+ private Translation translation;
+
+ @Override
+ public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
+ TranslationInterface<?> trans = TRANSLATION_MAPPER.get(translation.type());
+ if (ObjectUtil.isNotNull(trans)) {
+ // 濡傛灉鏄犲皠瀛楁涓嶄负绌� 鍒欏彇鏄犲皠瀛楁鐨勫��
+ if (StringUtils.isNotBlank(translation.mapper())) {
+ value = ReflectUtils.invokeGetter(gen.getCurrentValue(), translation.mapper());
+ }
+ // 濡傛灉涓� null 鐩存帴鍐欏嚭
+ if (ObjectUtil.isNull(value)) {
+ gen.writeNull();
+ return;
+ }
+ Object result = trans.translation(value, translation.other());
+ gen.writeObject(result);
+ } else {
+ gen.writeObject(value);
+ }
+ }
+
+ @Override
+ public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {
+ Translation translation = property.getAnnotation(Translation.class);
+ if (Objects.nonNull(translation)) {
+ this.translation = translation;
+ return this;
+ }
+ return prov.findValueSerializer(property.getType(), property);
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-translation/src/main/java/org/dromara/common/translation/core/impl/DeptNameTranslationImpl.java b/ruoyi-common/ruoyi-common-translation/src/main/java/org/dromara/common/translation/core/impl/DeptNameTranslationImpl.java
new file mode 100644
index 0000000..b88af57
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-translation/src/main/java/org/dromara/common/translation/core/impl/DeptNameTranslationImpl.java
@@ -0,0 +1,26 @@
+package org.dromara.common.translation.core.impl;
+
+import org.dromara.common.translation.annotation.TranslationType;
+import org.dromara.common.translation.constant.TransConstant;
+import org.dromara.common.translation.core.TranslationInterface;
+import org.dromara.system.api.RemoteDeptService;
+import lombok.AllArgsConstructor;
+import org.apache.dubbo.config.annotation.DubboReference;
+
+/**
+ * 閮ㄩ棬缈昏瘧瀹炵幇
+ *
+ * @author Lion Li
+ */
+@AllArgsConstructor
+@TranslationType(type = TransConstant.DEPT_ID_TO_NAME)
+public class DeptNameTranslationImpl implements TranslationInterface<String> {
+
+ @DubboReference
+ private RemoteDeptService remoteDeptService;
+
+ @Override
+ public String translation(Object key, String other) {
+ return remoteDeptService.selectDeptNameByIds(key.toString());
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-translation/src/main/java/org/dromara/common/translation/core/impl/DictTypeTranslationImpl.java b/ruoyi-common/ruoyi-common-translation/src/main/java/org/dromara/common/translation/core/impl/DictTypeTranslationImpl.java
new file mode 100644
index 0000000..538d932
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-translation/src/main/java/org/dromara/common/translation/core/impl/DictTypeTranslationImpl.java
@@ -0,0 +1,28 @@
+package org.dromara.common.translation.core.impl;
+
+import org.dromara.common.core.service.DictService;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.translation.annotation.TranslationType;
+import org.dromara.common.translation.constant.TransConstant;
+import org.dromara.common.translation.core.TranslationInterface;
+import lombok.AllArgsConstructor;
+
+/**
+ * 瀛楀吀缈昏瘧瀹炵幇
+ *
+ * @author Lion Li
+ */
+@AllArgsConstructor
+@TranslationType(type = TransConstant.DICT_TYPE_TO_LABEL)
+public class DictTypeTranslationImpl implements TranslationInterface<String> {
+
+ private final DictService dictService;
+
+ @Override
+ public String translation(Object key, String other) {
+ if (key instanceof String && StringUtils.isNotBlank(other)) {
+ return dictService.getDictLabel(other, key.toString());
+ }
+ return null;
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-translation/src/main/java/org/dromara/common/translation/core/impl/NicknameTranslationImpl.java b/ruoyi-common/ruoyi-common-translation/src/main/java/org/dromara/common/translation/core/impl/NicknameTranslationImpl.java
new file mode 100644
index 0000000..b73478e
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-translation/src/main/java/org/dromara/common/translation/core/impl/NicknameTranslationImpl.java
@@ -0,0 +1,26 @@
+package org.dromara.common.translation.core.impl;
+
+import lombok.AllArgsConstructor;
+import org.apache.dubbo.config.annotation.DubboReference;
+import org.dromara.common.translation.annotation.TranslationType;
+import org.dromara.common.translation.constant.TransConstant;
+import org.dromara.common.translation.core.TranslationInterface;
+import org.dromara.system.api.RemoteUserService;
+
+/**
+ * 鐢ㄦ埛鏄电О缈昏瘧瀹炵幇
+ *
+ * @author may
+ */
+@AllArgsConstructor
+@TranslationType(type = TransConstant.USER_ID_TO_NICKNAME)
+public class NicknameTranslationImpl implements TranslationInterface<String> {
+
+ @DubboReference
+ private RemoteUserService remoteUserService;
+
+ @Override
+ public String translation(Object key, String other) {
+ return remoteUserService.selectNicknameById((Long) key);
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-translation/src/main/java/org/dromara/common/translation/core/impl/OssUrlTranslationImpl.java b/ruoyi-common/ruoyi-common-translation/src/main/java/org/dromara/common/translation/core/impl/OssUrlTranslationImpl.java
new file mode 100644
index 0000000..b9fe839
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-translation/src/main/java/org/dromara/common/translation/core/impl/OssUrlTranslationImpl.java
@@ -0,0 +1,26 @@
+package org.dromara.common.translation.core.impl;
+
+import org.dromara.common.translation.annotation.TranslationType;
+import org.dromara.common.translation.constant.TransConstant;
+import org.dromara.common.translation.core.TranslationInterface;
+import org.dromara.resource.api.RemoteFileService;
+import lombok.AllArgsConstructor;
+import org.apache.dubbo.config.annotation.DubboReference;
+
+/**
+ * OSS缈昏瘧瀹炵幇
+ *
+ * @author Lion Li
+ */
+@AllArgsConstructor
+@TranslationType(type = TransConstant.OSS_ID_TO_URL)
+public class OssUrlTranslationImpl implements TranslationInterface<String> {
+
+ @DubboReference(mock = "true")
+ private RemoteFileService ossService;
+
+ @Override
+ public String translation(Object key, String other) {
+ return ossService.selectUrlByIds(key.toString());
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-translation/src/main/java/org/dromara/common/translation/core/impl/UserNameTranslationImpl.java b/ruoyi-common/ruoyi-common-translation/src/main/java/org/dromara/common/translation/core/impl/UserNameTranslationImpl.java
new file mode 100644
index 0000000..c442302
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-translation/src/main/java/org/dromara/common/translation/core/impl/UserNameTranslationImpl.java
@@ -0,0 +1,26 @@
+package org.dromara.common.translation.core.impl;
+
+import org.dromara.common.translation.annotation.TranslationType;
+import org.dromara.common.translation.constant.TransConstant;
+import org.dromara.common.translation.core.TranslationInterface;
+import org.dromara.system.api.RemoteUserService;
+import lombok.AllArgsConstructor;
+import org.apache.dubbo.config.annotation.DubboReference;
+
+/**
+ * 鐢ㄦ埛鍚嶇炕璇戝疄鐜�
+ *
+ * @author Lion Li
+ */
+@AllArgsConstructor
+@TranslationType(type = TransConstant.USER_ID_TO_NAME)
+public class UserNameTranslationImpl implements TranslationInterface<String> {
+
+ @DubboReference
+ private RemoteUserService remoteUserService;
+
+ @Override
+ public String translation(Object key, String other) {
+ return remoteUserService.selectUserNameById((Long) key);
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-translation/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/ruoyi-common/ruoyi-common-translation/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..ad40205
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-translation/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1,6 @@
+org.dromara.common.translation.config.TranslationConfig
+org.dromara.common.translation.core.impl.DeptNameTranslationImpl
+org.dromara.common.translation.core.impl.DictTypeTranslationImpl
+org.dromara.common.translation.core.impl.OssUrlTranslationImpl
+org.dromara.common.translation.core.impl.UserNameTranslationImpl
+org.dromara.common.translation.core.impl.NicknameTranslationImpl
diff --git a/ruoyi-common/ruoyi-common-web/pom.xml b/ruoyi-common/ruoyi-common-web/pom.xml
new file mode 100644
index 0000000..5ef7e92
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-web/pom.xml
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common</artifactId>
+ <version>${revision}</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>ruoyi-common-web</artifactId>
+
+ <description>
+ ruoyi-common-web web鏈嶅姟
+ </description>
+
+ <dependencies>
+
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-core</artifactId>
+ </dependency>
+
+ <!-- SpringBoot Web瀹瑰櫒 -->
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-web</artifactId>
+ <exclusions>
+ <exclusion>
+ <artifactId>spring-boot-starter-tomcat</artifactId>
+ <groupId>org.springframework.boot</groupId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ <!-- web 瀹瑰櫒浣跨敤 undertow 鎬ц兘鏇村己 -->
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-undertow</artifactId>
+ </dependency>
+
+ <!-- SpringBoot Actuator -->
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-actuator</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>com.alibaba.cloud</groupId>
+ <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
+ <optional>true</optional>
+ </dependency>
+
+ <dependency>
+ <groupId>net.dreamlu</groupId>
+ <artifactId>mica-metrics</artifactId>
+ <version>2.7.6</version>
+ <exclusions>
+ <exclusion>
+ <groupId>net.dreamlu</groupId>
+ <artifactId>mica-core</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+
+ <dependency>
+ <groupId>net.dreamlu</groupId>
+ <artifactId>mica-core</artifactId>
+ <version>2.7.6</version>
+ <scope>provided</scope>
+ </dependency>
+ </dependencies>
+
+</project>
diff --git a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/I18nConfig.java b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/I18nConfig.java
new file mode 100644
index 0000000..4e212cb
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/I18nConfig.java
@@ -0,0 +1,22 @@
+package org.dromara.common.web.config;
+
+import org.dromara.common.web.core.I18nLocaleResolver;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.web.servlet.LocaleResolver;
+
+/**
+ * 鍥介檯鍖栭厤缃�
+ *
+ * @author Lion Li
+ */
+@AutoConfiguration(before = WebMvcAutoConfiguration.class)
+public class I18nConfig {
+
+ @Bean
+ public LocaleResolver localeResolver() {
+ return new I18nLocaleResolver();
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/UndertowConfig.java b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/UndertowConfig.java
new file mode 100644
index 0000000..421ce6d
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/UndertowConfig.java
@@ -0,0 +1,30 @@
+package org.dromara.common.web.config;
+
+import io.undertow.server.DefaultByteBufferPool;
+import io.undertow.websockets.jsr.WebSocketDeploymentInfo;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.web.embedded.undertow.UndertowServletWebServerFactory;
+import org.springframework.boot.web.server.WebServerFactoryCustomizer;
+
+/**
+ * Undertow 鑷畾涔夐厤缃�
+ *
+ * @author Lion Li
+ */
+@AutoConfiguration
+public class UndertowConfig implements WebServerFactoryCustomizer<UndertowServletWebServerFactory> {
+
+ /**
+ * 璁剧疆 Undertow 鐨� websocket 缂撳啿姹�
+ */
+ @Override
+ public void customize(UndertowServletWebServerFactory factory) {
+ // 榛樿涓嶇洿鎺ュ垎閰嶅唴瀛� 濡傛灉椤圭洰涓娇鐢ㄤ簡 websocket 寤鸿鐩存帴鍒嗛厤
+ factory.addDeploymentInfoCustomizers(deploymentInfo -> {
+ WebSocketDeploymentInfo webSocketDeploymentInfo = new WebSocketDeploymentInfo();
+ webSocketDeploymentInfo.setBuffers(new DefaultByteBufferPool(false, 512));
+ deploymentInfo.addServletContextAttribute("io.undertow.websockets.jsr.WebSocketDeploymentInfo", webSocketDeploymentInfo);
+ });
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/core/BaseController.java b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/core/BaseController.java
new file mode 100644
index 0000000..2808fa8
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/core/BaseController.java
@@ -0,0 +1,32 @@
+package org.dromara.common.web.core;
+
+import org.dromara.common.core.domain.R;
+
+/**
+ * web灞傞�氱敤鏁版嵁澶勭悊
+ *
+ * @author Lion Li
+ */
+public class BaseController {
+
+ /**
+ * 鍝嶅簲杩斿洖缁撴灉
+ *
+ * @param rows 褰卞搷琛屾暟
+ * @return 鎿嶄綔缁撴灉
+ */
+ protected R<Void> toAjax(int rows) {
+ return rows > 0 ? R.ok() : R.fail();
+ }
+
+ /**
+ * 鍝嶅簲杩斿洖缁撴灉
+ *
+ * @param result 缁撴灉
+ * @return 鎿嶄綔缁撴灉
+ */
+ protected R<Void> toAjax(boolean result) {
+ return result ? R.ok() : R.fail();
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/core/I18nLocaleResolver.java b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/core/I18nLocaleResolver.java
new file mode 100644
index 0000000..98ddd06
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/core/I18nLocaleResolver.java
@@ -0,0 +1,31 @@
+package org.dromara.common.web.core;
+
+import org.springframework.web.servlet.LocaleResolver;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import java.util.Locale;
+
+/**
+ * 鑾峰彇璇锋眰澶村浗闄呭寲淇℃伅
+ *
+ * @author Lion Li
+ */
+public class I18nLocaleResolver implements LocaleResolver {
+
+ @Override
+ public Locale resolveLocale(HttpServletRequest httpServletRequest) {
+ String language = httpServletRequest.getHeader("content-language");
+ Locale locale = Locale.getDefault();
+ if (language != null && language.length() > 0) {
+ String[] split = language.split("_");
+ locale = new Locale(split[0], split[1]);
+ }
+ return locale;
+ }
+
+ @Override
+ public void setLocale(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Locale locale) {
+
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/ruoyi-common/ruoyi-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..cdad79a
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1,2 @@
+org.dromara.common.web.config.I18nConfig
+org.dromara.common.web.config.UndertowConfig
diff --git a/ruoyi-common/ruoyi-common-web/src/main/resources/logback-common.xml b/ruoyi-common/ruoyi-common-web/src/main/resources/logback-common.xml
new file mode 100644
index 0000000..89eaa97
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-web/src/main/resources/logback-common.xml
@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<included>
+
+ <property name="log.pattern" value="%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"/>
+
+ <!-- 鎺у埗鍙拌緭鍑� -->
+ <appender name="file_console" class="ch.qos.logback.core.rolling.RollingFileAppender">
+ <file>${log.path}/console.log</file>
+ <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+ <!-- 鏃ュ織鏂囦欢鍚嶆牸寮� -->
+ <fileNamePattern>${log.path}/console.%d{yyyy-MM-dd}.log</fileNamePattern>
+ <!-- 鏃ュ織鏈�澶� 1澶� -->
+ <maxHistory>1</maxHistory>
+ </rollingPolicy>
+ <encoder>
+ <pattern>${log.pattern}</pattern>
+ <charset>utf-8</charset>
+ </encoder>
+ <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
+ <!-- 杩囨护鐨勭骇鍒� -->
+ <level>INFO</level>
+ </filter>
+ </appender>
+
+ <!-- 绯荤粺鏃ュ織杈撳嚭 -->
+ <appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
+ <file>${log.path}/info.log</file>
+ <!-- 寰幆鏀跨瓥锛氬熀浜庢椂闂村垱寤烘棩蹇楁枃浠� -->
+ <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+ <!-- 鏃ュ織鏂囦欢鍚嶆牸寮� -->
+ <fileNamePattern>${log.path}/info.%d{yyyy-MM-dd}.log</fileNamePattern>
+ <!-- 鏃ュ織鏈�澶х殑鍘嗗彶 60澶� -->
+ <maxHistory>60</maxHistory>
+ </rollingPolicy>
+ <encoder>
+ <pattern>${log.pattern}</pattern>
+ </encoder>
+ <filter class="ch.qos.logback.classic.filter.LevelFilter">
+ <!-- 杩囨护鐨勭骇鍒� -->
+ <level>INFO</level>
+ <!-- 鍖归厤鏃剁殑鎿嶄綔锛氭帴鏀讹紙璁板綍锛� -->
+ <onMatch>ACCEPT</onMatch>
+ <!-- 涓嶅尮閰嶆椂鐨勬搷浣滐細鎷掔粷锛堜笉璁板綍锛� -->
+ <onMismatch>DENY</onMismatch>
+ </filter>
+ </appender>
+
+ <appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
+ <file>${log.path}/error.log</file>
+ <!-- 寰幆鏀跨瓥锛氬熀浜庢椂闂村垱寤烘棩蹇楁枃浠� -->
+ <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+ <!-- 鏃ュ織鏂囦欢鍚嶆牸寮� -->
+ <fileNamePattern>${log.path}/error.%d{yyyy-MM-dd}.log</fileNamePattern>
+ <!-- 鏃ュ織鏈�澶х殑鍘嗗彶 60澶� -->
+ <maxHistory>60</maxHistory>
+ </rollingPolicy>
+ <encoder>
+ <pattern>${log.pattern}</pattern>
+ </encoder>
+ <filter class="ch.qos.logback.classic.filter.LevelFilter">
+ <!-- 杩囨护鐨勭骇鍒� -->
+ <level>ERROR</level>
+ <!-- 鍖归厤鏃剁殑鎿嶄綔锛氭帴鏀讹紙璁板綍锛� -->
+ <onMatch>ACCEPT</onMatch>
+ <!-- 涓嶅尮閰嶆椂鐨勬搷浣滐細鎷掔粷锛堜笉璁板綍锛� -->
+ <onMismatch>DENY</onMismatch>
+ </filter>
+ </appender>
+
+ <!-- info寮傛杈撳嚭 -->
+ <appender name="async_info" class="ch.qos.logback.classic.AsyncAppender">
+ <!-- 涓嶄涪澶辨棩蹇�.榛樿鐨�,濡傛灉闃熷垪鐨�80%宸叉弧,鍒欎細涓㈠純TRACT銆丏EBUG銆両NFO绾у埆鐨勬棩蹇� -->
+ <discardingThreshold>0</discardingThreshold>
+ <!-- 鏇存敼榛樿鐨勯槦鍒楃殑娣卞害,璇ュ�间細褰卞搷鎬ц兘.榛樿鍊间负256 -->
+ <queueSize>512</queueSize>
+ <!-- 娣诲姞闄勫姞鐨刟ppender,鏈�澶氬彧鑳芥坊鍔犱竴涓� -->
+ <appender-ref ref="file_info"/>
+ </appender>
+
+ <!-- error寮傛杈撳嚭 -->
+ <appender name="async_error" class="ch.qos.logback.classic.AsyncAppender">
+ <!-- 涓嶄涪澶辨棩蹇�.榛樿鐨�,濡傛灉闃熷垪鐨�80%宸叉弧,鍒欎細涓㈠純TRACT銆丏EBUG銆両NFO绾у埆鐨勬棩蹇� -->
+ <discardingThreshold>0</discardingThreshold>
+ <!-- 鏇存敼榛樿鐨勯槦鍒楃殑娣卞害,璇ュ�间細褰卞搷鎬ц兘.榛樿鍊间负256 -->
+ <queueSize>512</queueSize>
+ <!-- 娣诲姞闄勫姞鐨刟ppender,鏈�澶氬彧鑳芥坊鍔犱竴涓� -->
+ <appender-ref ref="file_error"/>
+ </appender>
+
+ <!--绯荤粺鎿嶄綔鏃ュ織-->
+ <root level="info">
+ <appender-ref ref="async_info"/>
+ <appender-ref ref="async_error"/>
+ <appender-ref ref="file_console"/>
+ </root>
+</included>
diff --git a/ruoyi-common/ruoyi-common-websocket/pom.xml b/ruoyi-common/ruoyi-common-websocket/pom.xml
new file mode 100644
index 0000000..db86dcb
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-websocket/pom.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common</artifactId>
+ <version>${revision}</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>ruoyi-common-websocket</artifactId>
+
+ <description>
+ ruoyi-common-websocket 妯″潡
+ </description>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-core</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-redis</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-satoken</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-json</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-websocket</artifactId>
+ </dependency>
+ </dependencies>
+</project>
diff --git a/ruoyi-common/ruoyi-common-websocket/src/main/java/org/dromara/common/websocket/config/WebSocketConfig.java b/ruoyi-common/ruoyi-common-websocket/src/main/java/org/dromara/common/websocket/config/WebSocketConfig.java
new file mode 100644
index 0000000..30d109e
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-websocket/src/main/java/org/dromara/common/websocket/config/WebSocketConfig.java
@@ -0,0 +1,60 @@
+package org.dromara.common.websocket.config;
+
+import cn.hutool.core.util.StrUtil;
+import org.dromara.common.websocket.config.properties.WebSocketProperties;
+import org.dromara.common.websocket.handler.PlusWebSocketHandler;
+import org.dromara.common.websocket.interceptor.PlusWebSocketInterceptor;
+import org.dromara.common.websocket.listener.WebSocketTopicListener;
+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.web.socket.WebSocketHandler;
+import org.springframework.web.socket.config.annotation.EnableWebSocket;
+import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
+import org.springframework.web.socket.server.HandshakeInterceptor;
+
+/**
+ * WebSocket 閰嶇疆
+ *
+ * @author zendwang
+ */
+@AutoConfiguration
+@ConditionalOnProperty(value = "websocket.enabled", havingValue = "true")
+@EnableConfigurationProperties(WebSocketProperties.class)
+@EnableWebSocket
+public class WebSocketConfig {
+
+ @Bean
+ public WebSocketConfigurer webSocketConfigurer(HandshakeInterceptor handshakeInterceptor,
+ WebSocketHandler webSocketHandler,
+ WebSocketProperties webSocketProperties) {
+ if (StrUtil.isBlank(webSocketProperties.getPath())) {
+ webSocketProperties.setPath("/websocket");
+ }
+
+ if (StrUtil.isBlank(webSocketProperties.getAllowedOrigins())) {
+ webSocketProperties.setAllowedOrigins("*");
+ }
+
+ return registry -> registry
+ .addHandler(webSocketHandler, webSocketProperties.getPath())
+ .addInterceptors(handshakeInterceptor)
+ .setAllowedOrigins(webSocketProperties.getAllowedOrigins());
+ }
+
+ @Bean
+ public HandshakeInterceptor handshakeInterceptor() {
+ return new PlusWebSocketInterceptor();
+ }
+
+ @Bean
+ public WebSocketHandler webSocketHandler() {
+ return new PlusWebSocketHandler();
+ }
+
+ @Bean
+ public WebSocketTopicListener topicListener() {
+ return new WebSocketTopicListener();
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-websocket/src/main/java/org/dromara/common/websocket/config/properties/WebSocketProperties.java b/ruoyi-common/ruoyi-common-websocket/src/main/java/org/dromara/common/websocket/config/properties/WebSocketProperties.java
new file mode 100644
index 0000000..d629fe5
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-websocket/src/main/java/org/dromara/common/websocket/config/properties/WebSocketProperties.java
@@ -0,0 +1,26 @@
+package org.dromara.common.websocket.config.properties;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * WebSocket 閰嶇疆椤�
+ *
+ * @author zendwang
+ */
+@ConfigurationProperties("websocket")
+@Data
+public class WebSocketProperties {
+
+ private Boolean enabled;
+
+ /**
+ * 璺緞
+ */
+ private String path;
+
+ /**
+ * 璁剧疆璁块棶婧愬湴鍧�
+ */
+ private String allowedOrigins;
+}
diff --git a/ruoyi-common/ruoyi-common-websocket/src/main/java/org/dromara/common/websocket/constant/WebSocketConstants.java b/ruoyi-common/ruoyi-common-websocket/src/main/java/org/dromara/common/websocket/constant/WebSocketConstants.java
new file mode 100644
index 0000000..54eb447
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-websocket/src/main/java/org/dromara/common/websocket/constant/WebSocketConstants.java
@@ -0,0 +1,28 @@
+package org.dromara.common.websocket.constant;
+
+/**
+ * websocket鐨勫父閲忛厤缃�
+ *
+ * @author zendwang
+ */
+public interface WebSocketConstants {
+ /**
+ * websocketSession涓殑鍙傛暟鐨刱ey
+ */
+ String LOGIN_USER_KEY = "loginUser";
+
+ /**
+ * 璁㈤槄鐨勯閬�
+ */
+ String WEB_SOCKET_TOPIC = "global:websocket";
+
+ /**
+ * 鍓嶇蹇冭烦妫�鏌ョ殑鍛戒护
+ */
+ String PING = "ping";
+
+ /**
+ * 鏈嶅姟绔績璺虫仮澶嶇殑瀛楃涓�
+ */
+ String PONG = "pong";
+}
diff --git a/ruoyi-common/ruoyi-common-websocket/src/main/java/org/dromara/common/websocket/dto/WebSocketMessageDto.java b/ruoyi-common/ruoyi-common-websocket/src/main/java/org/dromara/common/websocket/dto/WebSocketMessageDto.java
new file mode 100644
index 0000000..e2d4456
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-websocket/src/main/java/org/dromara/common/websocket/dto/WebSocketMessageDto.java
@@ -0,0 +1,29 @@
+package org.dromara.common.websocket.dto;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 娑堟伅鐨刣to
+ *
+ * @author zendwang
+ */
+@Data
+public class WebSocketMessageDto implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 闇�瑕佹帹閫佸埌鐨剆ession key 鍒楄〃
+ */
+ private List<Long> sessionKeys;
+
+ /**
+ * 闇�瑕佸彂閫佺殑娑堟伅
+ */
+ private String message;
+}
diff --git a/ruoyi-common/ruoyi-common-websocket/src/main/java/org/dromara/common/websocket/handler/PlusWebSocketHandler.java b/ruoyi-common/ruoyi-common-websocket/src/main/java/org/dromara/common/websocket/handler/PlusWebSocketHandler.java
new file mode 100644
index 0000000..b92f0cf
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-websocket/src/main/java/org/dromara/common/websocket/handler/PlusWebSocketHandler.java
@@ -0,0 +1,102 @@
+package org.dromara.common.websocket.handler;
+
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.websocket.dto.WebSocketMessageDto;
+import org.dromara.common.websocket.holder.WebSocketSessionHolder;
+import org.dromara.common.websocket.utils.WebSocketUtils;
+import org.dromara.system.api.model.LoginUser;
+import org.springframework.web.socket.*;
+import org.springframework.web.socket.handler.AbstractWebSocketHandler;
+
+import java.util.List;
+
+import static org.dromara.common.websocket.constant.WebSocketConstants.LOGIN_USER_KEY;
+
+/**
+ * WebSocketHandler 瀹炵幇绫�
+ *
+ * @author zendwang
+ */
+@Slf4j
+public class PlusWebSocketHandler extends AbstractWebSocketHandler {
+
+ /**
+ * 杩炴帴鎴愬姛鍚�
+ */
+ @Override
+ public void afterConnectionEstablished(WebSocketSession session) {
+ LoginUser loginUser = (LoginUser) session.getAttributes().get(LOGIN_USER_KEY);
+ WebSocketSessionHolder.addSession(loginUser.getUserId(), session);
+ log.info("[connect] sessionId: {},userId:{},userType:{}", session.getId(), loginUser.getUserId(), loginUser.getUserType());
+ }
+
+ /**
+ * 澶勭悊鍙戦�佹潵鐨勬枃鏈秷鎭�
+ *
+ * @param session
+ * @param message
+ * @throws Exception
+ */
+ @Override
+ protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
+ LoginUser loginUser = (LoginUser) session.getAttributes().get(LOGIN_USER_KEY);
+ List<Long> userIds = List.of(loginUser.getUserId());
+ WebSocketMessageDto webSocketMessageDto = new WebSocketMessageDto();
+ webSocketMessageDto.setSessionKeys(userIds);
+ webSocketMessageDto.setMessage(message.getPayload());
+ WebSocketUtils.publishMessage(webSocketMessageDto);
+ }
+
+ @Override
+ protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) throws Exception {
+ super.handleBinaryMessage(session, message);
+ }
+
+ /**
+ * 蹇冭烦鐩戞祴鐨勫洖澶�
+ *
+ * @param session
+ * @param message
+ * @throws Exception
+ */
+ @Override
+ protected void handlePongMessage(WebSocketSession session, PongMessage message) throws Exception {
+ WebSocketUtils.sendPongMessage(session);
+ }
+
+ /**
+ * 杩炴帴鍑洪敊鏃�
+ *
+ * @param session
+ * @param exception
+ * @throws Exception
+ */
+ @Override
+ public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
+ log.error("[transport error] sessionId: {} , exception:{}", session.getId(), exception.getMessage());
+ }
+
+ /**
+ * 杩炴帴鍏抽棴鍚�
+ *
+ * @param session
+ * @param status
+ */
+ @Override
+ public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
+ LoginUser loginUser = (LoginUser) session.getAttributes().get(LOGIN_USER_KEY);
+ WebSocketSessionHolder.removeSession(loginUser.getUserId());
+ log.info("[disconnect] sessionId: {},userId:{},userType:{}", session.getId(), loginUser.getUserId(), loginUser.getUserType());
+ }
+
+ /**
+ * 鏄惁鏀寔鍒嗙墖娑堟伅
+ *
+ * @return
+ */
+ @Override
+ public boolean supportsPartialMessages() {
+ return false;
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-websocket/src/main/java/org/dromara/common/websocket/holder/WebSocketSessionHolder.java b/ruoyi-common/ruoyi-common-websocket/src/main/java/org/dromara/common/websocket/holder/WebSocketSessionHolder.java
new file mode 100644
index 0000000..de8c5a7
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-websocket/src/main/java/org/dromara/common/websocket/holder/WebSocketSessionHolder.java
@@ -0,0 +1,42 @@
+package org.dromara.common.websocket.holder;
+
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import org.springframework.web.socket.WebSocketSession;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * WebSocketSession 鐢ㄤ簬淇濆瓨褰撳墠鎵�鏈夊湪绾跨殑浼氳瘽淇℃伅
+ *
+ * @author zendwang
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class WebSocketSessionHolder {
+
+ private static final Map<Long, WebSocketSession> USER_SESSION_MAP = new ConcurrentHashMap<>();
+
+ public static void addSession(Long sessionKey, WebSocketSession session) {
+ USER_SESSION_MAP.put(sessionKey, session);
+ }
+
+ public static void removeSession(Long sessionKey) {
+ if (USER_SESSION_MAP.containsKey(sessionKey)) {
+ USER_SESSION_MAP.remove(sessionKey);
+ }
+ }
+
+ public static WebSocketSession getSessions(Long sessionKey) {
+ return USER_SESSION_MAP.get(sessionKey);
+ }
+
+ public static Set<Long> getSessionsAll() {
+ return USER_SESSION_MAP.keySet();
+ }
+
+ public static Boolean existSession(Long sessionKey) {
+ return USER_SESSION_MAP.containsKey(sessionKey);
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-websocket/src/main/java/org/dromara/common/websocket/interceptor/PlusWebSocketInterceptor.java b/ruoyi-common/ruoyi-common-websocket/src/main/java/org/dromara/common/websocket/interceptor/PlusWebSocketInterceptor.java
new file mode 100644
index 0000000..fda4b59
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-websocket/src/main/java/org/dromara/common/websocket/interceptor/PlusWebSocketInterceptor.java
@@ -0,0 +1,51 @@
+package org.dromara.common.websocket.interceptor;
+
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.satoken.utils.LoginHelper;
+import org.dromara.system.api.model.LoginUser;
+import org.springframework.http.server.ServerHttpRequest;
+import org.springframework.http.server.ServerHttpResponse;
+import org.springframework.web.socket.WebSocketHandler;
+import org.springframework.web.socket.server.HandshakeInterceptor;
+
+import java.util.Map;
+
+import static org.dromara.common.websocket.constant.WebSocketConstants.LOGIN_USER_KEY;
+
+/**
+ * WebSocket鎻℃墜璇锋眰鐨勬嫤鎴櫒
+ *
+ * @author zendwang
+ */
+@Slf4j
+public class PlusWebSocketInterceptor implements HandshakeInterceptor {
+
+ /**
+ * 鎻℃墜鍓�
+ *
+ * @param request request
+ * @param response response
+ * @param wsHandler wsHandler
+ * @param attributes attributes
+ * @return 鏄惁鎻℃墜鎴愬姛
+ */
+ @Override
+ public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) {
+ LoginUser loginUser = LoginHelper.getLoginUser();
+ attributes.put(LOGIN_USER_KEY, loginUser);
+ return true;
+ }
+
+ /**
+ * 鎻℃墜鍚�
+ *
+ * @param request request
+ * @param response response
+ * @param wsHandler wsHandler
+ * @param exception 寮傚父
+ */
+ @Override
+ public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
+
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-websocket/src/main/java/org/dromara/common/websocket/listener/WebSocketTopicListener.java b/ruoyi-common/ruoyi-common-websocket/src/main/java/org/dromara/common/websocket/listener/WebSocketTopicListener.java
new file mode 100644
index 0000000..01528d0
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-websocket/src/main/java/org/dromara/common/websocket/listener/WebSocketTopicListener.java
@@ -0,0 +1,43 @@
+package org.dromara.common.websocket.listener;
+
+import cn.hutool.core.collection.CollUtil;
+import org.dromara.common.websocket.holder.WebSocketSessionHolder;
+import org.dromara.common.websocket.utils.WebSocketUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.ApplicationArguments;
+import org.springframework.boot.ApplicationRunner;
+import org.springframework.core.Ordered;
+
+/**
+ * WebSocket 涓婚璁㈤槄鐩戝惉鍣�
+ *
+ * @author zendwang
+ */
+@Slf4j
+public class WebSocketTopicListener implements ApplicationRunner, Ordered {
+
+ @Override
+ public void run(ApplicationArguments args) throws Exception {
+ WebSocketUtils.subscribeMessage((message) -> {
+ log.info("WebSocket涓婚璁㈤槄鏀跺埌娑堟伅session keys={} message={}", message.getSessionKeys(), message.getMessage());
+ // 濡傛灉key涓嶄负绌哄氨鎸夌収key鍙戞秷鎭� 濡傛灉涓虹┖灏辩兢鍙�
+ if (CollUtil.isNotEmpty(message.getSessionKeys())) {
+ message.getSessionKeys().forEach(key -> {
+ if (WebSocketSessionHolder.existSession(key)) {
+ WebSocketUtils.sendMessage(key, message.getMessage());
+ }
+ });
+ } else {
+ WebSocketSessionHolder.getSessionsAll().forEach(key -> {
+ WebSocketUtils.sendMessage(key, message.getMessage());
+ });
+ }
+ });
+ log.info("鍒濆鍖朩ebSocket涓婚璁㈤槄鐩戝惉鍣ㄦ垚鍔�");
+ }
+
+ @Override
+ public int getOrder() {
+ return -1;
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-websocket/src/main/java/org/dromara/common/websocket/utils/WebSocketUtils.java b/ruoyi-common/ruoyi-common-websocket/src/main/java/org/dromara/common/websocket/utils/WebSocketUtils.java
new file mode 100644
index 0000000..99bb845
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-websocket/src/main/java/org/dromara/common/websocket/utils/WebSocketUtils.java
@@ -0,0 +1,110 @@
+package org.dromara.common.websocket.utils;
+
+import cn.hutool.core.collection.CollUtil;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.redis.utils.RedisUtils;
+import org.dromara.common.websocket.dto.WebSocketMessageDto;
+import org.dromara.common.websocket.holder.WebSocketSessionHolder;
+import org.springframework.web.socket.PongMessage;
+import org.springframework.web.socket.TextMessage;
+import org.springframework.web.socket.WebSocketMessage;
+import org.springframework.web.socket.WebSocketSession;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+import static org.dromara.common.websocket.constant.WebSocketConstants.WEB_SOCKET_TOPIC;
+
+/**
+ * 宸ュ叿绫�
+ *
+ * @author zendwang
+ */
+@Slf4j
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class WebSocketUtils {
+
+ /**
+ * 鍙戦�佹秷鎭�
+ *
+ * @param sessionKey session涓婚敭 涓�鑸负鐢ㄦ埛id
+ * @param message 娑堟伅鏂囨湰
+ */
+ public static void sendMessage(Long sessionKey, String message) {
+ WebSocketSession session = WebSocketSessionHolder.getSessions(sessionKey);
+ sendMessage(session, message);
+ }
+
+ /**
+ * 璁㈤槄娑堟伅
+ *
+ * @param consumer 鑷畾涔夊鐞�
+ */
+ public static void subscribeMessage(Consumer<WebSocketMessageDto> consumer) {
+ RedisUtils.subscribe(WEB_SOCKET_TOPIC, WebSocketMessageDto.class, consumer);
+ }
+
+ /**
+ * 鍙戝竷璁㈤槄鐨勬秷鎭�
+ *
+ * @param webSocketMessage 娑堟伅瀵硅薄
+ */
+ public static void publishMessage(WebSocketMessageDto webSocketMessage) {
+ List<Long> unsentSessionKeys = new ArrayList<>();
+ // 褰撳墠鏈嶅姟鍐卻ession,鐩存帴鍙戦�佹秷鎭�
+ for (Long sessionKey : webSocketMessage.getSessionKeys()) {
+ if (WebSocketSessionHolder.existSession(sessionKey)) {
+ WebSocketUtils.sendMessage(sessionKey, webSocketMessage.getMessage());
+ continue;
+ }
+ unsentSessionKeys.add(sessionKey);
+ }
+ // 涓嶅湪褰撳墠鏈嶅姟鍐卻ession,鍙戝竷璁㈤槄娑堟伅
+ if (CollUtil.isNotEmpty(unsentSessionKeys)) {
+ WebSocketMessageDto broadcastMessage = new WebSocketMessageDto();
+ broadcastMessage.setMessage(webSocketMessage.getMessage());
+ broadcastMessage.setSessionKeys(unsentSessionKeys);
+ RedisUtils.publish(WEB_SOCKET_TOPIC, broadcastMessage, consumer -> {
+ log.info("WebSocket鍙戦�佷富棰樿闃呮秷鎭痶opic:{} session keys:{} message:{}",
+ WEB_SOCKET_TOPIC, unsentSessionKeys, webSocketMessage.getMessage());
+ });
+ }
+ }
+
+ /**
+ * 鍙戝竷璁㈤槄鐨勬秷鎭�(缇ゅ彂)
+ *
+ * @param message 娑堟伅鍐呭
+ */
+ public static void publishAll(String message) {
+ WebSocketMessageDto broadcastMessage = new WebSocketMessageDto();
+ broadcastMessage.setMessage(message);
+ RedisUtils.publish(WEB_SOCKET_TOPIC, broadcastMessage, consumer -> {
+ log.info("WebSocket鍙戦�佷富棰樿闃呮秷鎭痶opic:{} message:{}", WEB_SOCKET_TOPIC, message);
+ });
+ }
+
+ public static void sendPongMessage(WebSocketSession session) {
+ sendMessage(session, new PongMessage());
+ }
+
+ public static void sendMessage(WebSocketSession session, String message) {
+ sendMessage(session, new TextMessage(message));
+ }
+
+ private static void sendMessage(WebSocketSession session, WebSocketMessage<?> message) {
+ if (session == null || !session.isOpen()) {
+ log.warn("[send] session浼氳瘽宸茬粡鍏抽棴");
+ } else {
+ try {
+ session.sendMessage(message);
+ } catch (IOException e) {
+ log.error("[send] session({}) 鍙戦�佹秷鎭�({}) 寮傚父", session, message, e);
+ }
+ }
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/ruoyi-common/ruoyi-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..c3a7305
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+org.dromara.common.websocket.config.WebSocketConfig
--
Gitblit v1.9.1