Commit 28c818e9bcbb62eec212b417e059bdb480ade30e

Authored by puhui999
2 parents ed920f3f 4fb4af2c

Merge remote-tracking branch 'yudao/master-jdk17' into master-jdk17

# Conflicts:
#	yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java
Showing 118 changed files with 1495 additions and 581 deletions

Too many changes.

To preserve performance only 100 of 118 files are displayed.

README.md
1 1 <p align="center">
2   - <img src="https://img.shields.io/badge/Spring%20Boot-3.4.1-blue.svg" alt="Downloads">
  2 + <img src="https://img.shields.io/badge/Spring%20Boot-3.4.5-blue.svg" alt="Downloads">
3 3 <img src="https://img.shields.io/badge/Vue-3.2-blue.svg" alt="Downloads">
4 4 <img src="https://img.shields.io/github/license/YunaiV/ruoyi-vue-pro" alt="Downloads" />
5 5 </p>
... ... @@ -308,7 +308,7 @@
308 308  
309 309 | 框架 | 说明 | 版本 | 学习指南 |
310 310 |---------------------------------------------------------------------------------------------|------------------|----------------|----------------------------------------------------------------|
311   -| [Spring Boot](https://spring.io/projects/spring-boot) | 应用开发框架 | 3.4.1 | [文档](https://github.com/YunaiV/SpringBoot-Labs) |
  311 +| [Spring Boot](https://spring.io/projects/spring-boot) | 应用开发框架 | 3.4.5 | [文档](https://github.com/YunaiV/SpringBoot-Labs) |
312 312 | [MySQL](https://www.mysql.com/cn/) | 数据库服务器 | 5.7 / 8.0+ | |
313 313 | [Druid](https://github.com/alibaba/druid) | JDBC 连接池、监控组件 | 1.2.23 | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?yudao) |
314 314 | [MyBatis Plus](https://mp.baomidou.com/) | MyBatis 增强工具包 | 3.5.7 | [文档](http://www.iocoder.cn/Spring-Boot/MyBatis/?yudao) |
... ...
... ... @@ -42,7 +42,7 @@
42 42 <flatten-maven-plugin.version>1.6.0</flatten-maven-plugin.version>
43 43 <!-- 看看咋放到 bom 里 -->
44 44 <lombok.version>1.18.36</lombok.version>
45   - <spring.boot.version>3.4.1</spring.boot.version>
  45 + <spring.boot.version>3.4.5</spring.boot.version>
46 46 <mapstruct.version>1.6.3</mapstruct.version>
47 47 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
48 48 </properties>
... ...
sql/mysql/ruoyi-vue-pro.sql
... ... @@ -1055,7 +1055,7 @@ INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `st
1055 1055 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');
1056 1056 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');
1057 1057 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');
1059 1059 COMMIT;
1060 1060  
1061 1061 -- ----------------------------
... ...
yudao-dependencies/pom.xml
... ... @@ -17,14 +17,13 @@
17 17 <revision>2.4.2-SNAPSHOT</revision>
18 18 <flatten-maven-plugin.version>1.6.0</flatten-maven-plugin.version>
19 19 <!-- 统一依赖管理 -->
20   - <spring.boot.version>3.4.1</spring.boot.version>
  20 + <spring.boot.version>3.4.5</spring.boot.version>
21 21 <!-- Web 相关 -->
22   - <springdoc.version>2.7.0</springdoc.version>
  22 + <springdoc.version>2.8.3</springdoc.version>
23 23 <knife4j.version>4.6.0</knife4j.version>
24 24 <!-- DB 相关 -->
25 25 <druid.version>1.2.24</druid.version>
26   - <mybatis.version>3.5.17</mybatis.version>
27   - <mybatis-plus.version>3.5.9</mybatis-plus.version>
  26 + <mybatis-plus.version>3.5.10.1</mybatis-plus.version>
28 27 <dynamic-datasource.version>4.3.1</dynamic-datasource.version>
29 28 <mybatis-plus-join.version>1.4.13</mybatis-plus-join.version>
30 29 <easy-trans.version>3.0.6</easy-trans.version>
... ... @@ -39,7 +38,7 @@
39 38 <lock4j.version>2.2.7</lock4j.version>
40 39 <!-- 监控相关 -->
41 40 <skywalking.version>9.0.0</skywalking.version>
42   - <spring-boot-admin.version>3.4.1</spring-boot-admin.version>
  41 + <spring-boot-admin.version>3.4.5</spring-boot-admin.version>
43 42 <opentracing.version>0.33.0</opentracing.version>
44 43 <!-- Test 测试相关 -->
45 44 <podam.version>8.0.2.RELEASE</podam.version>
... ... @@ -48,7 +47,7 @@
48 47 <!-- Bpm 工作流相关 -->
49 48 <flowable.version>7.0.1</flowable.version>
50 49 <!-- 工具类相关 -->
51   - <captcha-plus.version>2.0.3</captcha-plus.version>
  50 + <anji-plus-captcha.version>1.4.0</anji-plus-captcha.version>
52 51 <jsoup.version>1.18.3</jsoup.version>
53 52 <lombok.version>1.18.36</lombok.version>
54 53 <mapstruct.version>1.6.3</mapstruct.version>
... ... @@ -71,9 +70,10 @@
71 70 <!-- 三方云服务相关 -->
72 71 <commons-io.version>2.17.0</commons-io.version>
73 72 <commons-compress.version>1.27.1</commons-compress.version>
74   - <aws-java-sdk-s3.version>1.12.777</aws-java-sdk-s3.version>
75   - <justauth.version>2.0.5</justauth.version>
76   - <jimureport.version>1.8.1</jimureport.version>
  73 + <awssdk.version>2.30.14</awssdk.version>
  74 + <justauth.version>1.16.7</justauth.version>
  75 + <justauth-starter.version>1.4.0</justauth-starter.version>
  76 + <jimureport.version>1.9.4</jimureport.version>
77 77 <weixin-java.version>4.7.2.B</weixin-java.version>
78 78 </properties>
79 79  
... ... @@ -174,11 +174,6 @@
174 174 <version>${druid.version}</version>
175 175 </dependency>
176 176 <dependency>
177   - <groupId>org.mybatis</groupId>
178   - <artifactId>mybatis</artifactId>
179   - <version>${mybatis.version}</version>
180   - </dependency>
181   - <dependency>
182 177 <groupId>com.baomidou</groupId>
183 178 <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
184 179 <version>${mybatis-plus.version}</version>
... ... @@ -534,9 +529,9 @@
534 529 </dependency>
535 530  
536 531 <dependency>
537   - <groupId>com.xingyuv</groupId>
538   - <artifactId>spring-boot-starter-captcha-plus</artifactId>
539   - <version>${captcha-plus.version}</version>
  532 + <groupId>com.anji-plus</groupId>
  533 + <artifactId>captcha-spring-boot-starter</artifactId> <!-- 验证码,一般用于登录使用 -->
  534 + <version>${anji-plus-captcha.version}</version>
540 535 </dependency>
541 536  
542 537 <dependency>
... ... @@ -553,21 +548,20 @@
553 548  
554 549 <!-- 三方云服务相关 -->
555 550 <dependency>
556   - <groupId>com.amazonaws</groupId>
557   - <artifactId>aws-java-sdk-s3</artifactId>
558   - <version>${aws-java-sdk-s3.version}</version>
  551 + <groupId>software.amazon.awssdk</groupId>
  552 + <artifactId>s3</artifactId>
  553 + <version>${awssdk.version}</version>
559 554 </dependency>
560 555  
561 556 <dependency>
562   - <groupId>com.xingyuv</groupId>
563   - <artifactId>spring-boot-starter-justauth</artifactId> <!-- 社交登陆(例如说,个人微信、企业微信等等) -->
  557 + <groupId>me.zhyd.oauth</groupId>
  558 + <artifactId>JustAuth</artifactId> <!-- 社交登陆(例如说,个人微信、企业微信等等) -->
564 559 <version>${justauth.version}</version>
565   - <exclusions>
566   - <exclusion>
567   - <groupId>cn.hutool</groupId>
568   - <artifactId>hutool-core</artifactId>
569   - </exclusion>
570   - </exclusions>
  560 + </dependency>
  561 + <dependency>
  562 + <groupId>com.xkcoding.justauth</groupId>
  563 + <artifactId>justauth-spring-boot-starter</artifactId>
  564 + <version>${justauth-starter.version}</version>
571 565 </dependency>
572 566  
573 567 <dependency>
... ... @@ -591,10 +585,15 @@
591 585 <groupId>org.jeecgframework.jimureport</groupId>
592 586 <artifactId>jimureport-spring-boot3-starter-fastjson2</artifactId>
593 587 <version>${jimureport.version}</version>
  588 + </dependency>
  589 + <dependency>
  590 + <groupId>org.jeecgframework.jimureport</groupId>
  591 + <artifactId>jimubi-spring-boot3-starter</artifactId>
  592 + <version>${jimureport.version}</version>
594 593 <exclusions>
595 594 <exclusion>
596   - <groupId>com.alibaba</groupId>
597   - <artifactId>druid</artifactId>
  595 + <groupId>com.github.jsqlparser</groupId>
  596 + <artifactId>jsqlparser</artifactId>
598 597 </exclusion>
599 598 </exclusions>
600 599 </dependency>
... ...
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java
... ... @@ -11,6 +11,7 @@ import java.util.function.*;
11 11 import java.util.stream.Collectors;
12 12 import java.util.stream.Stream;
13 13  
  14 +import static cn.hutool.core.convert.Convert.toCollection;
14 15 import static java.util.Arrays.asList;
15 16  
16 17 /**
... ... @@ -335,4 +336,17 @@ public class CollectionUtils {
335 336 return list.stream().filter(Objects::nonNull).flatMap(Collection::stream).collect(Collectors.toList());
336 337 }
337 338  
  339 + /**
  340 + * 转换为 LinkedHashSet
  341 + *
  342 + * @param <T> 元素类型
  343 + * @param elementType 集合中元素类型
  344 + * @param value 被转换的值
  345 + * @return {@link LinkedHashSet}
  346 + */
  347 + @SuppressWarnings("unchecked")
  348 + public static <T> LinkedHashSet<T> toLinkedHashSet(Class<T> elementType, Object value) {
  349 + return (LinkedHashSet<T>) toCollection(LinkedHashSet.class, elementType, value);
  350 + }
  351 +
338 352 }
339 353 \ No newline at end of file
... ...
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/io/FileUtils.java
1 1 package cn.iocoder.yudao.framework.common.util.io;
2 2  
3   -import cn.hutool.core.io.FileTypeUtil;
4 3 import cn.hutool.core.io.FileUtil;
5   -import cn.hutool.core.io.file.FileNameUtil;
6 4 import cn.hutool.core.util.IdUtil;
7   -import cn.hutool.core.util.StrUtil;
8   -import cn.hutool.crypto.digest.DigestUtil;
9 5 import lombok.SneakyThrows;
10 6  
11   -import java.io.ByteArrayInputStream;
12 7 import java.io.File;
13 8  
14 9 /**
... ... @@ -63,22 +58,4 @@ public class FileUtils {
63 58 return file;
64 59 }
65 60  
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   -
84 61 }
... ...
yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/config/YudaoRedisMQConsumerAutoConfiguration.java
... ... @@ -6,6 +6,7 @@ import cn.hutool.system.SystemUtil;
6 6 import cn.iocoder.yudao.framework.common.enums.DocumentEnum;
7 7 import cn.iocoder.yudao.framework.mq.redis.core.RedisMQTemplate;
8 8 import cn.iocoder.yudao.framework.mq.redis.core.job.RedisPendingMessageResendJob;
  9 +import cn.iocoder.yudao.framework.mq.redis.core.job.RedisStreamMessageCleanupJob;
9 10 import cn.iocoder.yudao.framework.mq.redis.core.pubsub.AbstractRedisChannelMessageListener;
10 11 import cn.iocoder.yudao.framework.mq.redis.core.stream.AbstractRedisStreamMessageListener;
11 12 import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration;
... ... @@ -74,6 +75,17 @@ public class YudaoRedisMQConsumerAutoConfiguration {
74 75 }
75 76  
76 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 +
  88 + /**
77 89 * 创建 Redis Stream 集群消费的容器
78 90 *
79 91 * 基础知识:<a href="https://www.geek-book.com/src/docs/redis/redis/redis.io/commands/xreadgroup.html">Redis Stream 的 xreadgroup 命令</a>
... ...
yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/core/job/RedisPendingMessageResendJob.java
... ... @@ -23,13 +23,13 @@ import java.util.Objects;
23 23 @AllArgsConstructor
24 24 public class RedisPendingMessageResendJob {
25 25  
26   - private static final String LOCK_KEY = "redis:pending:msg:lock";
  26 + private static final String LOCK_KEY = "redis:stream:pending-message-resend:lock";
27 27  
28 28 /**
29 29 * 消息超时时间,默认 5 分钟
30 30 *
31 31 * 1. 超时的消息才会被重新投递
32   - * 2. 由于定时任务 1 分钟一次,消息超时后不会被立即重投,极端情况下消息5分钟过期后,再等 1 分钟才会被扫瞄到
  32 + * 2. 由于定时任务 1 分钟一次,消息超时后不会被立即重投,极端情况下消息 5 分钟过期后,再等 1 分钟才会被扫瞄到
33 33 */
34 34 private static final int EXPIRE_TIME = 5 * 60;
35 35  
... ... @@ -39,7 +39,7 @@ public class RedisPendingMessageResendJob {
39 39 private final RedissonClient redissonClient;
40 40  
41 41 /**
42   - * 一分钟执行一次,这里选择每分钟的35秒执行,是为了避免整点任务过多的问题
  42 + * 一分钟执行一次,这里选择每分钟的 35 秒执行,是为了避免整点任务过多的问题
43 43 */
44 44 @Scheduled(cron = "35 * * * * ?")
45 45 public void messageResend() {
... ...
yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/core/job/RedisStreamMessageCleanupJob.java 0 → 100644
  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 +}
0 73 \ No newline at end of file
... ...
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/form/BpmTaskCandidateFormUserStrategy.java
1 1 package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.form;
2 2  
3   -import cn.hutool.core.convert.Convert;
4 3 import cn.hutool.core.lang.Assert;
  4 +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
5 5 import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy;
6 6 import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.user.BpmTaskCandidateUserStrategy;
7 7 import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
... ... @@ -33,7 +33,7 @@ public class BpmTaskCandidateFormUserStrategy implements BpmTaskCandidateStrateg
33 33 @Override
34 34 public Set<Long> calculateUsersByTask(DelegateExecution execution, String param) {
35 35 Object result = execution.getVariable(param);
36   - return Convert.toSet(Long.class, result);
  36 + return CollectionUtils.toLinkedHashSet(Long.class, result);
37 37 }
38 38  
39 39 @Override
... ... @@ -41,7 +41,7 @@ public class BpmTaskCandidateFormUserStrategy implements BpmTaskCandidateStrateg
41 41 String param, Long startUserId, String processDefinitionId,
42 42 Map<String, Object> processVariables) {
43 43 Object result = processVariables == null ? null : processVariables.get(param);
44   - return Convert.toSet(Long.class, result);
  44 + return CollectionUtils.toLinkedHashSet(Long.class, result);
45 45 }
46 46  
47 47 }
... ...
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/other/BpmTaskCandidateExpressionStrategy.java
1 1 package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.other;
2 2  
3   -import cn.hutool.core.convert.Convert;
  3 +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
4 4 import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy;
5 5 import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
6 6 import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
... ... @@ -37,7 +37,7 @@ public class BpmTaskCandidateExpressionStrategy implements BpmTaskCandidateStrat
37 37 @Override
38 38 public Set<Long> calculateUsersByTask(DelegateExecution execution, String param) {
39 39 Object result = FlowableUtils.getExpressionValue(execution, param);
40   - return Convert.toSet(Long.class, result);
  40 + return CollectionUtils.toLinkedHashSet(Long.class, result);
41 41 }
42 42  
43 43 @Override
... ... @@ -46,7 +46,7 @@ public class BpmTaskCandidateExpressionStrategy implements BpmTaskCandidateStrat
46 46 Map<String, Object> variables = processVariables == null ? new HashMap<>() : processVariables;
47 47 try {
48 48 Object result = FlowableUtils.getExpressionValue(variables, param);
49   - return Convert.toSet(Long.class, result);
  49 + return CollectionUtils.toLinkedHashSet(Long.class, result);
50 50 } catch (FlowableException ex) {
51 51 // 预测未运行的节点时候,表达式如果包含 execution 或者不存在的流程变量会抛异常,
52 52 log.warn("[calculateUsersByActivity][表达式({}) 变量({}) 解析报错", param, variables, ex);
... ...
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java
... ... @@ -6,7 +6,6 @@ import cn.hutool.core.lang.Assert;
6 6 import cn.hutool.core.util.*;
7 7 import cn.hutool.extra.spring.SpringUtil;
8 8 import cn.iocoder.yudao.framework.common.pojo.PageResult;
9   -import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
10 9 import cn.iocoder.yudao.framework.common.util.date.DateUtils;
11 10 import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
12 11 import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
... ... @@ -882,6 +881,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
882 881 return;
883 882 }
884 883 runExecutionIds.add(task.getExecutionId());
  884 +
885 885 // 判断是否分配给自己任务,因为会签任务,一个节点会有多个任务
886 886 if (isAssignUserTask(userId, task)) { // 情况一:自己的任务,进行 RETURN 标记
887 887 // 2.1.1 添加评论
... ...
yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/api/file/FileApi.java
1 1 package cn.iocoder.yudao.module.infra.api.file;
2 2  
  3 +import jakarta.validation.constraints.NotEmpty;
  4 +
3 5 /**
4 6 * 文件 API 接口
5 7 *
... ... @@ -14,28 +16,30 @@ public interface FileApi {
14 16 * @return 文件路径
15 17 */
16 18 default String createFile(byte[] content) {
17   - return createFile(null, null, content);
  19 + return createFile(content, null, null, null);
18 20 }
19 21  
20 22 /**
21 23 * 保存文件,并返回文件的访问路径
22 24 *
23   - * @param path 文件路径
24 25 * @param content 文件内容
  26 + * @param name 文件名称,允许空
25 27 * @return 文件路径
26 28 */
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);
29 31 }
30 32  
31 33 /**
32 34 * 保存文件,并返回文件的访问路径
33 35 *
34   - * @param name 文件名称
35   - * @param path 文件路径
36 36 * @param content 文件内容
  37 + * @param name 文件名称,允许空
  38 + * @param directory 目录,允许空
  39 + * @param type 文件的 MIME 类型,允许空
37 40 * @return 文件路径
38 41 */
39   - String createFile(String name, String path, byte[] content);
  42 + String createFile(@NotEmpty(message = "文件内容不能为空") byte[] content,
  43 + String name, String directory, String type);
40 44  
41 45 }
... ...
yudao-module-infra/yudao-module-infra-biz/pom.xml
... ... @@ -115,9 +115,10 @@
115 115 <groupId>com.jcraft</groupId>
116 116 <artifactId>jsch</artifactId> <!-- 文件客户端:解决 sftp 连接 -->
117 117 </dependency>
  118 + <!-- 文件客户端:解决阿里云、腾讯云、minio 等 S3 连接 -->
118 119 <dependency>
119   - <groupId>com.amazonaws</groupId>
120   - <artifactId>aws-java-sdk-s3</artifactId><!-- 文件客户端:解决阿里云、腾讯云、minio 等 S3 连接 -->
  120 + <groupId>software.amazon.awssdk</groupId>
  121 + <artifactId>s3</artifactId>
121 122 </dependency>
122 123  
123 124 <dependency>
... ...
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/api/file/FileApiImpl.java
1 1 package cn.iocoder.yudao.module.infra.api.file;
2 2  
3 3 import cn.iocoder.yudao.module.infra.service.file.FileService;
  4 +import jakarta.annotation.Resource;
4 5 import org.springframework.stereotype.Service;
5 6 import org.springframework.validation.annotation.Validated;
6 7  
7   -import jakarta.annotation.Resource;
8   -
9 8 /**
10 9 * 文件 API 实现类
11 10 *
... ... @@ -19,8 +18,8 @@ public class FileApiImpl implements FileApi {
19 18 private FileService fileService;
20 19  
21 20 @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);
24 23 }
25 24  
26 25 }
... ...
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java
... ... @@ -11,6 +11,7 @@ import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO;
11 11 import cn.iocoder.yudao.module.infra.service.file.FileService;
12 12 import io.swagger.v3.oas.annotations.Operation;
13 13 import io.swagger.v3.oas.annotations.Parameter;
  14 +import io.swagger.v3.oas.annotations.Parameters;
14 15 import io.swagger.v3.oas.annotations.tags.Tag;
15 16 import jakarta.annotation.Resource;
16 17 import jakarta.annotation.security.PermitAll;
... ... @@ -41,14 +42,21 @@ public class FileController {
41 42 @Operation(summary = "上传文件", description = "模式一:后端上传文件")
42 43 public CommonResult<String> uploadFile(FileUploadReqVO uploadReqVO) throws Exception {
43 44 MultipartFile file = uploadReqVO.getFile();
44   - String path = uploadReqVO.getPath();
45   - return success(fileService.createFile(file.getOriginalFilename(), path, IoUtil.readBytes(file.getInputStream())));
  45 + byte[] content = IoUtil.readBytes(file.getInputStream());
  46 + return success(fileService.createFile(content, file.getOriginalFilename(),
  47 + uploadReqVO.getDirectory(), file.getContentType()));
46 48 }
47 49  
48 50 @GetMapping("/presigned-url")
49 51 @Operation(summary = "获取文件预签名地址", description = "模式二:前端上传文件:用于前端直接上传七牛、阿里云 OSS 等文件存储器")
50   - public CommonResult<FilePresignedUrlRespVO> getFilePresignedUrl(@RequestParam("path") String path) throws Exception {
51   - return success(fileService.getFilePresignedUrl(path));
  52 + @Parameters({
  53 + @Parameter(name = "name", description = "文件名称", required = true),
  54 + @Parameter(name = "directory", description = "文件目录")
  55 + })
  56 + public CommonResult<FilePresignedUrlRespVO> getFilePresignedUrl(
  57 + @RequestParam("name") String name,
  58 + @RequestParam(value = "directory", required = false) String directory) {
  59 + return success(fileService.getFilePresignedUrl(name, directory));
52 60 }
53 61  
54 62 @PostMapping("/create")
... ...
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/file/FilePresignedUrlRespVO.java
... ... @@ -14,7 +14,8 @@ public class FilePresignedUrlRespVO {
14 14 @Schema(description = "配置编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "11")
15 15 private Long configId;
16 16  
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")
18 19 private String uploadUrl;
19 20  
20 21 /**
... ... @@ -26,4 +27,12 @@ public class FilePresignedUrlRespVO {
26 27 example = "https://test.yudao.iocoder.cn/758d3a5387507358c7236de4c8f96de1c7f5097ff6a7722b34772fb7b76b140f.png")
27 28 private String url;
28 29  
  30 + /**
  31 + * 为什么要返回 path 字段?
  32 + *
  33 + * 前端上传完文件后,需要调用 createFile 记录下 path 路径
  34 + */
  35 + @Schema(description = "文件路径", requiredMode = Schema.RequiredMode.REQUIRED, example = "xxx.png")
  36 + private String path;
  37 +
29 38 }
... ...
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/file/FileUploadReqVO.java
... ... @@ -14,7 +14,7 @@ public class FileUploadReqVO {
14 14 @NotNull(message = "文件附件不能为空")
15 15 private MultipartFile file;
16 16  
17   - @Schema(description = "文件附件", example = "yudaoyuanma.png")
18   - private String path;
  17 + @Schema(description = "文件目录", example = "XXX/YYY")
  18 + private String directory;
19 19  
20 20 }
... ...
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/app/file/AppFileController.java
... ... @@ -7,6 +7,8 @@ import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePresigned
7 7 import cn.iocoder.yudao.module.infra.controller.app.file.vo.AppFileUploadReqVO;
8 8 import cn.iocoder.yudao.module.infra.service.file.FileService;
9 9 import io.swagger.v3.oas.annotations.Operation;
  10 +import io.swagger.v3.oas.annotations.Parameter;
  11 +import io.swagger.v3.oas.annotations.Parameters;
10 12 import io.swagger.v3.oas.annotations.tags.Tag;
11 13 import jakarta.annotation.Resource;
12 14 import jakarta.annotation.security.PermitAll;
... ... @@ -33,15 +35,21 @@ public class AppFileController {
33 35 @PermitAll
34 36 public CommonResult<String> uploadFile(AppFileUploadReqVO uploadReqVO) throws Exception {
35 37 MultipartFile file = uploadReqVO.getFile();
36   - String path = uploadReqVO.getPath();
37   - return success(fileService.createFile(file.getOriginalFilename(), path, IoUtil.readBytes(file.getInputStream())));
  38 + byte[] content = IoUtil.readBytes(file.getInputStream());
  39 + return success(fileService.createFile(content, file.getOriginalFilename(),
  40 + uploadReqVO.getDirectory(), file.getContentType()));
38 41 }
39 42  
40 43 @GetMapping("/presigned-url")
41 44 @Operation(summary = "获取文件预签名地址", description = "模式二:前端上传文件:用于前端直接上传七牛、阿里云 OSS 等文件存储器")
42   - @PermitAll
43   - public CommonResult<FilePresignedUrlRespVO> getFilePresignedUrl(@RequestParam("path") String path) throws Exception {
44   - return success(fileService.getFilePresignedUrl(path));
  45 + @Parameters({
  46 + @Parameter(name = "name", description = "文件名称", required = true),
  47 + @Parameter(name = "directory", description = "文件目录")
  48 + })
  49 + public CommonResult<FilePresignedUrlRespVO> getFilePresignedUrl(
  50 + @RequestParam("name") String name,
  51 + @RequestParam(value = "directory", required = false) String directory) {
  52 + return success(fileService.getFilePresignedUrl(name, directory));
45 53 }
46 54  
47 55 @PostMapping("/create")
... ...
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/app/file/vo/AppFileUploadReqVO.java
... ... @@ -14,7 +14,7 @@ public class AppFileUploadReqVO {
14 14 @NotNull(message = "文件附件不能为空")
15 15 private MultipartFile file;
16 16  
17   - @Schema(description = "文件附件", example = "yudaoyuanma.png")
18   - private String path;
  17 + @Schema(description = "文件目录", example = "XXX/YYY")
  18 + private String directory;
19 19  
20 20 }
... ...
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/codegen/CodegenTableMapper.java
... ... @@ -23,8 +23,7 @@ public interface CodegenTableMapper extends BaseMapperX&lt;CodegenTableDO&gt; {
23 23 .likeIfPresent(CodegenTableDO::getTableComment, pageReqVO.getTableComment())
24 24 .likeIfPresent(CodegenTableDO::getClassName, pageReqVO.getClassName())
25 25 .betweenIfPresent(CodegenTableDO::getCreateTime, pageReqVO.getCreateTime())
26   - .orderByDesc(CodegenTableDO::getUpdateTime)
27   - );
  26 + .orderByDesc(CodegenTableDO::getUpdateTime));
28 27 }
29 28  
30 29 default List<CodegenTableDO> selectListByDataSourceConfigId(Long dataSourceConfigId) {
... ...
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/config/ConfigMapper.java
... ... @@ -19,7 +19,8 @@ public interface ConfigMapper extends BaseMapperX&lt;ConfigDO&gt; {
19 19 .likeIfPresent(ConfigDO::getName, reqVO.getName())
20 20 .likeIfPresent(ConfigDO::getConfigKey, reqVO.getKey())
21 21 .eqIfPresent(ConfigDO::getType, reqVO.getType())
22   - .betweenIfPresent(ConfigDO::getCreateTime, reqVO.getCreateTime()));
  22 + .betweenIfPresent(ConfigDO::getCreateTime, reqVO.getCreateTime())
  23 + .orderByDesc(ConfigDO::getId));
23 24 }
24 25  
25 26 }
... ...
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/job/JobMapper.java
... ... @@ -24,7 +24,7 @@ public interface JobMapper extends BaseMapperX&lt;JobDO&gt; {
24 24 .likeIfPresent(JobDO::getName, reqVO.getName())
25 25 .eqIfPresent(JobDO::getStatus, reqVO.getStatus())
26 26 .likeIfPresent(JobDO::getHandlerName, reqVO.getHandlerName())
27   - );
  27 + .orderByDesc(JobDO::getId));
28 28 }
29 29  
30 30 }
... ...
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/enums/codegen/CodegenFrontTypeEnum.java
... ... @@ -13,10 +13,14 @@ import lombok.Getter;
13 13 public enum CodegenFrontTypeEnum {
14 14  
15 15 VUE2_ELEMENT_UI(10), // Vue2 Element UI 标准模版
  16 +
16 17 VUE3_ELEMENT_PLUS(20), // Vue3 Element Plus 标准模版
  18 +
17 19 VUE3_VBEN2_ANTD_SCHEMA(30), // Vue3 VBEN2 + ANTD + Schema 模版
  20 +
18 21 VUE3_VBEN5_ANTD_SCHEMA(40), // Vue3 VBEN5 + ANTD + schema 模版
19   - VUE3_VBEN5_ANTD(50), // Vue3 VBEN5 + ANTD 模版
  22 +
  23 + VUE3_VBEN5_ANTD_GENERAL(41), // Vue3 VBEN5 + ANTD 标准模版
20 24 ;
21 25  
22 26 /**
... ...
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/ftp/FtpFileClient.java
... ... @@ -26,12 +26,6 @@ public class FtpFileClient extends AbstractFileClient&lt;FtpFileClientConfig&gt; {
26 26  
27 27 @Override
28 28 protected void doInit() {
29   - // 把配置的 \ 替换成 /, 如果路径配置 \a\test, 替换成 /a/test, 替换方法已经处理 null 情况
30   - config.setBasePath(StrUtil.replace(config.getBasePath(), StrUtil.BACKSLASH, StrUtil.SLASH));
31   - // ftp的路径是 / 结尾
32   - if (!config.getBasePath().endsWith(StrUtil.SLASH)) {
33   - config.setBasePath(config.getBasePath() + StrUtil.SLASH);
34   - }
35 29 // 初始化 Ftp 对象
36 30 this.ftp = new Ftp(config.getHost(), config.getPort(), config.getUsername(), config.getPassword(),
37 31 CharsetUtil.CHARSET_UTF_8, null, null, FtpMode.valueOf(config.getMode()));
... ... @@ -43,8 +37,8 @@ public class FtpFileClient extends AbstractFileClient&lt;FtpFileClientConfig&gt; {
43 37 String filePath = getFilePath(path);
44 38 String fileName = FileUtil.getName(filePath);
45 39 String dir = StrUtil.removeSuffix(filePath, fileName);
46   - ftp.reconnectIfTimeout();
47   - boolean success = ftp.upload(dir, fileName, new ByteArrayInputStream(content));
  40 + reconnectIfTimeout();
  41 + boolean success = ftp.upload(dir, fileName, new ByteArrayInputStream(content)); // 不需要主动创建目录,ftp 内部已经处理(见源码)
48 42 if (!success) {
49 43 throw new FtpException(StrUtil.format("上传文件到目标目录 ({}) 失败", filePath));
50 44 }
... ... @@ -55,7 +49,7 @@ public class FtpFileClient extends AbstractFileClient&lt;FtpFileClientConfig&gt; {
55 49 @Override
56 50 public void delete(String path) {
57 51 String filePath = getFilePath(path);
58   - ftp.reconnectIfTimeout();
  52 + reconnectIfTimeout();
59 53 ftp.delFile(filePath);
60 54 }
61 55  
... ... @@ -65,13 +59,17 @@ public class FtpFileClient extends AbstractFileClient&lt;FtpFileClientConfig&gt; {
65 59 String fileName = FileUtil.getName(filePath);
66 60 String dir = StrUtil.removeSuffix(filePath, fileName);
67 61 ByteArrayOutputStream out = new ByteArrayOutputStream();
68   - ftp.reconnectIfTimeout();
  62 + reconnectIfTimeout();
69 63 ftp.download(dir, fileName, out);
70 64 return out.toByteArray();
71 65 }
72 66  
73 67 private String getFilePath(String path) {
74   - return config.getBasePath() + path;
  68 + return config.getBasePath() + StrUtil.SLASH + path;
  69 + }
  70 +
  71 + private synchronized void reconnectIfTimeout() {
  72 + ftp.reconnectIfTimeout();
75 73 }
76 74  
77 75 }
78 76 \ No newline at end of file
... ...
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/local/LocalFileClient.java
... ... @@ -18,10 +18,6 @@ public class LocalFileClient extends AbstractFileClient&lt;LocalFileClientConfig&gt; {
18 18  
19 19 @Override
20 20 protected void doInit() {
21   - // 补全风格。例如说 Linux 是 /,Windows 是 \
22   - if (!config.getBasePath().endsWith(File.separator)) {
23   - config.setBasePath(config.getBasePath() + File.separator);
24   - }
25 21 }
26 22  
27 23 @Override
... ... @@ -46,7 +42,7 @@ public class LocalFileClient extends AbstractFileClient&lt;LocalFileClientConfig&gt; {
46 42 }
47 43  
48 44 private String getFilePath(String path) {
49   - return config.getBasePath() + path;
  45 + return config.getBasePath() + File.separator + path;
50 46 }
51 47  
52 48 }
... ...
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/S3FileClient.java
... ... @@ -4,29 +4,31 @@ import cn.hutool.core.io.IoUtil;
4 4 import cn.hutool.core.util.StrUtil;
5 5 import cn.hutool.http.HttpUtil;
6 6 import cn.iocoder.yudao.module.infra.framework.file.core.client.AbstractFileClient;
7   -import com.amazonaws.HttpMethod;
8   -import com.amazonaws.auth.AWSStaticCredentialsProvider;
9   -import com.amazonaws.auth.BasicAWSCredentials;
10   -import com.amazonaws.client.builder.AwsClientBuilder;
11   -import com.amazonaws.services.s3.AmazonS3Client;
12   -import com.amazonaws.services.s3.AmazonS3ClientBuilder;
13   -import com.amazonaws.services.s3.model.ObjectMetadata;
14   -import com.amazonaws.services.s3.model.S3Object;
  7 +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
  8 +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
  9 +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
  10 +import software.amazon.awssdk.core.sync.RequestBody;
  11 +import software.amazon.awssdk.regions.Region;
  12 +import software.amazon.awssdk.services.s3.S3Client;
  13 +import software.amazon.awssdk.services.s3.S3Configuration;
  14 +import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
  15 +import software.amazon.awssdk.services.s3.model.GetObjectRequest;
  16 +import software.amazon.awssdk.services.s3.model.PutObjectRequest;
  17 +import software.amazon.awssdk.services.s3.presigner.S3Presigner;
  18 +import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest;
15 19  
16   -import java.io.ByteArrayInputStream;
17   -import java.util.Date;
18   -import java.util.concurrent.TimeUnit;
  20 +import java.net.URI;
  21 +import java.time.Duration;
19 22  
20 23 /**
21 24 * 基于 S3 协议的文件客户端,实现 MinIO、阿里云、腾讯云、七牛云、华为云等云服务
22   - * <p>
23   - * S3 协议的客户端,采用亚马逊提供的 software.amazon.awssdk.s3 库
24 25 *
25 26 * @author 芋道源码
26 27 */
27 28 public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
28 29  
29   - private AmazonS3Client client;
  30 + private S3Client client;
  31 + private S3Presigner presigner;
30 32  
31 33 public S3FileClient(Long id, S3FileClientConfig config) {
32 34 super(id, config);
... ... @@ -38,31 +40,80 @@ public class S3FileClient extends AbstractFileClient&lt;S3FileClientConfig&gt; {
38 40 if (StrUtil.isEmpty(config.getDomain())) {
39 41 config.setDomain(buildDomain());
40 42 }
41   - // 初始化客户端
42   - client = (AmazonS3Client)AmazonS3ClientBuilder.standard()
43   - .withCredentials(buildCredentials())
44   - .withEndpointConfiguration(buildEndpointConfiguration())
  43 + // 初始化 S3 客户端
  44 + Region region = Region.of("us-east-1"); // 必须填,但填什么都行,常见的值有 "us-east-1",不填会报错
  45 + AwsCredentialsProvider credentialsProvider = StaticCredentialsProvider.create(
  46 + AwsBasicCredentials.create(config.getAccessKey(), config.getAccessSecret()));
  47 + URI endpoint = URI.create(buildEndpoint());
  48 + S3Configuration serviceConfiguration = S3Configuration.builder() // Path-style 访问
  49 + .pathStyleAccessEnabled(Boolean.TRUE.equals(config.getEnablePathStyleAccess()))
  50 + .chunkedEncodingEnabled(false) // 禁用分块编码,参见 https://t.zsxq.com/kBy57
  51 + .build();
  52 + client = S3Client.builder()
  53 + .credentialsProvider(credentialsProvider)
  54 + .region(region)
  55 + .endpointOverride(endpoint)
  56 + .serviceConfiguration(serviceConfiguration)
  57 + .build();
  58 + presigner = S3Presigner.builder()
  59 + .credentialsProvider(credentialsProvider)
  60 + .region(region)
  61 + .endpointOverride(endpoint)
  62 + .serviceConfiguration(serviceConfiguration)
45 63 .build();
46 64 }
47 65  
48   - /**
49   - * 基于 config 秘钥,构建 S3 客户端的认证信息
50   - *
51   - * @return S3 客户端的认证信息
52   - */
53   - private AWSStaticCredentialsProvider buildCredentials() {
54   - return new AWSStaticCredentialsProvider(
55   - new BasicAWSCredentials(config.getAccessKey(), config.getAccessSecret()));
  66 + @Override
  67 + public String upload(byte[] content, String path, String type) {
  68 + // 构造 PutObjectRequest
  69 + PutObjectRequest putRequest = PutObjectRequest.builder()
  70 + .bucket(config.getBucket())
  71 + .key(path)
  72 + .contentType(type)
  73 + .contentLength((long) content.length)
  74 + .build();
  75 + // 上传文件
  76 + client.putObject(putRequest, RequestBody.fromBytes(content));
  77 + // 拼接返回路径
  78 + return config.getDomain() + "/" + path;
  79 + }
  80 +
  81 + @Override
  82 + public void delete(String path) {
  83 + DeleteObjectRequest deleteRequest = DeleteObjectRequest.builder()
  84 + .bucket(config.getBucket())
  85 + .key(path)
  86 + .build();
  87 + client.deleteObject(deleteRequest);
  88 + }
  89 +
  90 + @Override
  91 + public byte[] getContent(String path) {
  92 + GetObjectRequest getRequest = GetObjectRequest.builder()
  93 + .bucket(config.getBucket())
  94 + .key(path)
  95 + .build();
  96 + return IoUtil.readBytes(client.getObject(getRequest));
  97 + }
  98 +
  99 + @Override
  100 + public FilePresignedUrlRespDTO getPresignedObjectUrl(String path) {
  101 + Duration expiration = Duration.ofHours(24);
  102 + return new FilePresignedUrlRespDTO(getPresignedUrl(path, expiration), config.getDomain() + "/" + path);
56 103 }
57 104  
58 105 /**
59   - * 构建 S3 客户端的 Endpoint 配置,包括 region、endpoint
  106 + * 生成动态的预签名上传 URL
60 107 *
61   - * @return S3 客户端的 EndpointConfiguration 配置
  108 + * @param path 相对路径
  109 + * @param expiration 过期时间
  110 + * @return 生成的上传 URL
62 111 */
63   - private AwsClientBuilder.EndpointConfiguration buildEndpointConfiguration() {
64   - return new AwsClientBuilder.EndpointConfiguration(config.getEndpoint(),
65   - null); // 无需设置 region
  112 + private String getPresignedUrl(String path, Duration expiration) {
  113 + return presigner.presignPutObject(PutObjectPresignRequest.builder()
  114 + .signatureDuration(expiration)
  115 + .putObjectRequest(b -> b.bucket(config.getBucket()).key(path))
  116 + .build()).url().toString();
66 117 }
67 118  
68 119 /**
... ... @@ -79,40 +130,17 @@ public class S3FileClient extends AbstractFileClient&lt;S3FileClientConfig&gt; {
79 130 return StrUtil.format("https://{}.{}", config.getBucket(), config.getEndpoint());
80 131 }
81 132  
82   - @Override
83   - public String upload(byte[] content, String path, String type) throws Exception {
84   - // 元数据,主要用于设置文件类型
85   - ObjectMetadata objectMetadata = new ObjectMetadata();
86   - objectMetadata.setContentType(type);
87   - objectMetadata.setContentLength(content.length); // 如果不设置,会有 “ No content length specified for stream data” 警告日志
88   - // 执行上传
89   - client.putObject(config.getBucket(),
90   - path, // 相对路径
91   - new ByteArrayInputStream(content), // 文件内容
92   - objectMetadata);
93   -
94   - // 拼接返回路径
95   - return config.getDomain() + "/" + path;
96   - }
97   -
98   - @Override
99   - public void delete(String path) throws Exception {
100   - client.deleteObject(config.getBucket(), path);
101   - }
102   -
103   - @Override
104   - public byte[] getContent(String path) throws Exception {
105   - S3Object tempS3Object = client.getObject(config.getBucket(), path);
106   - return IoUtil.readBytes(tempS3Object.getObjectContent());
107   - }
108   -
109   - @Override
110   - public FilePresignedUrlRespDTO getPresignedObjectUrl(String path) throws Exception {
111   - // 设定过期时间为 10 分钟。取值范围:1 秒 ~ 7 天
112   - Date expiration = new Date(System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(10));
113   - // 生成上传 URL
114   - String uploadUrl = String.valueOf(client.generatePresignedUrl(config.getBucket(), path, expiration , HttpMethod.PUT));
115   - return new FilePresignedUrlRespDTO(uploadUrl, config.getDomain() + "/" + path);
  133 + /**
  134 + * 节点地址补全协议头
  135 + *
  136 + * @return 节点地址
  137 + */
  138 + private String buildEndpoint() {
  139 + // 如果已经是 http 或者 https,则不进行拼接
  140 + if (HttpUtil.isHttp(config.getEndpoint()) || HttpUtil.isHttps(config.getEndpoint())) {
  141 + return config.getEndpoint();
  142 + }
  143 + return StrUtil.format("https://{}", config.getEndpoint());
116 144 }
117 145  
118 146 }
... ...
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/S3FileClientConfig.java
... ... @@ -67,6 +67,12 @@ public class S3FileClientConfig implements FileClientConfig {
67 67 @NotNull(message = "accessSecret 不能为空")
68 68 private String accessSecret;
69 69  
  70 + /**
  71 + * 是否启用 PathStyle 访问
  72 + */
  73 + @NotNull(message = "enablePathStyleAccess 不能为空")
  74 + private Boolean enablePathStyleAccess;
  75 +
70 76 @SuppressWarnings("RedundantIfStatement")
71 77 @AssertTrue(message = "domain 不能为空")
72 78 @JsonIgnore
... ...
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/sftp/SftpFileClient.java
... ... @@ -22,10 +22,6 @@ public class SftpFileClient extends AbstractFileClient&lt;SftpFileClientConfig&gt; {
22 22  
23 23 @Override
24 24 protected void doInit() {
25   - // 补全风格。例如说 Linux 是 /,Windows 是 \
26   - if (!config.getBasePath().endsWith(File.separator)) {
27   - config.setBasePath(config.getBasePath() + File.separator);
28   - }
29 25 // 初始化 Ftp 对象
30 26 this.sftp = new Sftp(config.getHost(), config.getPort(), config.getUsername(), config.getPassword());
31 27 }
... ... @@ -35,6 +31,8 @@ public class SftpFileClient extends AbstractFileClient&lt;SftpFileClientConfig&gt; {
35 31 // 执行写入
36 32 String filePath = getFilePath(path);
37 33 File file = FileUtils.createTempFile(content);
  34 + reconnectIfTimeout();
  35 + sftp.mkDirs(FileUtil.getParent(filePath, 1)); // 需要创建父目录,不然会报错
38 36 sftp.upload(filePath, file);
39 37 // 拼接返回路径
40 38 return super.formatFileUrl(config.getDomain(), path);
... ... @@ -43,6 +41,7 @@ public class SftpFileClient extends AbstractFileClient&lt;SftpFileClientConfig&gt; {
43 41 @Override
44 42 public void delete(String path) {
45 43 String filePath = getFilePath(path);
  44 + reconnectIfTimeout();
46 45 sftp.delFile(filePath);
47 46 }
48 47  
... ... @@ -50,12 +49,17 @@ public class SftpFileClient extends AbstractFileClient&lt;SftpFileClientConfig&gt; {
50 49 public byte[] getContent(String path) {
51 50 String filePath = getFilePath(path);
52 51 File destFile = FileUtils.createTempFile();
  52 + reconnectIfTimeout();
53 53 sftp.download(filePath, destFile);
54 54 return FileUtil.readBytes(destFile);
55 55 }
56 56  
57 57 private String getFilePath(String path) {
58   - return config.getBasePath() + path;
  58 + return config.getBasePath() + File.separator + path;
  59 + }
  60 +
  61 + private synchronized void reconnectIfTimeout() {
  62 + sftp.reconnectIfTimeout();
59 63 }
60 64  
61 65 }
... ...
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/utils/FileTypeUtils.java
... ... @@ -6,7 +6,10 @@ import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
6 6 import com.alibaba.ttl.TransmittableThreadLocal;
7 7 import jakarta.servlet.http.HttpServletResponse;
8 8 import lombok.SneakyThrows;
  9 +import lombok.extern.slf4j.Slf4j;
9 10 import org.apache.tika.Tika;
  11 +import org.apache.tika.mime.MimeTypeException;
  12 +import org.apache.tika.mime.MimeTypes;
10 13  
11 14 import java.io.IOException;
12 15  
... ... @@ -15,12 +18,13 @@ import java.io.IOException;
15 18 *
16 19 * @author 芋道源码
17 20 */
  21 +@Slf4j
18 22 public class FileTypeUtils {
19 23  
20 24 private static final ThreadLocal<Tika> TIKA = TransmittableThreadLocal.withInitial(Tika::new);
21 25  
22 26 /**
23   - * 获得文件的 mineType,对于doc,jar等文件会有误差
  27 + * 获得文件的 mineType,对于 doc,jar 等文件会有误差
24 28 *
25 29 * @param data 文件内容
26 30 * @return mineType 无法识别时会返回“application/octet-stream”
... ... @@ -31,7 +35,7 @@ public class FileTypeUtils {
31 35 }
32 36  
33 37 /**
34   - * 已知文件名,获取文件类型,在某些情况下比通过字节数组准确,例如使用jar文件时,通过名字更为准确
  38 + * 已知文件名,获取文件类型,在某些情况下比通过字节数组准确,例如使用 jar 文件时,通过名字更为准确
35 39 *
36 40 * @param name 文件名
37 41 * @return mineType 无法识别时会返回“application/octet-stream”
... ... @@ -52,6 +56,23 @@ public class FileTypeUtils {
52 56 }
53 57  
54 58 /**
  59 + * 根据 mineType 获得文件后缀
  60 + *
  61 + * 注意:如果获取不到,或者发生异常,都返回 null
  62 + *
  63 + * @param mineType 类型
  64 + * @return 后缀,例如说 .pdf
  65 + */
  66 + public static String getExtension(String mineType) {
  67 + try {
  68 + return MimeTypes.getDefaultMimeTypes().forName(mineType).getExtension();
  69 + } catch (MimeTypeException e) {
  70 + log.warn("[getExtension][获取文件后缀({}) 失败]", mineType, e);
  71 + return null;
  72 + }
  73 + }
  74 +
  75 + /**
55 76 * 返回附件
56 77 *
57 78 * @param response 响应
... ...
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java
... ... @@ -164,21 +164,21 @@ public class CodegenEngine {
164 164 .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_SCHEMA.getType(), vue3Vben5AntdSchemaTemplatePath("views/modules/list_sub_erp.vue"), // 特殊:主子表专属逻辑
165 165 vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-list.vue"))
166 166 // VUE3_VBEN5_ANTD
167   - .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD.getType(), vue3Vben5AntdGeneralTemplatePath("views/index.vue"),
  167 + .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("views/index.vue"),
168 168 vue3FilePath("views/${table.moduleName}/${table.businessName}/index.vue"))
169   - .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD.getType(), vue3Vben5AntdGeneralTemplatePath("views/form.vue"),
  169 + .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("views/form.vue"),
170 170 vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/form.vue"))
171   - .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD.getType(), vue3Vben5AntdGeneralTemplatePath("api/api.ts"),
  171 + .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("api/api.ts"),
172 172 vue3FilePath("api/${table.moduleName}/${table.businessName}/index.ts"))
173   - .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD.getType(), vue3Vben5AntdGeneralTemplatePath("views/modules/form_sub_normal.vue"), // 特殊:主子表专属逻辑
  173 + .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("views/modules/form_sub_normal.vue"), // 特殊:主子表专属逻辑
174 174 vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue"))
175   - .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD.getType(), vue3Vben5AntdGeneralTemplatePath("views/modules/form_sub_inner.vue"), // 特殊:主子表专属逻辑
  175 + .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("views/modules/form_sub_inner.vue"), // 特殊:主子表专属逻辑
176 176 vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue"))
177   - .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD.getType(), vue3Vben5AntdGeneralTemplatePath("views/modules/form_sub_erp.vue"), // 特殊:主子表专属逻辑
  177 + .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("views/modules/form_sub_erp.vue"), // 特殊:主子表专属逻辑
178 178 vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue"))
179   - .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD.getType(), vue3Vben5AntdGeneralTemplatePath("views/modules/list_sub_inner.vue"), // 特殊:主子表专属逻辑
  179 + .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("views/modules/list_sub_inner.vue"), // 特殊:主子表专属逻辑
180 180 vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-list.vue"))
181   - .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD.getType(), vue3Vben5AntdGeneralTemplatePath("views/modules/list_sub_erp.vue"), // 特殊:主子表专属逻辑
  181 + .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("views/modules/list_sub_erp.vue"), // 特殊:主子表专属逻辑
182 182 vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-list.vue"))
183 183 .build();
184 184  
... ...
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileService.java
... ... @@ -5,6 +5,7 @@ import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FileCreateReq
5 5 import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO;
6 6 import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePresignedUrlRespVO;
7 7 import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO;
  8 +import jakarta.validation.constraints.NotEmpty;
8 9  
9 10 /**
10 11 * 文件 Service 接口
... ... @@ -24,12 +25,24 @@ public interface FileService {
24 25 /**
25 26 * 保存文件,并返回文件的访问路径
26 27 *
27   - * @param name 文件名称
28   - * @param path 文件路径
29 28 * @param content 文件内容
  29 + * @param name 文件名称,允许空
  30 + * @param directory 目录,允许空
  31 + * @param type 文件的 MIME 类型,允许空
30 32 * @return 文件路径
31 33 */
32   - String createFile(String name, String path, byte[] content);
  34 + String createFile(@NotEmpty(message = "文件内容不能为空") byte[] content,
  35 + String name, String directory, String type);
  36 +
  37 + /**
  38 + * 生成文件预签名地址信息
  39 + *
  40 + * @param name 文件名
  41 + * @param directory 目录
  42 + * @return 预签名地址信息
  43 + */
  44 + FilePresignedUrlRespVO getFilePresignedUrl(@NotEmpty(message = "文件名不能为空") String name,
  45 + String directory);
33 46  
34 47 /**
35 48 * 创建文件
... ... @@ -55,12 +68,4 @@ public interface FileService {
55 68 */
56 69 byte[] getFileContent(Long configId, String path) throws Exception;
57 70  
58   - /**
59   - * 生成文件预签名地址信息
60   - *
61   - * @param path 文件路径
62   - * @return 预签名地址信息
63   - */
64   - FilePresignedUrlRespVO getFilePresignedUrl(String path) throws Exception;
65   -
66 71 }
... ...
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImpl.java
1 1 package cn.iocoder.yudao.module.infra.service.file;
2 2  
  3 +import cn.hutool.core.date.LocalDateTimeUtil;
  4 +import cn.hutool.core.io.FileUtil;
3 5 import cn.hutool.core.lang.Assert;
4 6 import cn.hutool.core.util.StrUtil;
  7 +import cn.hutool.crypto.digest.DigestUtil;
5 8 import cn.iocoder.yudao.framework.common.pojo.PageResult;
6   -import cn.iocoder.yudao.framework.common.util.io.FileUtils;
7 9 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
8   -import cn.iocoder.yudao.module.infra.framework.file.core.client.FileClient;
9   -import cn.iocoder.yudao.module.infra.framework.file.core.client.s3.FilePresignedUrlRespDTO;
10   -import cn.iocoder.yudao.module.infra.framework.file.core.utils.FileTypeUtils;
11 10 import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FileCreateReqVO;
12 11 import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO;
13 12 import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePresignedUrlRespVO;
14 13 import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO;
15 14 import cn.iocoder.yudao.module.infra.dal.mysql.file.FileMapper;
  15 +import cn.iocoder.yudao.module.infra.framework.file.core.client.FileClient;
  16 +import cn.iocoder.yudao.module.infra.framework.file.core.client.s3.FilePresignedUrlRespDTO;
  17 +import cn.iocoder.yudao.module.infra.framework.file.core.utils.FileTypeUtils;
  18 +import com.google.common.annotations.VisibleForTesting;
16 19 import jakarta.annotation.Resource;
17 20 import lombok.SneakyThrows;
18 21 import org.springframework.stereotype.Service;
19 22  
  23 +import static cn.hutool.core.date.DatePattern.PURE_DATE_PATTERN;
20 24 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
21 25 import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_NOT_EXISTS;
22 26  
... ... @@ -28,6 +32,20 @@ import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_NOT_EX
28 32 @Service
29 33 public class FileServiceImpl implements FileService {
30 34  
  35 + /**
  36 + * 上传文件的前缀,是否包含日期(yyyyMMdd)
  37 + *
  38 + * 目的:按照日期,进行分目录
  39 + */
  40 + static boolean PATH_PREFIX_DATE_ENABLE = true;
  41 + /**
  42 + * 上传文件的后缀,是否包含时间戳
  43 + *
  44 + * 目的:保证文件的唯一性,避免覆盖
  45 + * 定制:可按需调整成 UUID、或者其他方式
  46 + */
  47 + static boolean PATH_SUFFIX_TIMESTAMP_ENABLE = true;
  48 +
31 49 @Resource
32 50 private FileConfigService fileConfigService;
33 51  
... ... @@ -41,34 +59,82 @@ public class FileServiceImpl implements FileService {
41 59  
42 60 @Override
43 61 @SneakyThrows
44   - public String createFile(String name, String path, byte[] content) {
45   - // 计算默认的 path 名
46   - String type = FileTypeUtils.getMineType(content, name);
47   - if (StrUtil.isEmpty(path)) {
48   - path = FileUtils.generatePath(content, name);
  62 + public String createFile(byte[] content, String name, String directory, String type) {
  63 + // 1.1 处理 type 为空的情况
  64 + if (StrUtil.isEmpty(type)) {
  65 + type = FileTypeUtils.getMineType(content, name);
49 66 }
50   - // 如果 name 为空,则使用 path 填充
  67 + // 1.2 处理 name 为空的情况
51 68 if (StrUtil.isEmpty(name)) {
52   - name = path;
  69 + name = DigestUtil.sha256Hex(content);
  70 + }
  71 + if (StrUtil.isEmpty(FileUtil.extName(name))) {
  72 + // 如果 name 没有后缀 type,则补充后缀
  73 + String extension = FileTypeUtils.getExtension(type);
  74 + if (StrUtil.isNotEmpty(extension)) {
  75 + name = name + extension;
  76 + }
53 77 }
54 78  
55   - // 上传到文件存储器
  79 + // 2.1 生成上传的 path,需要保证唯一
  80 + String path = generateUploadPath(name, directory);
  81 + // 2.2 上传到文件存储器
56 82 FileClient client = fileConfigService.getMasterFileClient();
57 83 Assert.notNull(client, "客户端(master) 不能为空");
58 84 String url = client.upload(content, path, type);
59 85  
60   - // 保存到数据库
61   - FileDO file = new FileDO();
62   - file.setConfigId(client.getId());
63   - file.setName(name);
64   - file.setPath(path);
65   - file.setUrl(url);
66   - file.setType(type);
67   - file.setSize(content.length);
68   - fileMapper.insert(file);
  86 + // 3. 保存到数据库
  87 + fileMapper.insert(new FileDO().setConfigId(client.getId())
  88 + .setName(name).setPath(path).setUrl(url)
  89 + .setType(type).setSize(content.length));
69 90 return url;
70 91 }
71 92  
  93 + @VisibleForTesting
  94 + String generateUploadPath(String name, String directory) {
  95 + // 1. 生成前缀、后缀
  96 + String prefix = null;
  97 + if (PATH_PREFIX_DATE_ENABLE) {
  98 + prefix = LocalDateTimeUtil.format(LocalDateTimeUtil.now(), PURE_DATE_PATTERN);
  99 + }
  100 + String suffix = null;
  101 + if (PATH_SUFFIX_TIMESTAMP_ENABLE) {
  102 + suffix = String.valueOf(System.currentTimeMillis());
  103 + }
  104 +
  105 + // 2.1 先拼接 suffix 后缀
  106 + if (StrUtil.isNotEmpty(suffix)) {
  107 + String ext = FileUtil.extName(name);
  108 + if (StrUtil.isNotEmpty(ext)) {
  109 + name = FileUtil.mainName(name) + StrUtil.C_UNDERLINE + suffix + StrUtil.DOT + ext;
  110 + } else {
  111 + name = name + StrUtil.C_UNDERLINE + suffix;
  112 + }
  113 + }
  114 + // 2.2 再拼接 prefix 前缀
  115 + if (StrUtil.isNotEmpty(prefix)) {
  116 + name = prefix + StrUtil.SLASH + name;
  117 + }
  118 + // 2.3 最后拼接 directory 目录
  119 + if (StrUtil.isNotEmpty(directory)) {
  120 + name = directory + StrUtil.SLASH + name;
  121 + }
  122 + return name;
  123 + }
  124 +
  125 + @Override
  126 + @SneakyThrows
  127 + public FilePresignedUrlRespVO getFilePresignedUrl(String name, String directory) {
  128 + // 1. 生成上传的 path,需要保证唯一
  129 + String path = generateUploadPath(name, directory);
  130 +
  131 + // 2. 获取文件预签名地址
  132 + FileClient fileClient = fileConfigService.getMasterFileClient();
  133 + FilePresignedUrlRespDTO presignedObjectUrl = fileClient.getPresignedObjectUrl(path);
  134 + return BeanUtils.toBean(presignedObjectUrl, FilePresignedUrlRespVO.class,
  135 + object -> object.setConfigId(fileClient.getId()).setPath(path));
  136 + }
  137 +
72 138 @Override
73 139 public Long createFile(FileCreateReqVO createReqVO) {
74 140 FileDO file = BeanUtils.toBean(createReqVO, FileDO.class);
... ... @@ -105,12 +171,4 @@ public class FileServiceImpl implements FileService {
105 171 return client.getContent(path);
106 172 }
107 173  
108   - @Override
109   - public FilePresignedUrlRespVO getFilePresignedUrl(String path) throws Exception {
110   - FileClient fileClient = fileConfigService.getMasterFileClient();
111   - FilePresignedUrlRespDTO presignedObjectUrl = fileClient.getPresignedObjectUrl(path);
112   - return BeanUtils.toBean(presignedObjectUrl, FilePresignedUrlRespVO.class,
113   - object -> object.setConfigId(fileClient.getId()));
114   - }
115   -
116 174 }
... ...
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_vben5_antd/general/views/form.vue.vm
... ... @@ -147,7 +147,7 @@ const [Modal, modalApi] = useVbenModal({
147 147 key: 'action_process_msg',
148 148 });
149 149 } finally {
150   - modalApi.lock(false);
  150 + modalApi.unlock();
151 151 }
152 152 },
153 153 async onOpenChange(isOpen: boolean) {
... ... @@ -165,7 +165,7 @@ const [Modal, modalApi] = useVbenModal({
165 165 try {
166 166 data = await get${simpleClassName}(data.id);
167 167 } finally {
168   - modalApi.lock(false);
  168 + modalApi.unlock();
169 169 }
170 170 }
171 171 formData.value = data;
... ...
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_vben5_antd/general/views/modules/form_sub_erp.vue.vm
... ... @@ -24,6 +24,13 @@
24 24 });
25 25  
26 26 const [Form, formApi] = useVbenForm({
  27 + commonConfig: {
  28 + componentProps: {
  29 + class: 'w-full',
  30 + },
  31 + formItemClass: 'col-span-2',
  32 + labelWidth: 80,
  33 + },
27 34 layout: 'horizontal',
28 35 schema: use${subSimpleClassName}FormSchema(),
29 36 showDefaultActions: false
... ... @@ -50,7 +57,7 @@
50 57 key: 'action_process_msg',
51 58 });
52 59 } finally {
53   - modalApi.lock(false);
  60 + modalApi.unlock();
54 61 }
55 62 },
56 63 async onOpenChange(isOpen: boolean) {
... ... @@ -69,7 +76,7 @@
69 76 try {
70 77 data = await get${subSimpleClassName}(data.id);
71 78 } finally {
72   - modalApi.lock(false);
  79 + modalApi.unlock();
73 80 }
74 81 }
75 82 // 设置到 values
... ...
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_vben5_antd/general/views/modules/form_sub_normal.vue.vm
... ... @@ -96,9 +96,16 @@ watch(
96 96 );
97 97 #else
98 98 const [Form, formApi] = useVbenForm({
99   -layout: 'horizontal',
100   -schema: use${subSimpleClassName}FormSchema(),
101   -showDefaultActions: false
  99 + commonConfig: {
  100 + componentProps: {
  101 + class: 'w-full',
  102 + },
  103 + formItemClass: 'col-span-2',
  104 + labelWidth: 80,
  105 + },
  106 + layout: 'horizontal',
  107 + schema: use${subSimpleClassName}FormSchema(),
  108 + showDefaultActions: false
102 109 });
103 110  
104 111 /** 暴露出表单校验方法和表单值获取方法 */
... ...
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_vben5_antd/schema/views/data.ts.vm
1   -import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
2 1 import type { VbenFormSchema } from '#/adapter/form';
3   -import type { OnActionClickFn } from '#/adapter/vxe-table';
  2 +import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
4 3 import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
5 4  
6 5 import { z } from '#/adapter/form';
... ... @@ -40,7 +39,6 @@ export function useFormSchema(): VbenFormSchema[] {
40 39 });
41 40 return handleTree(data);
42 41 },
43   - class: 'w-full',
44 42 labelField: '${treeNameColumn.javaField}',
45 43 valueField: 'id',
46 44 childrenField: 'children',
... ... @@ -90,7 +88,6 @@ export function useFormSchema(): VbenFormSchema[] {
90 88 options: [],
91 89 #end
92 90 placeholder: '请选择${comment}',
93   - class: 'w-full',
94 91 },
95 92 #elseif($column.htmlType == "checkbox")## 多选框
96 93 component: 'Checkbox',
... ... @@ -128,7 +125,6 @@ export function useFormSchema(): VbenFormSchema[] {
128 125 component: 'InputNumber',
129 126 componentProps: {
130 127 min: 0,
131   - class: 'w-full',
132 128 controlsPosition: 'right',
133 129 placeholder: '请输入${comment}',
134 130 },
... ... @@ -326,7 +322,6 @@ export function use${subSimpleClassName}FormSchema(): VbenFormSchema[] {
326 322 options: [],
327 323 #end
328 324 placeholder: '请选择${comment}',
329   - class: 'w-full',
330 325 },
331 326 #elseif($column.htmlType == "checkbox")## 多选框
332 327 component: 'Checkbox',
... ... @@ -364,7 +359,6 @@ export function use${subSimpleClassName}FormSchema(): VbenFormSchema[] {
364 359 component: 'InputNumber',
365 360 componentProps: {
366 361 min: 0,
367   - class: 'w-full',
368 362 controlsPosition: 'right',
369 363 placeholder: '请输入${comment}',
370 364 },
... ... @@ -601,7 +595,6 @@ export function use${subSimpleClassName}GridColumns(
601 595 options: [],
602 596 #end
603 597 placeholder: '请选择${comment}',
604   - class: 'w-full',
605 598 },
606 599 #elseif($column.htmlType == "checkbox")## 多选框
607 600 component: 'Checkbox',
... ... @@ -639,7 +632,6 @@ export function use${subSimpleClassName}GridColumns(
639 632 component: 'InputNumber',
640 633 componentProps: {
641 634 min: 0,
642   - class: 'w-full',
643 635 controlsPosition: 'right',
644 636 placeholder: '请输入${comment}',
645 637 },
... ... @@ -682,4 +674,4 @@ export function use${subSimpleClassName}GridColumns(
682 674  
683 675 #end
684 676 #end
685   -#end
686 677 \ No newline at end of file
  678 +#end
... ...
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_vben5_antd/schema/views/form.vue.vm
... ... @@ -42,6 +42,13 @@ const getTitle = computed(() =&gt; {
42 42 #end
43 43  
44 44 const [Form, formApi] = useVbenForm({
  45 + commonConfig: {
  46 + componentProps: {
  47 + class: 'w-full',
  48 + },
  49 + formItemClass: 'col-span-2',
  50 + labelWidth: 80,
  51 + },
45 52 layout: 'horizontal',
46 53 schema: useFormSchema(),
47 54 showDefaultActions: false
... ... @@ -100,7 +107,7 @@ const [Modal, modalApi] = useVbenModal({
100 107 key: 'action_process_msg',
101 108 });
102 109 } finally {
103   - modalApi.lock(false);
  110 + modalApi.unlock();
104 111 }
105 112 },
106 113 async onOpenChange(isOpen: boolean) {
... ... @@ -118,7 +125,7 @@ const [Modal, modalApi] = useVbenModal({
118 125 try {
119 126 data = await get${simpleClassName}(data.id);
120 127 } finally {
121   - modalApi.lock(false);
  128 + modalApi.unlock();
122 129 }
123 130 }
124 131 // 设置到 values
... ...
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_vben5_antd/schema/views/modules/form_sub_erp.vue.vm
... ... @@ -24,6 +24,13 @@
24 24 });
25 25  
26 26 const [Form, formApi] = useVbenForm({
  27 + commonConfig: {
  28 + componentProps: {
  29 + class: 'w-full',
  30 + },
  31 + formItemClass: 'col-span-2',
  32 + labelWidth: 80,
  33 + },
27 34 layout: 'horizontal',
28 35 schema: use${subSimpleClassName}FormSchema(),
29 36 showDefaultActions: false
... ... @@ -50,7 +57,7 @@
50 57 key: 'action_process_msg',
51 58 });
52 59 } finally {
53   - modalApi.lock(false);
  60 + modalApi.unlock();
54 61 }
55 62 },
56 63 async onOpenChange(isOpen: boolean) {
... ... @@ -69,7 +76,7 @@
69 76 try {
70 77 data = await get${subSimpleClassName}(data.id);
71 78 } finally {
72   - modalApi.lock(false);
  79 + modalApi.unlock();
73 80 }
74 81 }
75 82 // 设置到 values
... ...
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_vben5_antd/schema/views/modules/form_sub_normal.vue.vm
... ... @@ -96,9 +96,16 @@ watch(
96 96 );
97 97 #else
98 98 const [Form, formApi] = useVbenForm({
99   -layout: 'horizontal',
100   -schema: use${subSimpleClassName}FormSchema(),
101   -showDefaultActions: false
  99 + commonConfig: {
  100 + componentProps: {
  101 + class: 'w-full',
  102 + },
  103 + formItemClass: 'col-span-2',
  104 + labelWidth: 80,
  105 + },
  106 + layout: 'horizontal',
  107 + schema: use${subSimpleClassName}FormSchema(),
  108 + showDefaultActions: false
102 109 });
103 110  
104 111 /** 暴露出表单校验方法和表单值获取方法 */
... ...
yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/framework/file/core/ftp/FtpFileClientTest.java
... ... @@ -8,8 +8,23 @@ import cn.iocoder.yudao.module.infra.framework.file.core.client.ftp.FtpFileClien
8 8 import org.junit.jupiter.api.Disabled;
9 9 import org.junit.jupiter.api.Test;
10 10  
  11 +/**
  12 + * {@link FtpFileClient} 集成测试
  13 + *
  14 + * @author 芋道源码
  15 + */
11 16 public class FtpFileClientTest {
12 17  
  18 +// docker run -d \
  19 +// -p 2121:21 -p 30000-30009:30000-30009 \
  20 +// -e FTP_USER=foo \
  21 +// -e FTP_PASS=pass \
  22 +// -e PASV_ADDRESS=127.0.0.1 \
  23 +// -e PASV_MIN_PORT=30000 \
  24 +// -e PASV_MAX_PORT=30009 \
  25 +// -v $(pwd)/ftp-data:/home/vsftpd \
  26 +// fauria/vsftpd
  27 +
13 28 @Test
14 29 @Disabled
15 30 public void test() {
... ... @@ -17,10 +32,10 @@ public class FtpFileClientTest {
17 32 FtpFileClientConfig config = new FtpFileClientConfig();
18 33 config.setDomain("http://127.0.0.1:48080");
19 34 config.setBasePath("/home/ftp");
20   - config.setHost("kanchai.club");
21   - config.setPort(221);
22   - config.setUsername("");
23   - config.setPassword("");
  35 + config.setHost("127.0.0.1");
  36 + config.setPort(2121);
  37 + config.setUsername("foo");
  38 + config.setPassword("pass");
24 39 config.setMode(FtpMode.Passive.name());
25 40 FtpFileClient client = new FtpFileClient(0L, config);
26 41 client.init();
... ...
yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/framework/file/core/sftp/SftpFileClientTest.java
... ... @@ -7,19 +7,29 @@ import cn.iocoder.yudao.module.infra.framework.file.core.client.sftp.SftpFileCli
7 7 import org.junit.jupiter.api.Disabled;
8 8 import org.junit.jupiter.api.Test;
9 9  
  10 +/**
  11 + * {@link SftpFileClient} 集成测试
  12 + *
  13 + * @author 芋道源码
  14 + */
10 15 public class SftpFileClientTest {
11 16  
  17 +// docker run -p 2222:22 -d \
  18 +// -v $(pwd)/sftp-data:/home/foo/upload \
  19 +// atmoz/sftp \
  20 +// foo:pass:1001
  21 +
12 22 @Test
13 23 @Disabled
14 24 public void test() {
15 25 // 创建客户端
16 26 SftpFileClientConfig config = new SftpFileClientConfig();
17 27 config.setDomain("http://127.0.0.1:48080");
18   - config.setBasePath("/home/ftp");
19   - config.setHost("kanchai.club");
20   - config.setPort(222);
21   - config.setUsername("");
22   - config.setPassword("");
  28 + config.setBasePath("/upload"); // 注意,这个是相对路径,不是实际 linux 上的路径!!!
  29 + config.setHost("127.0.0.1");
  30 + config.setPort(2222);
  31 + config.setUsername("foo");
  32 + config.setPassword("pass");
23 33 SftpFileClient client = new SftpFileClient(0L, config);
24 34 client.init();
25 35 // 上传文件
... ...
yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImplTest.java
... ... @@ -3,19 +3,20 @@ package cn.iocoder.yudao.module.infra.service.file;
3 3 import cn.hutool.core.io.resource.ResourceUtil;
4 4 import cn.iocoder.yudao.framework.common.pojo.PageResult;
5 5 import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
6   -import cn.iocoder.yudao.module.infra.framework.file.core.client.FileClient;
7 6 import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
8 7 import cn.iocoder.yudao.framework.test.core.util.AssertUtils;
9 8 import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO;
10 9 import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO;
11 10 import cn.iocoder.yudao.module.infra.dal.mysql.file.FileMapper;
  11 +import cn.iocoder.yudao.module.infra.framework.file.core.client.FileClient;
  12 +import jakarta.annotation.Resource;
  13 +import org.junit.jupiter.api.BeforeEach;
12 14 import org.junit.jupiter.api.Test;
13 15 import org.springframework.boot.test.mock.mockito.MockBean;
14 16 import org.springframework.context.annotation.Import;
15 17  
16   -import jakarta.annotation.Resource;
17   -
18 18 import java.time.LocalDateTime;
  19 +import java.util.concurrent.atomic.AtomicReference;
19 20  
20 21 import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildTime;
21 22 import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
... ... @@ -29,7 +30,7 @@ import static org.mockito.Mockito.*;
29 30 public class FileServiceImplTest extends BaseDbUnitTest {
30 31  
31 32 @Resource
32   - private FileService fileService;
  33 + private FileServiceImpl fileService;
33 34  
34 35 @Resource
35 36 private FileMapper fileMapper;
... ... @@ -37,6 +38,12 @@ public class FileServiceImplTest extends BaseDbUnitTest {
37 38 @MockBean
38 39 private FileConfigService fileConfigService;
39 40  
  41 + @BeforeEach
  42 + public void setUp() {
  43 + FileServiceImpl.PATH_PREFIX_DATE_ENABLE = true;
  44 + FileServiceImpl.PATH_SUFFIX_TIMESTAMP_ENABLE = true;
  45 + }
  46 +
40 47 @Test
41 48 public void testGetFilePage() {
42 49 // mock 数据
... ... @@ -70,28 +77,69 @@ public class FileServiceImplTest extends BaseDbUnitTest {
70 77 AssertUtils.assertPojoEquals(dbFile, pageResult.getList().get(0));
71 78 }
72 79  
  80 + /**
  81 + * content、name、directory、type 都非空
  82 + */
73 83 @Test
74   - public void testCreateFile_success() throws Exception {
  84 + public void testCreateFile_success_01() throws Exception {
75 85 // 准备参数
76   - String path = randomString();
77 86 byte[] content = ResourceUtil.readBytes("file/erweima.jpg");
  87 + String name = "单测文件名";
  88 + String directory = randomString();
  89 + String type = "image/jpeg";
78 90 // mock Master 文件客户端
79 91 FileClient client = mock(FileClient.class);
80 92 when(fileConfigService.getMasterFileClient()).thenReturn(client);
81 93 String url = randomString();
82   - when(client.upload(same(content), same(path), eq("image/jpeg"))).thenReturn(url);
  94 + AtomicReference<String> pathRef = new AtomicReference<>();
  95 + when(client.upload(same(content), argThat(path -> {
  96 + assertTrue(path.matches(directory + "/\\d{8}/" + name + "_\\d+.jpg"));
  97 + pathRef.set(path);
  98 + return true;
  99 + }), eq(type))).thenReturn(url);
83 100 when(client.getId()).thenReturn(10L);
84   - String name = "单测文件名";
85 101 // 调用
86   - String result = fileService.createFile(name, path, content);
  102 + String result = fileService.createFile(content, name, directory, type);
87 103 // 断言
88 104 assertEquals(result, url);
89 105 // 校验数据
90   - FileDO file = fileMapper.selectOne(FileDO::getPath, path);
  106 + FileDO file = fileMapper.selectOne(FileDO::getUrl, url);
91 107 assertEquals(10L, file.getConfigId());
92   - assertEquals(path, file.getPath());
  108 + assertEquals(pathRef.get(), file.getPath());
93 109 assertEquals(url, file.getUrl());
94   - assertEquals("image/jpeg", file.getType());
  110 + assertEquals(type, file.getType());
  111 + assertEquals(content.length, file.getSize());
  112 + }
  113 +
  114 + /**
  115 + * content 非空,其它都空
  116 + */
  117 + @Test
  118 + public void testCreateFile_success_02() throws Exception {
  119 + // 准备参数
  120 + byte[] content = ResourceUtil.readBytes("file/erweima.jpg");
  121 + // mock Master 文件客户端
  122 + String type = "image/jpeg";
  123 + FileClient client = mock(FileClient.class);
  124 + when(fileConfigService.getMasterFileClient()).thenReturn(client);
  125 + String url = randomString();
  126 + AtomicReference<String> pathRef = new AtomicReference<>();
  127 + when(client.upload(same(content), argThat(path -> {
  128 + assertTrue(path.matches("\\d{8}/6318848e882d8a7e7e82789d87608f684ee52d41966bfc8cad3ce15aad2b970e_\\d+\\.jpg"));
  129 + pathRef.set(path);
  130 + return true;
  131 + }), eq(type))).thenReturn(url);
  132 + when(client.getId()).thenReturn(10L);
  133 + // 调用
  134 + String result = fileService.createFile(content, null, null, null);
  135 + // 断言
  136 + assertEquals(result, url);
  137 + // 校验数据
  138 + FileDO file = fileMapper.selectOne(FileDO::getUrl, url);
  139 + assertEquals(10L, file.getConfigId());
  140 + assertEquals(pathRef.get(), file.getPath());
  141 + assertEquals(url, file.getUrl());
  142 + assertEquals(type, file.getType());
95 143 assertEquals(content.length, file.getSize());
96 144 }
97 145  
... ... @@ -140,4 +188,122 @@ public class FileServiceImplTest extends BaseDbUnitTest {
140 188 assertSame(result, content);
141 189 }
142 190  
  191 + @Test
  192 + public void testGenerateUploadPath_AllEnabled() {
  193 + // 准备参数
  194 + String name = "test.jpg";
  195 + String directory = "avatar";
  196 + FileServiceImpl.PATH_PREFIX_DATE_ENABLE = true;
  197 + FileServiceImpl.PATH_SUFFIX_TIMESTAMP_ENABLE = true;
  198 +
  199 + // 调用
  200 + String path = fileService.generateUploadPath(name, directory);
  201 +
  202 + // 断言
  203 + // 格式为:avatar/yyyyMMdd/test_timestamp.jpg
  204 + assertTrue(path.startsWith(directory + "/"));
  205 + // 包含日期格式:8 位数字,如 20240517
  206 + assertTrue(path.matches(directory + "/\\d{8}/test_\\d+\\.jpg"));
  207 + }
  208 +
  209 + @Test
  210 + public void testGenerateUploadPath_PrefixEnabled_SuffixDisabled() {
  211 + // 准备参数
  212 + String name = "test.jpg";
  213 + String directory = "avatar";
  214 + FileServiceImpl.PATH_PREFIX_DATE_ENABLE = true;
  215 + FileServiceImpl.PATH_SUFFIX_TIMESTAMP_ENABLE = false;
  216 +
  217 + // 调用
  218 + String path = fileService.generateUploadPath(name, directory);
  219 +
  220 + // 断言
  221 + // 格式为:avatar/yyyyMMdd/test.jpg
  222 + assertTrue(path.startsWith(directory + "/"));
  223 + // 包含日期格式:8 位数字,如 20240517
  224 + assertTrue(path.matches(directory + "/\\d{8}/test\\.jpg"));
  225 + }
  226 +
  227 + @Test
  228 + public void testGenerateUploadPath_PrefixDisabled_SuffixEnabled() {
  229 + // 准备参数
  230 + String name = "test.jpg";
  231 + String directory = "avatar";
  232 + FileServiceImpl.PATH_PREFIX_DATE_ENABLE = false;
  233 + FileServiceImpl.PATH_SUFFIX_TIMESTAMP_ENABLE = true;
  234 +
  235 + // 调用
  236 + String path = fileService.generateUploadPath(name, directory);
  237 +
  238 + // 断言
  239 + // 格式为:avatar/test_timestamp.jpg
  240 + assertTrue(path.startsWith(directory + "/"));
  241 + assertTrue(path.matches(directory + "/test_\\d+\\.jpg"));
  242 + }
  243 +
  244 + @Test
  245 + public void testGenerateUploadPath_AllDisabled() {
  246 + // 准备参数
  247 + String name = "test.jpg";
  248 + String directory = "avatar";
  249 + FileServiceImpl.PATH_PREFIX_DATE_ENABLE = false;
  250 + FileServiceImpl.PATH_SUFFIX_TIMESTAMP_ENABLE = false;
  251 +
  252 + // 调用
  253 + String path = fileService.generateUploadPath(name, directory);
  254 +
  255 + // 断言
  256 + // 格式为:avatar/test.jpg
  257 + assertEquals(directory + "/" + name, path);
  258 + }
  259 +
  260 + @Test
  261 + public void testGenerateUploadPath_NoExtension() {
  262 + // 准备参数
  263 + String name = "test";
  264 + String directory = "avatar";
  265 + FileServiceImpl.PATH_PREFIX_DATE_ENABLE = true;
  266 + FileServiceImpl.PATH_SUFFIX_TIMESTAMP_ENABLE = true;
  267 +
  268 + // 调用
  269 + String path = fileService.generateUploadPath(name, directory);
  270 +
  271 + // 断言
  272 + // 格式为:avatar/yyyyMMdd/test_timestamp
  273 + assertTrue(path.startsWith(directory + "/"));
  274 + assertTrue(path.matches(directory + "/\\d{8}/test_\\d+"));
  275 + }
  276 +
  277 + @Test
  278 + public void testGenerateUploadPath_DirectoryNull() {
  279 + // 准备参数
  280 + String name = "test.jpg";
  281 + String directory = null;
  282 + FileServiceImpl.PATH_PREFIX_DATE_ENABLE = true;
  283 + FileServiceImpl.PATH_SUFFIX_TIMESTAMP_ENABLE = true;
  284 +
  285 + // 调用
  286 + String path = fileService.generateUploadPath(name, directory);
  287 +
  288 + // 断言
  289 + // 格式为:yyyyMMdd/test_timestamp.jpg
  290 + assertTrue(path.matches("\\d{8}/test_\\d+\\.jpg"));
  291 + }
  292 +
  293 + @Test
  294 + public void testGenerateUploadPath_DirectoryEmpty() {
  295 + // 准备参数
  296 + String name = "test.jpg";
  297 + String directory = "";
  298 + FileServiceImpl.PATH_PREFIX_DATE_ENABLE = true;
  299 + FileServiceImpl.PATH_SUFFIX_TIMESTAMP_ENABLE = true;
  300 +
  301 + // 调用
  302 + String path = fileService.generateUploadPath(name, directory);
  303 +
  304 + // 断言
  305 + // 格式为:yyyyMMdd/test_timestamp.jpg
  306 + assertTrue(path.matches("\\d{8}/test_\\d+\\.jpg"));
  307 + }
  308 +
143 309 }
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwareCreateReqVO.java
... ... @@ -5,24 +5,22 @@ import jakarta.validation.constraints.NotEmpty;
5 5 import jakarta.validation.constraints.NotNull;
6 6 import lombok.Data;
7 7  
8   -import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
9   -
10 8 @Schema(description = "管理后台 - IoT OTA 固件创建 Request VO")
11 9 @Data
12 10 public class IotOtaFirmwareCreateReqVO {
13 11  
14   - @Schema(description = "固件名称", requiredMode = REQUIRED, example = "智能开关固件")
  12 + @Schema(description = "固件名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "智能开关固件")
15 13 @NotEmpty(message = "固件名称不能为空")
16 14 private String name;
17 15  
18 16 @Schema(description = "固件描述", example = "某品牌型号固件,测试用")
19 17 private String description;
20 18  
21   - @Schema(description = "版本号", requiredMode = REQUIRED, example = "1.0.0")
  19 + @Schema(description = "版本号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1.0.0")
22 20 @NotEmpty(message = "版本号不能为空")
23 21 private String version;
24 22  
25   - @Schema(description = "产品编号", requiredMode = REQUIRED, example = "1024")
  23 + @Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
26 24 @NotNull(message = "产品编号不能为空")
27 25 private String productId;
28 26  
... ... @@ -30,7 +28,7 @@ public class IotOtaFirmwareCreateReqVO {
30 28 // TODO @li:是不是必传哈
31 29 private String signMethod;
32 30  
33   - @Schema(description = "固件文件 URL", requiredMode = REQUIRED, example = "https://www.iocoder.cn/yudao-firmware.zip")
  31 + @Schema(description = "固件文件 URL", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/yudao-firmware.zip")
34 32 @NotEmpty(message = "固件文件 URL 不能为空")
35 33 private String fileUrl;
36 34  
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwareRespVO.java
... ... @@ -7,8 +7,6 @@ import com.fhs.core.trans.vo.VO;
7 7 import io.swagger.v3.oas.annotations.media.Schema;
8 8 import lombok.Data;
9 9  
10   -import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
11   -
12 10 @Data
13 11 @Schema(description = "管理后台 - IoT OTA 固件 Response VO")
14 12 public class IotOtaFirmwareRespVO implements VO {
... ... @@ -16,12 +14,12 @@ public class IotOtaFirmwareRespVO implements VO {
16 14 /**
17 15 * 固件编号
18 16 */
19   - @Schema(description = "固件编号", requiredMode = REQUIRED, example = "1024")
  17 + @Schema(description = "固件编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
20 18 private Long id;
21 19 /**
22 20 * 固件名称
23 21 */
24   - @Schema(description = "固件名称", requiredMode = REQUIRED, example = "OTA固件")
  22 + @Schema(description = "固件名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "OTA固件")
25 23 private String name;
26 24 /**
27 25 * 固件描述
... ... @@ -31,7 +29,7 @@ public class IotOtaFirmwareRespVO implements VO {
31 29 /**
32 30 * 版本号
33 31 */
34   - @Schema(description = "版本号", requiredMode = REQUIRED, example = "1.0.0")
  32 + @Schema(description = "版本号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1.0.0")
35 33 private String version;
36 34  
37 35 /**
... ... @@ -39,7 +37,7 @@ public class IotOtaFirmwareRespVO implements VO {
39 37 * <p>
40 38 * 关联 {@link cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO#getId()}
41 39 */
42   - @Schema(description = "产品编号", requiredMode = REQUIRED, example = "1024")
  40 + @Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
43 41 @Trans(type = TransType.SIMPLE, target = IotProductDO.class, fields = {"name"}, refs = {"productName"})
44 42 private String productId;
45 43 /**
... ... @@ -47,12 +45,12 @@ public class IotOtaFirmwareRespVO implements VO {
47 45 * <p>
48 46 * 冗余 {@link cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO#getProductKey()}
49 47 */
50   - @Schema(description = "产品标识", requiredMode = REQUIRED, example = "iot-product-key")
  48 + @Schema(description = "产品标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "iot-product-key")
51 49 private String productKey;
52 50 /**
53 51 * 产品名称
54 52 */
55   - @Schema(description = "产品名称", requiredMode = REQUIRED, example = "OTA产品")
  53 + @Schema(description = "产品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "OTA产品")
56 54 private String productName;
57 55 /**
58 56 * 签名方式
... ... @@ -69,12 +67,12 @@ public class IotOtaFirmwareRespVO implements VO {
69 67 /**
70 68 * 固件文件大小
71 69 */
72   - @Schema(description = "固件文件大小", requiredMode = REQUIRED, example = "1024")
  70 + @Schema(description = "固件文件大小", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
73 71 private Long fileSize;
74 72 /**
75 73 * 固件文件 URL
76 74 */
77   - @Schema(description = "固件文件 URL", requiredMode = REQUIRED, example = "https://www.iocoder.cn")
  75 + @Schema(description = "固件文件 URL", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn")
78 76 private String fileUrl;
79 77 /**
80 78 * 自定义信息,建议使用 JSON 格式
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwareUpdateReqVO.java
... ... @@ -5,18 +5,16 @@ import jakarta.validation.constraints.NotEmpty;
5 5 import jakarta.validation.constraints.NotNull;
6 6 import lombok.Data;
7 7  
8   -import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
9   -
10 8 @Schema(description = "管理后台 - IoT OTA 固件更新 Request VO")
11 9 @Data
12 10 public class IotOtaFirmwareUpdateReqVO {
13 11  
14   - @Schema(description = "固件编号", requiredMode = REQUIRED, example = "1024")
  12 + @Schema(description = "固件编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
15 13 @NotNull(message = "固件编号不能为空")
16 14 private Long id;
17 15  
18 16 // TODO @li:name 是不是可以飞必传哈
19   - @Schema(description = "固件名称", requiredMode = REQUIRED, example = "智能开关固件")
  17 + @Schema(description = "固件名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "智能开关固件")
20 18 @NotEmpty(message = "固件名称不能为空")
21 19 private String name;
22 20  
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/record/IotOtaUpgradeRecordPageReqVO.java
... ... @@ -5,8 +5,6 @@ import io.swagger.v3.oas.annotations.media.Schema;
5 5 import jakarta.validation.constraints.NotNull;
6 6 import lombok.Data;
7 7  
8   -import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
9   -
10 8 @Data
11 9 @Schema(description = "管理后台 - IoT OTA 升级记录分页 Request VO")
12 10 public class IotOtaUpgradeRecordPageReqVO extends PageParam {
... ... @@ -17,7 +15,7 @@ public class IotOtaUpgradeRecordPageReqVO extends PageParam {
17 15 * <p>
18 16 * 该字段用于标识升级任务的唯一编号,不能为空。
19 17 */
20   - @Schema(description = "升级任务编号", requiredMode = REQUIRED, example = "1024")
  18 + @Schema(description = "升级任务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
21 19 @NotNull(message = "升级任务编号不能为空")
22 20 private Long taskId;
23 21  
... ... @@ -26,7 +24,7 @@ public class IotOtaUpgradeRecordPageReqVO extends PageParam {
26 24 * <p>
27 25 * 该字段用于标识设备的名称,通常用于区分不同的设备。
28 26 */
29   - @Schema(description = "设备标识", requiredMode = REQUIRED, example = "摄像头A1-1")
  27 + @Schema(description = "设备标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "摄像头A1-1")
30 28 private String deviceName;
31 29  
32 30 }
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/record/IotOtaUpgradeRecordRespVO.java
... ... @@ -10,8 +10,6 @@ import lombok.Data;
10 10  
11 11 import java.time.LocalDateTime;
12 12  
13   -import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
14   -
15 13 @Data
16 14 @Schema(description = "管理后台 - IoT OTA 升级记录 Response VO")
17 15 public class IotOtaUpgradeRecordRespVO {
... ... @@ -19,73 +17,73 @@ public class IotOtaUpgradeRecordRespVO {
19 17 /**
20 18 * 升级记录编号
21 19 */
22   - @Schema(description = "升级记录编号", requiredMode = REQUIRED, example = "1024")
  20 + @Schema(description = "升级记录编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
23 21 private Long id;
24 22 /**
25 23 * 固件编号
26 24 * <p>
27 25 * 关联 {@link IotOtaFirmwareDO#getId()}
28 26 */
29   - @Schema(description = "固件编号", requiredMode = REQUIRED, example = "1024")
  27 + @Schema(description = "固件编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
30 28 @Trans(type = TransType.SIMPLE, target = IotOtaFirmwareDO.class, fields = {"version"}, refs = {"firmwareVersion"})
31 29 private Long firmwareId;
32 30 /**
33 31 * 固件版本
34 32 */
35   - @Schema(description = "固件版本", requiredMode = REQUIRED, example = "v1.0.0")
  33 + @Schema(description = "固件版本", requiredMode = Schema.RequiredMode.REQUIRED, example = "v1.0.0")
36 34 private String firmwareVersion;
37 35 /**
38 36 * 任务编号
39 37 * <p>
40 38 * 关联 {@link IotOtaUpgradeTaskDO#getId()}
41 39 */
42   - @Schema(description = "任务编号", requiredMode = REQUIRED, example = "1024")
  40 + @Schema(description = "任务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
43 41 private Long taskId;
44 42 /**
45 43 * 产品标识
46 44 * <p>
47 45 * 关联 {@link cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO#getId()}
48 46 */
49   - @Schema(description = "产品标识", requiredMode = REQUIRED, example = "iot")
  47 + @Schema(description = "产品标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "iot")
50 48 private String productKey;
51 49 /**
52 50 * 设备名称
53 51 * <p>
54 52 * 关联 {@link cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO#getId()}
55 53 */
56   - @Schema(description = "设备名称", requiredMode = REQUIRED, example = "iot")
  54 + @Schema(description = "设备名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "iot")
57 55 private String deviceName;
58 56 /**
59 57 * 设备编号
60 58 * <p>
61 59 * 关联 {@link cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO#getId()}
62 60 */
63   - @Schema(description = "设备编号", requiredMode = REQUIRED, example = "1024")
  61 + @Schema(description = "设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
64 62 private String deviceId;
65 63 /**
66 64 * 来源的固件编号
67 65 * <p>
68 66 * 关联 {@link IotDeviceDO#getFirmwareId()}
69 67 */
70   - @Schema(description = "来源的固件编号", requiredMode = REQUIRED, example = "1024")
  68 + @Schema(description = "来源的固件编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
71 69 @Trans(type = TransType.SIMPLE, target = IotOtaFirmwareDO.class, fields = {"version"}, refs = {"fromFirmwareVersion"})
72 70 private Long fromFirmwareId;
73 71 /**
74 72 * 来源的固件版本
75 73 */
76   - @Schema(description = "来源的固件版本", requiredMode = REQUIRED, example = "v1.0.0")
  74 + @Schema(description = "来源的固件版本", requiredMode = Schema.RequiredMode.REQUIRED, example = "v1.0.0")
77 75 private String fromFirmwareVersion;
78 76 /**
79 77 * 升级状态
80 78 * <p>
81 79 * 关联 {@link cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeRecordStatusEnum}
82 80 */
83   - @Schema(description = "升级状态", requiredMode = REQUIRED, allowableValues = {"0", "10", "20", "30", "40", "50"})
  81 + @Schema(description = "升级状态", requiredMode = Schema.RequiredMode.REQUIRED, allowableValues = {"0", "10", "20", "30", "40", "50"})
84 82 private Integer status;
85 83 /**
86 84 * 升级进度,百分比
87 85 */
88   - @Schema(description = "升级进度,百分比", requiredMode = REQUIRED, example = "10")
  86 + @Schema(description = "升级进度,百分比", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
89 87 private Integer progress;
90 88 /**
91 89 * 升级进度描述
... ... @@ -93,17 +91,17 @@ public class IotOtaUpgradeRecordRespVO {
93 91 * 注意,只记录设备最后一次的升级进度描述
94 92 * 如果想看历史记录,可以查看 {@link cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceLogDO} 设备日志
95 93 */
96   - @Schema(description = "升级进度描述", requiredMode = REQUIRED, example = "10")
  94 + @Schema(description = "升级进度描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
97 95 private String description;
98 96 /**
99 97 * 升级开始时间
100 98 */
101   - @Schema(description = "升级开始时间", requiredMode = REQUIRED, example = "2022-07-08 07:30:00")
  99 + @Schema(description = "升级开始时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2022-07-08 07:30:00")
102 100 private LocalDateTime startTime;
103 101 /**
104 102 * 升级结束时间
105 103 */
106   - @Schema(description = "升级结束时间", requiredMode = REQUIRED, example = "2022-07-08 07:30:00")
  104 + @Schema(description = "升级结束时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2022-07-08 07:30:00")
107 105 private LocalDateTime endTime;
108 106  
109 107 }
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/task/IotOtaUpgradeTaskPageReqVO.java
... ... @@ -5,8 +5,6 @@ import io.swagger.v3.oas.annotations.media.Schema;
5 5 import jakarta.validation.constraints.NotNull;
6 6 import lombok.Data;
7 7  
8   -import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
9   -
10 8 @Data
11 9 @Schema(description = "管理后台 - IoT OTA 升级任务分页 Request VO")
12 10 public class IotOtaUpgradeTaskPageReqVO extends PageParam {
... ... @@ -21,7 +19,7 @@ public class IotOtaUpgradeTaskPageReqVO extends PageParam {
21 19 * 固件编号字段,用于唯一标识固件,不能为空
22 20 */
23 21 @NotNull(message = "固件编号不能为空")
24   - @Schema(description = "固件编号", requiredMode = REQUIRED, example = "1024")
  22 + @Schema(description = "固件编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
25 23 private Long firmwareId;
26 24  
27 25 }
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/task/IotOtaUpgradeTaskRespVO.java
... ... @@ -9,8 +9,6 @@ import lombok.Data;
9 9 import java.time.LocalDateTime;
10 10 import java.util.List;
11 11  
12   -import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
13   -
14 12 @Data
15 13 @Schema(description = "管理后台 - IoT OTA 升级任务 Response VO")
16 14 public class IotOtaUpgradeTaskRespVO implements VO {
... ... @@ -18,12 +16,12 @@ public class IotOtaUpgradeTaskRespVO implements VO {
18 16 /**
19 17 * 任务编号
20 18 */
21   - @Schema(description = "任务编号", requiredMode = REQUIRED, example = "1024")
  19 + @Schema(description = "任务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
22 20 private Long id;
23 21 /**
24 22 * 任务名称
25 23 */
26   - @Schema(description = "任务名称", requiredMode = REQUIRED, example = "升级任务")
  24 + @Schema(description = "任务名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "升级任务")
27 25 private String name;
28 26 /**
29 27 * 任务描述
... ... @@ -35,31 +33,31 @@ public class IotOtaUpgradeTaskRespVO implements VO {
35 33 * <p>
36 34 * 关联 {@link IotOtaFirmwareDO#getId()}
37 35 */
38   - @Schema(description = "固件编号", requiredMode = REQUIRED, example = "1024")
  36 + @Schema(description = "固件编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
39 37 private Long firmwareId;
40 38 /**
41 39 * 任务状态
42 40 * <p>
43 41 * 关联 {@link cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeTaskStatusEnum}
44 42 */
45   - @Schema(description = "任务状态", requiredMode = REQUIRED, allowableValues = {"10", "20", "21", "30"})
  43 + @Schema(description = "任务状态", requiredMode = Schema.RequiredMode.REQUIRED, allowableValues = {"10", "20", "21", "30"})
46 44 private Integer status;
47 45 /**
48 46 * 任务状态名称
49 47 */
50   - @Schema(description = "任务状态名称", requiredMode = REQUIRED, example = "进行中")
  48 + @Schema(description = "任务状态名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "进行中")
51 49 private String statusName;
52 50 /**
53 51 * 升级范围
54 52 * <p>
55 53 * 关联 {@link cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeTaskScopeEnum}
56 54 */
57   - @Schema(description = "升级范围", requiredMode = REQUIRED, allowableValues = {"1", "2"})
  55 + @Schema(description = "升级范围", requiredMode = Schema.RequiredMode.REQUIRED, allowableValues = {"1", "2"})
58 56 private Integer scope;
59 57 /**
60 58 * 设备数量
61 59 */
62   - @Schema(description = "设备数量", requiredMode = REQUIRED, example = "1024")
  60 + @Schema(description = "设备数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
63 61 private Long deviceCount;
64 62 /**
65 63 * 选中的设备编号数组
... ... @@ -78,7 +76,7 @@ public class IotOtaUpgradeTaskRespVO implements VO {
78 76 /**
79 77 * 创建时间
80 78 */
81   - @Schema(description = "创建时间", requiredMode = REQUIRED, example = "2022-07-08 07:30:00")
  79 + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2022-07-08 07:30:00")
82 80 private LocalDateTime createTime;
83 81  
84 82 }
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/task/IotOtaUpgradeTaskSaveReqVO.java
... ... @@ -11,8 +11,6 @@ import lombok.Data;
11 11  
12 12 import java.util.List;
13 13  
14   -import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
15   -
16 14 @Data
17 15 @Schema(description = "管理后台 - IoT OTA 升级任务创建/修改 Request VO")
18 16 public class IotOtaUpgradeTaskSaveReqVO {
... ... @@ -24,7 +22,7 @@ public class IotOtaUpgradeTaskSaveReqVO {
24 22 * 任务名称
25 23 */
26 24 @NotEmpty(message = "任务名称不能为空")
27   - @Schema(description = "任务名称", requiredMode = REQUIRED, example = "升级任务")
  25 + @Schema(description = "任务名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "升级任务")
28 26 private String name;
29 27  
30 28 /**
... ... @@ -39,7 +37,7 @@ public class IotOtaUpgradeTaskSaveReqVO {
39 37 * 关联 {@link IotOtaFirmwareDO#getId()}
40 38 */
41 39 @NotNull(message = "固件编号不能为空")
42   - @Schema(description = "固件编号", requiredMode = REQUIRED, example = "1024")
  40 + @Schema(description = "固件编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
43 41 private Long firmwareId;
44 42  
45 43 /**
... ... @@ -49,7 +47,7 @@ public class IotOtaUpgradeTaskSaveReqVO {
49 47 */
50 48 @NotNull(message = "升级范围不能为空")
51 49 @InEnum(value = IotOtaUpgradeTaskScopeEnum.class)
52   - @Schema(description = "升级范围", requiredMode = REQUIRED, example = "1")
  50 + @Schema(description = "升级范围", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
53 51 private Integer scope;
54 52  
55 53 /**
... ... @@ -57,7 +55,7 @@ public class IotOtaUpgradeTaskSaveReqVO {
57 55 * <p>
58 56 * 关联 {@link IotDeviceDO#getId()}
59 57 */
60   - @Schema(description = "选中的设备编号数组", requiredMode = REQUIRED, example = "[1,2,3,4]")
  58 + @Schema(description = "选中的设备编号数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1,2,3,4]")
61 59 private List<Long> deviceIds;
62 60  
63 61 }
... ...
yudao-module-mall/yudao-module-product-api/src/main/java/cn/iocoder/yudao/module/product/api/sku/ProductSkuApi.java
... ... @@ -5,6 +5,9 @@ import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuUpdateStockReqDTO;
5 5  
6 6 import java.util.Collection;
7 7 import java.util.List;
  8 +import java.util.Map;
  9 +
  10 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
8 11  
9 12 /**
10 13 * 商品 SKU API 接口
... ... @@ -31,6 +34,16 @@ public interface ProductSkuApi {
31 34 List<ProductSkuRespDTO> getSkuList(Collection<Long> ids);
32 35  
33 36 /**
  37 + * 批量查询 SKU MAP
  38 + *
  39 + * @param ids SKU 编号列表
  40 + * @return SKU MAP
  41 + */
  42 + default Map<Long, ProductSkuRespDTO> getSkuMap(Collection<Long> ids) {
  43 + return convertMap(getSkuList(ids), ProductSkuRespDTO::getId);
  44 + }
  45 +
  46 + /**
34 47 * 批量查询 SKU 数组
35 48 *
36 49 * @param spuIds SPU 编号列表
... ...
yudao-module-mall/yudao-module-product-api/src/main/java/cn/iocoder/yudao/module/product/api/spu/ProductSpuApi.java
... ... @@ -30,7 +30,7 @@ public interface ProductSpuApi {
30 30 * @param ids SPU 编号列表
31 31 * @return SPU MAP
32 32 */
33   - default Map<Long, ProductSpuRespDTO> getSpusMap(Collection<Long> ids) {
  33 + default Map<Long, ProductSpuRespDTO> getSpuMap(Collection<Long> ids) {
34 34 return convertMap(getSpuList(ids), ProductSpuRespDTO::getId);
35 35 }
36 36  
... ...
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/history/vo/ProductBrowseHistoryRespVO.java
... ... @@ -4,17 +4,15 @@ import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
4 4 import io.swagger.v3.oas.annotations.media.Schema;
5 5 import lombok.Data;
6 6  
7   -import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
8   -
9 7 @Schema(description = "管理后台 - 商品浏览记录 Response VO")
10 8 @Data
11 9 @ExcelIgnoreUnannotated
12 10 public class ProductBrowseHistoryRespVO {
13 11  
14   - @Schema(description = "编号", requiredMode = REQUIRED, example = "1")
  12 + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
15 13 private Long id;
16 14  
17   - @Schema(description = "商品 SPU 编号", requiredMode = REQUIRED, example = "29502")
  15 + @Schema(description = "商品 SPU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "29502")
18 16 private Long spuId;
19 17  
20 18 // ========== 商品相关字段 ==========
... ... @@ -34,4 +32,4 @@ public class ProductBrowseHistoryRespVO {
34 32 @Schema(description = "库存", example = "100")
35 33 private Integer stock;
36 34  
37   -}
38 35 \ No newline at end of file
  36 +}
... ...
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/favorite/vo/AppFavoriteBatchReqVO.java
... ... @@ -6,13 +6,11 @@ import lombok.Data;
6 6 import jakarta.validation.constraints.NotEmpty;
7 7 import java.util.List;
8 8  
9   -import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
10   -
11 9 @Schema(description = "用户 APP - 商品收藏的批量 Request VO") // 用于收藏、取消收藏、获取收藏
12 10 @Data
13 11 public class AppFavoriteBatchReqVO {
14 12  
15   - @Schema(description = "商品 SPU 编号数组", requiredMode = REQUIRED, example = "29502")
  13 + @Schema(description = "商品 SPU 编号数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "29502")
16 14 @NotEmpty(message = "商品 SPU 编号数组不能为空")
17 15 private List<Long> spuIds;
18 16  
... ...
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/favorite/vo/AppFavoriteReqVO.java
... ... @@ -5,13 +5,11 @@ import lombok.Data;
5 5  
6 6 import jakarta.validation.constraints.NotNull;
7 7  
8   -import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
9   -
10 8 @Schema(description = "用户 APP - 商品收藏的单个 Request VO") // 用于收藏、取消收藏、获取收藏
11 9 @Data
12 10 public class AppFavoriteReqVO {
13 11  
14   - @Schema(description = "商品 SPU 编号", requiredMode = REQUIRED, example = "29502")
  12 + @Schema(description = "商品 SPU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "29502")
15 13 @NotNull(message = "商品 SPU 编号不能为空")
16 14 private Long spuId;
17 15  
... ...
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/favorite/vo/AppFavoriteRespVO.java
... ... @@ -2,16 +2,15 @@ package cn.iocoder.yudao.module.product.controller.app.favorite.vo;
2 2  
3 3 import io.swagger.v3.oas.annotations.media.Schema;
4 4 import lombok.Data;
5   -import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
6 5  
7 6 @Schema(description = "用户 App - 商品收藏 Response VO")
8 7 @Data
9 8 public class AppFavoriteRespVO {
10 9  
11   - @Schema(description = "编号", requiredMode = REQUIRED, example = "1")
  10 + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
12 11 private Long id;
13 12  
14   - @Schema(description = "商品 SPU 编号", requiredMode = REQUIRED, example = "29502")
  13 + @Schema(description = "商品 SPU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "29502")
15 14 private Long spuId;
16 15  
17 16 // ========== 商品相关字段 ==========
... ...
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/history/vo/AppProductBrowseHistoryDeleteReqVO.java
... ... @@ -6,13 +6,11 @@ import lombok.Data;
6 6  
7 7 import java.util.List;
8 8  
9   -import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
10   -
11 9 @Schema(description = "用户 APP - 删除商品浏览记录的 Request VO")
12 10 @Data
13 11 public class AppProductBrowseHistoryDeleteReqVO {
14 12  
15   - @Schema(description = "商品 SPU 编号数组", requiredMode = REQUIRED, example = "29502")
  13 + @Schema(description = "商品 SPU 编号数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "29502")
16 14 @NotEmpty(message = "商品 SPU 编号数组不能为空")
17 15 private List<Long> spuIds;
18 16  
... ...
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/history/vo/AppProductBrowseHistoryRespVO.java
... ... @@ -3,33 +3,31 @@ package cn.iocoder.yudao.module.product.controller.app.history.vo;
3 3 import io.swagger.v3.oas.annotations.media.Schema;
4 4 import lombok.Data;
5 5  
6   -import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
7   -
8 6 @Schema(description = "用户 App - 商品浏览记录 Response VO")
9 7 @Data
10 8 public class AppProductBrowseHistoryRespVO {
11 9  
12   - @Schema(description = "编号", requiredMode = REQUIRED, example = "1")
  10 + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
13 11 private Long id;
14 12  
15   - @Schema(description = "商品 SPU 编号", requiredMode = REQUIRED, example = "29502")
  13 + @Schema(description = "商品 SPU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "29502")
16 14 private Long spuId;
17 15  
18 16 // ========== 商品相关字段 ==========
19 17  
20   - @Schema(description = "商品 SPU 名称", requiredMode = REQUIRED, example = "赵六")
  18 + @Schema(description = "商品 SPU 名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "赵六")
21 19 private String spuName;
22 20  
23   - @Schema(description = "商品封面图", requiredMode = REQUIRED, example = "https://www.iocoder.cn/pic.png")
  21 + @Schema(description = "商品封面图", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/pic.png")
24 22 private String picUrl;
25 23  
26   - @Schema(description = "商品单价", requiredMode = REQUIRED, example = "50")
  24 + @Schema(description = "商品单价", requiredMode = Schema.RequiredMode.REQUIRED, example = "50")
27 25 private Integer price;
28 26  
29   - @Schema(description = "商品销量", requiredMode = REQUIRED, example = "60")
  27 + @Schema(description = "商品销量", requiredMode = Schema.RequiredMode.REQUIRED, example = "60")
30 28 private Integer salesCount;
31 29  
32   - @Schema(description = "库存", requiredMode = REQUIRED, example = "80")
  30 + @Schema(description = "库存", requiredMode = Schema.RequiredMode.REQUIRED, example = "80")
33 31 private Integer stock;
34 32  
35 33 }
... ...
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/sku/ProductSkuMapper.java
... ... @@ -6,6 +6,8 @@ import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
6 6 import cn.iocoder.yudao.module.product.dal.dataobject.sku.ProductSkuDO;
7 7 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
8 8 import org.apache.ibatis.annotations.Mapper;
  9 +import org.apache.ibatis.annotations.Param;
  10 +import org.apache.ibatis.annotations.Select;
9 11  
10 12 import java.util.Collection;
11 13 import java.util.List;
... ... @@ -13,6 +15,9 @@ import java.util.List;
13 15 @Mapper
14 16 public interface ProductSkuMapper extends BaseMapperX<ProductSkuDO> {
15 17  
  18 + @Select("SELECT * FROM product_sku WHERE id = #{id}")
  19 + ProductSkuDO selectByIdIncludeDeleted(@Param("id") Long id);
  20 +
16 21 default List<ProductSkuDO> selectListBySpuId(Long spuId) {
17 22 return selectList(ProductSkuDO::getSpuId, spuId);
18 23 }
... ...
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/spu/ProductSpuMapper.java
... ... @@ -11,6 +11,8 @@ import cn.iocoder.yudao.module.product.enums.ProductConstants;
11 11 import cn.iocoder.yudao.module.product.enums.spu.ProductSpuStatusEnum;
12 12 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
13 13 import org.apache.ibatis.annotations.Mapper;
  14 +import org.apache.ibatis.annotations.Param;
  15 +import org.apache.ibatis.annotations.Select;
14 16  
15 17 import java.util.Objects;
16 18 import java.util.Set;
... ... @@ -18,6 +20,9 @@ import java.util.Set;
18 20 @Mapper
19 21 public interface ProductSpuMapper extends BaseMapperX<ProductSpuDO> {
20 22  
  23 + @Select("SELECT * FROM product_spu WHERE id = #{id}")
  24 + ProductSpuDO selectByIdIncludeDeleted(@Param("id") Long id);
  25 +
21 26 /**
22 27 * 获取商品 SPU 分页列表数据
23 28 *
... ...
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/comment/ProductCommentServiceImpl.java
... ... @@ -91,7 +91,7 @@ public class ProductCommentServiceImpl implements ProductCommentService {
91 91 }
92 92  
93 93 private ProductSkuDO validateSku(Long skuId) {
94   - ProductSkuDO sku = productSkuService.getSku(skuId);
  94 + ProductSkuDO sku = productSkuService.getSku(skuId, true);
95 95 if (sku == null) {
96 96 throw exception(SKU_NOT_EXISTS);
97 97 }
... ... @@ -99,7 +99,7 @@ public class ProductCommentServiceImpl implements ProductCommentService {
99 99 }
100 100  
101 101 private ProductSpuDO validateSpu(Long spuId) {
102   - ProductSpuDO spu = productSpuService.getSpu(spuId);
  102 + ProductSpuDO spu = productSpuService.getSpu(spuId, true);
103 103 if (null == spu) {
104 104 throw exception(SPU_NOT_EXISTS);
105 105 }
... ...
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/sku/ProductSkuService.java
... ... @@ -30,6 +30,15 @@ public interface ProductSkuService {
30 30 ProductSkuDO getSku(Long id);
31 31  
32 32 /**
  33 + * 获得商品 SKU 信息
  34 + *
  35 + * @param id 编号
  36 + * @param includeDeleted 是否包含已删除的
  37 + * @return 商品 SKU 信息
  38 + */
  39 + ProductSkuDO getSku(Long id, boolean includeDeleted);
  40 +
  41 + /**
33 42 * 获得商品 SKU 列表
34 43 *
35 44 * @param ids 编号
... ...
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/sku/ProductSkuServiceImpl.java
... ... @@ -69,6 +69,14 @@ public class ProductSkuServiceImpl implements ProductSkuService {
69 69 }
70 70  
71 71 @Override
  72 + public ProductSkuDO getSku(Long id, boolean includeDeleted) {
  73 + if (includeDeleted) {
  74 + return productSkuMapper.selectByIdIncludeDeleted(id);
  75 + }
  76 + return getSku(id);
  77 + }
  78 +
  79 + @Override
72 80 public List<ProductSkuDO> getSkuList(Collection<Long> ids) {
73 81 if (CollUtil.isEmpty(ids)) {
74 82 return ListUtil.empty();
... ...
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/spu/ProductSpuService.java
... ... @@ -52,6 +52,15 @@ public interface ProductSpuService {
52 52 ProductSpuDO getSpu(Long id);
53 53  
54 54 /**
  55 + * 获得商品 SPU
  56 + *
  57 + * @param id 编号
  58 + * @param includeDeleted 是否包含已删除的
  59 + * @return 商品 SPU
  60 + */
  61 + ProductSpuDO getSpu(Long id, boolean includeDeleted);
  62 +
  63 + /**
55 64 * 获得商品 SPU 列表
56 65 *
57 66 * @param ids 编号数组
... ...
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/spu/ProductSpuServiceImpl.java
... ... @@ -190,6 +190,14 @@ public class ProductSpuServiceImpl implements ProductSpuService {
190 190 }
191 191  
192 192 @Override
  193 + public ProductSpuDO getSpu(Long id, boolean includeDeleted) {
  194 + if (includeDeleted) {
  195 + return productSpuMapper.selectByIdIncludeDeleted(id);
  196 + }
  197 + return getSpu(id);
  198 + }
  199 +
  200 + @Override
193 201 public List<ProductSpuDO> getSpuList(Collection<Long> ids) {
194 202 if (CollUtil.isEmpty(ids)) {
195 203 return Collections.emptyList();
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/point/PointActivityController.java
... ... @@ -124,7 +124,7 @@ public class PointActivityController {
124 124 List<PointProductDO> products = pointActivityService.getPointProductListByActivityIds(
125 125 convertSet(activityList, PointActivityDO::getId));
126 126 Map<Long, List<PointProductDO>> productsMap = convertMultiMap(products, PointProductDO::getActivityId);
127   - Map<Long, ProductSpuRespDTO> spuMap = productSpuApi.getSpusMap(
  127 + Map<Long, ProductSpuRespDTO> spuMap = productSpuApi.getSpuMap(
128 128 convertSet(activityList, PointActivityDO::getSpuId));
129 129 List<PointActivityRespVO> result = BeanUtils.toBean(activityList, PointActivityRespVO.class);
130 130 result.forEach(activity -> {
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/coupon/vo/template/AppCouponTemplateRespVO.java
1 1 package cn.iocoder.yudao.module.promotion.controller.app.coupon.vo.template;
2 2  
3 3 import io.swagger.v3.oas.annotations.media.Schema;
  4 +import jakarta.validation.constraints.Min;
4 5 import lombok.Data;
5 6  
6   -import jakarta.validation.constraints.Min;
7 7 import java.time.LocalDateTime;
8 8 import java.util.List;
9 9  
... ... @@ -20,6 +20,9 @@ public class AppCouponTemplateRespVO {
20 20 @Schema(description = "优惠券说明", example = "优惠券使用说明")
21 21 private String description;
22 22  
  23 + @Schema(description = "发行总量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") // -1 - 则表示不限制发放数量
  24 + private Integer totalCount;
  25 +
23 26 @Schema(description = "每人限领个数", requiredMode = Schema.RequiredMode.REQUIRED, example = "66") // -1 - 则表示不限制
24 27 private Integer takeLimitCount;
25 28  
... ... @@ -62,6 +65,9 @@ public class AppCouponTemplateRespVO {
62 65 @Schema(description = "折扣上限", example = "100") // 单位:分,仅在 discountType 为 PERCENT 使用
63 66 private Integer discountLimitPrice;
64 67  
  68 + @Schema(description = "领取优惠券的数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  69 + private Integer takeCount;
  70 +
65 71 // ========== 用户相关字段 ==========
66 72  
67 73 @Schema(description = "是否可以领取", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/point/AppPointActivityController.java
... ... @@ -104,7 +104,7 @@ public class AppPointActivityController {
104 104 List<PointProductDO> products = pointActivityService.getPointProductListByActivityIds(
105 105 convertSet(activityList, PointActivityDO::getId));
106 106 Map<Long, List<PointProductDO>> productsMap = convertMultiMap(products, PointProductDO::getActivityId);
107   - Map<Long, ProductSpuRespDTO> spuMap = productSpuApi.getSpusMap(
  107 + Map<Long, ProductSpuRespDTO> spuMap = productSpuApi.getSpuMap(
108 108 convertSet(activityList, PointActivityDO::getSpuId));
109 109 List<AppPointActivityRespVO> result = BeanUtils.toBean(activityList, AppPointActivityRespVO.class);
110 110 result.forEach(activity -> {
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java
... ... @@ -22,6 +22,7 @@ import cn.iocoder.yudao.module.promotion.enums.coupon.CouponTemplateValidityType
22 22 import jakarta.annotation.Resource;
23 23 import lombok.extern.slf4j.Slf4j;
24 24 import org.springframework.stereotype.Service;
  25 +import org.springframework.transaction.annotation.Propagation;
25 26 import org.springframework.transaction.annotation.Transactional;
26 27 import org.springframework.validation.annotation.Validated;
27 28  
... ... @@ -180,7 +181,7 @@ public class CouponServiceImpl implements CouponService {
180 181 * @param couponId 模版编号
181 182 * @param userId 用户编号
182 183 */
183   - @Transactional(rollbackFor = Exception.class)
  184 + @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW) // 每次调用开启一个新的事务,避免在一个大的事务里面
184 185 public void invalidateCoupon(Long couponId, Long userId) {
185 186 if (couponId == null || couponId <= 0) {
186 187 return;
... ... @@ -270,13 +271,17 @@ public class CouponServiceImpl implements CouponService {
270 271 if (CollUtil.isEmpty(userIds)) {
271 272 throw exception(COUPON_TEMPLATE_USER_ALREADY_TAKE);
272 273 }
273   -
274 274 // 校验模板
275 275 if (couponTemplate == null) {
276 276 throw exception(COUPON_TEMPLATE_NOT_EXISTS);
277 277 }
278   - // 校验剩余数量
279   - if (ObjUtil.notEqual(couponTemplate.getTakeLimitCount(), CouponTemplateDO.TIME_LIMIT_COUNT_MAX) // 非不限制
  278 + // 校验领取方式
  279 + if (ObjUtil.notEqual(couponTemplate.getTakeType(), takeType.getType())) {
  280 + throw exception(COUPON_TEMPLATE_CANNOT_TAKE);
  281 + }
  282 + // 校验发放数量不能过小(仅在 CouponTakeTypeEnum.USER 用户领取时)
  283 + if (CouponTakeTypeEnum.isUser(couponTemplate.getTakeType())
  284 + && ObjUtil.notEqual(couponTemplate.getTakeLimitCount(), CouponTemplateDO.TIME_LIMIT_COUNT_MAX) // 非不限制
280 285 && couponTemplate.getTakeCount() + userIds.size() > couponTemplate.getTotalCount()) {
281 286 throw exception(COUPON_TEMPLATE_NOT_ENOUGH);
282 287 }
... ... @@ -286,10 +291,6 @@ public class CouponServiceImpl implements CouponService {
286 291 throw exception(COUPON_TEMPLATE_EXPIRED);
287 292 }
288 293 }
289   - // 校验领取方式
290   - if (ObjectUtil.notEqual(couponTemplate.getTakeType(), takeType.getType())) {
291   - throw exception(COUPON_TEMPLATE_CANNOT_TAKE);
292   - }
293 294 }
294 295  
295 296 /**
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/aftersale/AppAfterSaleController.java
1 1 package cn.iocoder.yudao.module.trade.controller.app.aftersale;
2 2  
3 3 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
4   -import cn.iocoder.yudao.framework.common.pojo.PageParam;
5 4 import cn.iocoder.yudao.framework.common.pojo.PageResult;
  5 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
6 6 import cn.iocoder.yudao.module.trade.controller.app.aftersale.vo.AppAfterSaleCreateReqVO;
7 7 import cn.iocoder.yudao.module.trade.controller.app.aftersale.vo.AppAfterSaleDeliveryReqVO;
  8 +import cn.iocoder.yudao.module.trade.controller.app.aftersale.vo.AppAfterSalePageReqVO;
8 9 import cn.iocoder.yudao.module.trade.controller.app.aftersale.vo.AppAfterSaleRespVO;
9   -import cn.iocoder.yudao.module.trade.convert.aftersale.AfterSaleConvert;
  10 +import cn.iocoder.yudao.module.trade.dal.dataobject.aftersale.AfterSaleDO;
10 11 import cn.iocoder.yudao.module.trade.service.aftersale.AfterSaleService;
11 12 import io.swagger.v3.oas.annotations.Operation;
12 13 import io.swagger.v3.oas.annotations.Parameter;
... ... @@ -31,16 +32,17 @@ public class AppAfterSaleController {
31 32  
32 33 @GetMapping(value = "/page")
33 34 @Operation(summary = "获得售后分页")
34   - public CommonResult<PageResult<AppAfterSaleRespVO>> getAfterSalePage(PageParam pageParam) {
35   - return success(AfterSaleConvert.INSTANCE.convertPage02(
36   - afterSaleService.getAfterSalePage(getLoginUserId(), pageParam)));
  35 + public CommonResult<PageResult<AppAfterSaleRespVO>> getAfterSalePage(AppAfterSalePageReqVO pageReqVO) {
  36 + PageResult<AfterSaleDO> pageResult = afterSaleService.getAfterSalePage(getLoginUserId(), pageReqVO);
  37 + return success(BeanUtils.toBean(pageResult, AppAfterSaleRespVO.class));
37 38 }
38 39  
39 40 @GetMapping(value = "/get")
40 41 @Operation(summary = "获得售后订单")
41 42 @Parameter(name = "id", description = "售后编号", required = true, example = "1")
42 43 public CommonResult<AppAfterSaleRespVO> getAfterSale(@RequestParam("id") Long id) {
43   - return success(AfterSaleConvert.INSTANCE.convert(afterSaleService.getAfterSale(getLoginUserId(), id)));
  44 + AfterSaleDO afterSale = afterSaleService.getAfterSale(getLoginUserId(), id);
  45 + return success(BeanUtils.toBean(afterSale, AppAfterSaleRespVO.class));
44 46 }
45 47  
46 48 @PostMapping(value = "/create")
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/aftersale/vo/AppAfterSalePageReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.trade.controller.app.aftersale.vo;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  4 +import io.swagger.v3.oas.annotations.media.Schema;
  5 +import lombok.Data;
  6 +import lombok.EqualsAndHashCode;
  7 +import lombok.ToString;
  8 +
  9 +import java.util.Set;
  10 +
  11 +@Schema(description = "用户 App - 交易售后分页 Request VO")
  12 +@Data
  13 +@EqualsAndHashCode(callSuper = true)
  14 +@ToString(callSuper = true)
  15 +public class AppAfterSalePageReqVO extends PageParam {
  16 +
  17 + @Schema(description = "售后状态", example = "10, 20")
  18 + private Set<Integer> statuses;
  19 +
  20 +}
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/base/spu/AppProductSpuBaseRespVO.java
... ... @@ -3,8 +3,6 @@ package cn.iocoder.yudao.module.trade.controller.app.base.spu;
3 3 import io.swagger.v3.oas.annotations.media.Schema;
4 4 import lombok.Data;
5 5  
6   -import java.util.List;
7   -
8 6 /**
9 7 * 商品 SPU 基础 Response VO
10 8 *
... ... @@ -25,4 +23,10 @@ public class AppProductSpuBaseRespVO {
25 23 @Schema(description = "商品分类编号", example = "1")
26 24 private Long categoryId;
27 25  
  26 + @Schema(description = "商品库存", requiredMode = Schema.RequiredMode.REQUIRED, example = "10000")
  27 + private Integer stock;
  28 +
  29 + @Schema(description = "商品状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  30 + private Integer status;
  31 +
28 32 }
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/delivery/vo/pickup/AppDeliveryPickUpStoreRespVO.java
1 1 package cn.iocoder.yudao.module.trade.controller.app.delivery.vo.pickup;
2 2  
  3 +import com.fasterxml.jackson.annotation.JsonFormat;
3 4 import io.swagger.v3.oas.annotations.media.Schema;
  5 +import jakarta.validation.constraints.NotNull;
4 6 import lombok.Data;
5 7  
  8 +import java.time.LocalTime;
  9 +
6 10 @Schema(description = "用户 App - 自提门店 Response VO")
7 11 @Data
8 12 public class AppDeliveryPickUpStoreRespVO {
... ... @@ -28,6 +32,16 @@ public class AppDeliveryPickUpStoreRespVO {
28 32 @Schema(description = "门店详细地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "复旦大学路 188 号")
29 33 private String detailAddress;
30 34  
  35 + @Schema(description = "营业开始时间", requiredMode = Schema.RequiredMode.REQUIRED)
  36 + @NotNull(message = "营业开始时间不能为空")
  37 + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "HH:mm")
  38 + private LocalTime openingTime;
  39 +
  40 + @Schema(description = "营业结束时间", requiredMode = Schema.RequiredMode.REQUIRED)
  41 + @NotNull(message = "营业结束时间不能为空")
  42 + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "HH:mm")
  43 + private LocalTime closingTime;
  44 +
31 45 @Schema(description = "纬度", requiredMode = Schema.RequiredMode.REQUIRED, example = "5.88")
32 46 private Double latitude;
33 47  
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/aftersale/AfterSaleConvert.java
... ... @@ -11,7 +11,6 @@ import cn.iocoder.yudao.module.trade.controller.admin.base.member.user.MemberUse
11 11 import cn.iocoder.yudao.module.trade.controller.admin.base.product.property.ProductPropertyValueDetailRespVO;
12 12 import cn.iocoder.yudao.module.trade.controller.admin.order.vo.TradeOrderBaseVO;
13 13 import cn.iocoder.yudao.module.trade.controller.app.aftersale.vo.AppAfterSaleCreateReqVO;
14   -import cn.iocoder.yudao.module.trade.controller.app.aftersale.vo.AppAfterSaleRespVO;
15 14 import cn.iocoder.yudao.module.trade.dal.dataobject.aftersale.AfterSaleDO;
16 15 import cn.iocoder.yudao.module.trade.dal.dataobject.aftersale.AfterSaleLogDO;
17 16 import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO;
... ... @@ -63,10 +62,6 @@ public interface AfterSaleConvert {
63 62  
64 63 ProductPropertyValueDetailRespVO convert(ProductPropertyValueDetailRespDTO bean);
65 64  
66   - AppAfterSaleRespVO convert(AfterSaleDO bean);
67   -
68   - PageResult<AppAfterSaleRespVO> convertPage02(PageResult<AfterSaleDO> page);
69   -
70 65 default AfterSaleDetailRespVO convert(AfterSaleDO afterSale, TradeOrderDO order, TradeOrderItemDO orderItem,
71 66 MemberUserRespDTO user, List<AfterSaleLogDO> logs) {
72 67 AfterSaleDetailRespVO respVO = convert02(afterSale);
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/cart/TradeCartConvert.java
1 1 package cn.iocoder.yudao.module.trade.convert.cart;
2 2  
  3 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
3 4 import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO;
4 5 import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO;
5 6 import cn.iocoder.yudao.module.product.enums.spu.ProductSpuStatusEnum;
... ... @@ -33,21 +34,18 @@ public interface TradeCartConvert {
33 34 cartVO.setId(cart.getId()).setCount(cart.getCount()).setSelected(cart.getSelected());
34 35 ProductSpuRespDTO spu = spuMap.get(cart.getSpuId());
35 36 ProductSkuRespDTO sku = skuMap.get(cart.getSkuId());
36   - cartVO.setSpu(convert(spu)).setSku(convert(sku));
  37 + cartVO.setSpu(BeanUtils.toBean(spu, AppProductSpuBaseRespVO.class))
  38 + .setSku(BeanUtils.toBean(sku, AppProductSkuBaseRespVO.class));
37 39 // 如果 SPU 不存在,或者下架,或者库存不足,说明是无效的
38 40 if (spu == null
39 41 || !ProductSpuStatusEnum.isEnable(spu.getStatus())
40 42 || spu.getStock() <= 0) {
41   - cartVO.setSelected(false); // 强制设置成不可选中
42 43 invalidList.add(cartVO);
43 44 } else {
44   - // 虽然 SKU 可能也会不存在,但是可以通过购物车重新选择
45 45 validList.add(cartVO);
46 46 }
47 47 });
48 48 return new AppCartListRespVO().setValidList(validList).setInvalidList(invalidList);
49 49 }
50   - AppProductSpuBaseRespVO convert(ProductSpuRespDTO spu);
51   - AppProductSkuBaseRespVO convert(ProductSkuRespDTO sku);
52 50  
53 51 }
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/aftersale/AfterSaleMapper.java
1 1 package cn.iocoder.yudao.module.trade.dal.mysql.aftersale;
2 2  
3   -import cn.iocoder.yudao.framework.common.pojo.PageParam;
4 3 import cn.iocoder.yudao.framework.common.pojo.PageResult;
5 4 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
6 5 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
7 6 import cn.iocoder.yudao.module.trade.controller.admin.aftersale.vo.AfterSalePageReqVO;
  7 +import cn.iocoder.yudao.module.trade.controller.app.aftersale.vo.AppAfterSalePageReqVO;
8 8 import cn.iocoder.yudao.module.trade.dal.dataobject.aftersale.AfterSaleDO;
9 9 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
10 10 import org.apache.ibatis.annotations.Mapper;
... ... @@ -27,9 +27,10 @@ public interface AfterSaleMapper extends BaseMapperX&lt;AfterSaleDO&gt; {
27 27 .orderByDesc(AfterSaleDO::getId));
28 28 }
29 29  
30   - default PageResult<AfterSaleDO> selectPage(Long userId, PageParam pageParam) {
31   - return selectPage(pageParam, new LambdaQueryWrapperX<AfterSaleDO>()
32   - .eqIfPresent(AfterSaleDO::getUserId, userId)
  30 + default PageResult<AfterSaleDO> selectPage(Long userId, AppAfterSalePageReqVO pageReqVO) {
  31 + return selectPage(pageReqVO, new LambdaQueryWrapperX<AfterSaleDO>()
  32 + .eq(AfterSaleDO::getUserId, userId)
  33 + .inIfPresent(AfterSaleDO::getStatus, pageReqVO.getStatuses())
33 34 .orderByDesc(AfterSaleDO::getId));
34 35 }
35 36  
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/aftersale/AfterSaleService.java
1 1 package cn.iocoder.yudao.module.trade.service.aftersale;
2 2  
3   -import cn.iocoder.yudao.framework.common.pojo.PageParam;
4 3 import cn.iocoder.yudao.framework.common.pojo.PageResult;
5 4 import cn.iocoder.yudao.module.trade.controller.admin.aftersale.vo.AfterSaleDisagreeReqVO;
6 5 import cn.iocoder.yudao.module.trade.controller.admin.aftersale.vo.AfterSalePageReqVO;
7 6 import cn.iocoder.yudao.module.trade.controller.admin.aftersale.vo.AfterSaleRefuseReqVO;
8 7 import cn.iocoder.yudao.module.trade.controller.app.aftersale.vo.AppAfterSaleCreateReqVO;
9 8 import cn.iocoder.yudao.module.trade.controller.app.aftersale.vo.AppAfterSaleDeliveryReqVO;
  9 +import cn.iocoder.yudao.module.trade.controller.app.aftersale.vo.AppAfterSalePageReqVO;
10 10 import cn.iocoder.yudao.module.trade.dal.dataobject.aftersale.AfterSaleDO;
11 11  
12 12 /**
... ... @@ -28,10 +28,10 @@ public interface AfterSaleService {
28 28 * 【会员】获得售后订单分页
29 29 *
30 30 * @param userId 用户编号
31   - * @param pageParam 分页参数
  31 + * @param pageReqVO 分页参数
32 32 * @return 售后订单分页
33 33 */
34   - PageResult<AfterSaleDO> getAfterSalePage(Long userId, PageParam pageParam);
  34 + PageResult<AfterSaleDO> getAfterSalePage(Long userId, AppAfterSalePageReqVO pageReqVO);
35 35  
36 36 /**
37 37 * 【会员】获得售后单
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/aftersale/AfterSaleServiceImpl.java
... ... @@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.trade.service.aftersale;
3 3 import cn.hutool.core.map.MapUtil;
4 4 import cn.hutool.core.util.ObjectUtil;
5 5 import cn.hutool.core.util.StrUtil;
6   -import cn.iocoder.yudao.framework.common.pojo.PageParam;
7 6 import cn.iocoder.yudao.framework.common.pojo.PageResult;
8 7 import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
9 8 import cn.iocoder.yudao.module.pay.api.refund.PayRefundApi;
... ... @@ -16,6 +15,7 @@ import cn.iocoder.yudao.module.trade.controller.admin.aftersale.vo.AfterSalePage
16 15 import cn.iocoder.yudao.module.trade.controller.admin.aftersale.vo.AfterSaleRefuseReqVO;
17 16 import cn.iocoder.yudao.module.trade.controller.app.aftersale.vo.AppAfterSaleCreateReqVO;
18 17 import cn.iocoder.yudao.module.trade.controller.app.aftersale.vo.AppAfterSaleDeliveryReqVO;
  18 +import cn.iocoder.yudao.module.trade.controller.app.aftersale.vo.AppAfterSalePageReqVO;
19 19 import cn.iocoder.yudao.module.trade.convert.aftersale.AfterSaleConvert;
20 20 import cn.iocoder.yudao.module.trade.dal.dataobject.aftersale.AfterSaleDO;
21 21 import cn.iocoder.yudao.module.trade.dal.dataobject.delivery.DeliveryExpressDO;
... ... @@ -36,6 +36,7 @@ import cn.iocoder.yudao.module.trade.framework.order.config.TradeOrderProperties
36 36 import cn.iocoder.yudao.module.trade.service.delivery.DeliveryExpressService;
37 37 import cn.iocoder.yudao.module.trade.service.order.TradeOrderQueryService;
38 38 import cn.iocoder.yudao.module.trade.service.order.TradeOrderUpdateService;
  39 +import jakarta.annotation.Resource;
39 40 import lombok.extern.slf4j.Slf4j;
40 41 import org.springframework.context.annotation.Lazy;
41 42 import org.springframework.stereotype.Service;
... ... @@ -44,7 +45,6 @@ import org.springframework.transaction.support.TransactionSynchronization;
44 45 import org.springframework.transaction.support.TransactionSynchronizationManager;
45 46 import org.springframework.validation.annotation.Validated;
46 47  
47   -import jakarta.annotation.Resource;
48 48 import java.time.LocalDateTime;
49 49  
50 50 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
... ... @@ -87,8 +87,8 @@ public class AfterSaleServiceImpl implements AfterSaleService {
87 87 }
88 88  
89 89 @Override
90   - public PageResult<AfterSaleDO> getAfterSalePage(Long userId, PageParam pageParam) {
91   - return tradeAfterSaleMapper.selectPage(userId, pageParam);
  90 + public PageResult<AfterSaleDO> getAfterSalePage(Long userId, AppAfterSalePageReqVO pageReqVO) {
  91 + return tradeAfterSaleMapper.selectPage(userId, pageReqVO);
92 92 }
93 93  
94 94 @Override
... ... @@ -386,7 +386,7 @@ public class AfterSaleServiceImpl implements AfterSaleService {
386 386 public void afterCommit() {
387 387 // 创建退款单
388 388 PayRefundCreateReqDTO createReqDTO = AfterSaleConvert.INSTANCE.convert(userIp, afterSale, tradeOrderProperties)
389   - .setReason(StrUtil.format("退款【{}】", afterSale.getSpuName()));;
  389 + .setReason(StrUtil.format("退款【{}】", afterSale.getSpuName()));
390 390 Long payRefundId = payRefundApi.createRefund(createReqDTO);
391 391 // 更新售后单的退款单号
392 392 tradeAfterSaleMapper.updateById(new AfterSaleDO().setId(afterSale.getId()).setPayRefundId(payRefundId));
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageWithdrawServiceImpl.java
... ... @@ -135,7 +135,7 @@ public class BrokerageWithdrawServiceImpl implements BrokerageWithdrawService {
135 135 private Long createPayTransfer(BrokerageWithdrawDO withdraw) {
136 136 // 1.1 获取微信 openid
137 137 SocialUserRespDTO socialUser = socialUserApi.getSocialUserByUserId(
138   - UserTypeEnum.MEMBER.getValue(), withdraw.getUserId(), SocialTypeEnum.WECHAT_MINI_APP.getType());
  138 + UserTypeEnum.MEMBER.getValue(), withdraw.getUserId(), SocialTypeEnum.WECHAT_MINI_PROGRAM.getType());
139 139 // TODO @luchi:这里,需要校验非空。如果空的话,要有业务异常哈;
140 140 // 1.2 构建请求
141 141 PayTransferCreateReqDTO payTransferCreateReqDTO = new PayTransferCreateReqDTO()
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java
... ... @@ -545,6 +545,14 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
545 545 if (ObjectUtil.notEqual(order.getStatus(), TradeOrderStatusEnum.UNPAID.getStatus())) {
546 546 throw exception(ORDER_CANCEL_FAIL_STATUS_NOT_UNPAID);
547 547 }
  548 + // 1.3 校验是否支持延迟(不允许取消)
  549 + if (TradeOrderStatusEnum.isUnpaid(order.getStatus())) {
  550 + PayOrderRespDTO payOrder = payOrderApi.getOrder(order.getPayOrderId());
  551 + if (payOrder != null && PayOrderStatusEnum.isSuccess(payOrder.getStatus())) {
  552 + log.warn("[cancelOrderByMember][order({}) 支付单已支付(支付回调延迟),不支持取消]", order.getId());
  553 + throw exception(ORDER_CANCEL_FAIL_STATUS_NOT_UNPAID);
  554 + }
  555 + }
548 556  
549 557 // 2. 取消订单
550 558 cancelOrder0(order, TradeOrderCancelTypeEnum.MEMBER_CANCEL);
... ... @@ -581,6 +589,15 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
581 589 @Transactional(rollbackFor = Exception.class)
582 590 @TradeOrderLog(operateType = TradeOrderOperateTypeEnum.SYSTEM_CANCEL)
583 591 public void cancelOrderBySystem(TradeOrderDO order) {
  592 + // 校验是否支持延迟(不允许取消)
  593 + if (TradeOrderStatusEnum.isUnpaid(order.getStatus())) {
  594 + PayOrderRespDTO payOrder = payOrderApi.getOrder(order.getPayOrderId());
  595 + if (payOrder != null && PayOrderStatusEnum.isSuccess(payOrder.getStatus())) {
  596 + log.warn("[cancelOrderBySystem][order({}) 支付单已支付(支付回调延迟),不支持取消]", order.getId());
  597 + return;
  598 + }
  599 + }
  600 +
584 601 cancelOrder0(order, TradeOrderCancelTypeEnum.PAY_TIMEOUT);
585 602 }
586 603  
... ... @@ -895,12 +912,11 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
895 912 if (order == null) {
896 913 throw exception(ORDER_NOT_FOUND);
897 914 }
898   -
899 915 // 1.3 校验订单是否支付
900 916 if (!order.getPayStatus()) {
901 917 throw exception(ORDER_CANCEL_PAID_FAIL, "已支付");
902 918 }
903   - // 1.3 校验订单是否未退款
  919 + // 1.4 校验订单是否未退款
904 920 if (ObjUtil.notEqual(TradeOrderRefundStatusEnum.NONE.getStatus(), order.getRefundStatus())) {
905 921 throw exception(ORDER_CANCEL_PAID_FAIL, "未退款");
906 922 }
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeBrokerageOrderHandler.java
... ... @@ -20,6 +20,7 @@ import org.springframework.stereotype.Component;
20 20  
21 21 import jakarta.annotation.Resource;
22 22 import java.util.List;
  23 +import java.util.Map;
23 24  
24 25 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
25 26  
... ... @@ -101,13 +102,17 @@ public class TradeBrokerageOrderHandler implements TradeOrderHandler {
101 102 protected void addBrokerage(Long userId, List<TradeOrderItemDO> orderItems) {
102 103 MemberUserRespDTO user = memberUserApi.getUser(userId);
103 104 Assert.notNull(user);
104   - ProductSpuRespDTO spu = productSpuApi.getSpu(orderItems.get(0).getSpuId());
105   - Assert.notNull(spu);
106   - ProductSkuRespDTO sku = productSkuApi.getSku(orderItems.get(0).getSkuId());
  105 + Map<Long, ProductSpuRespDTO> spusMap = productSpuApi.getSpuMap(convertList(orderItems, TradeOrderItemDO::getSpuId));
  106 + Map<Long, ProductSkuRespDTO> skusMap = productSkuApi.getSkuMap(convertList(orderItems, TradeOrderItemDO::getSkuId));
107 107  
108 108 // 每一个订单项,都会去生成分销记录
109   - List<BrokerageAddReqBO> addList = convertList(orderItems,
110   - item -> TradeOrderConvert.INSTANCE.convert(user, item, spu, sku));
  109 + List<BrokerageAddReqBO> addList = convertList(orderItems, item -> {
  110 + ProductSpuRespDTO spu = spusMap.get(item.getSpuId());
  111 + Assert.notNull(spu);
  112 + ProductSkuRespDTO sku = skusMap.get(item.getSkuId());
  113 + Assert.notNull(sku);
  114 + return TradeOrderConvert.INSTANCE.convert(user, item, spu, sku);
  115 + });
111 116 brokerageRecordService.addBrokerage(userId, BrokerageRecordBizTypeEnum.ORDER, addList);
112 117 }
113 118  
... ...
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/auth/MemberAuthServiceImpl.java
1 1 package cn.iocoder.yudao.module.member.service.auth;
2 2  
3 3 import cn.hutool.core.lang.Assert;
4   -import cn.hutool.core.util.ObjectUtil;
5 4 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
6 5 import cn.iocoder.yudao.framework.common.enums.TerminalEnum;
7 6 import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
... ... @@ -27,11 +26,11 @@ import cn.iocoder.yudao.module.system.enums.logger.LoginResultEnum;
27 26 import cn.iocoder.yudao.module.system.enums.oauth2.OAuth2ClientConstants;
28 27 import cn.iocoder.yudao.module.system.enums.sms.SmsSceneEnum;
29 28 import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum;
  29 +import jakarta.annotation.Resource;
30 30 import lombok.extern.slf4j.Slf4j;
31 31 import org.springframework.stereotype.Service;
32 32 import org.springframework.transaction.annotation.Transactional;
33 33  
34   -import jakarta.annotation.Resource;
35 34 import java.util.Objects;
36 35  
37 36 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
... ... @@ -147,7 +146,7 @@ public class MemberAuthServiceImpl implements MemberAuthService {
147 146  
148 147 // 绑定社交用户
149 148 String openid = socialUserApi.bindSocialUser(new SocialUserBindReqDTO(user.getId(), getUserType().getValue(),
150   - SocialTypeEnum.WECHAT_MINI_APP.getType(), reqVO.getLoginCode(), reqVO.getState()));
  149 + SocialTypeEnum.WECHAT_MINI_PROGRAM.getType(), reqVO.getLoginCode(), reqVO.getState()));
151 150  
152 151 // 创建 Token 令牌,记录登录日志
153 152 return createTokenAfterLoginSuccess(user, user.getMobile(), LoginLogTypeEnum.LOGIN_SOCIAL, openid);
... ...
yudao-module-member/yudao-module-member-biz/src/test/java/cn/iocoder/yudao/module/member/service/user/MemberUserServiceImplTest.java
... ... @@ -5,12 +5,12 @@ import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
5 5 import cn.iocoder.yudao.framework.common.util.collection.ArrayUtils;
6 6 import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration;
7 7 import cn.iocoder.yudao.framework.test.core.ut.BaseDbAndRedisUnitTest;
8   -import cn.iocoder.yudao.module.infra.api.file.FileApi;
9 8 import cn.iocoder.yudao.module.member.controller.app.user.vo.AppMemberUserUpdateMobileReqVO;
10 9 import cn.iocoder.yudao.module.member.dal.dataobject.user.MemberUserDO;
11 10 import cn.iocoder.yudao.module.member.dal.mysql.user.MemberUserMapper;
12 11 import cn.iocoder.yudao.module.member.service.auth.MemberAuthServiceImpl;
13 12 import cn.iocoder.yudao.module.system.api.sms.SmsCodeApi;
  13 +import jakarta.annotation.Resource;
14 14 import org.junit.jupiter.api.Disabled;
15 15 import org.junit.jupiter.api.Test;
16 16 import org.springframework.boot.test.mock.mockito.MockBean;
... ... @@ -18,7 +18,6 @@ import org.springframework.context.annotation.Import;
18 18 import org.springframework.data.redis.core.StringRedisTemplate;
19 19 import org.springframework.security.crypto.password.PasswordEncoder;
20 20  
21   -import jakarta.annotation.Resource;
22 21 import java.util.function.Consumer;
23 22  
24 23 import static cn.hutool.core.util.RandomUtil.randomEle;
... ... @@ -53,8 +52,6 @@ public class MemberUserServiceImplTest extends BaseDbAndRedisUnitTest {
53 52  
54 53 @MockBean
55 54 private SmsCodeApi smsCodeApi;
56   - @MockBean
57   - private FileApi fileApi;
58 55  
59 56 // TODO 芋艿:后续重构这个单测
60 57 // @Test
... ... @@ -73,25 +70,6 @@ public class MemberUserServiceImplTest extends BaseDbAndRedisUnitTest {
73 70 // // 断言
74 71 // assertEquals(newNickName,nickname);
75 72 // }
76   -//
77   -// @Test
78   -// public void testUpdateAvatar_success() throws Exception {
79   -// // mock 数据
80   -// MemberUserDO dbUser = randomUserDO();
81   -// userMapper.insert(dbUser);
82   -//
83   -// // 准备参数
84   -// Long userId = dbUser.getId();
85   -// byte[] avatarFileBytes = randomBytes(10);
86   -// ByteArrayInputStream avatarFile = new ByteArrayInputStream(avatarFileBytes);
87   -// // mock 方法
88   -// String avatar = randomString();
89   -// when(fileApi.createFile(eq(avatarFileBytes))).thenReturn(avatar);
90   -// // 调用
91   -// String str = memberUserService.updateUserAvatar(userId, avatarFile);
92   -// // 断言
93   -// assertEquals(avatar, str);
94   -// }
95 73  
96 74 @Test
97 75 @Disabled // TODO 芋艿:后续再修复
... ...
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/material/MpMaterialServiceImpl.java
... ... @@ -218,7 +218,7 @@ public class MpMaterialServiceImpl implements MpMaterialService {
218 218  
219 219 private String uploadFile(String mediaId, File file) {
220 220 String path = mediaId + "." + FileTypeUtil.getType(file);
221   - return fileApi.createFile(path, FileUtil.readBytes(file));
  221 + return fileApi.createFile(FileUtil.readBytes(file), path);
222 222 }
223 223  
224 224 }
... ...
yudao-module-report/yudao-module-report-biz/pom.xml
... ... @@ -64,6 +64,10 @@
64 64 <groupId>org.jeecgframework.jimureport</groupId>
65 65 <artifactId>jimureport-spring-boot3-starter-fastjson2</artifactId>
66 66 </dependency>
  67 + <dependency>
  68 + <groupId>org.jeecgframework.jimureport</groupId>
  69 + <artifactId>jimubi-spring-boot3-starter</artifactId>
  70 + </dependency>
67 71  
68 72 <dependency>
69 73 <groupId>cn.iocoder.boot</groupId>
... ...
yudao-module-report/yudao-module-report-biz/src/main/java/cn/iocoder/yudao/module/report/framework/jmreport/config/JmReportConfiguration.java
1 1 package cn.iocoder.yudao.module.report.framework.jmreport.config;
2 2  
3 3 import cn.iocoder.yudao.framework.security.config.SecurityProperties;
4   -import cn.iocoder.yudao.module.system.api.oauth2.OAuth2TokenApi;
  4 +import cn.iocoder.yudao.module.report.framework.jmreport.core.service.JmOnlDragExternalServiceImpl;
5 5 import cn.iocoder.yudao.module.report.framework.jmreport.core.service.JmReportTokenServiceImpl;
  6 +import cn.iocoder.yudao.module.system.api.oauth2.OAuth2TokenApi;
6 7 import cn.iocoder.yudao.module.system.api.permission.PermissionApi;
7 8 import org.jeecg.modules.jmreport.api.JmReportTokenServiceI;
8 9 import org.springframework.context.annotation.Bean;
9 10 import org.springframework.context.annotation.ComponentScan;
10 11 import org.springframework.context.annotation.Configuration;
  12 +import org.springframework.context.annotation.Primary;
11 13  
12 14 /**
13 15 * 积木报表的配置类
... ... @@ -19,11 +21,16 @@ import org.springframework.context.annotation.Configuration;
19 21 public class JmReportConfiguration {
20 22  
21 23 @Bean
22   - @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
23 24 public JmReportTokenServiceI jmReportTokenService(OAuth2TokenApi oAuth2TokenApi,
24 25 PermissionApi permissionApi,
25 26 SecurityProperties securityProperties) {
26 27 return new JmReportTokenServiceImpl(oAuth2TokenApi, permissionApi, securityProperties);
27 28 }
28 29  
  30 + @Bean // 暂时注释:可以按需实现后打开
  31 + @Primary
  32 + public JmOnlDragExternalServiceImpl jmOnlDragExternalService2() {
  33 + return new JmOnlDragExternalServiceImpl();
  34 + }
  35 +
29 36 }
... ...
yudao-module-report/yudao-module-report-biz/src/main/java/cn/iocoder/yudao/module/report/framework/jmreport/core/service/JmOnlDragExternalServiceImpl.java 0 → 100644
  1 +package cn.iocoder.yudao.module.report.framework.jmreport.core.service;
  2 +
  3 +import com.alibaba.fastjson.JSONObject;
  4 +import lombok.RequiredArgsConstructor;
  5 +import org.jeecg.modules.drag.service.IOnlDragExternalService;
  6 +import org.jeecg.modules.drag.vo.DragDictModel;
  7 +import org.jeecg.modules.drag.vo.DragLogDTO;
  8 +
  9 +import java.util.List;
  10 +import java.util.Map;
  11 +
  12 +/**
  13 + * {@link IOnlDragExternalService} 实现类,提供积木仪表盘的查询等功能
  14 + *
  15 + * 实现可参考:
  16 + * 1. <a href="https://github.com/jeecgboot/jimureport/blob/master/jimureport-example/src/main/java/com/jeecg/modules/jmreport/extend/JimuDragExternalServiceImpl.java">jimureport-example</a>
  17 + * 2. <a href="https://gitee.com/jeecg/JeecgBoot/blob/master/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/config/jimureport/JimuDragExternalServiceImpl.java">JeecgBoot 集成</a>
  18 + *
  19 + * @author 芋道源码
  20 + */
  21 +@RequiredArgsConstructor
  22 +public class JmOnlDragExternalServiceImpl implements IOnlDragExternalService {
  23 +
  24 + // ========== DictItem 相关 ==========
  25 +
  26 + @Override
  27 + public Map<String, List<DragDictModel>> getManyDictItems(List<String> codeList, List<JSONObject> tableDictList) {
  28 + return IOnlDragExternalService.super.getManyDictItems(codeList, tableDictList);
  29 + }
  30 +
  31 + @Override
  32 + public List<DragDictModel> getDictItems(String dictCode) {
  33 + return IOnlDragExternalService.super.getDictItems(dictCode);
  34 + }
  35 +
  36 + @Override
  37 + public List<DragDictModel> getTableDictItems(String dictTable, String dictText, String dictCode) {
  38 + return IOnlDragExternalService.super.getTableDictItems(dictTable, dictText, dictCode);
  39 + }
  40 +
  41 + @Override
  42 + public List<DragDictModel> getCategoryTreeDictItems(List<String> ids) {
  43 + return IOnlDragExternalService.super.getCategoryTreeDictItems(ids);
  44 + }
  45 +
  46 + @Override
  47 + public List<DragDictModel> getUserDictItems(List<String> ids) {
  48 + return IOnlDragExternalService.super.getUserDictItems(ids);
  49 + }
  50 +
  51 + @Override
  52 + public List<DragDictModel> getDeptsDictItems(List<String> ids) {
  53 + return IOnlDragExternalService.super.getDeptsDictItems(ids);
  54 + }
  55 +
  56 + // ========== Log 相关 ==========
  57 +
  58 + @Override
  59 + public void addLog(DragLogDTO dto) {
  60 + IOnlDragExternalService.super.addLog(dto);
  61 + }
  62 +
  63 + @Override
  64 + public void addLog(String logMsg, int logType, int operateType) {
  65 + IOnlDragExternalService.super.addLog(logMsg, logType, operateType);
  66 + }
  67 +
  68 +}
... ...
yudao-module-report/yudao-module-report-biz/src/main/java/cn/iocoder/yudao/module/report/framework/security/config/SecurityConfiguration.java
... ... @@ -18,7 +18,11 @@ public class SecurityConfiguration {
18 18  
19 19 @Override
20 20 public void customize(AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry registry) {
21   - registry.requestMatchers("/jmreport/**").permitAll(); // 积木报表
  21 + // 积木报表
  22 + registry.requestMatchers("/jmreport/**").permitAll();
  23 + // 积木仪表盘
  24 + registry.requestMatchers("/drag/**").permitAll();
  25 + registry.requestMatchers("/jimubi/**").permitAll();
22 26 }
23 27  
24 28 };
... ...
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java
... ... @@ -45,6 +45,7 @@ public interface ErrorCodeConstants {
45 45 ErrorCode USER_COUNT_MAX = new ErrorCode(1_002_003_008, "创建用户失败,原因:超过租户最大租户配额({})!");
46 46 ErrorCode USER_IMPORT_INIT_PASSWORD = new ErrorCode(1_002_003_009, "初始密码不能为空");
47 47 ErrorCode USER_MOBILE_NOT_EXISTS = new ErrorCode(1_002_003_010, "该手机号尚未注册");
  48 + ErrorCode USER_REGISTER_DISABLED = new ErrorCode(1_002_003_011, "注册功能已关闭");
48 49  
49 50 // ========== 部门模块 1-002-004-000 ==========
50 51 ErrorCode DEPT_NAME_DUPLICATE = new ErrorCode(1_002_004_000, "已经存在该名字的部门");
... ...
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/social/SocialTypeEnum.java
... ... @@ -52,7 +52,7 @@ public enum SocialTypeEnum implements ArrayValuable&lt;Integer&gt; {
52 52 *
53 53 * @see <a href="https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html">接入文档</a>
54 54 */
55   - WECHAT_MINI_APP(34, "WECHAT_MINI_APP"),
  55 + WECHAT_MINI_PROGRAM(34, "WECHAT_MINI_PROGRAM"),
56 56 ;
57 57  
58 58 public static final Integer[] ARRAYS = Arrays.stream(values()).map(SocialTypeEnum::getType).toArray(Integer[]::new);
... ...
yudao-module-system/yudao-module-system-biz/pom.xml
... ... @@ -97,8 +97,12 @@
97 97  
98 98 <!-- 三方云服务相关 -->
99 99 <dependency>
100   - <groupId>com.xingyuv</groupId>
101   - <artifactId>spring-boot-starter-justauth</artifactId> <!-- 社交登陆(例如说,个人微信、企业微信等等) -->
  100 + <groupId>me.zhyd.oauth</groupId>
  101 + <artifactId>JustAuth</artifactId> <!-- 社交登陆(例如说,个人微信、企业微信等等) -->
  102 + </dependency>
  103 + <dependency>
  104 + <groupId>com.xkcoding.justauth</groupId>
  105 + <artifactId>justauth-spring-boot-starter</artifactId>
102 106 </dependency>
103 107  
104 108 <dependency>
... ... @@ -111,8 +115,8 @@
111 115 </dependency>
112 116  
113 117 <dependency>
114   - <groupId>com.xingyuv</groupId>
115   - <artifactId>spring-boot-starter-captcha-plus</artifactId> <!-- 验证码,一般用于登录使用 -->
  118 + <groupId>com.anji-plus</groupId>
  119 + <artifactId>captcha-spring-boot-starter</artifactId> <!-- 验证码,一般用于登录使用 -->
116 120 </dependency>
117 121  
118 122 <dependency>
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/social/SocialClientApiImpl.java
... ... @@ -84,7 +84,7 @@ public class SocialClientApiImpl implements SocialClientApi {
84 84  
85 85 // 2. 获得社交用户
86 86 SocialUserRespDTO socialUser = socialUserService.getSocialUserByUserId(reqDTO.getUserType(), reqDTO.getUserId(),
87   - SocialTypeEnum.WECHAT_MINI_APP.getType());
  87 + SocialTypeEnum.WECHAT_MINI_PROGRAM.getType());
88 88 if (StrUtil.isBlankIfStr(socialUser.getOpenid())) {
89 89 log.warn("[sendWxaSubscribeMessage][reqDTO({}) 发送订阅消息失败,原因:会员 openid 缺失]", reqDTO);
90 90 return;
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/captcha/CaptchaController.java
... ... @@ -2,20 +2,19 @@ package cn.iocoder.yudao.module.system.controller.admin.captcha;
2 2  
3 3 import cn.hutool.core.util.StrUtil;
4 4 import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
5   -import com.xingyuv.captcha.model.common.ResponseModel;
6   -import com.xingyuv.captcha.model.vo.CaptchaVO;
7   -import com.xingyuv.captcha.service.CaptchaService;
  5 +import com.anji.captcha.model.common.ResponseModel;
  6 +import com.anji.captcha.model.vo.CaptchaVO;
  7 +import com.anji.captcha.service.CaptchaService;
8 8 import io.swagger.v3.oas.annotations.Operation;
9 9 import io.swagger.v3.oas.annotations.tags.Tag;
  10 +import jakarta.annotation.Resource;
  11 +import jakarta.annotation.security.PermitAll;
  12 +import jakarta.servlet.http.HttpServletRequest;
10 13 import org.springframework.web.bind.annotation.PostMapping;
11 14 import org.springframework.web.bind.annotation.RequestBody;
12 15 import org.springframework.web.bind.annotation.RequestMapping;
13 16 import org.springframework.web.bind.annotation.RestController;
14 17  
15   -import jakarta.annotation.Resource;
16   -import jakarta.annotation.security.PermitAll;
17   -import jakarta.servlet.http.HttpServletRequest;
18   -
19 18 @Tag(name = "管理后台 - 验证码")
20 19 @RestController("adminCaptchaController")
21 20 @RequestMapping("/system/captcha")
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserProfileController.java
... ... @@ -23,14 +23,11 @@ import jakarta.validation.Valid;
23 23 import lombok.extern.slf4j.Slf4j;
24 24 import org.springframework.validation.annotation.Validated;
25 25 import org.springframework.web.bind.annotation.*;
26   -import org.springframework.web.multipart.MultipartFile;
27 26  
28 27 import java.util.List;
29 28  
30   -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
31 29 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
32 30 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
33   -import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_IS_EMPTY;
34 31  
35 32 @Tag(name = "管理后台 - 用户个人中心")
36 33 @RestController
... ... @@ -79,16 +76,4 @@ public class UserProfileController {
79 76 return success(true);
80 77 }
81 78  
82   - @Deprecated // TODO @芋艿:逐步替换到 updateUserProfile 接口
83   - @RequestMapping(value = "/update-avatar",
84   - method = {RequestMethod.POST, RequestMethod.PUT}) // 解决 uni-app 不支持 Put 上传文件的问题
85   - @Operation(summary = "上传用户个人头像")
86   - public CommonResult<String> updateUserAvatar(@RequestParam("avatarFile") MultipartFile file) throws Exception {
87   - if (file.isEmpty()) {
88   - throw exception(FILE_IS_EMPTY);
89   - }
90   - String avatar = userService.updateUserAvatar(getLoginUserId(), file.getInputStream());
91   - return success(avatar);
92   - }
93   -
94 79 }
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/social/SocialClientDO.java
... ... @@ -7,8 +7,8 @@ import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum;
7 7 import com.baomidou.mybatisplus.annotation.KeySequence;
8 8 import com.baomidou.mybatisplus.annotation.TableId;
9 9 import com.baomidou.mybatisplus.annotation.TableName;
10   -import com.xingyuv.jushauth.config.AuthConfig;
11 10 import lombok.*;
  11 +import me.zhyd.oauth.config.AuthConfig;
12 12  
13 13 /**
14 14 * 社交客户端 DO
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/mail/MailAccountMapper.java
... ... @@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.system.dal.mysql.mail;
3 3 import cn.iocoder.yudao.framework.common.pojo.PageResult;
4 4 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
5 5 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
6   -import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX;
7 6 import cn.iocoder.yudao.module.system.controller.admin.mail.vo.account.MailAccountPageReqVO;
8 7 import cn.iocoder.yudao.module.system.dal.dataobject.mail.MailAccountDO;
9 8 import org.apache.ibatis.annotations.Mapper;
... ... @@ -14,7 +13,8 @@ public interface MailAccountMapper extends BaseMapperX&lt;MailAccountDO&gt; {
14 13 default PageResult<MailAccountDO> selectPage(MailAccountPageReqVO pageReqVO) {
15 14 return selectPage(pageReqVO, new LambdaQueryWrapperX<MailAccountDO>()
16 15 .likeIfPresent(MailAccountDO::getMail, pageReqVO.getMail())
17   - .likeIfPresent(MailAccountDO::getUsername , pageReqVO.getUsername()));
  16 + .likeIfPresent(MailAccountDO::getUsername , pageReqVO.getUsername())
  17 + .orderByDesc(MailAccountDO::getId));
18 18 }
19 19  
20 20 }
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/mail/MailTemplateMapper.java
... ... @@ -3,14 +3,9 @@ package cn.iocoder.yudao.module.system.dal.mysql.mail;
3 3 import cn.iocoder.yudao.framework.common.pojo.PageResult;
4 4 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
5 5 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
6   -import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX;
7 6 import cn.iocoder.yudao.module.system.controller.admin.mail.vo.template.MailTemplatePageReqVO;
8 7 import cn.iocoder.yudao.module.system.dal.dataobject.mail.MailTemplateDO;
9   -import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsTemplateDO;
10 8 import org.apache.ibatis.annotations.Mapper;
11   -import org.apache.ibatis.annotations.Select;
12   -
13   -import java.util.Date;
14 9  
15 10 @Mapper
16 11 public interface MailTemplateMapper extends BaseMapperX<MailTemplateDO> {
... ... @@ -21,7 +16,8 @@ public interface MailTemplateMapper extends BaseMapperX&lt;MailTemplateDO&gt; {
21 16 .likeIfPresent(MailTemplateDO::getCode, pageReqVO.getCode())
22 17 .likeIfPresent(MailTemplateDO::getName, pageReqVO.getName())
23 18 .eqIfPresent(MailTemplateDO::getAccountId, pageReqVO.getAccountId())
24   - .betweenIfPresent(MailTemplateDO::getCreateTime, pageReqVO.getCreateTime()));
  19 + .betweenIfPresent(MailTemplateDO::getCreateTime, pageReqVO.getCreateTime())
  20 + .orderByDesc(MailTemplateDO::getId));
25 21 }
26 22  
27 23 default Long selectCountByAccountId(Long accountId) {
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/captcha/config/YudaoCaptchaConfiguration.java
1 1 package cn.iocoder.yudao.module.system.framework.captcha.config;
2 2  
3 3 import cn.iocoder.yudao.module.system.framework.captcha.core.RedisCaptchaServiceImpl;
4   -import com.xingyuv.captcha.properties.AjCaptchaProperties;
5   -import com.xingyuv.captcha.service.CaptchaCacheService;
6   -import com.xingyuv.captcha.service.impl.CaptchaServiceFactory;
  4 +import com.anji.captcha.config.AjCaptchaAutoConfiguration;
  5 +import com.anji.captcha.properties.AjCaptchaProperties;
  6 +import com.anji.captcha.service.CaptchaCacheService;
  7 +import com.anji.captcha.service.impl.CaptchaServiceFactory;
  8 +import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
7 9 import org.springframework.context.annotation.Bean;
8 10 import org.springframework.context.annotation.Configuration;
  11 +import org.springframework.context.annotation.Primary;
9 12 import org.springframework.data.redis.core.StringRedisTemplate;
10 13  
11 14 /**
... ... @@ -14,9 +17,11 @@ import org.springframework.data.redis.core.StringRedisTemplate;
14 17 * @author 芋道源码
15 18 */
16 19 @Configuration(proxyBeanMethods = false)
  20 +@ImportAutoConfiguration(AjCaptchaAutoConfiguration.class) // 目的:解决 aj-captcha 针对 SpringBoot 3.X 自动配置不生效的问题
17 21 public class YudaoCaptchaConfiguration {
18 22  
19   - @Bean
  23 + @Bean(name = "AjCaptchaCacheService")
  24 + @Primary
20 25 public CaptchaCacheService captchaCacheService(AjCaptchaProperties config,
21 26 StringRedisTemplate stringRedisTemplate) {
22 27 CaptchaCacheService captchaCacheService = CaptchaServiceFactory.getCache(config.getCacheType().name());
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/captcha/core/RedisCaptchaServiceImpl.java
1 1 package cn.iocoder.yudao.module.system.framework.captcha.core;
2 2  
3   -import com.xingyuv.captcha.service.CaptchaCacheService;
  3 +import com.anji.captcha.service.CaptchaCacheService;
4 4 import lombok.Setter;
5 5 import org.springframework.data.redis.core.StringRedisTemplate;
6 6  
... ... @@ -28,7 +28,7 @@ public class RedisCaptchaServiceImpl implements CaptchaCacheService {
28 28  
29 29 @Override
30 30 public boolean exists(String key) {
31   - return Boolean.TRUE.equals(stringRedisTemplate.hasKey(key));
  31 + return stringRedisTemplate.hasKey(key);
32 32 }
33 33  
34 34 @Override
... ...