Compare commits

...

109 Commits

Author SHA1 Message Date
数据小王子
5476f543f3
!10 fix: 修复swagger definition失效,展示全部接口的问题
Merge pull request !10 from objcfeng/cf/fix/springdoc-config
2025-02-24 01:25:10 +00:00
objcfeng
c820bb4e74 fix: 修复swagger definition失效,展示全部接口的问题 2025-02-22 11:07:55 +08:00
数据小王子
d4f30e7de7 mybatis-flex升级:1.9.3 => 1.9.4 2024-07-29 09:52:08 +08:00
数据小王子
384a3f9b92
!9 新增ListenerManager用以任务调度、三方接口回调等可自由控制审计字段
Merge pull request !9 from Ice/master
2024-07-01 01:31:43 +00:00
Ice
1e9abcb3f5 add:ListenerManager,用以任务调度及三方接口回调情况下自由控制审计字段(目前会因拿不到上下文用户信息将自填充的审计字段改为null值) 2024-06-27 18:24:05 +08:00
数据小王子
ee3948d8e3 mybatis-flex升级:1.9.0 => 1.9.3 2024-06-18 11:43:03 +08:00
数据小王子
4117bd6cc5
!8 修复可能导致同步套餐失败的bug
Merge pull request !8 from xiaoduan/修复可能导致同步套餐失败的bug
2024-06-18 03:38:05 +00:00
xuhaoran
0cbeb5b2df 修复可能导致同步套餐失败的bug 2024-06-07 11:09:07 +08:00
数据小王子
335f806566 mybatis-flex升级:1.8.9 => 1.9.0 2024-05-31 09:08:21 +08:00
数据小王子
8655030c7b
!7 pagehelper多数据源自动识别对应方言的分页
Merge pull request !7 from 秦白起/Pull-Request
2024-05-15 09:10:59 +00:00
GeHui
2635d74af5 ⚙️ config(common): pagehelper多数据源自动识别对应方言的分页
1.yaml里的配置是pagehelper-spring-boot-starter
  的,对pagehelper来说没用
2.使用配置类PagehelperConfig

(cherry picked from commit 2303b18a78679a3211f1caf620eb2adbdfd3cedc)
(cherry picked from commit f066ec91bc93f5a81aa7f826d227fb1b4d3fed04)
2024-05-15 15:40:49 +08:00
数据小王子
da91600948 sa-token升级:1.37.0 -> 1.38.0 2024-05-13 16:27:42 +08:00
数据小王子
08cda7b792 mybatis-flex升级:1.8.8 => 1.8.9 2024-05-11 17:52:55 +08:00
数据小王子
a0957a76c1
!6 sql脚本注释规范,避免执行出错
Merge pull request !6 from 齐家/master
2024-05-10 01:19:32 +00:00
slp
fe227e62cd sql注释规范化,否则某些软件执行时不识别会报错 2024-05-09 16:56:22 +08:00
数据小王子
502b692fee OssClient升级 2024-04-30 15:36:52 +08:00
数据小王子
d28e5fadb5 排除定时任务 2024-04-26 16:48:48 +08:00
数据小王子
935da2f093 spring-boot升级:v3.2.4 => 3.2.5 2024-04-21 08:06:16 +08:00
数据小王子
e78c09ddbe mybatis-flex升级:1.8.7 => 1.8.8 2024-04-19 09:20:53 +08:00
数据小王子
ae9c718ddd edit_columns字段添加注释 2024-04-17 09:06:13 +08:00
数据小王子
844aa24b43
!5 代码生成页面的编辑页列数允许多列
Merge pull request !5 from 晨曦/0416
2024-04-16 11:34:16 +00:00
晨曦
d86def6036 编辑页的列数由原先1列调整为多(1-4)列 2024-04-16 18:57:41 +08:00
晨曦
108bd8e7c4 编辑页的列数由原先1列调整为多(1-4)列 2024-04-16 17:39:27 +08:00
晨曦
c8738f6eb0 编辑页的列数由原先1列调整为多(1-4)列 2024-04-16 15:21:43 +08:00
晨曦
6174ca90e1 编辑页的列数由原先1列调整为多(1-4)列 2024-04-16 15:20:55 +08:00
数据小王子
3d7fd649fd 代码生成器增加导入功能 2024-04-12 21:33:42 +08:00
数据小王子
4224ffdc64 演示模块增加导入功能 2024-04-12 21:27:02 +08:00
数据小王子
0b3f82660f 更新基类TreeEntity 2024-04-12 21:17:53 +08:00
数据小王子
8230404035 树表中增加ancestors字段 2024-04-12 21:14:12 +08:00
数据小王子
4c5bb90361 字典类型添加排序字段 2024-04-11 11:55:19 +08:00
数据小王子
15b8251601 对type、status、gender:代码生成器不再自动为HtmlType赋予默认值,让用户自己选择 2024-04-10 17:00:11 +08:00
数据小王子
0e86f9a111 mybatis-flex升级:1.8.6 => 1.8.7 2024-04-10 16:43:29 +08:00
数据小王子
5de6e70ca1 更新开发文档 2024-04-09 10:48:56 +08:00
数据小王子
ee3272918b 引入另一个任务调度框架:EasyRetry 2024-04-08 21:08:10 +08:00
数据小王子
2219f02a17
!3 针对加密和docker部署
Merge pull request !3 from Duke_yzl/master
2024-04-08 12:17:08 +00:00
yuzl6
4606755e42 复原 2024-04-08 18:39:12 +08:00
yuzl6
d057d654fa 测试生产bug 2024-04-08 17:59:03 +08:00
数据小王子
8263a252f1 分离PowerJob的sql成单独文件 2024-04-08 14:12:02 +08:00
yuzl6
2c98a64d17 Merge remote-tracking branch 'origin/master' 2024-04-08 10:51:49 +08:00
yuzl6
2c721c0a09 增加请求动态加密,docker部署(适配流水线方式推送镜像) 2024-04-08 10:51:31 +08:00
yuzl6
8555e6d684 限制内存使用大小003 2024-04-08 10:35:07 +08:00
yuzl6
70d2864488 限制内存使用大小003 2024-04-08 10:15:18 +08:00
yuzl6
6517cdd084 限制内存使用大小002 2024-04-08 09:59:29 +08:00
yuzl6
226d18b627 限制内存使用大小001 2024-04-08 09:33:03 +08:00
yuzl6
0d91f15061 限制内存使用大小001 2024-04-08 09:14:55 +08:00
yuzl6
54b134e942 限制内存使用大小 2024-04-08 09:08:29 +08:00
yuzl6
a53709481c 部署文件 2024-04-07 23:33:43 +08:00
yuzl6
7a67dce923 部署文件 2024-04-07 20:57:27 +08:00
yuzl6
048fb6abcb 部署文件 2024-04-07 00:05:57 +08:00
yuzl6
3573a71af3 部署文件 2024-04-06 23:59:03 +08:00
yuzl6
41ea876a96 部署文件 2024-04-06 23:58:27 +08:00
yuzl6
d494c551d5 部署文件 2024-04-06 20:03:08 +08:00
yuzl6
445ef5763d 部署文件 2024-04-06 17:08:40 +08:00
yuzl6
92b3efb87d 部署文件 2024-04-06 15:36:38 +08:00
yuzl6
b07033ba51 部署文件 2024-04-06 15:25:47 +08:00
yuzl6
fad4f8e19e 部署文件 2024-04-06 15:18:52 +08:00
yuzl6
0b99d54edf 修改发布环境 2024-04-06 14:53:21 +08:00
yuzl6
596cf10a1c 增加请求加密 动态获取密钥 2024-04-06 14:31:25 +08:00
数据小王子
659caa1d0e
!2 修复接口文档访问错误的BUG
Merge pull request !2 from Curtion/master
2024-04-05 03:44:48 +00:00
Curtion
4358633176 修复接口文档访问错误的BUG 2024-04-04 19:00:14 +08:00
数据小王子
9660f65dad mybatis-flex升级:1.8.5 => 1.8.6 2024-04-03 08:38:17 +08:00
数据小王子
f57b1092e4 mybatis-flex升级:1.8.2 => 1.8.5 2024-04-02 08:50:17 +08:00
数据小王子
00e3ee95c3 依赖更新:
update springboot 3.2.3 => 3.2.4
update springdoc 2.3.0 => 2.4.0
update springboot-admin 3.2.2 => 3.2.3
update redisson 3.27.0 => 3.27.2
update sms4j 3.1.1 => 3.2.0
update hutool 5.8.26 => 5.8.27
2024-04-01 08:58:13 +08:00
数据小王子
e5b182a8ef 更新ip离线数据库 2024-04-01 08:57:33 +08:00
数据小王子
458f60be07 代码生成:新增导入菜单SQL脚本 2024-03-30 17:04:20 +08:00
数据小王子
32fd0311b3 更新AWS SDK 版本到2.25.15 2024-03-25 09:07:24 +08:00
数据小王子
d60ca890a2 代码生成:完善ts前端单表生成vue页面 2024-03-25 09:02:22 +08:00
数据小王子
9d6fc4f539 代码生成器ts前端:树表增加导出按钮 2024-03-22 21:18:55 +08:00
数据小王子
f3b3bec8b0 完善代码生成器ts前端类型文件 2024-03-22 21:08:15 +08:00
数据小王子
bcd06f3d2b 数据库脚本:超级管理员的role_key由admin修改为SuperAdminRole 2024-03-12 11:15:23 +08:00
数据小王子
f8c699ebbd 数据库脚本更新:超级管理员的role_key由admin修改为SuperAdminRole 2024-03-12 11:12:50 +08:00
数据小王子
c91f9686d7 update 更新 mybatis 多包扫描配置 2024-03-06 10:41:21 +08:00
数据小王子
7fb5c26cee 依赖升级:spring-boot-admin 3.2.1 => 3.2.2 2024-03-06 10:35:12 +08:00
数据小王子
c1fb7b4484 优化 AsyncConfig 虚拟线程名称支持 2024-03-06 10:31:03 +08:00
数据小王子
918d67abef redisson支持虚拟线程 2024-03-06 10:27:43 +08:00
数据小王子
a2e1acb66c 修复 空指针null问题 2024-03-06 10:23:22 +08:00
数据小王子
050a48b134 优化 mybatis依赖设置为可选依赖 避免出现不应该注入的情况 2024-03-06 10:20:24 +08:00
数据小王子
b8ab413eab 优化RateLimiter注解使用体验 2024-03-06 10:14:00 +08:00
数据小王子
7c649bb5b2 依赖升级:mybatis-flex 1.8.1 => 1.8.2 2024-03-06 10:07:21 +08:00
数据小王子
c9cb308abf 优化 GET 方法响应体支持加密 2024-03-06 09:29:10 +08:00
数据小王子
0974e65c0a 删除观测用日志记录 2024-03-06 09:01:26 +08:00
数据小王子
fa75c7155a 新增:用户、部门、角色、岗位 下拉选接口与代码实现优化 2024-03-06 08:53:11 +08:00
数据小王子
2c180c89a9 依赖升级:mybatis-flex 1.8.0 => 1.8.1 2024-03-04 11:04:42 +08:00
数据小王子
f31983c2c7 修复 excel 表达式字典 下拉框导出格式错误 2024-03-01 15:34:44 +08:00
数据小王子
62c8e1c877 优化 OssFactory,采用双重校验锁 2024-03-01 15:29:44 +08:00
数据小王子
3bedaa7ddc 优化代码格式 2024-03-01 10:31:39 +08:00
数据小王子
e7b6dac49f 优化 登录消息 支持集群发送 2024-03-01 10:00:56 +08:00
数据小王子
d151c053a7 升级 awsS3 到2.X版本 支持异步与自动分片上传下载 2024-02-29 14:15:17 +08:00
数据小王子
fc2eab2a6d 修改登录问题 2024-02-29 14:09:15 +08:00
数据小王子
e28a2a404e 新增 正则工具类 字符串提取 字符串校验 2024-02-29 11:05:33 +08:00
数据小王子
773e257fa1 增加 SpringUtils.isVirtual 方法 2024-02-29 10:30:11 +08:00
数据小王子
651da9a92a 修复 类型判断问题 2024-02-29 10:16:58 +08:00
数据小王子
e5ca734018 修改数据库id_token字段宽度 2024-02-29 10:01:07 +08:00
数据小王子
5b131ad3fd fix 修复 用户登录查询部门缓存无法获取租户id问题 2024-02-28 14:54:56 +08:00
数据小王子
fe848f418a fix: LoginHelper类 login方法 存在 重复代码 2024-02-28 14:52:31 +08:00
数据小王子
6d484c0bda 引入caffeine本地缓存 2024-02-28 14:40:14 +08:00
数据小王子
f366e430f1 依赖升级:poi 5.2.3 => 5.2.5 2024-02-28 09:10:53 +08:00
数据小王子
7e9ba78376 Merge remote-tracking branch 'origin/master' 2024-02-27 17:32:10 +08:00
数据小王子
5a814b947c 重构登录日志 2024-02-27 17:31:16 +08:00
数据小王子
0af60f5ac5
!1 update script/sql/postgresql/postgresql-ruoyiflex-V5.X.sql.
Merge pull request !1 from yuzhihaomy/N/A
2024-02-27 09:15:56 +00:00
yuzhihaomy
6b8f3e2ed1
update script/sql/postgresql/postgresql-ruoyiflex-V5.X.sql.
793行sql少了个逗号

Signed-off-by: yuzhihaomy <17150515155@163.com>
2024-02-27 09:09:20 +00:00
数据小王子
3f829e271d 解决 token与token-session 过期时间不一致问题 2024-02-27 10:48:08 +08:00
数据小王子
8027f5c2d8 调整transmittable-thread-local依赖位置 2024-02-26 17:31:53 +08:00
数据小王子
29a620eb41 修复 部门树排序问题 2024-02-26 17:24:56 +08:00
数据小王子
885918d11f 依赖HikariCP连接池:5.0.1 => 5.1.0 2024-02-26 11:27:25 +08:00
数据小王子
6d31a7107b 依赖升级:mybatis-flex 1.7.9 => 1.8.0 2024-02-26 09:19:11 +08:00
数据小王子
29ec129069 优化代码生成提交数据提示语 2024-02-23 16:17:07 +08:00
数据小王子
3f3f3d793a 依赖升级:spring-boot 3.2.2 => 3.2.3
hutool 5.8.25 => 5.8.26
   redisson 3.26.0 => 3.27.0
2024-02-23 15:39:39 +08:00
数据小王子
2b7ba78eb1 修改版本号:V5.2.0-SNAPSHOT 2024-02-23 15:33:22 +08:00
145 changed files with 6752 additions and 1990 deletions

View File

@ -1,13 +1,13 @@
<p align="center">
<img alt="logo" src="https://gitee.com/dataprince/ruoyi-flex/raw/master/image/ruoyi-flex-logo.png">
</p>
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">Ruoyi-Flex V5.1.0</h1>
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">Ruoyi-Flex V5.2.0-SNAPSHOT</h1>
<h4 align="center">Ruoyi-Flex是基于JDK21、Spring Boot V3.2.X+平台 前后端分离的未来8年更快的Java开发框架</h4>
## 1、平台简介
Ruoyi-Flex是一套全部开源的快速开发平台针对”分布式集群与多租户“场景全方位升级使用MIT开源许可协议毫无保留给个人及企业免费使用。基于RuoYi-Vue、RuoYi-Vue-Plus集成MyBatis-Flex、JDK21、SpringBootV3.2.X+、Lombok、Sa-Token、SpringDoc、Hutool、SpringBoot Admin、PowerJob、Vue3、Element-Plus、MinIO等优秀开源软件支持PostgreSQL、MySQL开源数据库及其衍生分布式数据库。
Ruoyi-Flex是一套全部开源的快速开发平台针对”分布式集群与多租户“场景全方位升级使用MIT开源许可协议毫无保留给个人及企业免费使用。基于RuoYi-Vue、RuoYi-Vue-Plus集成MyBatis-Flex、JDK21、SpringBootV3.2.X+、Lombok、Sa-Token、SpringDoc、Hutool、SpringBoot Admin、EasyRetry、PowerJob、Vue3、Element-Plus、AntDesign-Vben、MinIO、Flowable等优秀开源软件支持PostgreSQL、MySQL开源数据库及其衍生分布式数据库。
## 2、系统特色
Ruoyi-Flex秉承“写的更少、性能更好、出错更低、交流通畅、快速入门” 的理念,为您带来全方位的赋能与提升:

Binary file not shown.

BIN
doc/~$oyi-Flex-Guide.docx Normal file

Binary file not shown.

93
pom.xml
View File

@ -13,47 +13,50 @@
<description>Ruoyi-Flex管理系统</description>
<properties>
<revision>5.1.0</revision>
<revision>5.2.0-SNAPSHOT</revision>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>21</java.version>
<spring-boot.version>3.2.2</spring-boot.version>
<mybatis-flex.version>1.7.9</mybatis-flex.version>
<satoken.version>1.37.0</satoken.version>
<HikariCP.version>5.0.1</HikariCP.version>
<spring-boot.version>3.2.5</spring-boot.version>
<mybatis-flex.version>1.9.4</mybatis-flex.version>
<satoken.version>1.38.0</satoken.version>
<HikariCP.version>5.1.0</HikariCP.version>
<bitwalker.version>1.21</bitwalker.version>
<caffeine.version>3.1.8</caffeine.version>
<kaptcha.version>2.3.3</kaptcha.version>
<pagehelper.version>6.1.0</pagehelper.version>
<fastjson.version>2.0.43</fastjson.version>
<oshi.version>6.4.8</oshi.version>
<commons.collections.version>3.2.2</commons.collections.version>
<poi.version>5.2.3</poi.version>
<poi.version>5.2.5</poi.version>
<easyexcel.version>3.3.3</easyexcel.version>
<velocity.version>2.3</velocity.version>
<jwt.version>0.9.1</jwt.version>
<servlet-api.version>6.0.0</servlet-api.version>
<guava.version>32.1.1-jre</guava.version>
<springdoc.version>2.3.0</springdoc.version>
<springdoc-openapi-starter-common.version>2.3.0</springdoc-openapi-starter-common.version>
<springdoc.version>2.4.0</springdoc.version>
<springdoc-openapi-starter-common.version>2.4.0</springdoc-openapi-starter-common.version>
<therapi-runtime-javadoc.version>0.15.0</therapi-runtime-javadoc.version>
<snakeyaml.version>2.2</snakeyaml.version>
<lombok.version>1.18.30</lombok.version>
<mapstruct-plus.version>1.3.6</mapstruct-plus.version>
<mapstruct-plus.lombok.version>0.2.0</mapstruct-plus.lombok.version>
<hutool.version>5.8.25</hutool.version>
<redisson.version>3.26.0</redisson.version>
<hutool.version>5.8.27</hutool.version>
<redisson.version>3.27.2</redisson.version>
<lock4j.version>2.2.7</lock4j.version>
<alibaba-ttl.version>2.14.4</alibaba-ttl.version>
<spring-boot-admin.version>3.2.1</spring-boot-admin.version>
<spring-boot-admin.version>3.2.3</spring-boot-admin.version>
<powerjob.version>4.3.6</powerjob.version>
<easyretry.version>3.2.0</easyretry.version>
<!-- 离线IP地址定位库 -->
<ip2region.version>2.7.0</ip2region.version>
<!-- OSS 配置 -->
<aws-java-sdk-s3.version>1.12.600</aws-java-sdk-s3.version>
<aws.sdk.version>2.25.15</aws.sdk.version>
<aws.crt.version>0.29.13</aws.crt.version>
<!-- 加解密依赖库 -->
<bcprov-jdk.version>1.77</bcprov-jdk.version>
<!-- SMS 配置 -->
<sms4j.version>3.1.1</sms4j.version>
<sms4j.version>3.2.0</sms4j.version>
<!-- findbugs消除打包警告 -->
<jsr305.version>3.0.2</jsr305.version>
<!-- 三方授权认证 -->
@ -65,6 +68,9 @@
<maven-compiler-plugin.verison>3.11.0</maven-compiler-plugin.verison>
<maven-surefire-plugin.version>3.1.2</maven-surefire-plugin.version>
<flatten-maven-plugin.version>1.5.0</flatten-maven-plugin.version>
<!--工作流配置-->
<flowable.version>7.0.0</flowable.version>
</properties>
<profiles>
@ -117,6 +123,15 @@
<scope>import</scope>
</dependency>
<!-- flowable 的依赖配置-->
<dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-bom</artifactId>
<version>${flowable.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- common 的依赖配置-->
<dependency>
<groupId>com.ruoyi</groupId>
@ -172,6 +187,13 @@
<version>${satoken.version}</version>
</dependency>
<!-- caffeine缓存 -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>${caffeine.version}</version>
</dependency>
<!-- servlet包 -->
<dependency>
<groupId>jakarta.servlet</groupId>
@ -332,11 +354,23 @@
<version>${ip2region.version}</version>
</dependency>
<!-- OSS 配置 -->
<!-- AWS SDK for Java 2.x -->
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-s3</artifactId>
<version>${aws-java-sdk-s3.version}</version>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
<version>${aws.sdk.version}</version>
</dependency>
<!-- 使用AWS基于 CRT 的 S3 客户端 -->
<dependency>
<groupId>software.amazon.awssdk.crt</groupId>
<artifactId>aws-crt</artifactId>
<version>${aws.crt.version}</version>
</dependency>
<!-- 基于 AWS CRT 的 S3 客户端的性能增强的 S3 传输管理器 -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3-transfer-manager</artifactId>
<version>${aws.sdk.version}</version>
</dependency>
<!-- 加解密依赖库 -->
@ -411,6 +445,23 @@
<version>${revision}</version>
</dependency>
<!-- EasyRetry Client -->
<dependency>
<groupId>com.aizuda</groupId>
<artifactId>easy-retry-client-starter</artifactId>
<version>${easyretry.version}</version>
</dependency>
<dependency>
<groupId>com.aizuda</groupId>
<artifactId>easy-retry-client-core</artifactId>
<version>${easyretry.version}</version>
</dependency>
<dependency>
<groupId>com.aizuda</groupId>
<artifactId>easy-retry-client-job-core</artifactId>
<version>${easyretry.version}</version>
</dependency>
<!-- 代码生成-->
<dependency>
<groupId>com.ruoyi</groupId>
@ -425,7 +476,6 @@
<version>${revision}</version>
</dependency>
<!-- demo模块 -->
<dependency>
<groupId>com.ruoyi</groupId>
@ -433,6 +483,13 @@
<version>${revision}</version>
</dependency>
<!-- 工作流模块 -->
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-workflow</artifactId>
<version>${revision}</version>
</dependency>
</dependencies>
</dependencyManagement>

8
ruoyi-admin/Dockerfile Normal file
View File

@ -0,0 +1,8 @@
# 使用官方的 Java 运行时作为父镜像
FROM registry.cn-qingdao.aliyuncs.com/yuzl1/jdk:21
# 将本地文件复制到容器中
COPY target/ruoyi-admin.jar /ruoyi-admin.jar
# 运行应用
ENTRYPOINT ["java","-jar","/ruoyi-admin.jar"]

View File

@ -78,11 +78,18 @@
<artifactId>spring-boot-admin-starter-client</artifactId>
</dependency>
<!-- powerjob 客户端 -->
<!-- <dependency>-->
<!-- <groupId>tech.powerjob</groupId>-->
<!-- <artifactId>powerjob-worker-spring-boot-starter</artifactId>-->
<!-- </dependency>-->
<dependency>
<groupId>com.aizuda</groupId>
<artifactId>easy-retry-client-starter</artifactId>
</dependency>
<dependency>
<groupId>com.aizuda</groupId>
<artifactId>easy-retry-client-core</artifactId>
</dependency>
<dependency>
<groupId>com.aizuda</groupId>
<artifactId>easy-retry-client-job-core</artifactId>
</dependency>
</dependencies>

View File

@ -3,6 +3,7 @@ package com.ruoyi;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration;
import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup;
/**
* 启动程序
@ -14,7 +15,9 @@ public class RuoYiApplication
{
public static void main(String[] args)
{
SpringApplication.run(RuoYiApplication.class, args);
SpringApplication application = new SpringApplication(RuoYiApplication.class);
application.setApplicationStartup(new BufferingApplicationStartup(2048));
application.run(args);
System.out.println("(♥◠‿◠)ノ゙ RuoYi-Flex-Boot启动成功 ლ(´ڡ`ლ)゙ \n" +
" ███████ ██ ██ ██ ████████ ██ \n" +
"░██░░░░██ ░░██ ██ ░░ ░██░░░░░ ░██ \n" +

View File

@ -5,8 +5,6 @@ import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import com.mybatisflex.core.query.QueryWrapper;
import com.ruoyi.common.core.constant.UserConstants;
import com.ruoyi.common.core.core.domain.AjaxResult;
import com.ruoyi.common.core.core.domain.model.LoginUser;
import com.ruoyi.common.core.core.domain.model.SocialLoginBody;
import com.ruoyi.common.core.utils.*;
import com.ruoyi.common.encrypt.annotation.ApiEncrypt;
@ -15,12 +13,11 @@ import com.ruoyi.common.security.utils.LoginHelper;
import com.ruoyi.common.social.config.properties.SocialLoginConfigProperties;
import com.ruoyi.common.social.config.properties.SocialProperties;
import com.ruoyi.common.social.utils.SocialUtils;
import com.ruoyi.common.tenant.helper.TenantHelper;
import com.ruoyi.common.websocket.dto.WebSocketMessageDto;
import com.ruoyi.common.websocket.utils.WebSocketUtils;
import com.ruoyi.system.domain.bo.SysTenantBo;
import com.ruoyi.system.domain.vo.SysClientVo;
import com.ruoyi.system.domain.vo.SysTenantVo;
import com.ruoyi.system.domain.vo.SysUserVo;
import com.ruoyi.system.service.*;
import com.ruoyi.web.domain.vo.LoginTenantVo;
import com.ruoyi.web.domain.vo.LoginVo;
@ -33,7 +30,6 @@ import lombok.extern.slf4j.Slf4j;
import com.ruoyi.common.core.core.domain.R;
import com.ruoyi.common.core.core.domain.model.LoginBody;
import com.ruoyi.common.core.core.domain.model.RegisterBody;
import com.ruoyi.system.domain.SysClient;
import com.ruoyi.web.service.IAuthStrategy;
import com.ruoyi.web.service.SysLoginService;
import me.zhyd.oauth.model.AuthResponse;
@ -110,7 +106,10 @@ public class AuthController {
Long userId = LoginHelper.getUserId();
scheduledExecutorService.schedule(() -> {
WebSocketUtils.sendMessage(userId, "欢迎登录RuoYi-Flex多租户管理系统");
WebSocketMessageDto dto = new WebSocketMessageDto();
dto.setMessage("欢迎登录RuoYi-Flex多租户管理系统");
dto.setSessionKeys(List.of(userId));
WebSocketUtils.publishMessage(dto);
}, 3, TimeUnit.SECONDS);
return R.ok(loginVo);

View File

@ -2,7 +2,10 @@ package com.ruoyi.web.controller;
import cn.dev33.satoken.annotation.SaIgnore;
import java.time.Duration;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.UUID;
import cn.hutool.captcha.AbstractCaptcha;
import cn.hutool.captcha.generator.CodeGenerator;
@ -10,11 +13,13 @@ import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.RandomUtil;
import com.ruoyi.common.core.annotation.RateLimiter;
import com.ruoyi.common.core.constant.GlobalConstants;
import com.ruoyi.common.core.core.domain.AjaxResult;
import com.ruoyi.common.core.enums.LimitType;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.core.utils.reflect.ReflectUtils;
import com.ruoyi.common.core.utils.SpringUtils;
import com.ruoyi.common.core.core.domain.R;
import com.ruoyi.common.encrypt.utils.RSAUtils;
import com.ruoyi.common.mail.config.properties.MailProperties;
import com.ruoyi.common.mail.utils.MailUtils;
import com.ruoyi.common.redis.utils.RedisUtils;
@ -74,6 +79,7 @@ public class CaptchaController
AbstractCaptcha captcha = SpringUtils.getBean(captchaProperties.getCategory().getClazz());
captcha.setGenerator(codeGenerator);
captcha.createCode();
// 如果是数学验证码使用SpEL表达式处理验证码结果
String code = captcha.getCode();
if (isMath) {
ExpressionParser parser = new SpelExpressionParser();
@ -133,4 +139,27 @@ public class CaptchaController
}
return R.ok();
}
@GetMapping("/genKeyPair")
public R genKeyPair() {
Map<String,String> map=new HashMap<>();
try {
log.info("开始生产rsa秘钥");
Map<String, Object> keyPair = RSAUtils.genKeyPair();
String publicKey = RSAUtils.getPublicKey(keyPair);
String privateKey = RSAUtils.getPrivateKey(keyPair);
log.info("privateKey"+privateKey);
String uuid="ruoyi_"+ UUID.randomUUID().toString().replace("-","");
RedisUtils.setCacheMapValue("loginRsa",uuid,privateKey);
RedisUtils.expire("loginRsa",60*60);
log.info("写入redis完成");
map.put("uuidPrivateKey",uuid);
map.put("RSA_PUBLIC_KEY",publicKey);
} catch (Exception e) {
return R.fail("生成RSA秘钥失败,"+e.getMessage());
}
return R.ok(map);
}
}

View File

@ -13,10 +13,19 @@ import lombok.Data;
@AutoMapper(target = SysTenantVo.class)
public class TenantListVo {
/**
* 租户编号
*/
private String tenantId;
/**
* 企业名称
*/
private String companyName;
/**
* 域名
*/
private String domain;
}

View File

@ -1,26 +1,30 @@
package com.ruoyi.common.security.listener;
package com.ruoyi.web.listener;
import cn.dev33.satoken.config.SaTokenConfig;
import cn.dev33.satoken.listener.SaTokenListener;
import cn.dev33.satoken.stp.SaLoginModel;
import cn.hutool.http.useragent.UserAgent;
import cn.hutool.http.useragent.UserAgentUtil;
import com.ruoyi.common.core.constant.CacheConstants;
import com.ruoyi.common.core.core.domain.dto.UserOnlineDTO;
import com.ruoyi.common.core.core.domain.model.LoginUser;
import com.ruoyi.common.core.enums.UserType;
import com.ruoyi.common.redis.utils.RedisUtils;
import com.ruoyi.common.security.utils.LoginHelper;
import com.ruoyi.common.core.utils.ip.AddressUtils;
import com.ruoyi.common.core.utils.ServletUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import com.ruoyi.common.core.constant.CacheConstants;
import com.ruoyi.common.core.constant.Constants;
import com.ruoyi.common.core.core.domain.dto.UserOnlineDTO;
import com.ruoyi.common.core.utils.MessageUtils;
import com.ruoyi.common.core.utils.ServletUtils;
import com.ruoyi.common.core.utils.SpringUtils;
import com.ruoyi.common.core.utils.ip.AddressUtils;
import com.ruoyi.common.log.event.LogininforEvent;
import com.ruoyi.common.redis.utils.RedisUtils;
import com.ruoyi.common.security.utils.LoginHelper;
import com.ruoyi.common.tenant.helper.TenantHelper;
import com.ruoyi.web.service.SysLoginService;
import org.springframework.stereotype.Component;
import java.time.Duration;
/**
* 用户行为 自定义侦听器
* 用户行为 侦听器的实现
*
* @author Lion Li
*/
@ -30,35 +34,46 @@ import java.time.Duration;
public class UserActionListener implements SaTokenListener {
private final SaTokenConfig tokenConfig;
private final SysLoginService loginService;
/**
* 每次登录时触发
*/
@Override
public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginModel loginModel) {
UserType userType = UserType.getUserType(loginId.toString());
if (userType == UserType.SYS_USER) {
UserAgent userAgent = UserAgentUtil.parse(ServletUtils.getRequest().getHeader("User-Agent"));
String ip = ServletUtils.getClientIP();
LoginUser user = LoginHelper.getLoginUser();
UserOnlineDTO dto = new UserOnlineDTO();
dto.setIpaddr(ip);
dto.setLoginLocation(AddressUtils.getRealAddressByIP(ip));
dto.setBrowser(userAgent.getBrowser().getName());
dto.setOs(userAgent.getOs().getName());
dto.setLoginTime(System.currentTimeMillis());
dto.setTokenId(tokenValue);
dto.setUserName(user.getUsername());
dto.setDeptName(user.getDeptName());
UserAgent userAgent = UserAgentUtil.parse(ServletUtils.getRequest().getHeader("User-Agent"));
String ip = ServletUtils.getClientIP();
UserOnlineDTO dto = new UserOnlineDTO();
dto.setIpaddr(ip);
dto.setLoginLocation(AddressUtils.getRealAddressByIP(ip));
dto.setBrowser(userAgent.getBrowser().getName());
dto.setOs(userAgent.getOs().getName());
dto.setLoginTime(System.currentTimeMillis());
dto.setTokenId(tokenValue);
String username = (String) loginModel.getExtra(LoginHelper.USER_NAME_KEY);
Long tenantId = (Long) loginModel.getExtra(LoginHelper.TENANT_KEY);
dto.setUserName(username);
dto.setClientKey((String) loginModel.getExtra(LoginHelper.CLIENT_KEY));
dto.setDeviceType(loginModel.getDevice());
dto.setDeptName((String) loginModel.getExtra(LoginHelper.DEPT_NAME_KEY));
TenantHelper.dynamic(tenantId, () -> {
if(tokenConfig.getTimeout() == -1) {
RedisUtils.setCacheObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue, dto);
} else {
RedisUtils.setCacheObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue, dto, Duration.ofSeconds(tokenConfig.getTimeout()));
}
log.info("user doLogin, userId:{}, token:{}", loginId, tokenValue);
} else if (userType == UserType.APP_USER) {
// app端 自行根据业务编写
}
});
// 记录登录日志
LogininforEvent logininforEvent = new LogininforEvent();
logininforEvent.setTenantId(tenantId);
logininforEvent.setUsername(username);
logininforEvent.setStatus(Constants.LOGIN_SUCCESS);
logininforEvent.setMessage(MessageUtils.message("user.login.success"));
logininforEvent.setRequest(ServletUtils.getRequest());
SpringUtils.context().publishEvent(logininforEvent);
// 更新登录信息
loginService.recordLoginInfo((Long) loginModel.getExtra(LoginHelper.USER_KEY), ip);
log.info("user doLogin, userId:{}, token:{}", loginId, tokenValue);
}
/**

View File

@ -5,7 +5,6 @@ import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import com.mybatisflex.core.tenant.TenantManager;
import com.ruoyi.common.core.constant.*;
import com.ruoyi.common.core.core.domain.dto.RoleDTO;
import com.ruoyi.common.core.enums.LoginType;
@ -21,21 +20,15 @@ import com.ruoyi.common.tenant.helper.TenantHelper;
import com.ruoyi.system.domain.SysUser;
import com.ruoyi.system.domain.bo.SysSocialBo;
import com.ruoyi.system.domain.bo.SysUserBo;
import com.ruoyi.system.domain.vo.SysSocialVo;
import com.ruoyi.system.domain.vo.SysTenantVo;
import com.ruoyi.system.domain.vo.SysUserVo;
import com.ruoyi.system.service.ISysPermissionService;
import com.ruoyi.system.service.ISysSocialService;
import com.ruoyi.system.service.ISysTenantService;
import com.ruoyi.system.domain.vo.*;
import com.ruoyi.system.service.*;
import jakarta.annotation.Resource;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import me.zhyd.oauth.model.AuthUser;
import org.springframework.beans.factory.annotation.Autowired;
import com.ruoyi.common.core.core.domain.model.LoginUser;
import com.ruoyi.common.core.utils.DateUtils;
import com.ruoyi.common.core.utils.MessageUtils;
import com.ruoyi.system.service.ISysUserService;
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Value;
@ -70,6 +63,12 @@ public class SysLoginService {
@Resource
private ISysUserService userService;
@Resource
private ISysDeptService deptService;
@Resource
private ISysRoleService roleService;
@Resource
private ISysTenantService tenantService;
@ -244,11 +243,17 @@ public class SysLoginService {
* @param userId 用户ID
*/
public void recordLoginInfo(Long userId, String ip) {
SysUserVo sysUserVo = userService.selectUserById(userId);
if (ObjectUtil.isNull(sysUserVo)) {
return;
}
SysUser sysUser = new SysUser();
sysUser.setUserId(userId);
sysUser.setLoginIp(ip);
sysUser.setLoginDate(DateUtils.getNowDate());
sysUser.setUpdateBy(userId);
sysUser.setVersion(sysUserVo.getVersion());
userService.updateById(sysUser);
}

View File

@ -5,7 +5,6 @@ import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.ObjectUtil;
import com.ruoyi.common.core.core.domain.model.EmailLoginBody;
import com.ruoyi.common.json.utils.JsonUtils;
import com.ruoyi.common.tenant.helper.TenantHelper;
import com.ruoyi.system.domain.vo.SysClientVo;
import com.ruoyi.system.domain.vo.SysUserVo;
import com.ruoyi.system.service.ISysUserService;
@ -70,9 +69,6 @@ public class EmailAuthStrategy implements IAuthStrategy {
// 生成token
LoginHelper.login(loginUser, model);
// loginService.recordLogininfor(loginUser.getTenantId(), user.getUserName(), Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"));
// loginService.recordLoginInfo(user.getUserId());
LoginVo loginVo = new LoginVo();
loginVo.setAccessToken(StpUtil.getTokenValue());
loginVo.setExpireIn(StpUtil.getTokenTimeout());

View File

@ -6,7 +6,6 @@ import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.ObjectUtil;
import com.ruoyi.common.core.core.domain.model.PasswordLoginBody;
import com.ruoyi.common.json.utils.JsonUtils;
import com.ruoyi.common.tenant.helper.TenantHelper;
import com.ruoyi.system.domain.vo.SysClientVo;
import com.ruoyi.system.service.ISysUserService;
import jakarta.annotation.Resource;
@ -26,7 +25,6 @@ import com.ruoyi.common.core.utils.ValidatorUtils;
import com.ruoyi.common.redis.utils.RedisUtils;
import com.ruoyi.common.security.utils.LoginHelper;
import com.ruoyi.common.web.config.properties.CaptchaProperties;
import com.ruoyi.system.domain.SysClient;
import com.ruoyi.system.domain.vo.SysUserVo;
import com.ruoyi.web.domain.vo.LoginVo;
import com.ruoyi.web.service.IAuthStrategy;
@ -81,9 +79,6 @@ public class PasswordAuthStrategy implements IAuthStrategy {
// 生成token
LoginHelper.login(loginUser, model);
// loginService.recordLogininfor(loginUser.getTenantId(), username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"));
// loginService.recordLoginInfo(user.getUserId(),user.getVersion());
LoginVo loginVo = new LoginVo();
loginVo.setAccessToken(StpUtil.getTokenValue());
loginVo.setExpireIn(StpUtil.getTokenTimeout());

View File

@ -3,10 +3,7 @@ package com.ruoyi.web.service.impl;
import cn.dev33.satoken.stp.SaLoginModel;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.http.HttpUtil;
import cn.hutool.http.Method;
import com.ruoyi.system.service.ISysUserService;
import jakarta.annotation.Resource;
import lombok.RequiredArgsConstructor;
@ -23,11 +20,9 @@ import com.ruoyi.common.json.utils.JsonUtils;
import com.ruoyi.common.security.utils.LoginHelper;
import com.ruoyi.common.social.config.properties.SocialProperties;
import com.ruoyi.common.social.utils.SocialUtils;
import com.ruoyi.common.tenant.helper.TenantHelper;
import com.ruoyi.system.domain.vo.SysClientVo;
import com.ruoyi.system.domain.vo.SysSocialVo;
import com.ruoyi.system.domain.vo.SysUserVo;
import com.ruoyi.system.mapper.SysUserMapper;
import com.ruoyi.system.service.ISysSocialService;
import com.ruoyi.web.domain.vo.LoginVo;
import com.ruoyi.web.service.IAuthStrategy;

View File

@ -12,7 +12,6 @@ import com.ruoyi.common.core.enums.UserStatus;
import com.ruoyi.common.core.utils.ValidatorUtils;
import com.ruoyi.common.json.utils.JsonUtils;
import com.ruoyi.common.security.utils.LoginHelper;
import com.ruoyi.system.domain.SysClient;
import com.ruoyi.system.domain.vo.SysClientVo;
import com.ruoyi.system.domain.vo.SysUserVo;
import com.ruoyi.web.domain.vo.LoginVo;

View File

@ -29,15 +29,15 @@ mybatis-flex:
# rewriteBatchedStatements=true 批处理优化 大幅提升批量插入更新删除性能(对数据库有性能损耗 使用批量操作应考虑性能问题)
type: ${spring.datasource.type}
# mysql数据库
# driver-class-name: com.mysql.cj.jdbc.Driver
# url: jdbc:mysql://localhost:3306/ruoyi-flex?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
# username: root
# password: Root@369
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/ruoyi-flex?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
username: root
password: Root@369
#postgresql数据库
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://localhost:5432/ruoyi-flex?useUnicode=true&characterEncoding=utf8&useSSL=true&autoReconnect=true&reWriteBatchedInserts=true
username: postgres
password: postgres@369
# driver-class-name: org.postgresql.Driver
# url: jdbc:postgresql://localhost:5432/ruoyi-flex?useUnicode=true&characterEncoding=utf8&useSSL=true&autoReconnect=true&reWriteBatchedInserts=true
# username: postgres
# password: postgres@369
# redis 单机配置(单机与集群只能开启一个另一个需要注释掉)
spring.data:
@ -80,7 +80,7 @@ redisson:
--- # 监控中心客户端配置
spring.boot.admin.client:
# 增加客户端开关
enabled: true
enabled: false
url: http://localhost:9090/admin
instance:
service-host-type: IP
@ -91,7 +91,7 @@ spring.boot.admin.client:
powerjob:
worker:
# 如何开启调度中心请查看文档教程
enabled: true
enabled: false
# 需要先在 powerjob 登录页执行应用注册后才能使用
app-name: ruoyi-worker
# 28080 端口 随着主应用端口飘逸 避免集群冲突
@ -103,6 +103,18 @@ powerjob:
max-appended-wf-context-length: 4096
max-result-length: 4096
--- # easy-retry 配置
easy-retry:
enabled: false
# 需要在EasyRetry后台组管理创建对应名称的组,然后创建任务的时候选择对应的组,才能正确分派任务
group-name: "ruoyi_group"
# EasyRetry接入验证令牌 详见 script/sql/easy_retry.sql `er_group_config` 表
token: "ER_cKqBTPzCsWA3VyuCfFoccmuIEGXjr5KT"
server:
host: 127.0.0.1
port: 1788
# 详见 script/sql/easy_retry.sql `er_namespace` 表
namespace: ${spring.profiles.active}
--- # mail 邮件发送
mail:

View File

@ -29,30 +29,15 @@ mybatis-flex:
# rewriteBatchedStatements=true 批处理优化 大幅提升批量插入更新删除性能(对数据库有性能损耗 使用批量操作应考虑性能问题)
type: ${spring.datasource.type}
# mysql数据库
# driver-class-name: com.mysql.cj.jdbc.Driver
# url: jdbc:mysql://localhost:3306/ruoyi-flex?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
# username: root
# password: Root@369
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/ruoyi-flex?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
username: root
password: Root@369
#postgresql数据库
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://localhost:5432/ruoyi-flex?useUnicode=true&characterEncoding=utf8&useSSL=true&autoReconnect=true&reWriteBatchedInserts=true
username: postgres
password: postgres@369
# # 数据源-2
# ds2:
# # 指定为HikariDataSource
# type: ${spring.datasource.type}
# # mysql数据库
# driver-class-name: com.mysql.cj.jdbc.Driver
# url: jdbc:mysql://localhost:3306/ruoyi-flex?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
# username: root
# password: Root@369
# #postgresql数据库
## driver-class-name: org.postgresql.Driver
## url: jdbc:postgresql://localhost:5432/ruoyi-flex?useUnicode=true&characterEncoding=utf8&useSSL=true&autoReconnect=true&reWriteBatchedInserts=true
## username: postgres
## password: postgres@369
# driver-class-name: org.postgresql.Driver
# url: jdbc:postgresql://localhost:5432/ruoyi-flex?useUnicode=true&characterEncoding=utf8&useSSL=true&autoReconnect=true&reWriteBatchedInserts=true
# username: postgres
# password: postgres@369
# redis 单机配置(单机与集群只能开启一个另一个需要注释掉)
spring.data:
@ -118,6 +103,19 @@ powerjob:
max-appended-wf-context-length: 4096
max-result-length: 4096
--- # easy-retry 配置
easy-retry:
enabled: false
# 需要在EasyRetry后台组管理创建对应名称的组,然后创建任务的时候选择对应的组,才能正确分派任务
group-name: "ruoyi_group"
# EasyRetry接入验证令牌 详见 script/sql/easy_retry.sql `er_group_config` 表
token: "ER_cKqBTPzCsWA3VyuCfFoccmuIEGXjr5KT"
server:
host: 127.0.0.1
port: 1788
# 详见 script/sql/easy_retry.sql `er_namespace` 表
namespace: ${spring.profiles.active}
--- # mail 邮件发送
mail:
enabled: false

View File

@ -9,7 +9,7 @@ ruoyi:
# 实例演示开关
demoEnabled: true
# 文件路径 示例( Windows配置D:/ruoyi/uploadPathLinux配置 /home/ruoyi/uploadPath
profile: D:/ruoyi/uploadPath
profile: /home/ruoyi/uploadPath
# 获取ip地址开关
addressEnabled: false
@ -124,7 +124,7 @@ pagehelper:
mybatis-flex:
# 搜索指定包别名
type-aliases-package: com.ruoyi.**.domain
# 不支持多包, 如有需要可在注解配置 或 提升扫包等级com.**.**.mapper
# 多包名使用 例如 org.dromara.**.mapper,org.xxx.**.mapper
mapper-package: com.ruoyi.**.mapper
# 配置mapper的扫描找到所有的mapper.xml映射文件
mapper-locations: classpath*:mapper/**/*Mapper.xml
@ -191,14 +191,12 @@ api-decrypt:
# SpringDoc配置
springdoc:
#需要扫描的包,可以配置多个,使用逗号分割
packages-to-scan: com.ruoyi
paths-to-exclude: #配置不包含在swagger文档中的api
- /api/test/**
- /api/mockito/data
swagger-ui:
enabled: true #开启/禁止swagger,prod可以设置为false
version: 5.10.3 #指定swagger-ui的版本号
version: 5.11.8 #指定swagger-ui的版本号
disable-swagger-default-url: true #禁用default petstore url
path: /swagger-ui.html #swagger页面
persistAuthorization: true # 持久化认证数据,如果设置为 true它会保留授权数据并且不会在浏览器关闭/刷新时丢失
@ -221,20 +219,24 @@ springdoc:
email: 738981257@qq.com
url: https://gitee.com/dataprince/ruoyi-flex
components:
# 鉴权方式配置
# 鉴权方式配置
security-schemes:
apiKey:
type: APIKEY
in: HEADER
name: ${sa-token.token-name}
group-configs:
- group: 1.演示模块
packages-to-scan: com.ruoyi.demo
- group: 2.通用模块
- group: 1.web模块
packages-to-scan: com.ruoyi.web
- group: 2.演示模块
packages-to-scan:
- com.ruoyi.demo
- com.ruoyi.mf
- group: 3.通用模块
packages-to-scan: com.ruoyi.common
- group: 3.系统模块
- group: 4.系统模块
packages-to-scan: com.ruoyi.system
- group: 4.代码生成模块
- group: 5.代码生成模块
packages-to-scan: com.ruoyi.generator
# 防止XSS攻击
@ -298,6 +300,8 @@ security:
- /captchaImage
- /captcha/get
- /captcha/check
- /genKeyPair
- /job/**
--- # Actuator 监控端点的配置项
management:

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 日志存放路径 -->
<property name="log.path" value="/home/ruoyi/logs" />
<property name="log.path" value="./home/ruoyi/logs" />
<!-- 日志输出格式 -->
<property name="log.pattern" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n" />
@ -11,7 +11,7 @@
<pattern>${log.pattern}</pattern>
</encoder>
</appender>
<!-- 系统日志输出 -->
<appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/sys-info.log</file>
@ -34,7 +34,7 @@
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/sys-error.log</file>
<!-- 循环政策:基于时间创建日志文件 -->
@ -56,7 +56,7 @@
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 用户访问日志输出 -->
<appender name="sys-user" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/sys-user.log</file>
@ -70,7 +70,7 @@
<pattern>${log.pattern}</pattern>
</encoder>
</appender>
<!-- 系统模块日志级别控制 -->
<logger name="com.ruoyi" level="info" />
<!-- Spring日志级别控制 -->
@ -79,15 +79,15 @@
<root level="info">
<appender-ref ref="console" />
</root>
<!--系统操作日志-->
<root level="info">
<appender-ref ref="file_info" />
<appender-ref ref="file_error" />
</root>
<!--系统用户操作日志-->
<logger name="sys-user" level="info">
<appender-ref ref="sys-user"/>
</logger>
</configuration>
</configuration>

View File

@ -14,7 +14,7 @@
</description>
<properties>
<revision>5.1.0</revision>
<revision>5.2.0-SNAPSHOT</revision>
</properties>
<dependencyManagement>

View File

@ -149,6 +149,11 @@
<artifactId>jsr305</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -6,6 +6,7 @@ import com.ruoyi.common.core.utils.SpringUtils;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.core.task.VirtualThreadTaskExecutor;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
@ -14,10 +15,10 @@ import java.util.concurrent.Executor;
/**
* 异步配置
*
* <p>
* 如果未使用虚拟线程则生效
* @author Lion Li
*/
@ConditionalOnProperty(prefix = "spring.threads.virtual", name = "enabled", havingValue = "false")
@AutoConfiguration
public class AsyncConfig implements AsyncConfigurer {
@ -26,6 +27,9 @@ public class AsyncConfig implements AsyncConfigurer {
*/
@Override
public Executor getAsyncExecutor() {
if(SpringUtils.isVirtual()) {
return new VirtualThreadTaskExecutor("async-");
}
return SpringUtils.getBean("scheduledExecutorService");
}

View File

@ -35,6 +35,11 @@ public interface CacheNames {
*/
String SYS_TENANT = GlobalConstants.GLOBAL_REDIS_KEY + "sys_tenant#30d";
/**
* 客户端
*/
String SYS_CLIENT = GlobalConstants.GLOBAL_REDIS_KEY + "sys_client#30d";
/**
* 用户账户
*/

View File

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

View File

@ -34,6 +34,16 @@ public class UserOnlineDTO implements Serializable {
*/
private String userName;
/**
* 客户端
*/
private String clientKey;
/**
* 设备类型
*/
private String deviceType;
/**
* 登录IP地址
*/

View File

@ -0,0 +1,52 @@
package com.ruoyi.common.core.factory;
import cn.hutool.core.lang.PatternPool;
import com.ruoyi.common.core.constant.RegexConstants;
import java.util.regex.Pattern;
/**
* 正则表达式模式池工厂
* <p>初始化的时候将正则表达式加入缓存池当中</p>
* <p>提高正则表达式的性能避免重复编译相同的正则表达式</p>
*
* @author 21001
*/
public class RegexPatternPoolFactory extends PatternPool {
/**
* 字典类型必须以字母开头且只能为小写字母数字下滑线
*/
public static final Pattern DICTIONARY_TYPE = get(RegexConstants.DICTIONARY_TYPE);
/**
* 身份证号码后6位
*/
public static final Pattern ID_CARD_LAST_6 = get(RegexConstants.ID_CARD_LAST_6);
/**
* QQ号码
*/
public static final Pattern QQ_NUMBER = get(RegexConstants.QQ_NUMBER);
/**
* 邮政编码
*/
public static final Pattern POSTAL_CODE = get(RegexConstants.POSTAL_CODE);
/**
* 注册账号
*/
public static final Pattern ACCOUNT = get(RegexConstants.ACCOUNT);
/**
* 密码包含至少8个字符包括大写字母小写字母数字和特殊字符
*/
public static final Pattern PASSWORD = get(RegexConstants.PASSWORD);
/**
* 通用状态0表示正常1表示停用
*/
public static final Pattern STATUS = get(RegexConstants.STATUS);
}

View File

@ -3,7 +3,9 @@ package com.ruoyi.common.core.utils;
import cn.hutool.extra.spring.SpringUtil;
import org.springframework.aop.framework.AopContext;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.boot.autoconfigure.thread.Threading;
import org.springframework.context.ApplicationContext;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
/**
@ -98,4 +100,8 @@ public final class SpringUtils extends SpringUtil
return StringUtils.isNotEmpty(activeProfiles) ? activeProfiles[0] : null;
}
public static boolean isVirtual() {
return Threading.VIRTUAL.isActive(getBean(Environment.class));
}
}

View File

@ -26,6 +26,8 @@ public class StringUtils extends org.apache.commons.lang3.StringUtils
{
public static final String SEPARATOR = ",";
public static final String SLASH = "/";
/** 空字符串 */
private static final String NULLSTR = "";

View File

@ -0,0 +1,30 @@
package com.ruoyi.common.core.utils.regex;
import cn.hutool.core.util.ReUtil;
import com.ruoyi.common.core.constant.RegexConstants;
/**
* 正则相关工具类
*
* @author Feng
*/
public final class RegexUtils extends ReUtil {
/**
* 从输入字符串中提取匹配的部分如果没有匹配则返回默认值
*
* @param input 要提取的输入字符串
* @param regex 用于匹配的正则表达式可以使用 {@link RegexConstants} 中定义的常量
* @param defaultInput 如果没有匹配时返回的默认值
* @return 如果找到匹配的部分则返回匹配的部分否则返回默认值
*/
public static String extractFromString(String input, String regex, String defaultInput) {
try {
return ReUtil.get(regex, input, 1);
} catch (Exception e) {
return defaultInput;
}
}
}

View File

@ -0,0 +1,105 @@
package com.ruoyi.common.core.utils.regex;
import cn.hutool.core.exceptions.ValidateException;
import cn.hutool.core.lang.Validator;
import com.ruoyi.common.core.factory.RegexPatternPoolFactory;
import java.util.regex.Pattern;
/**
* 正则字段校验器
* 主要验证字段非空是否为满足指定格式等
*
* @author Feng
*/
public class RegexValidator extends Validator {
/**
* 字典类型必须以字母开头且只能为小写字母数字下滑线
*/
public static final Pattern DICTIONARY_TYPE = RegexPatternPoolFactory.DICTIONARY_TYPE;
/**
* 身份证号码后6位
*/
public static final Pattern ID_CARD_LAST_6 = RegexPatternPoolFactory.ID_CARD_LAST_6;
/**
* QQ号码
*/
public static final Pattern QQ_NUMBER = RegexPatternPoolFactory.QQ_NUMBER;
/**
* 邮政编码
*/
public static final Pattern POSTAL_CODE = RegexPatternPoolFactory.POSTAL_CODE;
/**
* 注册账号
*/
public static final Pattern ACCOUNT = RegexPatternPoolFactory.ACCOUNT;
/**
* 密码包含至少8个字符包括大写字母小写字母数字和特殊字符
*/
public static final Pattern PASSWORD = RegexPatternPoolFactory.PASSWORD;
/**
* 通用状态0表示正常1表示停用
*/
public static final Pattern STATUS = RegexPatternPoolFactory.STATUS;
/**
* 检查输入的账号是否匹配预定义的规则
*
* @param value 要验证的账号
* @return 如果账号符合规则返回 true否则返回 false
*/
public static boolean isAccount(CharSequence value) {
return isMatchRegex(ACCOUNT, value);
}
/**
* 验证输入的账号是否符合规则如果不符合则抛出 ValidateException 异常
*
* @param value 要验证的账号
* @param errorMsg 验证失败时抛出的异常消息
* @param <T> CharSequence 的子类型
* @return 如果验证通过返回输入的账号
* @throws ValidateException 如果验证失败
*/
public static <T extends CharSequence> T validateAccount(T value, String errorMsg) throws ValidateException {
if (!isAccount(value)) {
throw new ValidateException(errorMsg);
}
return value;
}
/**
* 检查输入的状态是否匹配预定义的规则
*
* @param value 要验证的状态
* @return 如果状态符合规则返回 true否则返回 false
*/
public static boolean isStatus(CharSequence value) {
return isMatchRegex(STATUS, value);
}
/**
* 验证输入的状态是否符合规则如果不符合则抛出 ValidateException 异常
*
* @param value 要验证的状态
* @param errorMsg 验证失败时抛出的异常消息
* @param <T> CharSequence 的子类型
* @return 如果验证通过返回输入的状态
* @throws ValidateException 如果验证失败
*/
public static <T extends CharSequence> T validateStatus(T value, String errorMsg) throws ValidateException {
if (!isStatus(value)) {
throw new ValidateException(errorMsg);
}
return value;
}
}

View File

@ -1,6 +1,7 @@
package com.ruoyi.common.encrypt.core;
import cn.hutool.core.util.ReflectUtil;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import com.ruoyi.common.encrypt.annotation.EncryptField;
@ -19,6 +20,7 @@ import java.util.stream.Collectors;
* @version 4.6.0
*/
@Slf4j
@NoArgsConstructor
public class EncryptorManager {
/**

View File

@ -36,8 +36,9 @@ public class CryptoFilter implements Filter {
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest servletRequest = (HttpServletRequest) request;
HttpServletResponse servletResponse = (HttpServletResponse) response;
boolean responseFlag = false;
// 获取加密注解
ApiEncrypt apiEncrypt = this.getApiEncryptAnnotation(servletRequest);
boolean responseFlag = apiEncrypt != null && apiEncrypt.response();
ServletRequest requestWrapper = null;
ServletResponse responseWrapper = null;
EncryptResponseBodyWrapper responseBodyWrapper = null;
@ -48,12 +49,9 @@ public class CryptoFilter implements Filter {
if (HttpMethod.PUT.matches(servletRequest.getMethod()) || HttpMethod.POST.matches(servletRequest.getMethod())) {
// 是否存在加密标头
String headerValue = servletRequest.getHeader(properties.getHeaderFlag());
// 获取加密注解
ApiEncrypt apiEncrypt = this.getApiEncryptAnnotation(servletRequest);
responseFlag = apiEncrypt != null && apiEncrypt.response();
if (StringUtils.isNotBlank(headerValue)) {
// 请求解密
requestWrapper = new DecryptRequestBodyWrapper(servletRequest, properties.getPrivateKey(), properties.getHeaderFlag());
requestWrapper = new DecryptRequestBodyWrapper(servletRequest, headerValue);
} else {
// 是否有注解有就报错没有放行
if (ObjectUtil.isNotNull(apiEncrypt)) {
@ -64,13 +62,13 @@ public class CryptoFilter implements Filter {
return;
}
}
// 判断是否响应加密
if (responseFlag) {
responseBodyWrapper = new EncryptResponseBodyWrapper(servletResponse);
responseWrapper = responseBodyWrapper;
}
}
}
// 判断是否响应加密
if (responseFlag) {
responseBodyWrapper = new EncryptResponseBodyWrapper(servletResponse);
responseWrapper = responseBodyWrapper;
}
chain.doFilter(
ObjectUtil.defaultIfNull(requestWrapper, request),

View File

@ -2,12 +2,14 @@ package com.ruoyi.common.encrypt.filter;
import cn.hutool.core.io.IoUtil;
import com.ruoyi.common.encrypt.utils.EncryptUtils;
import com.ruoyi.common.redis.utils.RedisUtils;
import jakarta.servlet.ReadListener;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import com.ruoyi.common.core.constant.Constants;
import org.springframework.http.MediaType;
import org.springframework.web.util.UriUtils;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
@ -24,22 +26,20 @@ 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);
// 解密 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);
// 解密 body 采用 AES 加密
String decryptBody = EncryptUtils.decryptByAes(requestBody, aesPassword);
body = decryptBody.getBytes(StandardCharsets.UTF_8);
}
public DecryptRequestBodyWrapper(HttpServletRequest request, String privateKey) throws IOException {
super(request);
// 获取 AES 密码 采用 RSA 加密
String privateKeyValue = RedisUtils.getCacheMapValue("loginRsa", privateKey);
@Override
byte[] readBytes = IoUtil.readBytes(request.getInputStream(), false);
String requestBody = new String(readBytes, StandardCharsets.UTF_8);
// 解密 body 采用 AES 加密
String decryptBody = EncryptUtils.decryptByRsa(requestBody, privateKeyValue);
body = UriUtils.decode(decryptBody, StandardCharsets.UTF_8).getBytes(StandardCharsets.UTF_8);
}
@Override
public BufferedReader getReader() {
return new BufferedReader(new InputStreamReader(getInputStream()));
}

View File

@ -0,0 +1,363 @@
package com.ruoyi.common.encrypt.utils;
import org.apache.commons.codec.binary.Base64;
import javax.crypto.Cipher;
import java.io.ByteArrayOutputStream;
import java.security.*;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.HashMap;
import java.util.Map;
/**
* @NAME: RSAEncryptUtils
* @AUTHOR: gaoly
* @DATE: 2021/1/26 15:15
* @DES:
**/
public class RSAUtils {
/** */
/**
* 加密算法RSA
*/
public static final String KEY_ALGORITHM = "RSA";
/** */
/**
* 签名算法
*/
public static final String SIGNATURE_ALGORITHM = "MD5withRSA";
/** */
/**
* 获取公钥的key
*/
private static final String PUBLIC_KEY = "RSAPublicKey";
/** */
/**
* 获取私钥的key
*/
private static final String PRIVATE_KEY = "RSAPrivateKey";
/** */
/**
* RSA最大加密明文大小
*/
private static final int MAX_ENCRYPT_BLOCK = 117;
/** */
/**
* RSA最大解密密文大小
*/
private static final int MAX_DECRYPT_BLOCK = 128;
/** */
/**
* RSA 位数 如果采用2048 上面最大加密和最大解密则须填写: 245 256
*/
private static final int INITIALIZE_LENGTH = 1024;
/** */
/**
* <p>
* 生成密钥对(公钥和私钥)
* </p>
*
* @return
* @throws Exception
*/
public static Map<String, Object> genKeyPair() throws Exception {
KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance(KEY_ALGORITHM);
keyPairGen.initialize(INITIALIZE_LENGTH);
KeyPair keyPair = keyPairGen.generateKeyPair();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
Map<String, Object> keyMap = new HashMap<String, Object>(2);
keyMap.put(PUBLIC_KEY, publicKey);
keyMap.put(PRIVATE_KEY, privateKey);
return keyMap;
}
/** */
/**
* <p>
* 用私钥对信息生成数字签名
* </p>
*
* @param data
* 已加密数据
* @param privateKey
* 私钥(BASE64编码)
*
* @return
* @throws Exception
*/
public static String sign(byte[] data, String privateKey) throws Exception {
byte[] keyBytes = Base64.decodeBase64(privateKey);
PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
PrivateKey privateK = keyFactory.generatePrivate(pkcs8KeySpec);
Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);
signature.initSign(privateK);
signature.update(data);
return Base64.encodeBase64String(signature.sign());
}
/** */
/**
* <p>
* 校验数字签名
* </p>
*
* @param data
* 已加密数据
* @param publicKey
* 公钥(BASE64编码)
* @param sign
* 数字签名
*
* @return
* @throws Exception
*
*/
public static boolean verify(byte[] data, String publicKey, String sign) throws Exception {
byte[] keyBytes = Base64.decodeBase64(publicKey);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
PublicKey publicK = keyFactory.generatePublic(keySpec);
Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);
signature.initVerify(publicK);
signature.update(data);
return signature.verify(Base64.decodeBase64(sign));
}
/** */
/**
* <P>
* 私钥解密
* </p>
*
* @param encryptedData
* 已加密数据
* @param privateKey
* 私钥(BASE64编码)
* @return
* @throws Exception
*/
public static byte[] decryptByPrivateKey(byte[] encryptedData, String privateKey) throws Exception {
byte[] keyBytes = Base64.decodeBase64(privateKey);
PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
Key privateK = keyFactory.generatePrivate(pkcs8KeySpec);
Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
cipher.init(Cipher.DECRYPT_MODE, privateK);
int inputLen = encryptedData.length;
ByteArrayOutputStream out = new ByteArrayOutputStream();
int offSet = 0;
byte[] cache;
int i = 0;
// 对数据分段解密
while (inputLen - offSet > 0) {
if (inputLen - offSet > MAX_DECRYPT_BLOCK) {
cache = cipher.doFinal(encryptedData, offSet, MAX_DECRYPT_BLOCK);
} else {
cache = cipher.doFinal(encryptedData, offSet, inputLen - offSet);
}
out.write(cache, 0, cache.length);
i++;
offSet = i * MAX_DECRYPT_BLOCK;
}
byte[] decryptedData = out.toByteArray();
out.close();
return decryptedData;
}
/** */
/**
* <p>
* 公钥解密
* </p>
*
* @param encryptedData
* 已加密数据
* @param publicKey
* 公钥(BASE64编码)
* @return
* @throws Exception
*/
public static byte[] decryptByPublicKey(byte[] encryptedData, String publicKey) throws Exception {
byte[] keyBytes = Base64.decodeBase64(publicKey);
X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
Key publicK = keyFactory.generatePublic(x509KeySpec);
Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
cipher.init(Cipher.DECRYPT_MODE, publicK);
int inputLen = encryptedData.length;
ByteArrayOutputStream out = new ByteArrayOutputStream();
int offSet = 0;
byte[] cache;
int i = 0;
// 对数据分段解密
while (inputLen - offSet > 0) {
if (inputLen - offSet > MAX_DECRYPT_BLOCK) {
cache = cipher.doFinal(encryptedData, offSet, MAX_DECRYPT_BLOCK);
} else {
cache = cipher.doFinal(encryptedData, offSet, inputLen - offSet);
}
out.write(cache, 0, cache.length);
i++;
offSet = i * MAX_DECRYPT_BLOCK;
}
byte[] decryptedData = out.toByteArray();
out.close();
return decryptedData;
}
/** */
/**
* <p>
* 公钥加密
* </p>
*
* @param data
* 源数据
* @param publicKey
* 公钥(BASE64编码)
* @return
* @throws Exception
*/
public static byte[] encryptByPublicKey(byte[] data, String publicKey) throws Exception {
byte[] keyBytes = Base64.decodeBase64(publicKey);
X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
Key publicK = keyFactory.generatePublic(x509KeySpec);
// 对数据加密
Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
cipher.init(Cipher.ENCRYPT_MODE, publicK);
int inputLen = data.length;
ByteArrayOutputStream out = new ByteArrayOutputStream();
int offSet = 0;
byte[] cache;
int i = 0;
// 对数据分段加密
while (inputLen - offSet > 0) {
if (inputLen - offSet > MAX_ENCRYPT_BLOCK) {
cache = cipher.doFinal(data, offSet, MAX_ENCRYPT_BLOCK);
} else {
cache = cipher.doFinal(data, offSet, inputLen - offSet);
}
out.write(cache, 0, cache.length);
i++;
offSet = i * MAX_ENCRYPT_BLOCK;
}
byte[] encryptedData = out.toByteArray();
out.close();
return encryptedData;
}
/** */
/**
* <p>
* 私钥加密
* </p>
*
* @param data
* 源数据
* @param privateKey
* 私钥(BASE64编码)
* @return
* @throws Exception
*/
public static byte[] encryptByPrivateKey(byte[] data, String privateKey) throws Exception {
byte[] keyBytes = Base64.decodeBase64(privateKey);
PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
Key privateK = keyFactory.generatePrivate(pkcs8KeySpec);
Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
cipher.init(Cipher.ENCRYPT_MODE, privateK);
int inputLen = data.length;
ByteArrayOutputStream out = new ByteArrayOutputStream();
int offSet = 0;
byte[] cache;
int i = 0;
// 对数据分段加密
while (inputLen - offSet > 0) {
if (inputLen - offSet > MAX_ENCRYPT_BLOCK) {
cache = cipher.doFinal(data, offSet, MAX_ENCRYPT_BLOCK);
} else {
cache = cipher.doFinal(data, offSet, inputLen - offSet);
}
out.write(cache, 0, cache.length);
i++;
offSet = i * MAX_ENCRYPT_BLOCK;
}
byte[] encryptedData = out.toByteArray();
out.close();
return encryptedData;
}
/** */
/**
* <p>
* 获取私钥
* </p>
*
* @param keyMap
* 密钥对
* @return
* @throws Exception
*/
public static String getPrivateKey(Map<String, Object> keyMap) throws Exception {
Key key = (Key) keyMap.get(PRIVATE_KEY);
return Base64.encodeBase64String(key.getEncoded());
}
/** */
/**
* <p>
* 获取公钥
* </p>
*
* @param keyMap
* 密钥对
* @return
* @throws Exception
*/
public static String getPublicKey(Map<String, Object> keyMap) throws Exception {
Key key = (Key) keyMap.get(PUBLIC_KEY);
return Base64.encodeBase64String(key.getEncoded());
}
/**
* java端公钥加密
*/
public static String encryptedDataOnJava(String data, String PUBLICKEY) {
try {
data = Base64.encodeBase64String(encryptByPublicKey(data.getBytes(), PUBLICKEY));
} catch (Exception e) {
}
return data;
}
/**
* java端私钥解密
*/
public static String decryptDataOnJava(String data, String PRIVATEKEY) {
String temp = "";
try {
byte[] rs = Base64.decodeBase64(data);
temp = new String(RSAUtils.decryptByPrivateKey(rs, PRIVATEKEY),"UTF-8");
} catch (Exception e) {
}
return temp;
}
public static void main(String[] args) throws Exception{
}
}

View File

@ -11,6 +11,7 @@ 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 com.ruoyi.common.core.utils.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddressList;
@ -99,15 +100,16 @@ public class ExcelDownHandler implements SheetWriteHandler {
ExcelDictFormat format = field.getDeclaredAnnotation(ExcelDictFormat.class);
String dictType = format.dictType();
String converterExp = format.readConverterExp();
if (StrUtil.isNotBlank(dictType)) {
if (StringUtils.isNotBlank(dictType)) {
// 如果传递了字典名则依据字典建立下拉
Collection<String> values = Optional.ofNullable(dictService.getAllDictByDictType(dictType))
.orElseThrow(() -> new ServiceException(String.format("字典 %s 不存在", dictType)))
.values();
options = new ArrayList<>(values);
} else if (StrUtil.isNotBlank(converterExp)) {
} else if (StringUtils.isNotBlank(converterExp)) {
// 如果指定了确切的值则直接解析确切的值
options = StrUtil.split(converterExp, format.separator(), true, true);
List<String> strList = StringUtils.splitList(converterExp, format.separator());
options = StreamUtils.toList(strList, s -> StringUtils.split(s, "=")[1]);
}
} else if (field.isAnnotationPresent(ExcelEnumFormat.class)) {
// 否则如果指定了@ExcelEnumFormat则使用枚举的逻辑

View File

@ -38,6 +38,20 @@
<artifactId>powerjob-official-processors</artifactId>
</dependency>
<!-- EasyRetry client -->
<dependency>
<groupId>com.aizuda</groupId>
<artifactId>easy-retry-client-starter</artifactId>
</dependency>
<dependency>
<groupId>com.aizuda</groupId>
<artifactId>easy-retry-client-core</artifactId>
</dependency>
<dependency>
<groupId>com.aizuda</groupId>
<artifactId>easy-retry-client-job-core</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>

View File

@ -0,0 +1,37 @@
package com.ruoyi.common.job.config;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.spi.ILoggingEvent;
import com.aizuda.easy.retry.client.common.appender.EasyRetryLogbackAppender;
import com.aizuda.easy.retry.client.common.event.EasyRetryStartingEvent;
import com.aizuda.easy.retry.client.starter.EnableEasyRetry;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.EnableScheduling;
/**
* 启动定时任务
*
* @author dhb52
* @since 2024/3/12
*/
@AutoConfiguration
@ConditionalOnProperty(prefix = "easy-retry", name = "enabled", havingValue = "true")
@EnableScheduling
@EnableEasyRetry(group = "${easy-retry.group-name}")
public class EasyRetryConfig {
@EventListener(EasyRetryStartingEvent.class)
public void onStarting(EasyRetryStartingEvent event) {
LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
EasyRetryLogbackAppender<ILoggingEvent> ca = new EasyRetryLogbackAppender<>();
ca.setName("easy_log_appender");
ca.start();
Logger rootLogger = lc.getLogger(Logger.ROOT_LOGGER_NAME);
rootLogger.addAppender(ca);
}
}

View File

@ -0,0 +1,2 @@
com.ruoyi.common.job.config.PowerJobConfig
com.ruoyi.common.job.config.EasyRetryConfig

View File

@ -27,11 +27,6 @@
<artifactId>ruoyi-common-json</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -204,7 +204,7 @@ public class LogAspect {
public boolean isFilterObject(final Object o) {
Class<?> clazz = o.getClass();
if (clazz.isArray()) {
return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
return MultipartFile.class.isAssignableFrom(clazz.getComponentType());
} else if (Collection.class.isAssignableFrom(clazz)) {
Collection collection = (Collection) o;
for (Object value : collection) {

View File

@ -21,6 +21,11 @@
<artifactId>ruoyi-common-core</artifactId>
</dependency>
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-common-excel</artifactId>
</dependency>
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-common-security</artifactId>

View File

@ -4,6 +4,8 @@ import com.github.pagehelper.PageInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Properties;
/**
* Pagehelper分页兼用老项目
*
@ -14,6 +16,11 @@ public class PagehelperConfig {
@Bean
public PageInterceptor pageInterceptor(){
PageInterceptor pageInterceptor = new PageInterceptor();
Properties properties = new Properties();
properties.setProperty("supportMethodsArguments","true");
properties.setProperty("autoRuntimeDialect","true");
pageInterceptor.setProperties(properties);
return pageInterceptor;
}
}

View File

@ -1,6 +1,8 @@
package com.ruoyi.common.orm.core.domain;
import com.alibaba.excel.annotation.ExcelProperty;
import com.mybatisflex.annotation.Column;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.io.Serial;
@ -19,18 +21,21 @@ public class TreeEntity extends BaseEntity
@Serial
private static final long serialVersionUID = 1L;
/** 父菜单名称 */
/** 父名称 */
@Column(ignore = true)
private String parentName;
/** 父菜单ID */
/** 父亲ID */
@ExcelProperty(value = "上级编号")
@NotNull(message = "上级编号不能为空")
private Long parentId;
/** 显示顺序 */
@ExcelProperty(value = "显示顺序")
private Integer orderNum;
/** 祖级列表 */
@Column(ignore = true)
//@Column(ignore = true)
private String ancestors;
/** 子部门 */

View File

@ -1,6 +1,7 @@
package com.ruoyi.common.orm.handler;
import com.ruoyi.common.core.core.domain.R;
import com.ruoyi.common.core.utils.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.mybatis.spring.MyBatisSystemException;
import org.springframework.dao.DuplicateKeyException;
@ -35,7 +36,7 @@ public class MybatisExceptionHandler {
public R<Void> handleCannotFindDataSourceException(MyBatisSystemException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
String message = e.getMessage();
if ("CannotFindDataSourceException".contains(message)) {
if (StringUtils.contains("CannotFindDataSourceException", message)) {
log.error("请求地址'{}', 未找到数据源", requestURI);
return R.fail("未找到数据源,请联系管理员确认");
}

View File

@ -0,0 +1,183 @@
package com.ruoyi.common.orm.helper;
import lombok.extern.slf4j.Slf4j;
import java.util.function.Supplier;
/**
* 监听器管理
* <p>
* 考虑任务调度三方接口回调等可自由控制审计字段
*
* @author Ice
* @version 1.0
*/
@Slf4j
public class ListenerManager {
private ListenerManager() {}
private static ThreadLocal<Boolean> ignoreInsertListenerTl = ThreadLocal.withInitial(() -> Boolean.FALSE);
private static ThreadLocal<Boolean> ignoreUpdateListenerTl = ThreadLocal.withInitial(() -> Boolean.FALSE);
/**
* 设置 InsertListenerThreadLocal
* @param tl ThreadLocal
*/
public static synchronized void setInsertListenerTl(ThreadLocal<Boolean> tl) {
ignoreInsertListenerTl = tl;
}
/**
* 设置 UpdateListenerThreadLocal
* @param tl ThreadLocal
*/
public static synchronized void setUpdateListenerTl(ThreadLocal<Boolean> tl) {
ignoreUpdateListenerTl = tl;
}
/**
* 是否执行 InsertListener
* @return 是否执行
*/
public static boolean isDoInsertListener() {
return !ignoreInsertListenerTl.get();
}
/**
* 是否执行 UpdateListener
* @return 是否执行
*/
public static boolean isDoUpdateListener() {
return !ignoreUpdateListenerTl.get();
}
/**
* 忽略 Listener
*/
public static <T> T withoutListener(Supplier<T> supplier) {
try {
ignoreListener();
return supplier.get();
} finally {
restoreListener();
}
}
/**
* 忽略 Listener
*/
public static void withoutListener(Runnable runnable) {
try {
ignoreListener();
runnable.run();
} finally {
restoreListener();
}
}
/**
* 忽略 Listener
*/
public static void ignoreListener() {
ignoreInsertListenerTl.set(Boolean.TRUE);
ignoreUpdateListenerTl.set(Boolean.TRUE);
}
/**
* 恢复 Listener
*/
public static void restoreListener() {
ignoreInsertListenerTl.remove();
ignoreUpdateListenerTl.remove();
}
/**
* 忽略 InsertListener
*/
public static <T> T withoutInsertListener(Supplier<T> supplier) {
try {
ignoreInsertListener();
return supplier.get();
} finally {
restoreInsertListener();
}
}
/**
* 忽略 InsertListener
*/
public static void withoutInsertListener(Runnable runnable) {
try {
ignoreInsertListener();
runnable.run();
} finally {
restoreInsertListener();
}
}
/**
* 忽略 InsertListener
*/
public static void ignoreInsertListener() {
ignoreInsertListenerTl.set(Boolean.TRUE);
}
/**
* 恢复 InsertListener
*/
public static void restoreInsertListener() {
ignoreInsertListenerTl.remove();
}
/**
* 忽略 UpdateListener
*/
public static <T> T withoutUpdateListener(Supplier<T> supplier) {
try {
ignoreUpdateListener();
return supplier.get();
} finally {
restoreUpdateListener();
}
}
/**
* 忽略 UpdateListener
*/
public static void withoutUpdateListener(Runnable runnable) {
try {
ignoreUpdateListener();
runnable.run();
} finally {
restoreUpdateListener();
}
}
/**
* 忽略 UpdateListener
*/
public static void ignoreUpdateListener() {
ignoreUpdateListenerTl.set(Boolean.TRUE);
}
/**
* 恢复 UpdateListener
*/
public static void restoreUpdateListener() {
ignoreUpdateListenerTl.remove();
}
}

View File

@ -5,6 +5,7 @@ import cn.hutool.http.HttpStatus;
import com.mybatisflex.annotation.InsertListener;
import com.ruoyi.common.core.exception.ServiceException;
import com.ruoyi.common.orm.core.domain.BaseEntity;
import com.ruoyi.common.orm.helper.ListenerManager;
import com.ruoyi.common.security.utils.LoginHelper;
import java.util.Date;
@ -19,7 +20,7 @@ public class EntityInsertListener implements InsertListener {
@Override
public void onInsert(Object entity) {
try {
if (ObjectUtil.isNotNull(entity) && (entity instanceof BaseEntity)) {
if (ListenerManager.isDoInsertListener() && ObjectUtil.isNotNull(entity) && (entity instanceof BaseEntity)) {
BaseEntity baseEntity = (BaseEntity) entity;
Long loginUserId = LoginHelper.getUserId();

View File

@ -5,6 +5,7 @@ import cn.hutool.http.HttpStatus;
import com.mybatisflex.annotation.UpdateListener;
import com.ruoyi.common.core.exception.ServiceException;
import com.ruoyi.common.orm.core.domain.BaseEntity;
import com.ruoyi.common.orm.helper.ListenerManager;
import com.ruoyi.common.security.utils.LoginHelper;
import java.util.Date;
@ -18,7 +19,7 @@ public class EntityUpdateListener implements UpdateListener {
@Override
public void onUpdate(Object entity) {
try {
if (ObjectUtil.isNotNull(entity) && (entity instanceof BaseEntity)) {
if (ListenerManager.isDoUpdateListener() && ObjectUtil.isNotNull(entity) && (entity instanceof BaseEntity)) {
BaseEntity baseEntity = (BaseEntity) entity;
baseEntity.setUpdateBy(LoginHelper.getUserId());
baseEntity.setUpdateTime(new Date());

View File

@ -31,9 +31,44 @@
<artifactId>ruoyi-common-redis</artifactId>
</dependency>
<!-- AWS SDK for Java 2.x -->
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-s3</artifactId>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
<exclusions>
<!-- 将基于 Netty 的 HTTP 客户端从类路径中移除 -->
<exclusion>
<groupId>software.amazon.awssdk</groupId>
<artifactId>netty-nio-client</artifactId>
</exclusion>
<!-- 将基于 CRT 的 HTTP 客户端从类路径中移除 -->
<exclusion>
<groupId>software.amazon.awssdk</groupId>
<artifactId>aws-crt-client</artifactId>
</exclusion>
<!-- 将基于 Apache 的 HTTP 客户端从类路径中移除 -->
<exclusion>
<groupId>software.amazon.awssdk</groupId>
<artifactId>apache-client</artifactId>
</exclusion>
<!-- 将配置基于 URL 连接的 HTTP 客户端从类路径中移除 -->
<exclusion>
<groupId>software.amazon.awssdk</groupId>
<artifactId>url-connection-client</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 使用AWS基于 CRT 的 S3 客户端 -->
<dependency>
<groupId>software.amazon.awssdk.crt</groupId>
<artifactId>aws-crt</artifactId>
</dependency>
<!-- 基于 AWS CRT 的 S3 客户端的性能增强的 S3 传输管理器 -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3-transfer-manager</artifactId>
</dependency>
</dependencies>

View File

@ -2,73 +2,117 @@ package com.ruoyi.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 com.ruoyi.common.core.constant.Constants;
import com.ruoyi.common.core.utils.DateUtils;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.core.utils.file.FileUtils;
import com.ruoyi.common.oss.constant.OssConstant;
import com.ruoyi.common.oss.entity.UploadResult;
import com.ruoyi.common.oss.enumd.AccessPolicyType;
import com.ruoyi.common.oss.enumd.PolicyType;
import com.ruoyi.common.oss.exception.OssException;
import com.ruoyi.common.oss.properties.OssProperties;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.core.async.AsyncRequestBody;
import software.amazon.awssdk.core.async.BlockingInputStreamAsyncRequestBody;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3AsyncClient;
import software.amazon.awssdk.services.s3.S3Configuration;
import software.amazon.awssdk.services.s3.model.NoSuchBucketException;
import software.amazon.awssdk.services.s3.model.S3Exception;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
import software.amazon.awssdk.transfer.s3.S3TransferManager;
import software.amazon.awssdk.transfer.s3.model.*;
import software.amazon.awssdk.transfer.s3.progress.LoggingTransferListener;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URL;
import java.util.Date;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
/**
* S3 存储协议 所有兼容S3协议的云厂商均支持
* 阿里云 腾讯云 七牛云 minio
*
* @author Lion Li
* @author AprilWind
*/
public class OssClient {
/**
* 服务商
*/
private final String configKey;
/**
* 配置属性
*/
private final OssProperties properties;
private final AmazonS3 client;
/**
* Amazon S3 异步客户端
*/
private final S3AsyncClient client;
/**
* 用于管理 S3 数据传输的高级工具
*/
private final S3TransferManager transferManager;
/**
* AWS S3 预签名 URL 的生成器
*/
private final S3Presigner presigner;
/**
* 构造方法
*
* @param configKey 配置键
* @param ossProperties Oss配置属性
*/
public OssClient(String configKey, OssProperties ossProperties) {
this.configKey = configKey;
this.properties = ossProperties;
try {
AwsClientBuilder.EndpointConfiguration endpointConfig =
new AwsClientBuilder.EndpointConfiguration(properties.getEndpoint(), properties.getRegion());
// 创建 AWS 认证信息
StaticCredentialsProvider credentialsProvider = StaticCredentialsProvider.create(
AwsBasicCredentials.create(properties.getAccessKey(), properties.getSecretKey()));
AWSCredentials credentials = new BasicAWSCredentials(properties.getAccessKey(), properties.getSecretKey());
AWSCredentialsProvider credentialsProvider = new AWSStaticCredentialsProvider(credentials);
ClientConfiguration clientConfig = new ClientConfiguration();
if (OssConstant.IS_HTTPS.equals(properties.getIsHttps())) {
clientConfig.setProtocol(Protocol.HTTPS);
} else {
clientConfig.setProtocol(Protocol.HTTP);
}
AmazonS3ClientBuilder build = AmazonS3Client.builder()
.withEndpointConfiguration(endpointConfig)
.withClientConfiguration(clientConfig)
.withCredentials(credentialsProvider)
.disableChunkedEncoding();
if (!StringUtils.containsAny(properties.getEndpoint(), OssConstant.CLOUD_SERVICE)) {
// minio 使用https限制使用域名访问 需要此配置 站点填域名
build.enablePathStyleAccess();
}
this.client = build.build();
//MinIO 使用 HTTPS 限制使用域名访问站点填域名需要启用路径样式访问
boolean isStyle = !StringUtils.containsAny(properties.getEndpoint(), OssConstant.CLOUD_SERVICE);
//创建AWS基于 CRT S3 客户端
this.client = S3AsyncClient.crtBuilder()
.credentialsProvider(credentialsProvider)
.endpointOverride(URI.create(getEndpoint()))
.region(of())
.targetThroughputInGbps(20.0)
.minimumPartSizeInBytes(10 * 1025 * 1024L)
.checksumValidationEnabled(false)
.forcePathStyle(isStyle)
.build();
//AWS基于 CRT S3 AsyncClient 实例用作 S3 传输管理器的底层客户端
this.transferManager = S3TransferManager.builder().s3Client(this.client).build();
// 创建 S3 配置对象
S3Configuration config = S3Configuration.builder().chunkedEncodingEnabled(false)
.pathStyleAccessEnabled(isStyle).build();
// 创建 预签名 URL 的生成器 实例用于生成 S3 预签名 URL
this.presigner = S3Presigner.builder()
.region(of())
.credentialsProvider(credentialsProvider)
.endpointOverride(URI.create(getDomain()))
.serviceConfiguration(config)
.build();
// 创建存储桶
createBucket();
} catch (Exception e) {
if (e instanceof OssException) {
@ -78,126 +122,158 @@ public class OssClient {
}
}
/**
* 同步创建存储桶
* 如果存储桶不存在会进行创建如果存储桶存在不执行任何操作
*
* @throws OssException 当创建存储桶时发生异常时抛出
*/
public void createBucket() {
String bucketName = properties.getBucketName();
try {
String bucketName = properties.getBucketName();
if (client.doesBucketExistV2(bucketName)) {
return;
// 尝试获取存储桶的信息
client.headBucket(
x -> x.bucket(bucketName)
.build())
.join();
} catch (Exception ex) {
if (ex.getCause() instanceof NoSuchBucketException) {
try {
// 存储桶不存在尝试创建存储桶
client.createBucket(
x -> x.bucket(bucketName))
.join();
// 设置存储桶的访问策略Bucket Policy
client.putBucketPolicy(
x -> x.bucket(bucketName)
.policy(getPolicy(bucketName, getAccessPolicy().getPolicyType())))
.join();
} catch (S3Exception e) {
// 存储桶创建或策略设置失败
throw new OssException("创建Bucket失败, 请核对配置信息:[" + e.getMessage() + "]");
}
} else {
throw new OssException("判断Bucket是否存在失败请核对配置信息:[" + ex.getMessage() + "]");
}
CreateBucketRequest createBucketRequest = new CreateBucketRequest(bucketName);
AccessPolicyType accessPolicy = getAccessPolicy();
createBucketRequest.setCannedAcl(accessPolicy.getAcl());
client.createBucket(createBucketRequest);
client.setBucketPolicy(bucketName, getPolicy(bucketName, accessPolicy.getPolicyType()));
} catch (Exception e) {
throw new OssException("创建Bucket失败, 请核对配置信息:[" + e.getMessage() + "]");
}
}
public UploadResult upload(byte[] data, String path, String contentType) {
return upload(new ByteArrayInputStream(data), path, contentType);
/**
* 上传文件到 Amazon S3并返回上传结果
*
* @param filePath 本地文件路径
* @param key Amazon S3 中的对象键
* @param md5Digest 本地文件的 MD5 哈希值可选
* @return UploadResult 包含上传后的文件信息
* @throws OssException 如果上传失败抛出自定义异常
*/
public UploadResult upload(Path filePath, String key, String md5Digest) {
try {
// 构建上传请求对象
FileUpload fileUpload = transferManager.uploadFile(
x -> x.putObjectRequest(
y -> y.bucket(properties.getBucketName())
.key(key)
.contentMD5(StringUtils.isNotEmpty(md5Digest) ? md5Digest : null)
.build())
.addTransferListener(LoggingTransferListener.create())
.source(filePath).build());
// 等待上传完成并获取上传结果
CompletedFileUpload uploadResult = fileUpload.completionFuture().join();
String eTag = uploadResult.response().eTag();
// 提取上传结果中的 ETag并构建一个自定义的 UploadResult 对象
return UploadResult.builder().url(getUrl() + StringUtils.SLASH + key).filename(key).eTag(eTag).build();
} catch (Exception e) {
// 捕获异常并抛出自定义异常
throw new OssException("上传文件失败,请检查配置信息:[" + e.getMessage() + "]");
} finally {
// 无论上传是否成功最终都会删除临时文件
FileUtils.del(filePath);
}
}
public UploadResult upload(InputStream inputStream, String path, String contentType) {
/**
* 上传 InputStream Amazon S3
*
* @param inputStream 要上传的输入流
* @param key Amazon S3 中的对象键
* @param length 输入流的长度
* @return UploadResult 包含上传后的文件信息
* @throws OssException 如果上传失败抛出自定义异常
*/
public UploadResult upload(InputStream inputStream, String key, Long length) {
// 如果输入流不是 ByteArrayInputStream则将其读取为字节数组再创建 ByteArrayInputStream
if (!(inputStream instanceof ByteArrayInputStream)) {
inputStream = new ByteArrayInputStream(IoUtil.readBytes(inputStream));
}
try {
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentType(contentType);
metadata.setContentLength(inputStream.available());
PutObjectRequest putObjectRequest = new PutObjectRequest(properties.getBucketName(), path, inputStream, metadata);
// 设置上传对象的 Acl 为公共读
putObjectRequest.setCannedAcl(getAccessPolicy().getAcl());
client.putObject(putObjectRequest);
// 创建异步请求体length如果为空会报错
BlockingInputStreamAsyncRequestBody body = AsyncRequestBody.forBlockingInputStream(length);
// 使用 transferManager 进行上传
Upload upload = transferManager.upload(
x -> x.requestBody(body)
.putObjectRequest(
y -> y.bucket(properties.getBucketName())
.key(key)
.build())
.build());
// 将输入流写入请求体
body.writeInputStream(inputStream);
// 等待文件上传操作完成
CompletedUpload uploadResult = upload.completionFuture().join();
String eTag = uploadResult.response().eTag();
// 提取上传结果中的 ETag并构建一个自定义的 UploadResult 对象
return UploadResult.builder().url(getUrl() + StringUtils.SLASH + key).filename(key).eTag(eTag).build();
} catch (Exception e) {
throw new OssException("上传文件失败,请检查配置信息:[" + e.getMessage() + "]");
}
return UploadResult.builder().url(getUrl() + "/" + path).filename(path).build();
}
public UploadResult upload(File file, String path) {
try {
PutObjectRequest putObjectRequest = new PutObjectRequest(properties.getBucketName(), path, file);
// 设置上传对象的 Acl 为公共读
putObjectRequest.setCannedAcl(getAccessPolicy().getAcl());
client.putObject(putObjectRequest);
} catch (Exception e) {
throw new OssException("上传文件失败,请检查配置信息:[" + e.getMessage() + "]");
}
return UploadResult.builder().url(getUrl() + "/" + path).filename(path).build();
}
public void delete(String path) {
path = path.replace(getUrl() + "/", "");
try {
client.deleteObject(properties.getBucketName(), path);
} catch (Exception e) {
throw new OssException("删除文件失败,请检查配置信息:[" + e.getMessage() + "]");
}
}
public UploadResult uploadSuffix(byte[] data, String suffix, String contentType) {
return upload(data, getPath(properties.getPrefix(), suffix), contentType);
}
public UploadResult uploadSuffix(InputStream inputStream, String suffix, String contentType) {
return upload(inputStream, getPath(properties.getPrefix(), suffix), contentType);
}
public UploadResult uploadSuffix(File file, String suffix) {
return upload(file, getPath(properties.getPrefix(), suffix));
}
/**
* 获取文件元数据
* 下载文件从 Amazon S3 到临时目录
*
* @param path 完整文件路径
* @param path 文件在 Amazon S3 中的对象键
* @return 下载后的文件在本地的临时路径
* @throws OssException 如果下载失败抛出自定义异常
*/
public ObjectMetadata getObjectMetadata(String path) {
path = path.replace(getUrl() + "/", "");
S3Object object = client.getObject(properties.getBucketName(), path);
return object.getObjectMetadata();
public Path fileDownload(String path) {
// 构建临时文件
Path tempFilePath = FileUtils.createTempFile().toPath();
// 使用 S3TransferManager 下载文件
FileDownload downloadFile = transferManager.downloadFile(
x -> x.getObjectRequest(
y -> y.bucket(properties.getBucketName())
.key(removeBaseUrl(path))
.build())
.addTransferListener(LoggingTransferListener.create())
.destination(tempFilePath)
.build());
// 等待文件下载操作完成
downloadFile.completionFuture().join();
return tempFilePath;
}
public InputStream getObjectContent(String path) {
path = path.replace(getUrl() + "/", "");
S3Object object = client.getObject(properties.getBucketName(), path);
return object.getObjectContent();
}
public String getUrl() {
String domain = properties.getDomain();
String endpoint = properties.getEndpoint();
String header = OssConstant.IS_HTTPS.equals(properties.getIsHttps()) ? "https://" : "http://";
// 云服务商直接返回
if (StringUtils.containsAny(endpoint, OssConstant.CLOUD_SERVICE)) {
if (StringUtils.isNotBlank(domain)) {
return header + domain;
}
return header + properties.getBucketName() + "." + endpoint;
/**
* 删除云存储服务中指定路径下文件
*
* @param path 指定路径
*/
public void delete(String path) {
try {
client.deleteObject(
x -> x.bucket(properties.getBucketName())
.key(removeBaseUrl(path))
.build());
} catch (Exception e) {
throw new OssException("删除文件失败,请检查配置信息:[" + e.getMessage() + "]");
}
// minio 单独处理
if (StringUtils.isNotBlank(domain)) {
return header + domain + "/" + properties.getBucketName();
}
return header + endpoint + "/" + properties.getBucketName();
}
public String getPath(String prefix, String suffix) {
// 生成uuid
String uuid = IdUtil.fastSimpleUUID();
// 文件路径
String path = DateUtils.datePath() + "/" + uuid;
if (StringUtils.isNotBlank(prefix)) {
path = prefix + "/" + path;
}
return path + suffix;
}
public String getConfigKey() {
return configKey;
}
/**
@ -207,14 +283,189 @@ public class OssClient {
* @param second 授权时间
*/
public String getPrivateUrl(String objectKey, Integer second) {
GeneratePresignedUrlRequest generatePresignedUrlRequest =
new GeneratePresignedUrlRequest(properties.getBucketName(), objectKey)
.withMethod(HttpMethod.GET)
.withExpiration(new Date(System.currentTimeMillis() + 1000L * second));
URL url = client.generatePresignedUrl(generatePresignedUrlRequest);
// 使用 AWS S3 预签名 URL 的生成器 获取对象的预签名 URL
URL url = presigner.presignGetObject(
x -> x.signatureDuration(Duration.ofSeconds(second))
.getObjectRequest(
y -> y.bucket(properties.getBucketName())
.key(objectKey)
.build())
.build())
.url();
return url.toString();
}
/**
* 上传 byte[] 数据到 Amazon S3使用指定的后缀构造对象键
*
* @param data 要上传的 byte[] 数据
* @param suffix 对象键的后缀
* @return UploadResult 包含上传后的文件信息
* @throws OssException 如果上传失败抛出自定义异常
*/
public UploadResult uploadSuffix(byte[] data, String suffix) {
return upload(new ByteArrayInputStream(data), getPath(properties.getPrefix(), suffix), Long.valueOf(data.length));
}
/**
* 上传 InputStream Amazon S3使用指定的后缀构造对象键
*
* @param inputStream 要上传的输入流
* @param suffix 对象键的后缀
* @param length 输入流的长度
* @return UploadResult 包含上传后的文件信息
* @throws OssException 如果上传失败抛出自定义异常
*/
public UploadResult uploadSuffix(InputStream inputStream, String suffix, Long length) {
return upload(inputStream, getPath(properties.getPrefix(), suffix), length);
}
/**
* 上传文件到 Amazon S3使用指定的后缀构造对象键
*
* @param file 要上传的文件
* @param suffix 对象键的后缀
* @return UploadResult 包含上传后的文件信息
* @throws OssException 如果上传失败抛出自定义异常
*/
public UploadResult uploadSuffix(File file, String suffix) {
return upload(file.toPath(), getPath(properties.getPrefix(), suffix), null);
}
/**
* 获取文件输入流
*
* @param path 完整文件路径
* @return 输入流
*/
public InputStream getObjectContent(String path) throws IOException {
// 下载文件到临时目录
Path tempFilePath = fileDownload(path);
// 创建输入流
InputStream inputStream = Files.newInputStream(tempFilePath);
// 删除临时文件
FileUtils.del(tempFilePath);
// 返回对象内容的输入流
return inputStream;
}
/**
* 获取 S3 客户端的终端点 URL
*
* @return 终端点 URL
*/
public String getEndpoint() {
// 根据配置文件中的是否使用 HTTPS设置协议头部
String header = getIsHttps();
// 拼接协议头部和终端点得到完整的终端点 URL
return header + properties.getEndpoint();
}
/**
* 获取 S3 客户端的终端点 URL自定义域名
*
* @return 终端点 URL
*/
public String getDomain() {
// 从配置中获取域名终端点是否使用 HTTPS 等信息
String domain = properties.getDomain();
String endpoint = properties.getEndpoint();
String header = getIsHttps();
// 如果是云服务商直接返回域名或终端点
if (StringUtils.containsAny(endpoint, OssConstant.CLOUD_SERVICE)) {
return StringUtils.isNotEmpty(domain) ? header + domain : header + endpoint;
}
// 如果是 MinIO处理域名并返回
if (StringUtils.isNotEmpty(domain)) {
return domain.startsWith(Constants.HTTPS) || domain.startsWith(Constants.HTTP) ? domain : header + domain;
}
// 返回终端点
return header + endpoint;
}
/**
* 根据传入的 region 参数返回相应的 AWS 区域
* 如果 region 参数非空使用 Region.of 方法创建并返回对应的 AWS 区域对象
* 如果 region 参数为空返回一个默认的 AWS 区域例如us-east-1作为广泛支持的区域
*
* @return 对应的 AWS 区域对象或者默认的广泛支持的区域us-east-1
*/
public Region of() {
//AWS 区域字符串
String region = properties.getRegion();
// 如果 region 参数非空使用 Region.of 方法创建对应的 AWS 区域对象否则返回默认区域
return StringUtils.isNotEmpty(region) ? Region.of(region) : Region.US_EAST_1;
}
/**
* 获取云存储服务的URL
*
* @return 文件路径
*/
public String getUrl() {
String domain = properties.getDomain();
String endpoint = properties.getEndpoint();
String header = getIsHttps();
// 云服务商直接返回
if (StringUtils.containsAny(endpoint, OssConstant.CLOUD_SERVICE)) {
return header + (StringUtils.isNotEmpty(domain) ? domain : properties.getBucketName() + "." + endpoint);
}
// MinIO 单独处理
if (StringUtils.isNotEmpty(domain)) {
// 如果 domain "https://" "http://" 开头
return (domain.startsWith(Constants.HTTPS) || domain.startsWith(Constants.HTTP)) ?
domain + StringUtils.SLASH + properties.getBucketName() : header + domain + StringUtils.SLASH + properties.getBucketName();
}
return header + endpoint + StringUtils.SLASH + properties.getBucketName();
}
/**
* 生成一个符合特定规则的唯一的文件路径通过使用日期UUID前缀和后缀等元素的组合确保了文件路径的独一无二性
*
* @param prefix 前缀
* @param suffix 后缀
* @return 文件路径
*/
public String getPath(String prefix, String suffix) {
// 生成uuid
String uuid = IdUtil.fastSimpleUUID();
// 生成日期路径
String datePath = DateUtils.datePath();
// 拼接路径
String path = StringUtils.isNotEmpty(prefix) ?
prefix + StringUtils.SLASH + datePath + StringUtils.SLASH + uuid : datePath + StringUtils.SLASH + uuid;
return path + suffix;
}
/**
* 移除路径中的基础URL部分得到相对路径
*
* @param path 完整的路径包括基础URL和相对路径
* @return 去除基础URL后的相对路径
*/
public String removeBaseUrl(String path) {
return path.replace(getUrl() + StringUtils.SLASH, "");
}
/**
* 服务商
*/
public String getConfigKey() {
return configKey;
}
/**
* 获取是否使用 HTTPS 的配置并返回相应的协议头部
*
* @return 协议头部根据是否使用 HTTPS 返回 "https://" "http://"
*/
public String getIsHttps() {
return OssConstant.IS_HTTPS.equals(properties.getIsHttps()) ? Constants.HTTPS : Constants.HTTP;
}
/**
* 检查配置是否相同
*/
@ -231,32 +482,77 @@ public class OssClient {
return AccessPolicyType.getByType(properties.getAccessPolicy());
}
/**
* 生成 AWS S3 存储桶访问策略
*
* @param bucketName 存储桶
* @param policyType 桶策略类型
* @return 符合 AWS S3 存储桶访问策略格式的字符串
*/
private static String getPolicy(String bucketName, PolicyType policyType) {
StringBuilder builder = new StringBuilder();
builder.append("{\n\"Statement\": [\n{\n\"Action\": [\n");
builder.append(switch (policyType) {
case WRITE -> "\"s3:GetBucketLocation\",\n\"s3:ListBucketMultipartUploads\"\n";
case READ_WRITE -> "\"s3:GetBucketLocation\",\n\"s3:ListBucket\",\n\"s3:ListBucketMultipartUploads\"\n";
default -> "\"s3:GetBucketLocation\"\n";
});
builder.append("],\n\"Effect\": \"Allow\",\n\"Principal\": \"*\",\n\"Resource\": \"arn:aws:s3:::");
builder.append(bucketName);
builder.append("\"\n},\n");
if (policyType == PolicyType.READ) {
builder.append("{\n\"Action\": [\n\"s3:ListBucket\"\n],\n\"Effect\": \"Deny\",\n\"Principal\": \"*\",\n\"Resource\": \"arn:aws:s3:::");
builder.append(bucketName);
builder.append("\"\n},\n");
}
builder.append("{\n\"Action\": ");
builder.append(switch (policyType) {
case WRITE -> "[\n\"s3:AbortMultipartUpload\",\n\"s3:DeleteObject\",\n\"s3:ListMultipartUploadParts\",\n\"s3:PutObject\"\n],\n";
case READ_WRITE -> "[\n\"s3:AbortMultipartUpload\",\n\"s3:DeleteObject\",\n\"s3:GetObject\",\n\"s3:ListMultipartUploadParts\",\n\"s3:PutObject\"\n],\n";
default -> "\"s3:GetObject\",\n";
});
builder.append("\"Effect\": \"Allow\",\n\"Principal\": \"*\",\n\"Resource\": \"arn:aws:s3:::");
builder.append(bucketName);
builder.append("/*\"\n}\n],\n\"Version\": \"2012-10-17\"\n}\n");
return builder.toString();
String policy = switch (policyType) {
case WRITE -> """
{
"Version": "2012-10-17",
"Statement": []
}
""";
case READ_WRITE -> """
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": "*",
"Action": [
"s3:GetBucketLocation",
"s3:ListBucket",
"s3:ListBucketMultipartUploads"
],
"Resource": "arn:aws:s3:::bucketName"
},
{
"Effect": "Allow",
"Principal": "*",
"Action": [
"s3:AbortMultipartUpload",
"s3:DeleteObject",
"s3:GetObject",
"s3:ListMultipartUploadParts",
"s3:PutObject"
],
"Resource": "arn:aws:s3:::bucketName/*"
}
]
}
""";
case READ -> """
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": "*",
"Action": ["s3:GetBucketLocation"],
"Resource": "arn:aws:s3:::bucketName"
},
{
"Effect": "Deny",
"Principal": "*",
"Action": ["s3:ListBucket"],
"Resource": "arn:aws:s3:::bucketName"
},
{
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::bucketName/*"
}
]
}
""";
};
return policy.replaceAll("bucketName", bucketName);
}
}

View File

@ -21,4 +21,9 @@ public class UploadResult {
* 文件名
*/
private String filename;
/**
* 已上传对象的实体标记用来校验文件
*/
private String eTag;
}

View File

@ -1,8 +1,9 @@
package com.ruoyi.common.oss.enumd;
import com.amazonaws.services.s3.model.CannedAccessControlList;
import lombok.AllArgsConstructor;
import lombok.Getter;
import software.amazon.awssdk.services.s3.model.BucketCannedACL;
import software.amazon.awssdk.services.s3.model.ObjectCannedACL;
/**
* 桶访问策略配置
@ -16,27 +17,32 @@ public enum AccessPolicyType {
/**
* private
*/
PRIVATE("0", CannedAccessControlList.Private, PolicyType.WRITE),
PRIVATE("0", BucketCannedACL.PRIVATE, ObjectCannedACL.PRIVATE, PolicyType.WRITE),
/**
* public
*/
PUBLIC("1", CannedAccessControlList.PublicRead, PolicyType.READ),
PUBLIC("1", BucketCannedACL.PUBLIC_READ_WRITE, ObjectCannedACL.PUBLIC_READ_WRITE, PolicyType.READ_WRITE),
/**
* custom
*/
CUSTOM("2",CannedAccessControlList.PublicRead, PolicyType.READ);
CUSTOM("2", BucketCannedACL.PUBLIC_READ, ObjectCannedACL.PUBLIC_READ, PolicyType.READ);
/**
* 权限类型
* 权限类型数据库值
*/
private final String type;
/**
* 权限类型
*/
private final BucketCannedACL bucketCannedACL;
/**
* 文件对象 权限类型
*/
private final CannedAccessControlList acl;
private final ObjectCannedACL objectCannedACL;
/**
* 桶策略类型

View File

@ -13,6 +13,7 @@ import lombok.extern.slf4j.Slf4j;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;
/**
* 文件上传Factory
@ -23,6 +24,7 @@ import java.util.concurrent.ConcurrentHashMap;
public class OssFactory {
private static final Map<String, OssClient> CLIENT_CACHE = new ConcurrentHashMap<>();
private static final ReentrantLock LOCK = new ReentrantLock();
/**
* 获取默认实例
@ -39,7 +41,7 @@ public class OssFactory {
/**
* 根据类型获取实例
*/
public static synchronized OssClient instance(String configKey) {
public static OssClient instance(String configKey) {
String json = CacheUtils.get(CacheNames.SYS_OSS_CONFIG, configKey);
if (json == null) {
throw new OssException("系统异常, '" + configKey + "'配置信息不存在!");
@ -48,16 +50,19 @@ public class OssFactory {
// 使用租户标识避免多个租户相同key实例覆盖
String key = properties.getTenantId() + ":" + configKey;
OssClient client = CLIENT_CACHE.get(key);
if (client == null) {
CLIENT_CACHE.put(key, new OssClient(configKey, properties));
log.info("创建OSS实例 key => {}", configKey);
return CLIENT_CACHE.get(key);
}
// 配置不相同则重新构建
if (!client.checkPropertiesSame(properties)) {
CLIENT_CACHE.put(key, new OssClient(configKey, properties));
log.info("重载OSS实例 key => {}", configKey);
return CLIENT_CACHE.get(key);
// 客户端不存在或配置不相同则重新构建
if (client == null || !client.checkPropertiesSame(properties)) {
LOCK.lock();
try {
client = CLIENT_CACHE.get(key);
if (client == null || !client.checkPropertiesSame(properties)) {
CLIENT_CACHE.put(key, new OssClient(configKey, properties));
log.info("创建OSS实例 key => {}", configKey);
return CLIENT_CACHE.get(key);
}
} finally {
LOCK.unlock();
}
}
return client;
}

View File

@ -1,29 +1,29 @@
package com.ruoyi.common.ratelimiter.aspectj;
import cn.hutool.core.util.ArrayUtil;
import com.ruoyi.common.core.constant.GlobalConstants;
import com.ruoyi.common.core.exception.ServiceException;
import com.ruoyi.common.core.utils.MessageUtils;
import com.ruoyi.common.core.utils.ServletUtils;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.ratelimiter.annotation.RateLimiter;
import com.ruoyi.common.ratelimiter.enums.LimitType;
import com.ruoyi.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 com.ruoyi.common.core.constant.GlobalConstants;
import com.ruoyi.common.core.exception.ServiceException;
import com.ruoyi.common.core.utils.MessageUtils;
import com.ruoyi.common.core.utils.ServletUtils;
import com.ruoyi.common.core.utils.SpringUtils;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.ratelimiter.annotation.RateLimiter;
import com.ruoyi.common.ratelimiter.enums.LimitType;
import com.ruoyi.common.redis.utils.RedisUtils;
import org.redisson.api.RateType;
import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.context.expression.MethodBasedEvaluationContext;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.ParserContext;
import org.springframework.expression.common.TemplateParserContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import java.lang.reflect.Method;
@ -44,21 +44,18 @@ public class RateLimiterAspect {
* 定义spel解析模版
*/
private final ParserContext parserContext = new TemplateParserContext();
/**
* 定义spel上下文对象进行解析
*/
private final EvaluationContext context = new StandardEvaluationContext();
/**
* 方法参数解析器
*/
private final ParameterNameDiscoverer pnd = new DefaultParameterNameDiscoverer();
@Before("@annotation(rateLimiter)")
public void doBefore(JoinPoint point, RateLimiter rateLimiter) throws Throwable {
public void doBefore(JoinPoint point, RateLimiter rateLimiter) {
int time = rateLimiter.time();
int count = rateLimiter.count();
String combineKey = getCombineKey(rateLimiter, point);
try {
String combineKey = getCombineKey(rateLimiter, point);
RateType rateType = RateType.OVERALL;
if (rateLimiter.limitType() == LimitType.CLUSTER) {
rateType = RateType.PER_CLIENT;
@ -76,42 +73,29 @@ public class RateLimiterAspect {
if (e instanceof ServiceException) {
throw e;
} else {
throw new RuntimeException("服务器限流异常,请稍候再试");
throw new RuntimeException("服务器限流异常,请稍候再试", e);
}
}
}
public String getCombineKey(RateLimiter rateLimiter, JoinPoint point) {
private String getCombineKey(RateLimiter rateLimiter, JoinPoint point) {
String key = rateLimiter.key();
// 获取方法(通过方法签名来获取)
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
Class<?> targetClass = method.getDeclaringClass();
// 判断是否是spel格式
if (StringUtils.containsAny(key, "#")) {
// 获取参数值
if (StringUtils.isNotBlank(key)) {
MethodSignature signature = (MethodSignature) point.getSignature();
Method targetMethod = signature.getMethod();
Object[] args = point.getArgs();
// 获取方法上参数的名称
String[] parameterNames = pnd.getParameterNames(method);
if (ArrayUtil.isEmpty(parameterNames)) {
throw new ServiceException("限流key解析异常!请联系管理员!");
}
for (int i = 0; i < parameterNames.length; i++) {
context.setVariable(parameterNames[i], args[i]);
}
// 解析返回给key
try {
Expression expression;
if (StringUtils.startsWith(key, parserContext.getExpressionPrefix())
&& StringUtils.endsWith(key, parserContext.getExpressionSuffix())) {
expression = parser.parseExpression(key, parserContext);
} else {
expression = parser.parseExpression(key);
}
key = expression.getValue(context, String.class) + ":";
} catch (Exception e) {
throw new ServiceException("限流key解析异常!请联系管理员!");
//noinspection DataFlowIssue
MethodBasedEvaluationContext context =
new MethodBasedEvaluationContext(null, targetMethod, args, pnd);
context.setBeanResolver(new BeanFactoryResolver(SpringUtils.getBeanFactory()));
Expression expression;
if (StringUtils.startsWith(key, parserContext.getExpressionPrefix())
&& StringUtils.endsWith(key, parserContext.getExpressionSuffix())) {
expression = parser.parseExpression(key, parserContext);
} else {
expression = parser.parseExpression(key);
}
key = expression.getValue(context, String.class);
}
StringBuilder stringBuffer = new StringBuilder(GlobalConstants.RATE_LIMIT_KEY);
stringBuffer.append(ServletUtils.getRequest().getRequestURI()).append(":");

View File

@ -0,0 +1,7 @@
{
"com.ruoyi.common.ratelimiter.annotation.RateLimiter@key": {
"method": {
"parameters": true
}
}
}

View File

@ -46,6 +46,12 @@
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
<!-- caffeine缓存 -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,45 @@
package com.ruoyi.common.redis.config;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.ruoyi.common.redis.manager.FlexSpringCacheManager;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import java.util.concurrent.TimeUnit;
/**
* 缓存配置
*
* @author Lion Li
*/
@AutoConfiguration
@EnableCaching
public class CacheConfig {
/**
* caffeine 本地缓存处理器
*/
@Bean
public Cache<Object, Object> caffeine() {
return Caffeine.newBuilder()
// 设置最后一次写入或访问后经过固定时间过期
.expireAfterWrite(30, TimeUnit.SECONDS)
// 初始的缓存空间大小
.initialCapacity(100)
// 缓存的最大条数
.maximumSize(1000)
.build();
}
/**
* 自定义缓存管理器 整合spring-cache
*/
@Bean
public CacheManager cacheManager() {
return new FlexSpringCacheManager();
}
}

View File

@ -5,8 +5,9 @@ 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 com.ruoyi.common.core.utils.SpringUtils;
import lombok.extern.slf4j.Slf4j;
import com.ruoyi.common.redis.handler.KeyPrefixHandler;
import com.ruoyi.common.redis.manager.FlexSpringCacheManager;
import com.ruoyi.common.redis.config.properties.RedissonProperties;
import jakarta.annotation.Resource;
import org.redisson.client.codec.StringCodec;
@ -16,22 +17,18 @@ 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;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.task.VirtualThreadTaskExecutor;
/**
* redis配置
*
* @author Lion Li
*/
@Slf4j
@AutoConfiguration
@EnableCaching
@EnableConfigurationProperties(RedissonProperties.class)
public class RedisConfig {
private static final Logger log = LoggerFactory.getLogger(RedisConfig.class);
@Autowired
private RedissonProperties redissonProperties;
@Resource
@ -52,6 +49,9 @@ public class RedisConfig {
// 缓存 Lua 脚本 减少网络传输(redisson 大部分的功能都是基于 Lua 脚本实现)
.setUseScriptCache(true)
.setCodec(codec);
if (SpringUtils.isVirtual()) {
config.setNettyExecutor(new VirtualThreadTaskExecutor("redisson-"));
}
RedissonProperties.SingleServerConfig singleServerConfig = redissonProperties.getSingleServerConfig();
if (ObjectUtil.isNotNull(singleServerConfig)) {
// 使用单机模式
@ -86,14 +86,6 @@ public class RedisConfig {
};
}
/**
* 自定义缓存管理器 整合spring-cache
*/
@Bean
public CacheManager cacheManager() {
return new FlexSpringCacheManager();
}
/**
* redis集群配置 yml
*

View File

@ -0,0 +1,88 @@
package com.ruoyi.common.redis.manager;
import com.ruoyi.common.core.utils.SpringUtils;
import org.springframework.cache.Cache;
import java.util.concurrent.Callable;
/**
* Cache 装饰器模式(用于扩展 Caffeine 一级缓存)
*
* @author LionLi
*/
public class CaffeineCacheDecorator implements Cache {
private static final com.github.benmanes.caffeine.cache.Cache<Object, Object>
CAFFEINE = SpringUtils.getBean("caffeine");
private final Cache cache;
public CaffeineCacheDecorator(Cache cache) {
this.cache = cache;
}
@Override
public String getName() {
return cache.getName();
}
@Override
public Object getNativeCache() {
return cache.getNativeCache();
}
public String getUniqueKey(Object key) {
return cache.getName() + ":" + key;
}
@Override
public ValueWrapper get(Object key) {
Object o = CAFFEINE.get(getUniqueKey(key), k -> cache.get(key));
return (ValueWrapper) o;
}
@SuppressWarnings("unchecked")
public <T> T get(Object key, Class<T> type) {
Object o = CAFFEINE.get(getUniqueKey(key), k -> cache.get(key, type));
return (T) o;
}
@Override
public void put(Object key, Object value) {
cache.put(key, value);
}
public ValueWrapper putIfAbsent(Object key, Object value) {
return cache.putIfAbsent(key, value);
}
@Override
public void evict(Object key) {
evictIfPresent(key);
}
public boolean evictIfPresent(Object key) {
boolean b = cache.evictIfPresent(key);
if (b) {
CAFFEINE.invalidate(getUniqueKey(key));
}
return b;
}
@Override
public void clear() {
cache.clear();
}
public boolean invalidate() {
return cache.invalidate();
}
@SuppressWarnings("unchecked")
@Override
public <T> T get(Object key, Callable<T> valueLoader) {
Object o = CAFFEINE.get(getUniqueKey(key), k -> cache.get(key, valueLoader));
return (T) o;
}
}

View File

@ -33,7 +33,7 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* A {@link org.springframework.cache.CacheManager} implementation
* A {@link CacheManager} implementation
* backed by Redisson instance.
* <p>
* 修改 RedissonSpringCacheManager 源码
@ -156,7 +156,7 @@ public class FlexSpringCacheManager implements CacheManager {
private Cache createMap(String name, CacheConfig config) {
RMap<Object, Object> map = RedisUtils.getClient().getMap(name);
Cache cache = new RedissonCache(map, allowNullValues);
Cache cache = new CaffeineCacheDecorator(new RedissonCache(map, allowNullValues));
if (transactionAware) {
cache = new TransactionAwareCacheDecorator(cache);
}
@ -170,7 +170,7 @@ public class FlexSpringCacheManager implements CacheManager {
private Cache createMapCache(String name, CacheConfig config) {
RMapCache<Object, Object> map = RedisUtils.getClient().getMapCache(name);
Cache cache = new RedissonCache(map, config, allowNullValues);
Cache cache = new CaffeineCacheDecorator(new RedissonCache(map, config, allowNullValues));
if (transactionAware) {
cache = new TransactionAwareCacheDecorator(cache);
}

View File

@ -1 +1,2 @@
com.ruoyi.common.redis.config.RedisConfig
com.ruoyi.common.redis.config.CacheConfig

View File

@ -41,6 +41,12 @@
<artifactId>sa-token-jwt</artifactId>
</dependency>
<!-- caffeine缓存 -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -2,12 +2,15 @@ package com.ruoyi.common.security.core.dao;
import cn.dev33.satoken.dao.SaTokenDao;
import cn.dev33.satoken.util.SaFoxUtil;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.ruoyi.common.redis.utils.RedisUtils;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* Sa-Token持久层接口(使用框架自带RedisUtils实现 协议统一)
@ -16,12 +19,22 @@ import java.util.List;
*/
public class FlexSaTokenDao implements SaTokenDao {
private static final Cache<String, Object> CAFFEINE = Caffeine.newBuilder()
// 设置最后一次写入或访问后经过固定时间过期
.expireAfterWrite(5, TimeUnit.SECONDS)
// 初始的缓存空间大小
.initialCapacity(100)
// 缓存的最大条数
.maximumSize(1000)
.build();
/**
* 获取Value如无返空
*/
@Override
public String get(String key) {
return RedisUtils.getCacheObject(key);
Object o = CAFFEINE.get(key, k -> RedisUtils.getCacheObject(key));
return (String) o;
}
/**
@ -38,6 +51,7 @@ public class FlexSaTokenDao implements SaTokenDao {
} else {
RedisUtils.setCacheObject(key, value, Duration.ofSeconds(timeout));
}
CAFFEINE.put(key, value);
}
/**
@ -47,6 +61,7 @@ public class FlexSaTokenDao implements SaTokenDao {
public void update(String key, String value) {
if (RedisUtils.hasKey(key)) {
RedisUtils.setCacheObject(key, value, true);
CAFFEINE.put(key, value);
}
}
@ -81,7 +96,8 @@ public class FlexSaTokenDao implements SaTokenDao {
*/
@Override
public Object getObject(String key) {
return RedisUtils.getCacheObject(key);
Object o = CAFFEINE.get(key, k -> RedisUtils.getCacheObject(key));
return o;
}
/**
@ -98,6 +114,7 @@ public class FlexSaTokenDao implements SaTokenDao {
} else {
RedisUtils.setCacheObject(key, object, Duration.ofSeconds(timeout));
}
CAFFEINE.put(key, object);
}
/**
@ -107,6 +124,7 @@ public class FlexSaTokenDao implements SaTokenDao {
public void updateObject(String key, Object object) {
if (RedisUtils.hasKey(key)) {
RedisUtils.setCacheObject(key, object, true);
CAFFEINE.put(key, object);
}
}
@ -139,10 +157,14 @@ public class FlexSaTokenDao implements SaTokenDao {
/**
* 搜索数据
*/
@SuppressWarnings("unchecked")
@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);
String keyStr = prefix + "*" + keyword + "*";
return (List<String>) CAFFEINE.get(keyStr, k -> {
Collection<String> keys = RedisUtils.keys(keyStr);
List<String> list = new ArrayList<>(keys);
return SaFoxUtil.searchList(list, start, size, sortType);
});
}
}

View File

@ -15,7 +15,6 @@ import com.ruoyi.common.core.core.domain.model.LoginUser;
import com.ruoyi.common.core.enums.UserType;
import java.util.Set;
import java.util.function.Supplier;
/**
* 登录鉴权助手
@ -35,9 +34,10 @@ 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 USER_NAME_KEY = "userName";
public static final String DEPT_KEY = "deptId";
public static final String DEPT_NAME_KEY = "deptName";
public static final String CLIENT_KEY = "clientid";
public static final String TENANT_ADMIN_KEY = "isTenantAdmin";
/**
* 登录系统 基于 设备类型
@ -57,7 +57,10 @@ public class LoginHelper {
StpUtil.login(loginUser.getLoginId(),
model.setExtra(TENANT_KEY, loginUser.getTenantId())
.setExtra(USER_KEY, loginUser.getUserId())
.setExtra(DEPT_KEY, loginUser.getDeptId()));
.setExtra(USER_NAME_KEY, loginUser.getUsername())
.setExtra(DEPT_KEY, loginUser.getDeptId())
.setExtra(DEPT_NAME_KEY, loginUser.getDeptName())
);
StpUtil.getTokenSession().set(LOGIN_USER_KEY, loginUser);
}
@ -65,13 +68,11 @@ public class LoginHelper {
* 获取用户(多级缓存)
*/
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);
});
SaSession session = StpUtil.getTokenSession();
if (ObjectUtil.isNull(session)) {
return null;
}
return (LoginUser) session.get(LOGIN_USER_KEY);
}
/**
@ -89,7 +90,7 @@ public class LoginHelper {
* 获取用户id
*/
public static Long getUserId() {
return Convert.toLong(getExtra(USER_KEY));
return Convert.toLong(getExtra(USER_KEY));
}
/**
@ -106,8 +107,18 @@ public class LoginHelper {
return Convert.toLong(getExtra(DEPT_KEY));
}
/**
* 获取当前 Token 的扩展信息
*
* @param key 键值
* @return 对应的扩展数据
*/
private static Object getExtra(String key) {
return getStorageIfAbsentSet(key, () -> StpUtil.getExtra(key));
try {
return StpUtil.getExtra(key);
} catch (Exception e) {
return null;
}
}
/**
@ -135,12 +146,17 @@ public class LoginHelper {
return UserConstants.SUPER_ADMIN_ID.equals(userId);
}
/**
* 是否为超级管理员
*
* @return 结果
*/
public static boolean isSuperAdmin() {
return isSuperAdmin(getUserId());
}
/**
* 是否为超级管理员
* 是否为租户管理员
*
* @param rolePermission 角色权限标识组
* @return 结果
@ -150,27 +166,16 @@ public class LoginHelper {
}
public static boolean isTenantAdmin() {
Object value = getStorageIfAbsentSet(TENANT_ADMIN_KEY, () -> {
return isTenantAdmin(getLoginUser().getRolePermission());
});
return Convert.toBool(value);
return Convert.toBool(isTenantAdmin(getLoginUser().getRolePermission()));
}
/**
* 检查当前用户是否已登录
*
* @return 结果
*/
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;
}
}
}

View File

@ -19,6 +19,7 @@
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-common-orm</artifactId>
<optional>true</optional>
</dependency>
<dependency>
@ -26,11 +27,6 @@
<artifactId>ruoyi-common-redis</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -5,12 +5,14 @@ import com.mybatisflex.core.tenant.TenantFactory;
import com.ruoyi.common.security.utils.LoginHelper;
import com.ruoyi.common.tenant.helper.TenantHelper;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* 自定义租户工厂
*
* @author 数据小王子
*/
@Slf4j
@AllArgsConstructor
public class MyTenantFactory implements TenantFactory {

View File

@ -73,10 +73,6 @@
<artifactId>hutool-crypto</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -127,7 +127,7 @@ public class RepeatSubmitAspect {
public boolean isFilterObject(final Object o) {
Class<?> clazz = o.getClass();
if (clazz.isArray()) {
return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
return MultipartFile.class.isAssignableFrom(clazz.getComponentType());
} else if (Collection.class.isAssignableFrom(clazz)) {
Collection collection = (Collection) o;
for (Object value : collection) {

View File

@ -15,6 +15,7 @@
<modules>
<module>ruoyi-monitor</module>
<module>ruoyi-powerjob-server</module>
<module>ruoyi-easyretry-server</module>
</modules>
</project>

View File

@ -0,0 +1,18 @@
# 使用官方的 Java 运行时作为父镜像
FROM registry.cn-qingdao.aliyuncs.com/yuzl1/jdk:21
MAINTAINER Lion Li
RUN mkdir -p /ruoyi/easyretry/logs
WORKDIR /ruoyi/easyretry
ENV LANG=C.UTF-8 LC_ALL=C.UTF-8 JAVA_OPTS="-Xms512m -Xmx1024m"
EXPOSE 8800
EXPOSE 1788
ADD ./target/ruoyi-easyretry-server.jar ./app.jar
ENTRYPOINT java -Djava.security.egd=file:/dev/./urandom -jar app.jar \
-XX:+HeapDumpOnOutOfMemoryError -Xlog:gc*,:time,tags,level -XX:+UseZGC ${JAVA_OPTS}

View File

@ -0,0 +1,46 @@
<?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>com.ruoyi</groupId>
<artifactId>ruoyi-extra</artifactId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>jar</packaging>
<artifactId>ruoyi-easyretry-server</artifactId>
<dependencies>
<dependency>
<groupId>com.aizuda</groupId>
<artifactId>easy-retry-server-starter</artifactId>
<version>${easyretry.version}</version>
</dependency>
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-client</artifactId>
<version>${spring-boot-admin.version}</version>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,18 @@
package com.ruoyi.easyretry;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* EasyRetry Server 启动程序
*
* @author dhb52
*/
@SpringBootApplication
public class EasyRetryServerApplication {
public static void main(String[] args) {
SpringApplication.run(com.aizuda.easy.retry.server.EasyRetryServerApplication.class, args);
}
}

View File

@ -0,0 +1,56 @@
spring:
datasource:
type: com.zaxxer.hikari.HikariDataSource
# mysql数据库
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/ruoyi-flex?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: Root@369
# postgresql数据库
# driver-class-name: org.postgresql.Driver
# url: jdbc:postgresql://localhost:5432/ruoyi-flex?useUnicode=true&characterEncoding=utf8&useSSL=true&autoReconnect=true&reWriteBatchedInserts=true
# username: postgres
# password: postgres@369
hikari:
connection-timeout: 30000
validation-timeout: 5000
minimum-idle: 10
maximum-pool-size: 20
idle-timeout: 600000
max-lifetime: 900000
keepaliveTime: 30000
--- # easy-retry 服务端配置
easy-retry:
# 拉取重试数据的每批次的大小
retry-pull-page-size: 1000
# 拉取重试数据的每批次的大小
job-pull-page-size: 1000
# 服务端 netty 端口
netty-port: 1788
# 重试和死信表的分区总数
total-partition: 2
# 一个客户端每秒最多接收的重试数量指令
limiter: 1000
# 号段模式下步长配置
step: 100
# 日志保存时间(单位: day)
log-storage: 90
# 回调配置
callback:
#回调最大执行次数
max-count: 288
#间隔时间
trigger-interval: 900
mode: all
retry-max-pull-count: 10
--- # 监控中心配置
spring.boot.admin.client:
# 增加客户端开关
enabled: true
url: http://localhost:9090/admin
instance:
service-host-type: IP
username: ruoyi
password: 123456

View File

@ -0,0 +1,56 @@
spring:
datasource:
type: com.zaxxer.hikari.HikariDataSource
# mysql数据库
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/ruoyi-flex?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: Root@369
# postgresql数据库
#driver-class-name: org.postgresql.Driver
#url: jdbc:postgresql://localhost:5432/ruoyi-flex?useUnicode=true&characterEncoding=utf8&useSSL=true&autoReconnect=true&reWriteBatchedInserts=true
#username: postgres
#password: postgres@369
hikari:
connection-timeout: 30000
validation-timeout: 5000
minimum-idle: 10
maximum-pool-size: 20
idle-timeout: 600000
max-lifetime: 900000
keepaliveTime: 30000
--- # easy-retry 服务端配置
easy-retry:
# 拉取重试数据的每批次的大小
retry-pull-page-size: 1000
# 拉取重试数据的每批次的大小
job-pull-page-size: 1000
# 服务端 netty 端口
netty-port: 1788
# 重试和死信表的分区总数
total-partition: 2
# 一个客户端每秒最多接收的重试数量指令
limiter: 1000
# 号段模式下步长配置
step: 100
# 日志保存时间(单位: day)
log-storage: 90
# 回调配置
callback:
#回调最大执行次数
max-count: 288
#间隔时间
trigger-interval: 900
mode: all
retry-max-pull-count: 10
--- # 监控中心配置
spring.boot.admin.client:
# 增加客户端开关
enabled: true
url: http://localhost:9090/admin
instance:
service-host-type: IP
username: ruoyi
password: 123456

View File

@ -0,0 +1,40 @@
server:
port: 8800
servlet:
context-path: /easy-retry
spring:
application:
name: ruoyi-easyretry-server
profiles:
active: @profiles.active@
web:
resources:
static-locations: classpath:admin/
mybatis-plus:
typeAliasesPackage: com.aizuda.easy.retry.template.datasource.persistence.po
global-config:
db-config:
table-prefix: er_
where-strategy: NOT_EMPTY
capital-mode: false
logic-delete-value: 1
logic-not-delete-value: 0
configuration:
map-underscore-to-camel-case: true
cache-enabled: true
logging:
config: classpath:logback-plus.xml
management:
endpoints:
web:
exposure:
include: '*'
endpoint:
health:
show-details: ALWAYS
logfile:
external-file: ./logs/ruoyi-easyretry-server/console.log

View File

@ -0,0 +1,92 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<property name="log.path" value="./logs/ruoyi-easyretry-server" />
<property name="console.log.pattern"
value="%red(%d{yyyy-MM-dd HH:mm:ss}) %green([%thread]) %highlight(%-5level) %boldMagenta(%logger{36}%n) - %msg%n"/>
<property name="log.pattern" value="%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"/>
<!-- 控制台输出 -->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${console.log.pattern}</pattern>
<charset>utf-8</charset>
</encoder>
</appender>
<!-- 控制台输出 -->
<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>
<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>
<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>
<appender name ="async_info" class= "ch.qos.logback.classic.AsyncAppender">
<discardingThreshold >100</discardingThreshold>
<queueSize>1024</queueSize>
<appender-ref ref ="file_info"/>
</appender>
<appender name ="async_error" class= "ch.qos.logback.classic.AsyncAppender">
<discardingThreshold >100</discardingThreshold>
<queueSize>1024</queueSize>
<appender-ref ref ="file_error"/>
</appender>
<!-- EasyRetry appender -->
<appender name="easy_log_server_appender" class="com.aizuda.easy.retry.server.common.appender.EasyRetryServerLogbackAppender">
</appender>
<!-- 控制台输出日志级别 -->
<root level="info">
<appender-ref ref="console" />
<appender-ref ref="async_info" />
<appender-ref ref="async_error" />
<appender-ref ref="easy_log_server_appender" />
</root>
</configuration>

View File

@ -0,0 +1,8 @@
# 使用官方的 Java 运行时作为父镜像
FROM registry.cn-qingdao.aliyuncs.com/yuzl1/jdk:21
# 将本地文件复制到容器中
COPY target/ruoyi-monitor.jar /ruoyi-monitor.jar
# 运行应用
ENTRYPOINT ["java","-jar","/ruoyi-monitor.jar"]

View File

@ -0,0 +1,8 @@
# 使用官方的 Java 运行时作为父镜像
FROM registry.cn-qingdao.aliyuncs.com/yuzl1/jdk:21
# 将本地文件复制到容器中
COPY target/ruoyi-powerjob-server.jar /ruoyi-powerjob-server.jar
# 运行应用
ENTRYPOINT ["java","-jar","/ruoyi-powerjob-server.jar"]

View File

@ -2,15 +2,15 @@ oms.env=dev
####### Database properties(Configure according to the the environment) #######
spring.datasource.remote.hibernate.properties.hibernate.dialect=tech.powerjob.server.persistence.config.dialect.PowerJobPGDialect
spring.datasource.core.driver-class-name=org.postgresql.Driver
spring.datasource.core.jdbc-url=jdbc:postgresql://localhost:5432/ruoyi-flex?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
spring.datasource.core.username=postgres
spring.datasource.core.password=postgres@369
#spring.datasource.core.driver-class-name=org.postgresql.Driver
#spring.datasource.core.jdbc-url=jdbc:postgresql://localhost:5432/ruoyi-flex?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
#spring.datasource.core.username=postgres
#spring.datasource.core.password=postgres@369
## MySQL数据库连接参数
#spring.datasource.core.driver-class-name=com.mysql.cj.jdbc.Driver
#spring.datasource.core.jdbc-url=jdbc:mysql://localhost:3306/ruoyi-flex?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
#spring.datasource.core.username=root
#spring.datasource.core.password=Root@369
spring.datasource.core.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.core.jdbc-url=jdbc:mysql://localhost:3306/ruoyi-flex?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
spring.datasource.core.username=root
spring.datasource.core.password=Root@369
spring.datasource.core.maximum-pool-size=20
spring.datasource.core.minimum-idle=5

View File

@ -1,6 +1,12 @@
oms.env=prod
####### Database properties(Configure according to the the environment) #######
#spring.datasource.remote.hibernate.properties.hibernate.dialect=tech.powerjob.server.persistence.config.dialect.PowerJobPGDialect
#spring.datasource.core.driver-class-name=org.postgresql.Driver
#spring.datasource.core.jdbc-url=jdbc:postgresql://localhost:5432/ruoyi-flex?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
#spring.datasource.core.username=postgres
#spring.datasource.core.password=postgres@369
## MySQL数据库连接参数
spring.datasource.core.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.core.jdbc-url=jdbc:mysql://localhost:3306/ruoyi-flex?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
spring.datasource.core.username=root

View File

@ -1,5 +1,5 @@
# Http server port
server.port=7700
server.port=7070
spring.profiles.active=@profiles.active@
spring.main.banner-mode=log

View File

@ -1,11 +1,14 @@
package com.ruoyi.mf.controller;
import java.util.ArrayList;
import java.util.List;
import lombok.RequiredArgsConstructor;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.*;
import cn.dev33.satoken.annotation.SaCheckPermission;
import org.springframework.web.bind.annotation.*;
import org.springframework.validation.annotation.Validated;
import com.ruoyi.common.excel.core.ExcelResult;
import com.ruoyi.common.core.core.domain.R;
import com.ruoyi.common.excel.utils.ExcelUtil;
import com.ruoyi.common.log.annotation.Log;
@ -14,15 +17,18 @@ import com.ruoyi.common.web.annotation.RepeatSubmit;
import com.ruoyi.common.web.core.BaseController;
import jakarta.annotation.Resource;
import com.ruoyi.mf.domain.vo.MfProductVo;
import com.ruoyi.mf.domain.vo.MfProductImportVo;
import com.ruoyi.mf.domain.bo.MfProductBo;
import com.ruoyi.mf.listener.MfProductImportListener;
import com.ruoyi.mf.service.IMfProductService;
import org.springframework.web.multipart.MultipartFile;
/**
* 产品树Controller
*
* @author 数据小王子
* 2024-01-06
* 2024-04-12
*/
@Validated
@RequiredArgsConstructor
@ -56,6 +62,26 @@ public class MfProductController extends BaseController
ExcelUtil.exportExcel(list, "产品树", MfProductVo.class, response);
}
/**
* 导入数据
*
* @param file 导入文件
* @param updateSupport 是否更新已存在数据
*/
@Log(title = "产品树", businessType = BusinessType.IMPORT)
@SaCheckPermission("mf:product:import")
@PostMapping("/importData")
public R<Void> importData(MultipartFile file, boolean updateSupport) throws Exception {
ExcelResult<MfProductImportVo> result = ExcelUtil.importExcel(file.getInputStream(), MfProductImportVo.class, new MfProductImportListener(updateSupport));
return R.ok(result.getAnalysis());
}
@SaCheckPermission("mf:product:import")
@PostMapping("/importTemplate")
public void importTemplate(HttpServletResponse response) {
ExcelUtil.exportExcel(new ArrayList<>(), "产品树", MfProductImportVo.class, response);
}
/**
* 获取产品树详细信息
*/

View File

@ -1,11 +1,14 @@
package com.ruoyi.mf.controller;
import java.util.ArrayList;
import java.util.List;
import lombok.RequiredArgsConstructor;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.*;
import cn.dev33.satoken.annotation.SaCheckPermission;
import org.springframework.web.bind.annotation.*;
import org.springframework.validation.annotation.Validated;
import com.ruoyi.common.excel.core.ExcelResult;
import com.ruoyi.common.core.core.domain.R;
import com.ruoyi.common.excel.utils.ExcelUtil;
import com.ruoyi.common.log.annotation.Log;
@ -14,8 +17,11 @@ import com.ruoyi.common.web.annotation.RepeatSubmit;
import com.ruoyi.common.web.core.BaseController;
import jakarta.annotation.Resource;
import com.ruoyi.mf.domain.vo.MfStudentVo;
import com.ruoyi.mf.domain.vo.MfStudentImportVo;
import com.ruoyi.mf.domain.bo.MfStudentBo;
import com.ruoyi.mf.listener.MfStudentImportListener;
import com.ruoyi.mf.service.IMfStudentService;
import org.springframework.web.multipart.MultipartFile;
import com.ruoyi.common.orm.core.page.TableDataInfo;
@ -23,7 +29,7 @@ import com.ruoyi.common.orm.core.page.TableDataInfo;
* 学生信息表Controller
*
* @author 数据小王子
* 2024-01-05
* 2024-04-12
*/
@Validated
@RequiredArgsConstructor
@ -56,6 +62,26 @@ public class MfStudentController extends BaseController
ExcelUtil.exportExcel(list, "学生信息表", MfStudentVo.class, response);
}
/**
* 导入数据
*
* @param file 导入文件
* @param updateSupport 是否更新已存在数据
*/
@Log(title = "学生信息表", businessType = BusinessType.IMPORT)
@SaCheckPermission("mf:student:import")
@PostMapping("/importData")
public R<Void> importData(MultipartFile file, boolean updateSupport) throws Exception {
ExcelResult<MfStudentImportVo> result = ExcelUtil.importExcel(file.getInputStream(), MfStudentImportVo.class, new MfStudentImportListener(updateSupport));
return R.ok(result.getAnalysis());
}
@SaCheckPermission("mf:student:import")
@PostMapping("/importTemplate")
public void importTemplate(HttpServletResponse response) {
ExcelUtil.exportExcel(new ArrayList<>(), "学生信息表", MfStudentImportVo.class, response);
}
/**
* 获取学生信息表详细信息
*/

View File

@ -1,6 +1,5 @@
package com.ruoyi.mf.domain;
import com.mybatisflex.annotation.Column;
import com.mybatisflex.annotation.Id;
import com.mybatisflex.annotation.Table;
@ -13,7 +12,7 @@ import com.ruoyi.common.orm.core.domain.TreeEntity;
* 产品树对象 mf_product
*
* @author 数据小王子
* 2024-01-06
* 2024-04-12
*/
@Data
@EqualsAndHashCode(callSuper = true)
@ -23,7 +22,7 @@ public class MfProduct extends TreeEntity
@Serial
private static final long serialVersionUID = 1L;
/** 产品id */
/** 产品编号 */
@Id
private Long productId;

View File

@ -2,7 +2,6 @@ package com.ruoyi.mf.domain;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.mybatisflex.annotation.Column;
import com.mybatisflex.annotation.Id;
import com.mybatisflex.annotation.Table;
@ -15,7 +14,7 @@ import com.ruoyi.common.orm.core.domain.BaseEntity;
* 学生信息表对象 mf_student
*
* @author 数据小王子
* 2024-01-06
* 2024-04-12
*/
@Data
@EqualsAndHashCode(callSuper = true)

View File

@ -11,7 +11,7 @@ import com.ruoyi.common.orm.core.domain.TreeEntity;
* 产品树业务对象 mf_product
*
* @author 数据小王子
* @date 2024-01-06
* @date 2024-04-12
*/
@Data
@EqualsAndHashCode(callSuper = true)
@ -20,7 +20,7 @@ public class MfProductBo extends TreeEntity
{
/**
* 产品id
* 产品编号
*/
private Long productId;
@ -36,4 +36,5 @@ public class MfProductBo extends TreeEntity
@NotBlank(message = "产品状态0正常 1停用不能为空")
private String status;
}

View File

@ -13,7 +13,7 @@ import com.ruoyi.common.orm.core.domain.BaseEntity;
* 学生信息表业务对象 mf_student
*
* @author 数据小王子
* @date 2024-01-05
* @date 2024-04-12
*/
@Data
@EqualsAndHashCode(callSuper = true)
@ -63,4 +63,5 @@ public class MfStudentBo extends BaseEntity
@JsonFormat(pattern = "yyyy-MM-dd")
private Date studentBirthday;
}

View File

@ -0,0 +1,52 @@
package com.ruoyi.mf.domain.vo;
import com.alibaba.excel.annotation.ExcelProperty;
import com.ruoyi.common.excel.annotation.ExcelDictFormat;
import com.ruoyi.common.excel.convert.ExcelDictConvert;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import lombok.NoArgsConstructor;
/**
* 产品树导入视图对象 mf_product
*
* @author 数据小王子
* @date 2024-04-12
*/
@Data
@NoArgsConstructor
public class MfProductImportVo implements Serializable
{
@Serial
private static final long serialVersionUID = 1L;
/** 产品编号 */
@ExcelProperty(value = "产品编号")
private Long productId;
/** 上级编号 */
@ExcelProperty(value = "上级编号")
private Long parentId;
/** 产品名称 */
@ExcelProperty(value = "产品名称")
private String productName;
/** 显示顺序 */
@ExcelProperty(value = "显示顺序")
private Integer orderNum;
/** 产品状态0正常 1停用 */
@ExcelProperty(value = "产品状态", converter = ExcelDictConvert.class)
@ExcelDictFormat(dictType = "sys_student_status")
private String status;
/** 逻辑删除标志0代表存在 1代表删除 */
@ExcelProperty(value = "逻辑删除标志0代表存在 1代表删除")
private Integer delFlag;
}

View File

@ -17,7 +17,7 @@ import com.ruoyi.common.orm.core.domain.TreeEntity;
* 产品树视图对象 mf_product
*
* @author 数据小王子
* @date 2024-01-06
* @date 2024-04-12
*/
@Data
@ExcelIgnoreUnannotated
@ -29,8 +29,8 @@ public class MfProductVo extends TreeEntity implements Serializable
@Serial
private static final long serialVersionUID = 1L;
/** 产品id */
@ExcelProperty(value = "产品id")
/** 产品编号 */
@ExcelProperty(value = "产品编号")
private Long productId;
/** 产品名称 */
@ -46,4 +46,6 @@ public class MfProductVo extends TreeEntity implements Serializable
@ExcelProperty(value = "逻辑删除标志0代表存在 1代表删除")
private Integer delFlag;
}

View File

@ -0,0 +1,61 @@
package com.ruoyi.mf.domain.vo;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.alibaba.excel.annotation.ExcelProperty;
import com.ruoyi.common.excel.annotation.ExcelDictFormat;
import com.ruoyi.common.excel.convert.ExcelDictConvert;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import lombok.NoArgsConstructor;
/**
* 学生信息表导入视图对象 mf_student
*
* @author 数据小王子
* @date 2024-04-12
*/
@Data
@NoArgsConstructor
public class MfStudentImportVo implements Serializable
{
@Serial
private static final long serialVersionUID = 1L;
/** 学生名称 */
@ExcelProperty(value = "学生名称")
private String studentName;
/** 年龄 */
@ExcelProperty(value = "年龄")
private Integer studentAge;
/** 爱好0代码 1音乐 2电影 */
@ExcelProperty(value = "爱好", converter = ExcelDictConvert.class)
@ExcelDictFormat(dictType = "sys_student_hobby")
private String studentHobby;
/** 性别1男 2女 3未知 */
@ExcelProperty(value = "性别", converter = ExcelDictConvert.class)
@ExcelDictFormat(dictType = "sys_user_gender")
private String studentGender;
/** 状态0正常 1停用 */
@ExcelProperty(value = "状态", converter = ExcelDictConvert.class)
@ExcelDictFormat(dictType = "sys_student_status")
private String studentStatus;
/** 生日 */
@ExcelProperty(value = "生日")
private Date studentBirthday;
/** 逻辑删除标志0代表存在 1代表删除 */
@ExcelProperty(value = "逻辑删除标志0代表存在 1代表删除")
private Integer delFlag;
}

View File

@ -19,7 +19,7 @@ import com.ruoyi.common.orm.core.domain.BaseEntity;
* 学生信息表视图对象 mf_student
*
* @author 数据小王子
* @date 2024-01-05
* @date 2024-04-12
*/
@Data
@ExcelIgnoreUnannotated
@ -66,4 +66,6 @@ public class MfStudentVo extends BaseEntity implements Serializable
@ExcelProperty(value = "逻辑删除标志0代表存在 1代表删除")
private Integer delFlag;
}

View File

@ -0,0 +1,118 @@
package com.ruoyi.mf.listener;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.ObjectUtil;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.ruoyi.common.core.exception.ServiceException;
import com.ruoyi.common.core.utils.SpringUtils;
import com.ruoyi.common.core.utils.ValidatorUtils;
import com.ruoyi.common.excel.core.ExcelListener;
import com.ruoyi.common.excel.core.ExcelResult;
import com.ruoyi.mf.domain.bo.MfProductBo;
import com.ruoyi.mf.domain.vo.*;
import com.ruoyi.mf.service.*;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
/**
* 产品树自定义导入
*
* @author 数据小王子
*/
@Slf4j
public class MfProductImportListener extends AnalysisEventListener<MfProductImportVo> implements ExcelListener<MfProductImportVo> {
private final IMfProductService mfProductService;
private final Boolean isUpdateSupport;
private int successNum = 0;
private int failureNum = 0;
private final StringBuilder successMsg = new StringBuilder();
private final StringBuilder failureMsg = new StringBuilder();
public MfProductImportListener(Boolean isUpdateSupport) {
this.mfProductService = SpringUtils.getBean(IMfProductService.class);
this.isUpdateSupport = isUpdateSupport;
}
@Override
public void invoke(MfProductImportVo mfProductVo, AnalysisContext context) {
try {
MfProductBo mfProductBo = BeanUtil.toBean(mfProductVo, MfProductBo.class);
//TODO:根据某个字段查询数据库表中是否存在记录不存在就新增存在就更新
MfProductVo mfProductVo1 = null;
mfProductVo1 = mfProductService.selectById(mfProductVo.getProductId());
if (ObjectUtil.isNull(mfProductVo1)) {
//不存在就新增
mfProductBo.setVersion(0);
ValidatorUtils.validate(mfProductBo);
boolean inserted = mfProductService.insertWithPk(mfProductBo);//树表需要前台传来主键值
if (inserted) {
successNum++;
successMsg.append("<br/>").append(successNum).append("、产品树 记录导入成功");
return;
} else {
failureNum++;
failureMsg.append("<br/>").append(failureNum).append("、产品树 记录导入失败");
return;
}
} else if (isUpdateSupport) {
//存在就更新
mfProductBo.setProductId(mfProductVo1.getProductId());//主键
mfProductBo.setVersion(mfProductVo1.getVersion());
boolean updated = mfProductService.update(mfProductBo);
if (updated) {
successNum++;
successMsg.append("<br/>").append(successNum).append("、产品树 记录更新成功");
return;
} else {
failureNum++;
failureMsg.append("<br/>").append(failureNum).append("、产品树 记录更新失败");
return;
}
}
} catch (Exception e) {
failureNum++;
String msg = "<br/>" + failureNum + "、产品树 记录导入失败:";
failureMsg.append(msg).append(e.getMessage());
log.error(msg, e);
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
}
@Override
public ExcelResult<MfProductImportVo> getExcelResult() {
return new ExcelResult<>() {
@Override
public String getAnalysis() {
if (failureNum > 0) {
failureMsg.insert(0, "很抱歉,导入失败!共 " + failureNum + " 条数据没有成功导入,错误如下:");
throw new ServiceException(failureMsg.toString());
} else {
successMsg.insert(0, "恭喜您,数据已全部导入成功!共 " + successNum + " 条,数据如下:");
}
return successMsg.toString();
}
@Override
public List<MfProductImportVo> getList() {
return null;
}
@Override
public List<String> getErrorList() {
return null;
}
};
}
}

View File

@ -0,0 +1,118 @@
package com.ruoyi.mf.listener;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.ObjectUtil;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.ruoyi.common.core.exception.ServiceException;
import com.ruoyi.common.core.utils.SpringUtils;
import com.ruoyi.common.core.utils.ValidatorUtils;
import com.ruoyi.common.excel.core.ExcelListener;
import com.ruoyi.common.excel.core.ExcelResult;
import com.ruoyi.mf.domain.bo.MfStudentBo;
import com.ruoyi.mf.domain.vo.*;
import com.ruoyi.mf.service.*;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
/**
* 学生信息表自定义导入
*
* @author 数据小王子
*/
@Slf4j
public class MfStudentImportListener extends AnalysisEventListener<MfStudentImportVo> implements ExcelListener<MfStudentImportVo> {
private final IMfStudentService mfStudentService;
private final Boolean isUpdateSupport;
private int successNum = 0;
private int failureNum = 0;
private final StringBuilder successMsg = new StringBuilder();
private final StringBuilder failureMsg = new StringBuilder();
public MfStudentImportListener(Boolean isUpdateSupport) {
this.mfStudentService = SpringUtils.getBean(IMfStudentService.class);
this.isUpdateSupport = isUpdateSupport;
}
@Override
public void invoke(MfStudentImportVo mfStudentVo, AnalysisContext context) {
try {
MfStudentBo mfStudentBo = BeanUtil.toBean(mfStudentVo, MfStudentBo.class);
//TODO:根据某个字段查询数据库表中是否存在记录不存在就新增存在就更新
MfStudentVo mfStudentVo1 = null;
//mfStudentVo1 = mfStudentService.selectBySomefield(mfStudentVo.getSomefield());
if (ObjectUtil.isNull(mfStudentVo1)) {
//不存在就新增
mfStudentBo.setVersion(0);
ValidatorUtils.validate(mfStudentBo);
boolean inserted = mfStudentService.insert(mfStudentBo);
if (inserted) {
successNum++;
successMsg.append("<br/>").append(successNum).append("、学生信息表 记录导入成功");
return;
} else {
failureNum++;
failureMsg.append("<br/>").append(failureNum).append("、学生信息表 记录导入失败");
return;
}
} else if (isUpdateSupport) {
//存在就更新
mfStudentBo.setStudentId(mfStudentVo1.getStudentId());//主键
mfStudentBo.setVersion(mfStudentVo1.getVersion());
boolean updated = mfStudentService.update(mfStudentBo);
if (updated) {
successNum++;
successMsg.append("<br/>").append(successNum).append("、学生信息表 记录更新成功");
return;
} else {
failureNum++;
failureMsg.append("<br/>").append(failureNum).append("、学生信息表 记录更新失败");
return;
}
}
} catch (Exception e) {
failureNum++;
String msg = "<br/>" + failureNum + "、学生信息表 记录导入失败:";
failureMsg.append(msg).append(e.getMessage());
log.error(msg, e);
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
}
@Override
public ExcelResult<MfStudentImportVo> getExcelResult() {
return new ExcelResult<>() {
@Override
public String getAnalysis() {
if (failureNum > 0) {
failureMsg.insert(0, "很抱歉,导入失败!共 " + failureNum + " 条数据没有成功导入,错误如下:");
throw new ServiceException(failureMsg.toString());
} else {
successMsg.insert(0, "恭喜您,数据已全部导入成功!共 " + successNum + " 条,数据如下:");
}
return successMsg.toString();
}
@Override
public List<MfStudentImportVo> getList() {
return null;
}
@Override
public List<String> getErrorList() {
return null;
}
};
}
}

View File

@ -8,7 +8,7 @@ import com.ruoyi.mf.domain.MfProduct;
* 产品树Mapper接口
*
* @author 数据小王子
* 2024-01-06
* 2024-04-12
*/
@Mapper
public interface MfProductMapper extends BaseMapper<MfProduct>

View File

@ -8,7 +8,7 @@ import com.ruoyi.mf.domain.MfStudent;
* 学生信息表Mapper接口
*
* @author 数据小王子
* 2023-11-22
* 2024-04-12
*/
@Mapper
public interface MfStudentMapper extends BaseMapper<MfStudent>

View File

@ -10,7 +10,7 @@ import com.ruoyi.common.orm.core.service.IBaseService;
* 产品树Service接口
*
* @author 数据小王子
* 2024-01-06
* 2024-04-12
*/
public interface IMfProductService extends IBaseService<MfProduct>
{
@ -39,6 +39,14 @@ public interface IMfProductService extends IBaseService<MfProduct>
*/
boolean insert(MfProductBo mfProductBo);
/**
* 新增产品树前台提供主键值一般用于导入的场合
*
* @param mfProductBo 产品树Bo
* @return 结果:true 操作成功false 操作失败
*/
boolean insertWithPk(MfProductBo mfProductBo);
/**
* 修改产品树
*

View File

@ -11,7 +11,7 @@ import com.ruoyi.common.orm.core.page.TableDataInfo;
* 学生信息表Service接口
*
* @author 数据小王子
* 2024-01-05
* 2024-04-12
*/
public interface IMfStudentService extends IBaseService<MfStudent>
{
@ -47,6 +47,14 @@ public interface IMfStudentService extends IBaseService<MfStudent>
*/
boolean insert(MfStudentBo mfStudentBo);
/**
* 新增学生信息表前台提供主键值一般用于导入的场合
*
* @param mfStudentBo 学生信息表Bo
* @return 结果:true 操作成功false 操作失败
*/
boolean insertWithPk(MfStudentBo mfStudentBo);
/**
* 修改学生信息表
*

View File

@ -2,10 +2,18 @@ package com.ruoyi.mf.service.impl;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import cn.hutool.core.util.ObjectUtil;
import com.mybatisflex.core.paginate.Page;
import com.mybatisflex.core.query.QueryMethods;
import com.mybatisflex.core.query.QueryWrapper;
import com.mybatisflex.core.update.UpdateChain;
import com.ruoyi.common.core.utils.MapstructUtils;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.orm.core.page.PageQuery;
import com.ruoyi.common.orm.core.page.TableDataInfo;
import com.ruoyi.common.orm.core.service.impl.BaseServiceImpl;
import com.ruoyi.common.core.utils.DateUtils;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -20,7 +28,7 @@ import static com.ruoyi.mf.domain.table.MfProductTableDef.MF_PRODUCT;
* 产品树Service业务层处理
*
* @author 数据小王子
* 2024-01-06
* 2024-04-12
*/
@Service
public class MfProductServiceImpl extends BaseServiceImpl<MfProductMapper, MfProduct> implements IMfProductService
@ -80,9 +88,49 @@ public class MfProductServiceImpl extends BaseServiceImpl<MfProductMapper, MfPro
{
MfProduct mfProduct = MapstructUtils.convert(mfProductBo, MfProduct.class);
//获取祖级列表字段
Long parentId = mfProduct.getParentId();
if (parentId == 0) {
mfProduct.setAncestors("0");
} else {
MfProductVo parentMfProduct = selectById(mfProductBo.getParentId());
if (ObjectUtil.isNotNull(parentMfProduct)) {
mfProduct.setAncestors(parentMfProduct.getAncestors()+"," +parentId);
} else {
mfProduct.setAncestors("0");
}
}
return this.save(mfProduct);//使用全局配置的雪花算法主键生成器生成ID值
}
/**
* 新增产品树前台提供主键值一般用于导入的场合
*
* @param mfProductBo 产品树Bo
* @return 结果:true 操作成功false 操作失败
*/
@Override
public boolean insertWithPk(MfProductBo mfProductBo)
{
MfProduct mfProduct = MapstructUtils.convert(mfProductBo, MfProduct.class);
//获取祖级列表字段
Long parentId = mfProduct.getParentId();
if (parentId == 0) {
mfProduct.setAncestors("0");
} else {
MfProductVo parentMfProduct = selectById(mfProductBo.getParentId());
if (ObjectUtil.isNotNull(parentMfProduct)) {
mfProduct.setAncestors(parentMfProduct.getAncestors()+"," +parentId);
} else {
mfProduct.setAncestors("0");
}
}
return mfProductMapper.insertWithPk(mfProduct) > 0;//前台传来主键值
}
/**
* 修改产品树
*
@ -94,12 +142,46 @@ public class MfProductServiceImpl extends BaseServiceImpl<MfProductMapper, MfPro
{
MfProduct mfProduct = MapstructUtils.convert(mfProductBo, MfProduct.class);
if(ObjectUtil.isNotNull(mfProduct) && ObjectUtil.isNotNull(mfProduct.getProductId())) {
//更新祖级列表字段
MfProductVo newParentMfProduct = selectById(mfProduct.getParentId());
MfProductVo oldMfProduct = selectById(mfProduct.getProductId());
if ( ObjectUtil.isNotNull(newParentMfProduct) && ObjectUtil.isNotNull(oldMfProduct) ) {
String newAncestors = newParentMfProduct.getAncestors() + "," + newParentMfProduct.getProductId();
String oldAncestors = oldMfProduct.getAncestors();
mfProduct.setAncestors(newAncestors);
updateMfProductChildren(mfProduct.getProductId(), newAncestors, oldAncestors);
}
boolean updated = this.updateById(mfProduct);
return updated;
}
return false;
}
/**
* 修改子元素关系
*
* @param productId 主键ID
* @param newAncestors 新的父ID集合
* @param oldAncestors 旧的父ID集合
*/
@Transactional
public void updateMfProductChildren(Long productId, String newAncestors, String oldAncestors) {
QueryWrapper queryWrapper = QueryWrapper.create()
.from(MF_PRODUCT)
.where(QueryMethods.findInSet(QueryMethods.number(productId), MF_PRODUCT.ANCESTORS).gt(0));
List<MfProductVo> children = this.listAs(queryWrapper, MfProductVo.class);
for (MfProductVo child : children) {
child.setAncestors(child.getAncestors().replaceFirst(oldAncestors, newAncestors));
UpdateChain.of(MfProduct.class)
.set(MfProduct::getAncestors, child.getAncestors())
.where(MfProduct::getProductId).eq(child.getProductId())
.update();
}
}
/**
* 批量删除产品树
*

View File

@ -2,13 +2,18 @@ package com.ruoyi.mf.service.impl;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import cn.hutool.core.util.ObjectUtil;
import com.mybatisflex.core.paginate.Page;
import com.mybatisflex.core.query.QueryMethods;
import com.mybatisflex.core.query.QueryWrapper;
import com.mybatisflex.core.update.UpdateChain;
import com.ruoyi.common.core.utils.MapstructUtils;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.orm.core.page.PageQuery;
import com.ruoyi.common.orm.core.page.TableDataInfo;
import com.ruoyi.common.orm.core.service.impl.BaseServiceImpl;
import com.ruoyi.common.core.utils.DateUtils;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -23,7 +28,7 @@ import static com.ruoyi.mf.domain.table.MfStudentTableDef.MF_STUDENT;
* 学生信息表Service业务层处理
*
* @author 数据小王子
* 2024-01-05
* 2024-04-12
*/
@Service
public class MfStudentServiceImpl extends BaseServiceImpl<MfStudentMapper, MfStudent> implements IMfStudentService
@ -95,9 +100,25 @@ public class MfStudentServiceImpl extends BaseServiceImpl<MfStudentMapper, MfStu
{
MfStudent mfStudent = MapstructUtils.convert(mfStudentBo, MfStudent.class);
return this.save(mfStudent);//使用全局配置的雪花算法主键生成器生成ID值
}
/**
* 新增学生信息表前台提供主键值一般用于导入的场合
*
* @param mfStudentBo 学生信息表Bo
* @return 结果:true 操作成功false 操作失败
*/
@Override
public boolean insertWithPk(MfStudentBo mfStudentBo)
{
MfStudent mfStudent = MapstructUtils.convert(mfStudentBo, MfStudent.class);
return mfStudentMapper.insertWithPk(mfStudent) > 0;//前台传来主键值
}
/**
* 修改学生信息表
*
@ -115,6 +136,7 @@ public class MfStudentServiceImpl extends BaseServiceImpl<MfStudentMapper, MfStu
return false;
}
/**
* 批量删除学生信息表
*

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