Skip to content

Commit a557f07

Browse files
committed
Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/ruoyi-vue-pro
# Conflicts: # yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/api/file/FileApiImpl.java # yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImpl.java # yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImplTest.java # yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/coupon/vo/template/AppCouponTemplateRespVO.java # yudao-module-member/yudao-module-member-biz/src/test/java/cn/iocoder/yudao/module/member/service/user/MemberUserServiceImplTest.java # yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserService.java # yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImplTest.java
2 parents 74490ed + c2db16c commit a557f07

37 files changed

Lines changed: 562 additions & 230 deletions

File tree

sql/mysql/ruoyi-vue-pro.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1055,7 +1055,7 @@ INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `st
10551055
INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2172, 31, 'RABBITMQ', '31', 'iot_data_bridge_type_enum', 0, 'primary', '', '', '1', '2025-03-09 12:41:47', '1', '2025-03-17 09:40:46', b'0');
10561056
INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2173, 32, 'KAFKA', '32', 'iot_data_bridge_type_enum', 0, 'primary', '', '', '1', '2025-03-09 12:41:59', '1', '2025-03-17 09:40:46', b'0');
10571057
INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3000, 16, '百川智能', 'BaiChuan', 'ai_platform', 0, '', '', '', '1', '2025-03-23 12:15:46', '1', '2025-03-23 12:15:46', b'0');
1058-
INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3001, 50, 'Vben5.0 Ant Design Schema 模版', '50', 'infra_codegen_front_type', 0, '', '', NULL, '1', '2025-04-23 21:47:47', '1', '2025-04-23 21:47:47', b'0');
1058+
INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3001, 50, 'Vben5.0 Ant Design Schema 模版', '40', 'infra_codegen_front_type', 0, '', '', NULL, '1', '2025-04-23 21:47:47', '1', '2025-04-23 21:47:47', b'0');
10591059
COMMIT;
10601060

10611061
-- ----------------------------

yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/io/FileUtils.java

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,9 @@
11
package cn.iocoder.yudao.framework.common.util.io;
22

3-
import cn.hutool.core.io.FileTypeUtil;
43
import cn.hutool.core.io.FileUtil;
5-
import cn.hutool.core.io.file.FileNameUtil;
64
import cn.hutool.core.util.IdUtil;
7-
import cn.hutool.core.util.StrUtil;
8-
import cn.hutool.crypto.digest.DigestUtil;
95
import lombok.SneakyThrows;
106

11-
import java.io.ByteArrayInputStream;
127
import java.io.File;
138

149
/**
@@ -63,22 +58,4 @@ public static File createTempFile() {
6358
return file;
6459
}
6560

66-
/**
67-
* 生成文件路径
68-
*
69-
* @param content 文件内容
70-
* @param originalName 原始文件名
71-
* @return path,唯一不可重复
72-
*/
73-
public static String generatePath(byte[] content, String originalName) {
74-
String sha256Hex = DigestUtil.sha256Hex(content);
75-
// 情况一:如果存在 name,则优先使用 name 的后缀
76-
if (StrUtil.isNotBlank(originalName)) {
77-
String extName = FileNameUtil.extName(originalName);
78-
return StrUtil.isBlank(extName) ? sha256Hex : sha256Hex + "." + extName;
79-
}
80-
// 情况二:基于 content 计算
81-
return sha256Hex + '.' + FileTypeUtil.getType(new ByteArrayInputStream(content));
82-
}
83-
8461
}

yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/config/YudaoRedisMQConsumerAutoConfiguration.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import cn.iocoder.yudao.framework.common.enums.DocumentEnum;
77
import cn.iocoder.yudao.framework.mq.redis.core.RedisMQTemplate;
88
import cn.iocoder.yudao.framework.mq.redis.core.job.RedisPendingMessageResendJob;
9+
import cn.iocoder.yudao.framework.mq.redis.core.job.RedisStreamMessageCleanupJob;
910
import cn.iocoder.yudao.framework.mq.redis.core.pubsub.AbstractRedisChannelMessageListener;
1011
import cn.iocoder.yudao.framework.mq.redis.core.stream.AbstractRedisStreamMessageListener;
1112
import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration;
@@ -73,6 +74,17 @@ public RedisPendingMessageResendJob redisPendingMessageResendJob(List<AbstractRe
7374
return new RedisPendingMessageResendJob(listeners, redisTemplate, groupName, redissonClient);
7475
}
7576

77+
/**
78+
* 创建 Redis Stream 消息清理任务
79+
*/
80+
@Bean
81+
@ConditionalOnBean(AbstractRedisStreamMessageListener.class)
82+
public RedisStreamMessageCleanupJob redisStreamMessageCleanupJob(List<AbstractRedisStreamMessageListener<?>> listeners,
83+
RedisMQTemplate redisTemplate,
84+
RedissonClient redissonClient) {
85+
return new RedisStreamMessageCleanupJob(listeners, redisTemplate, redissonClient);
86+
}
87+
7688
/**
7789
* 创建 Redis Stream 集群消费的容器
7890
*

yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/core/job/RedisPendingMessageResendJob.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,13 @@
2323
@AllArgsConstructor
2424
public class RedisPendingMessageResendJob {
2525

26-
private static final String LOCK_KEY = "redis:pending:msg:lock";
26+
private static final String LOCK_KEY = "redis:stream:pending-message-resend:lock";
2727

2828
/**
2929
* 消息超时时间,默认 5 分钟
3030
*
3131
* 1. 超时的消息才会被重新投递
32-
* 2. 由于定时任务 1 分钟一次,消息超时后不会被立即重投,极端情况下消息5分钟过期后,再等 1 分钟才会被扫瞄到
32+
* 2. 由于定时任务 1 分钟一次,消息超时后不会被立即重投,极端情况下消息 5 分钟过期后,再等 1 分钟才会被扫瞄到
3333
*/
3434
private static final int EXPIRE_TIME = 5 * 60;
3535

@@ -39,7 +39,7 @@ public class RedisPendingMessageResendJob {
3939
private final RedissonClient redissonClient;
4040

4141
/**
42-
* 一分钟执行一次,这里选择每分钟的35秒执行,是为了避免整点任务过多的问题
42+
* 一分钟执行一次,这里选择每分钟的 35 秒执行,是为了避免整点任务过多的问题
4343
*/
4444
@Scheduled(cron = "35 * * * * ?")
4545
public void messageResend() {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package cn.iocoder.yudao.framework.mq.redis.core.job;
2+
3+
import cn.iocoder.yudao.framework.mq.redis.core.RedisMQTemplate;
4+
import cn.iocoder.yudao.framework.mq.redis.core.stream.AbstractRedisStreamMessageListener;
5+
import lombok.AllArgsConstructor;
6+
import lombok.extern.slf4j.Slf4j;
7+
import org.redisson.api.RLock;
8+
import org.redisson.api.RedissonClient;
9+
import org.springframework.data.redis.core.StreamOperations;
10+
import org.springframework.scheduling.annotation.Scheduled;
11+
12+
import java.util.List;
13+
14+
/**
15+
* Redis Stream 消息清理任务
16+
* 用于定期清理已消费的消息,防止内存占用过大
17+
*
18+
* @see <a href="https://www.cnblogs.com/nanxiang/p/16179519.html">记一次 redis stream 数据类型内存不释放问题</a>
19+
*
20+
* @author 芋道源码
21+
*/
22+
@Slf4j
23+
@AllArgsConstructor
24+
public class RedisStreamMessageCleanupJob {
25+
26+
private static final String LOCK_KEY = "redis:stream:message-cleanup:lock";
27+
28+
/**
29+
* 保留的消息数量,默认保留最近 10000 条消息
30+
*/
31+
private static final long MAX_COUNT = 10000;
32+
33+
private final List<AbstractRedisStreamMessageListener<?>> listeners;
34+
private final RedisMQTemplate redisTemplate;
35+
private final RedissonClient redissonClient;
36+
37+
/**
38+
* 每小时执行一次清理任务
39+
*/
40+
@Scheduled(cron = "0 0 * * * ?")
41+
public void cleanup() {
42+
RLock lock = redissonClient.getLock(LOCK_KEY);
43+
// 尝试加锁
44+
if (lock.tryLock()) {
45+
try {
46+
execute();
47+
} catch (Exception ex) {
48+
log.error("[cleanup][执行异常]", ex);
49+
} finally {
50+
lock.unlock();
51+
}
52+
}
53+
}
54+
55+
/**
56+
* 执行清理逻辑
57+
*/
58+
private void execute() {
59+
StreamOperations<String, Object, Object> ops = redisTemplate.getRedisTemplate().opsForStream();
60+
listeners.forEach(listener -> {
61+
try {
62+
// 使用 XTRIM 命令清理消息,只保留最近的 MAX_LEN 条消息
63+
Long trimCount = ops.trim(listener.getStreamKey(), MAX_COUNT, true);
64+
if (trimCount != null && trimCount > 0) {
65+
log.info("[execute][Stream({}) 清理消息数量({})]", listener.getStreamKey(), trimCount);
66+
}
67+
} catch (Exception ex) {
68+
log.error("[execute][Stream({}) 清理异常]", listener.getStreamKey(), ex);
69+
}
70+
});
71+
}
72+
}
Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package cn.iocoder.yudao.module.infra.api.file;
22

3+
import jakarta.validation.constraints.NotEmpty;
4+
35
/**
46
* 文件 API 接口
57
*
@@ -14,28 +16,30 @@ public interface FileApi {
1416
* @return 文件路径
1517
*/
1618
default String createFile(byte[] content) {
17-
return createFile(null, null, content);
19+
return createFile(content, null, null, null);
1820
}
1921

2022
/**
2123
* 保存文件,并返回文件的访问路径
2224
*
23-
* @param path 文件路径
2425
* @param content 文件内容
26+
* @param name 文件名称,允许空
2527
* @return 文件路径
2628
*/
27-
default String createFile(String path, byte[] content) {
28-
return createFile(null, path, content);
29+
default String createFile(byte[] content, String name) {
30+
return createFile(content, name, null, null);
2931
}
3032

3133
/**
3234
* 保存文件,并返回文件的访问路径
3335
*
34-
* @param name 文件名称
35-
* @param path 文件路径
3636
* @param content 文件内容
37+
* @param name 文件名称,允许空
38+
* @param directory 目录,允许空
39+
* @param type 文件的 MIME 类型,允许空
3740
* @return 文件路径
3841
*/
39-
String createFile(String name, String path, byte[] content);
42+
String createFile(@NotEmpty(message = "文件内容不能为空") byte[] content,
43+
String name, String directory, String type);
4044

4145
}

yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/api/file/FileApiImpl.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
package cn.iocoder.yudao.module.infra.api.file;
22

33
import cn.iocoder.yudao.module.infra.service.file.FileService;
4+
import jakarta.annotation.Resource;
45
import org.springframework.stereotype.Service;
56
import org.springframework.validation.annotation.Validated;
67

7-
import javax.annotation.Resource;
8-
98
/**
109
* 文件 API 实现类
1110
*
@@ -19,8 +18,8 @@ public class FileApiImpl implements FileApi {
1918
private FileService fileService;
2019

2120
@Override
22-
public String createFile(String name, String path, byte[] content) {
23-
return fileService.createFile(name, path, content);
21+
public String createFile(byte[] content, String name, String directory, String type) {
22+
return fileService.createFile(content, name, directory, type);
2423
}
2524

2625
}

yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import cn.iocoder.yudao.module.infra.service.file.FileService;
1212
import io.swagger.v3.oas.annotations.Operation;
1313
import io.swagger.v3.oas.annotations.Parameter;
14+
import io.swagger.v3.oas.annotations.Parameters;
1415
import io.swagger.v3.oas.annotations.tags.Tag;
1516
import lombok.extern.slf4j.Slf4j;
1617
import org.springframework.http.HttpStatus;
@@ -42,14 +43,21 @@ public class FileController {
4243
@Operation(summary = "上传文件", description = "模式一:后端上传文件")
4344
public CommonResult<String> uploadFile(FileUploadReqVO uploadReqVO) throws Exception {
4445
MultipartFile file = uploadReqVO.getFile();
45-
String path = uploadReqVO.getPath();
46-
return success(fileService.createFile(file.getOriginalFilename(), path, IoUtil.readBytes(file.getInputStream())));
46+
byte[] content = IoUtil.readBytes(file.getInputStream());
47+
return success(fileService.createFile(content, file.getOriginalFilename(),
48+
uploadReqVO.getDirectory(), file.getContentType()));
4749
}
4850

4951
@GetMapping("/presigned-url")
5052
@Operation(summary = "获取文件预签名地址", description = "模式二:前端上传文件:用于前端直接上传七牛、阿里云 OSS 等文件存储器")
51-
public CommonResult<FilePresignedUrlRespVO> getFilePresignedUrl(@RequestParam("path") String path) throws Exception {
52-
return success(fileService.getFilePresignedUrl(path));
53+
@Parameters({
54+
@Parameter(name = "name", description = "文件名称", required = true),
55+
@Parameter(name = "directory", description = "文件目录")
56+
})
57+
public CommonResult<FilePresignedUrlRespVO> getFilePresignedUrl(
58+
@RequestParam("name") String name,
59+
@RequestParam(value = "directory", required = false) String directory) {
60+
return success(fileService.getFilePresignedUrl(name, directory));
5361
}
5462

5563
@PostMapping("/create")

yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/file/FilePresignedUrlRespVO.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ public class FilePresignedUrlRespVO {
1414
@Schema(description = "配置编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "11")
1515
private Long configId;
1616

17-
@Schema(description = "文件上传 URL", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://s3.cn-south-1.qiniucs.com/ruoyi-vue-pro/758d3a5387507358c7236de4c8f96de1c7f5097ff6a7722b34772fb7b76b140f.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=3TvrJ70gl2Gt6IBe7_IZT1F6i_k0iMuRtyEv4EyS%2F20240217%2Fcn-south-1%2Fs3%2Faws4_request&X-Amz-Date=20240217T123222Z&X-Amz-Expires=600&X-Amz-SignedHeaders=host&X-Amz-Signature=a29f33770ab79bf523ccd4034d0752ac545f3c2a3b17baa1eb4e280cfdccfda5")
17+
@Schema(description = "文件上传 URL", requiredMode = Schema.RequiredMode.REQUIRED,
18+
example = "https://s3.cn-south-1.qiniucs.com/ruoyi-vue-pro/758d3a5387507358c7236de4c8f96de1c7f5097ff6a7722b34772fb7b76b140f.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=3TvrJ70gl2Gt6IBe7_IZT1F6i_k0iMuRtyEv4EyS%2F20240217%2Fcn-south-1%2Fs3%2Faws4_request&X-Amz-Date=20240217T123222Z&X-Amz-Expires=600&X-Amz-SignedHeaders=host&X-Amz-Signature=a29f33770ab79bf523ccd4034d0752ac545f3c2a3b17baa1eb4e280cfdccfda5")
1819
private String uploadUrl;
1920

2021
/**
@@ -26,4 +27,12 @@ public class FilePresignedUrlRespVO {
2627
example = "https://test.yudao.iocoder.cn/758d3a5387507358c7236de4c8f96de1c7f5097ff6a7722b34772fb7b76b140f.png")
2728
private String url;
2829

30+
/**
31+
* 为什么要返回 path 字段?
32+
*
33+
* 前端上传完文件后,需要调用 createFile 记录下 path 路径
34+
*/
35+
@Schema(description = "文件路径", requiredMode = Schema.RequiredMode.REQUIRED, example = "xxx.png")
36+
private String path;
37+
2938
}

yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/file/FileUploadReqVO.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public class FileUploadReqVO {
1414
@NotNull(message = "文件附件不能为空")
1515
private MultipartFile file;
1616

17-
@Schema(description = "文件附件", example = "yudaoyuanma.png")
18-
private String path;
17+
@Schema(description = "文件目录", example = "XXX/YYY")
18+
private String directory;
1919

2020
}

0 commit comments

Comments
 (0)