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
This commit is contained in:
puhui999 2025-05-03 13:34:23 +08:00
commit 28c818e9bc
117 changed files with 1495 additions and 581 deletions

View File

@ -1,5 +1,5 @@
<p align="center"> <p align="center">
<img src="https://img.shields.io/badge/Spring%20Boot-3.4.1-blue.svg" alt="Downloads"> <img src="https://img.shields.io/badge/Spring%20Boot-3.4.5-blue.svg" alt="Downloads">
<img src="https://img.shields.io/badge/Vue-3.2-blue.svg" alt="Downloads"> <img src="https://img.shields.io/badge/Vue-3.2-blue.svg" alt="Downloads">
<img src="https://img.shields.io/github/license/YunaiV/ruoyi-vue-pro" alt="Downloads" /> <img src="https://img.shields.io/github/license/YunaiV/ruoyi-vue-pro" alt="Downloads" />
</p> </p>
@ -308,7 +308,7 @@
| 框架 | 说明 | 版本 | 学习指南 | | 框架 | 说明 | 版本 | 学习指南 |
|---------------------------------------------------------------------------------------------|------------------|----------------|----------------------------------------------------------------| |---------------------------------------------------------------------------------------------|------------------|----------------|----------------------------------------------------------------|
| [Spring Boot](https://spring.io/projects/spring-boot) | 应用开发框架 | 3.4.1 | [文档](https://github.com/YunaiV/SpringBoot-Labs) | | [Spring Boot](https://spring.io/projects/spring-boot) | 应用开发框架 | 3.4.5 | [文档](https://github.com/YunaiV/SpringBoot-Labs) |
| [MySQL](https://www.mysql.com/cn/) | 数据库服务器 | 5.7 / 8.0+ | | | [MySQL](https://www.mysql.com/cn/) | 数据库服务器 | 5.7 / 8.0+ | |
| [Druid](https://github.com/alibaba/druid) | JDBC 连接池、监控组件 | 1.2.23 | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?yudao) | | [Druid](https://github.com/alibaba/druid) | JDBC 连接池、监控组件 | 1.2.23 | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?yudao) |
| [MyBatis Plus](https://mp.baomidou.com/) | MyBatis 增强工具包 | 3.5.7 | [文档](http://www.iocoder.cn/Spring-Boot/MyBatis/?yudao) | | [MyBatis Plus](https://mp.baomidou.com/) | MyBatis 增强工具包 | 3.5.7 | [文档](http://www.iocoder.cn/Spring-Boot/MyBatis/?yudao) |

View File

@ -42,7 +42,7 @@
<flatten-maven-plugin.version>1.6.0</flatten-maven-plugin.version> <flatten-maven-plugin.version>1.6.0</flatten-maven-plugin.version>
<!-- 看看咋放到 bom 里 --> <!-- 看看咋放到 bom 里 -->
<lombok.version>1.18.36</lombok.version> <lombok.version>1.18.36</lombok.version>
<spring.boot.version>3.4.1</spring.boot.version> <spring.boot.version>3.4.5</spring.boot.version>
<mapstruct.version>1.6.3</mapstruct.version> <mapstruct.version>1.6.3</mapstruct.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties> </properties>

View File

@ -1055,7 +1055,7 @@ INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `st
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'); 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');
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'); 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');
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'); 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');
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'); 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');
COMMIT; COMMIT;
-- ---------------------------- -- ----------------------------

View File

@ -17,14 +17,13 @@
<revision>2.4.2-SNAPSHOT</revision> <revision>2.4.2-SNAPSHOT</revision>
<flatten-maven-plugin.version>1.6.0</flatten-maven-plugin.version> <flatten-maven-plugin.version>1.6.0</flatten-maven-plugin.version>
<!-- 统一依赖管理 --> <!-- 统一依赖管理 -->
<spring.boot.version>3.4.1</spring.boot.version> <spring.boot.version>3.4.5</spring.boot.version>
<!-- Web 相关 --> <!-- Web 相关 -->
<springdoc.version>2.7.0</springdoc.version> <springdoc.version>2.8.3</springdoc.version>
<knife4j.version>4.6.0</knife4j.version> <knife4j.version>4.6.0</knife4j.version>
<!-- DB 相关 --> <!-- DB 相关 -->
<druid.version>1.2.24</druid.version> <druid.version>1.2.24</druid.version>
<mybatis.version>3.5.17</mybatis.version> <mybatis-plus.version>3.5.10.1</mybatis-plus.version>
<mybatis-plus.version>3.5.9</mybatis-plus.version>
<dynamic-datasource.version>4.3.1</dynamic-datasource.version> <dynamic-datasource.version>4.3.1</dynamic-datasource.version>
<mybatis-plus-join.version>1.4.13</mybatis-plus-join.version> <mybatis-plus-join.version>1.4.13</mybatis-plus-join.version>
<easy-trans.version>3.0.6</easy-trans.version> <easy-trans.version>3.0.6</easy-trans.version>
@ -39,7 +38,7 @@
<lock4j.version>2.2.7</lock4j.version> <lock4j.version>2.2.7</lock4j.version>
<!-- 监控相关 --> <!-- 监控相关 -->
<skywalking.version>9.0.0</skywalking.version> <skywalking.version>9.0.0</skywalking.version>
<spring-boot-admin.version>3.4.1</spring-boot-admin.version> <spring-boot-admin.version>3.4.5</spring-boot-admin.version>
<opentracing.version>0.33.0</opentracing.version> <opentracing.version>0.33.0</opentracing.version>
<!-- Test 测试相关 --> <!-- Test 测试相关 -->
<podam.version>8.0.2.RELEASE</podam.version> <podam.version>8.0.2.RELEASE</podam.version>
@ -48,7 +47,7 @@
<!-- Bpm 工作流相关 --> <!-- Bpm 工作流相关 -->
<flowable.version>7.0.1</flowable.version> <flowable.version>7.0.1</flowable.version>
<!-- 工具类相关 --> <!-- 工具类相关 -->
<captcha-plus.version>2.0.3</captcha-plus.version> <anji-plus-captcha.version>1.4.0</anji-plus-captcha.version>
<jsoup.version>1.18.3</jsoup.version> <jsoup.version>1.18.3</jsoup.version>
<lombok.version>1.18.36</lombok.version> <lombok.version>1.18.36</lombok.version>
<mapstruct.version>1.6.3</mapstruct.version> <mapstruct.version>1.6.3</mapstruct.version>
@ -71,9 +70,10 @@
<!-- 三方云服务相关 --> <!-- 三方云服务相关 -->
<commons-io.version>2.17.0</commons-io.version> <commons-io.version>2.17.0</commons-io.version>
<commons-compress.version>1.27.1</commons-compress.version> <commons-compress.version>1.27.1</commons-compress.version>
<aws-java-sdk-s3.version>1.12.777</aws-java-sdk-s3.version> <awssdk.version>2.30.14</awssdk.version>
<justauth.version>2.0.5</justauth.version> <justauth.version>1.16.7</justauth.version>
<jimureport.version>1.8.1</jimureport.version> <justauth-starter.version>1.4.0</justauth-starter.version>
<jimureport.version>1.9.4</jimureport.version>
<weixin-java.version>4.7.2.B</weixin-java.version> <weixin-java.version>4.7.2.B</weixin-java.version>
</properties> </properties>
@ -173,11 +173,6 @@
<artifactId>druid-spring-boot-3-starter</artifactId> <artifactId>druid-spring-boot-3-starter</artifactId>
<version>${druid.version}</version> <version>${druid.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>${mybatis.version}</version>
</dependency>
<dependency> <dependency>
<groupId>com.baomidou</groupId> <groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId> <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
@ -534,9 +529,9 @@
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.xingyuv</groupId> <groupId>com.anji-plus</groupId>
<artifactId>spring-boot-starter-captcha-plus</artifactId> <artifactId>captcha-spring-boot-starter</artifactId> <!-- 验证码,一般用于登录使用 -->
<version>${captcha-plus.version}</version> <version>${anji-plus-captcha.version}</version>
</dependency> </dependency>
<dependency> <dependency>
@ -553,21 +548,20 @@
<!-- 三方云服务相关 --> <!-- 三方云服务相关 -->
<dependency> <dependency>
<groupId>com.amazonaws</groupId> <groupId>software.amazon.awssdk</groupId>
<artifactId>aws-java-sdk-s3</artifactId> <artifactId>s3</artifactId>
<version>${aws-java-sdk-s3.version}</version> <version>${awssdk.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.xingyuv</groupId> <groupId>me.zhyd.oauth</groupId>
<artifactId>spring-boot-starter-justauth</artifactId> <!-- 社交登陆(例如说,个人微信、企业微信等等) --> <artifactId>JustAuth</artifactId> <!-- 社交登陆(例如说,个人微信、企业微信等等) -->
<version>${justauth.version}</version> <version>${justauth.version}</version>
<exclusions> </dependency>
<exclusion> <dependency>
<groupId>cn.hutool</groupId> <groupId>com.xkcoding.justauth</groupId>
<artifactId>hutool-core</artifactId> <artifactId>justauth-spring-boot-starter</artifactId>
</exclusion> <version>${justauth-starter.version}</version>
</exclusions>
</dependency> </dependency>
<dependency> <dependency>
@ -591,10 +585,15 @@
<groupId>org.jeecgframework.jimureport</groupId> <groupId>org.jeecgframework.jimureport</groupId>
<artifactId>jimureport-spring-boot3-starter-fastjson2</artifactId> <artifactId>jimureport-spring-boot3-starter-fastjson2</artifactId>
<version>${jimureport.version}</version> <version>${jimureport.version}</version>
</dependency>
<dependency>
<groupId>org.jeecgframework.jimureport</groupId>
<artifactId>jimubi-spring-boot3-starter</artifactId>
<version>${jimureport.version}</version>
<exclusions> <exclusions>
<exclusion> <exclusion>
<groupId>com.alibaba</groupId> <groupId>com.github.jsqlparser</groupId>
<artifactId>druid</artifactId> <artifactId>jsqlparser</artifactId>
</exclusion> </exclusion>
</exclusions> </exclusions>
</dependency> </dependency>

View File

@ -11,6 +11,7 @@ import java.util.function.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import static cn.hutool.core.convert.Convert.toCollection;
import static java.util.Arrays.asList; import static java.util.Arrays.asList;
/** /**
@ -335,4 +336,17 @@ public class CollectionUtils {
return list.stream().filter(Objects::nonNull).flatMap(Collection::stream).collect(Collectors.toList()); return list.stream().filter(Objects::nonNull).flatMap(Collection::stream).collect(Collectors.toList());
} }
/**
* 转换为 LinkedHashSet
*
* @param <T> 元素类型
* @param elementType 集合中元素类型
* @param value 被转换的值
* @return {@link LinkedHashSet}
*/
@SuppressWarnings("unchecked")
public static <T> LinkedHashSet<T> toLinkedHashSet(Class<T> elementType, Object value) {
return (LinkedHashSet<T>) toCollection(LinkedHashSet.class, elementType, value);
}
} }

View File

@ -1,14 +1,9 @@
package cn.iocoder.yudao.framework.common.util.io; package cn.iocoder.yudao.framework.common.util.io;
import cn.hutool.core.io.FileTypeUtil;
import cn.hutool.core.io.FileUtil; import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.file.FileNameUtil;
import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import java.io.ByteArrayInputStream;
import java.io.File; import java.io.File;
/** /**
@ -63,22 +58,4 @@ public class FileUtils {
return file; return file;
} }
/**
* 生成文件路径
*
* @param content 文件内容
* @param originalName 原始文件名
* @return path唯一不可重复
*/
public static String generatePath(byte[] content, String originalName) {
String sha256Hex = DigestUtil.sha256Hex(content);
// 情况一如果存在 name则优先使用 name 的后缀
if (StrUtil.isNotBlank(originalName)) {
String extName = FileNameUtil.extName(originalName);
return StrUtil.isBlank(extName) ? sha256Hex : sha256Hex + "." + extName;
}
// 情况二基于 content 计算
return sha256Hex + '.' + FileTypeUtil.getType(new ByteArrayInputStream(content));
}
} }

View File

@ -6,6 +6,7 @@ import cn.hutool.system.SystemUtil;
import cn.iocoder.yudao.framework.common.enums.DocumentEnum; import cn.iocoder.yudao.framework.common.enums.DocumentEnum;
import cn.iocoder.yudao.framework.mq.redis.core.RedisMQTemplate; import cn.iocoder.yudao.framework.mq.redis.core.RedisMQTemplate;
import cn.iocoder.yudao.framework.mq.redis.core.job.RedisPendingMessageResendJob; import cn.iocoder.yudao.framework.mq.redis.core.job.RedisPendingMessageResendJob;
import cn.iocoder.yudao.framework.mq.redis.core.job.RedisStreamMessageCleanupJob;
import cn.iocoder.yudao.framework.mq.redis.core.pubsub.AbstractRedisChannelMessageListener; import cn.iocoder.yudao.framework.mq.redis.core.pubsub.AbstractRedisChannelMessageListener;
import cn.iocoder.yudao.framework.mq.redis.core.stream.AbstractRedisStreamMessageListener; import cn.iocoder.yudao.framework.mq.redis.core.stream.AbstractRedisStreamMessageListener;
import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration; import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration;
@ -73,6 +74,17 @@ public class YudaoRedisMQConsumerAutoConfiguration {
return new RedisPendingMessageResendJob(listeners, redisTemplate, groupName, redissonClient); return new RedisPendingMessageResendJob(listeners, redisTemplate, groupName, redissonClient);
} }
/**
* 创建 Redis Stream 消息清理任务
*/
@Bean
@ConditionalOnBean(AbstractRedisStreamMessageListener.class)
public RedisStreamMessageCleanupJob redisStreamMessageCleanupJob(List<AbstractRedisStreamMessageListener<?>> listeners,
RedisMQTemplate redisTemplate,
RedissonClient redissonClient) {
return new RedisStreamMessageCleanupJob(listeners, redisTemplate, redissonClient);
}
/** /**
* 创建 Redis Stream 集群消费的容器 * 创建 Redis Stream 集群消费的容器
* *

View File

@ -23,13 +23,13 @@ import java.util.Objects;
@AllArgsConstructor @AllArgsConstructor
public class RedisPendingMessageResendJob { public class RedisPendingMessageResendJob {
private static final String LOCK_KEY = "redis:pending:msg:lock"; private static final String LOCK_KEY = "redis:stream:pending-message-resend:lock";
/** /**
* 消息超时时间默认 5 分钟 * 消息超时时间默认 5 分钟
* *
* 1. 超时的消息才会被重新投递 * 1. 超时的消息才会被重新投递
* 2. 由于定时任务 1 分钟一次消息超时后不会被立即重投极端情况下消息5分钟过期后再等 1 分钟才会被扫瞄到 * 2. 由于定时任务 1 分钟一次消息超时后不会被立即重投极端情况下消息 5 分钟过期后再等 1 分钟才会被扫瞄到
*/ */
private static final int EXPIRE_TIME = 5 * 60; private static final int EXPIRE_TIME = 5 * 60;
@ -39,7 +39,7 @@ public class RedisPendingMessageResendJob {
private final RedissonClient redissonClient; private final RedissonClient redissonClient;
/** /**
* 一分钟执行一次,这里选择每分钟的35秒执行是为了避免整点任务过多的问题 * 一分钟执行一次,这里选择每分钟的 35 秒执行是为了避免整点任务过多的问题
*/ */
@Scheduled(cron = "35 * * * * ?") @Scheduled(cron = "35 * * * * ?")
public void messageResend() { public void messageResend() {

View File

@ -0,0 +1,72 @@
package cn.iocoder.yudao.framework.mq.redis.core.job;
import cn.iocoder.yudao.framework.mq.redis.core.RedisMQTemplate;
import cn.iocoder.yudao.framework.mq.redis.core.stream.AbstractRedisStreamMessageListener;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.data.redis.core.StreamOperations;
import org.springframework.scheduling.annotation.Scheduled;
import java.util.List;
/**
* Redis Stream 消息清理任务
* 用于定期清理已消费的消息防止内存占用过大
*
* @see <a href="https://www.cnblogs.com/nanxiang/p/16179519.html">记一次 redis stream 数据类型内存不释放问题</a>
*
* @author 芋道源码
*/
@Slf4j
@AllArgsConstructor
public class RedisStreamMessageCleanupJob {
private static final String LOCK_KEY = "redis:stream:message-cleanup:lock";
/**
* 保留的消息数量默认保留最近 10000 条消息
*/
private static final long MAX_COUNT = 10000;
private final List<AbstractRedisStreamMessageListener<?>> listeners;
private final RedisMQTemplate redisTemplate;
private final RedissonClient redissonClient;
/**
* 每小时执行一次清理任务
*/
@Scheduled(cron = "0 0 * * * ?")
public void cleanup() {
RLock lock = redissonClient.getLock(LOCK_KEY);
// 尝试加锁
if (lock.tryLock()) {
try {
execute();
} catch (Exception ex) {
log.error("[cleanup][执行异常]", ex);
} finally {
lock.unlock();
}
}
}
/**
* 执行清理逻辑
*/
private void execute() {
StreamOperations<String, Object, Object> ops = redisTemplate.getRedisTemplate().opsForStream();
listeners.forEach(listener -> {
try {
// 使用 XTRIM 命令清理消息只保留最近的 MAX_LEN 条消息
Long trimCount = ops.trim(listener.getStreamKey(), MAX_COUNT, true);
if (trimCount != null && trimCount > 0) {
log.info("[execute][Stream({}) 清理消息数量({})]", listener.getStreamKey(), trimCount);
}
} catch (Exception ex) {
log.error("[execute][Stream({}) 清理异常]", listener.getStreamKey(), ex);
}
});
}
}

View File

@ -1,7 +1,7 @@
package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.form; package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.form;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.lang.Assert; import cn.hutool.core.lang.Assert;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy; import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.user.BpmTaskCandidateUserStrategy; import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.user.BpmTaskCandidateUserStrategy;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum; import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
@ -33,7 +33,7 @@ public class BpmTaskCandidateFormUserStrategy implements BpmTaskCandidateStrateg
@Override @Override
public Set<Long> calculateUsersByTask(DelegateExecution execution, String param) { public Set<Long> calculateUsersByTask(DelegateExecution execution, String param) {
Object result = execution.getVariable(param); Object result = execution.getVariable(param);
return Convert.toSet(Long.class, result); return CollectionUtils.toLinkedHashSet(Long.class, result);
} }
@Override @Override
@ -41,7 +41,7 @@ public class BpmTaskCandidateFormUserStrategy implements BpmTaskCandidateStrateg
String param, Long startUserId, String processDefinitionId, String param, Long startUserId, String processDefinitionId,
Map<String, Object> processVariables) { Map<String, Object> processVariables) {
Object result = processVariables == null ? null : processVariables.get(param); Object result = processVariables == null ? null : processVariables.get(param);
return Convert.toSet(Long.class, result); return CollectionUtils.toLinkedHashSet(Long.class, result);
} }
} }

View File

@ -1,6 +1,6 @@
package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.other; package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.other;
import cn.hutool.core.convert.Convert; import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy; import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum; import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils; import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
@ -37,7 +37,7 @@ public class BpmTaskCandidateExpressionStrategy implements BpmTaskCandidateStrat
@Override @Override
public Set<Long> calculateUsersByTask(DelegateExecution execution, String param) { public Set<Long> calculateUsersByTask(DelegateExecution execution, String param) {
Object result = FlowableUtils.getExpressionValue(execution, param); Object result = FlowableUtils.getExpressionValue(execution, param);
return Convert.toSet(Long.class, result); return CollectionUtils.toLinkedHashSet(Long.class, result);
} }
@Override @Override
@ -46,7 +46,7 @@ public class BpmTaskCandidateExpressionStrategy implements BpmTaskCandidateStrat
Map<String, Object> variables = processVariables == null ? new HashMap<>() : processVariables; Map<String, Object> variables = processVariables == null ? new HashMap<>() : processVariables;
try { try {
Object result = FlowableUtils.getExpressionValue(variables, param); Object result = FlowableUtils.getExpressionValue(variables, param);
return Convert.toSet(Long.class, result); return CollectionUtils.toLinkedHashSet(Long.class, result);
} catch (FlowableException ex) { } catch (FlowableException ex) {
// 预测未运行的节点时候表达式如果包含 execution 或者不存在的流程变量会抛异常 // 预测未运行的节点时候表达式如果包含 execution 或者不存在的流程变量会抛异常
log.warn("[calculateUsersByActivity][表达式({}) 变量({}) 解析报错", param, variables, ex); log.warn("[calculateUsersByActivity][表达式({}) 变量({}) 解析报错", param, variables, ex);

View File

@ -6,7 +6,6 @@ import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.*; import cn.hutool.core.util.*;
import cn.hutool.extra.spring.SpringUtil; import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
import cn.iocoder.yudao.framework.common.util.date.DateUtils; import cn.iocoder.yudao.framework.common.util.date.DateUtils;
import cn.iocoder.yudao.framework.common.util.number.NumberUtils; import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
@ -882,6 +881,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
return; return;
} }
runExecutionIds.add(task.getExecutionId()); runExecutionIds.add(task.getExecutionId());
// 判断是否分配给自己任务因为会签任务一个节点会有多个任务 // 判断是否分配给自己任务因为会签任务一个节点会有多个任务
if (isAssignUserTask(userId, task)) { // 情况一自己的任务进行 RETURN 标记 if (isAssignUserTask(userId, task)) { // 情况一自己的任务进行 RETURN 标记
// 2.1.1 添加评论 // 2.1.1 添加评论

View File

@ -1,5 +1,7 @@
package cn.iocoder.yudao.module.infra.api.file; package cn.iocoder.yudao.module.infra.api.file;
import jakarta.validation.constraints.NotEmpty;
/** /**
* 文件 API 接口 * 文件 API 接口
* *
@ -14,28 +16,30 @@ public interface FileApi {
* @return 文件路径 * @return 文件路径
*/ */
default String createFile(byte[] content) { default String createFile(byte[] content) {
return createFile(null, null, content); return createFile(content, null, null, null);
} }
/** /**
* 保存文件并返回文件的访问路径 * 保存文件并返回文件的访问路径
* *
* @param path 文件路径
* @param content 文件内容 * @param content 文件内容
* @param name 文件名称允许空
* @return 文件路径 * @return 文件路径
*/ */
default String createFile(String path, byte[] content) { default String createFile(byte[] content, String name) {
return createFile(null, path, content); return createFile(content, name, null, null);
} }
/** /**
* 保存文件并返回文件的访问路径 * 保存文件并返回文件的访问路径
* *
* @param name 文件名称
* @param path 文件路径
* @param content 文件内容 * @param content 文件内容
* @param name 文件名称允许空
* @param directory 目录允许空
* @param type 文件的 MIME 类型允许空
* @return 文件路径 * @return 文件路径
*/ */
String createFile(String name, String path, byte[] content); String createFile(@NotEmpty(message = "文件内容不能为空") byte[] content,
String name, String directory, String type);
} }

View File

@ -115,9 +115,10 @@
<groupId>com.jcraft</groupId> <groupId>com.jcraft</groupId>
<artifactId>jsch</artifactId> <!-- 文件客户端:解决 sftp 连接 --> <artifactId>jsch</artifactId> <!-- 文件客户端:解决 sftp 连接 -->
</dependency> </dependency>
<!-- 文件客户端解决阿里云、腾讯云、minio 等 S3 连接 -->
<dependency> <dependency>
<groupId>com.amazonaws</groupId> <groupId>software.amazon.awssdk</groupId>
<artifactId>aws-java-sdk-s3</artifactId><!-- 文件客户端解决阿里云、腾讯云、minio 等 S3 连接 --> <artifactId>s3</artifactId>
</dependency> </dependency>
<dependency> <dependency>

View File

@ -1,11 +1,10 @@
package cn.iocoder.yudao.module.infra.api.file; package cn.iocoder.yudao.module.infra.api.file;
import cn.iocoder.yudao.module.infra.service.file.FileService; import cn.iocoder.yudao.module.infra.service.file.FileService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import jakarta.annotation.Resource;
/** /**
* 文件 API 实现类 * 文件 API 实现类
* *
@ -19,8 +18,8 @@ public class FileApiImpl implements FileApi {
private FileService fileService; private FileService fileService;
@Override @Override
public String createFile(String name, String path, byte[] content) { public String createFile(byte[] content, String name, String directory, String type) {
return fileService.createFile(name, path, content); return fileService.createFile(content, name, directory, type);
} }
} }

View File

@ -11,6 +11,7 @@ import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO;
import cn.iocoder.yudao.module.infra.service.file.FileService; import cn.iocoder.yudao.module.infra.service.file.FileService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import jakarta.annotation.security.PermitAll; import jakarta.annotation.security.PermitAll;
@ -41,14 +42,21 @@ public class FileController {
@Operation(summary = "上传文件", description = "模式一:后端上传文件") @Operation(summary = "上传文件", description = "模式一:后端上传文件")
public CommonResult<String> uploadFile(FileUploadReqVO uploadReqVO) throws Exception { public CommonResult<String> uploadFile(FileUploadReqVO uploadReqVO) throws Exception {
MultipartFile file = uploadReqVO.getFile(); MultipartFile file = uploadReqVO.getFile();
String path = uploadReqVO.getPath(); byte[] content = IoUtil.readBytes(file.getInputStream());
return success(fileService.createFile(file.getOriginalFilename(), path, IoUtil.readBytes(file.getInputStream()))); return success(fileService.createFile(content, file.getOriginalFilename(),
uploadReqVO.getDirectory(), file.getContentType()));
} }
@GetMapping("/presigned-url") @GetMapping("/presigned-url")
@Operation(summary = "获取文件预签名地址", description = "模式二:前端上传文件:用于前端直接上传七牛、阿里云 OSS 等文件存储器") @Operation(summary = "获取文件预签名地址", description = "模式二:前端上传文件:用于前端直接上传七牛、阿里云 OSS 等文件存储器")
public CommonResult<FilePresignedUrlRespVO> getFilePresignedUrl(@RequestParam("path") String path) throws Exception { @Parameters({
return success(fileService.getFilePresignedUrl(path)); @Parameter(name = "name", description = "文件名称", required = true),
@Parameter(name = "directory", description = "文件目录")
})
public CommonResult<FilePresignedUrlRespVO> getFilePresignedUrl(
@RequestParam("name") String name,
@RequestParam(value = "directory", required = false) String directory) {
return success(fileService.getFilePresignedUrl(name, directory));
} }
@PostMapping("/create") @PostMapping("/create")

View File

@ -14,7 +14,8 @@ public class FilePresignedUrlRespVO {
@Schema(description = "配置编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "11") @Schema(description = "配置编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "11")
private Long configId; private Long configId;
@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") @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")
private String uploadUrl; private String uploadUrl;
/** /**
@ -26,4 +27,12 @@ public class FilePresignedUrlRespVO {
example = "https://test.yudao.iocoder.cn/758d3a5387507358c7236de4c8f96de1c7f5097ff6a7722b34772fb7b76b140f.png") example = "https://test.yudao.iocoder.cn/758d3a5387507358c7236de4c8f96de1c7f5097ff6a7722b34772fb7b76b140f.png")
private String url; private String url;
/**
* 为什么要返回 path 字段
*
* 前端上传完文件后需要调用 createFile 记录下 path 路径
*/
@Schema(description = "文件路径", requiredMode = Schema.RequiredMode.REQUIRED, example = "xxx.png")
private String path;
} }

View File

@ -14,7 +14,7 @@ public class FileUploadReqVO {
@NotNull(message = "文件附件不能为空") @NotNull(message = "文件附件不能为空")
private MultipartFile file; private MultipartFile file;
@Schema(description = "文件附件", example = "yudaoyuanma.png") @Schema(description = "文件目录", example = "XXX/YYY")
private String path; private String directory;
} }

View File

@ -7,6 +7,8 @@ import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePresigned
import cn.iocoder.yudao.module.infra.controller.app.file.vo.AppFileUploadReqVO; import cn.iocoder.yudao.module.infra.controller.app.file.vo.AppFileUploadReqVO;
import cn.iocoder.yudao.module.infra.service.file.FileService; import cn.iocoder.yudao.module.infra.service.file.FileService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import jakarta.annotation.security.PermitAll; import jakarta.annotation.security.PermitAll;
@ -33,15 +35,21 @@ public class AppFileController {
@PermitAll @PermitAll
public CommonResult<String> uploadFile(AppFileUploadReqVO uploadReqVO) throws Exception { public CommonResult<String> uploadFile(AppFileUploadReqVO uploadReqVO) throws Exception {
MultipartFile file = uploadReqVO.getFile(); MultipartFile file = uploadReqVO.getFile();
String path = uploadReqVO.getPath(); byte[] content = IoUtil.readBytes(file.getInputStream());
return success(fileService.createFile(file.getOriginalFilename(), path, IoUtil.readBytes(file.getInputStream()))); return success(fileService.createFile(content, file.getOriginalFilename(),
uploadReqVO.getDirectory(), file.getContentType()));
} }
@GetMapping("/presigned-url") @GetMapping("/presigned-url")
@Operation(summary = "获取文件预签名地址", description = "模式二:前端上传文件:用于前端直接上传七牛、阿里云 OSS 等文件存储器") @Operation(summary = "获取文件预签名地址", description = "模式二:前端上传文件:用于前端直接上传七牛、阿里云 OSS 等文件存储器")
@PermitAll @Parameters({
public CommonResult<FilePresignedUrlRespVO> getFilePresignedUrl(@RequestParam("path") String path) throws Exception { @Parameter(name = "name", description = "文件名称", required = true),
return success(fileService.getFilePresignedUrl(path)); @Parameter(name = "directory", description = "文件目录")
})
public CommonResult<FilePresignedUrlRespVO> getFilePresignedUrl(
@RequestParam("name") String name,
@RequestParam(value = "directory", required = false) String directory) {
return success(fileService.getFilePresignedUrl(name, directory));
} }
@PostMapping("/create") @PostMapping("/create")

View File

@ -14,7 +14,7 @@ public class AppFileUploadReqVO {
@NotNull(message = "文件附件不能为空") @NotNull(message = "文件附件不能为空")
private MultipartFile file; private MultipartFile file;
@Schema(description = "文件附件", example = "yudaoyuanma.png") @Schema(description = "文件目录", example = "XXX/YYY")
private String path; private String directory;
} }

View File

@ -23,8 +23,7 @@ public interface CodegenTableMapper extends BaseMapperX<CodegenTableDO> {
.likeIfPresent(CodegenTableDO::getTableComment, pageReqVO.getTableComment()) .likeIfPresent(CodegenTableDO::getTableComment, pageReqVO.getTableComment())
.likeIfPresent(CodegenTableDO::getClassName, pageReqVO.getClassName()) .likeIfPresent(CodegenTableDO::getClassName, pageReqVO.getClassName())
.betweenIfPresent(CodegenTableDO::getCreateTime, pageReqVO.getCreateTime()) .betweenIfPresent(CodegenTableDO::getCreateTime, pageReqVO.getCreateTime())
.orderByDesc(CodegenTableDO::getUpdateTime) .orderByDesc(CodegenTableDO::getUpdateTime));
);
} }
default List<CodegenTableDO> selectListByDataSourceConfigId(Long dataSourceConfigId) { default List<CodegenTableDO> selectListByDataSourceConfigId(Long dataSourceConfigId) {

View File

@ -19,7 +19,8 @@ public interface ConfigMapper extends BaseMapperX<ConfigDO> {
.likeIfPresent(ConfigDO::getName, reqVO.getName()) .likeIfPresent(ConfigDO::getName, reqVO.getName())
.likeIfPresent(ConfigDO::getConfigKey, reqVO.getKey()) .likeIfPresent(ConfigDO::getConfigKey, reqVO.getKey())
.eqIfPresent(ConfigDO::getType, reqVO.getType()) .eqIfPresent(ConfigDO::getType, reqVO.getType())
.betweenIfPresent(ConfigDO::getCreateTime, reqVO.getCreateTime())); .betweenIfPresent(ConfigDO::getCreateTime, reqVO.getCreateTime())
.orderByDesc(ConfigDO::getId));
} }
} }

View File

@ -24,7 +24,7 @@ public interface JobMapper extends BaseMapperX<JobDO> {
.likeIfPresent(JobDO::getName, reqVO.getName()) .likeIfPresent(JobDO::getName, reqVO.getName())
.eqIfPresent(JobDO::getStatus, reqVO.getStatus()) .eqIfPresent(JobDO::getStatus, reqVO.getStatus())
.likeIfPresent(JobDO::getHandlerName, reqVO.getHandlerName()) .likeIfPresent(JobDO::getHandlerName, reqVO.getHandlerName())
); .orderByDesc(JobDO::getId));
} }
} }

View File

@ -13,10 +13,14 @@ import lombok.Getter;
public enum CodegenFrontTypeEnum { public enum CodegenFrontTypeEnum {
VUE2_ELEMENT_UI(10), // Vue2 Element UI 标准模版 VUE2_ELEMENT_UI(10), // Vue2 Element UI 标准模版
VUE3_ELEMENT_PLUS(20), // Vue3 Element Plus 标准模版 VUE3_ELEMENT_PLUS(20), // Vue3 Element Plus 标准模版
VUE3_VBEN2_ANTD_SCHEMA(30), // Vue3 VBEN2 + ANTD + Schema 模版 VUE3_VBEN2_ANTD_SCHEMA(30), // Vue3 VBEN2 + ANTD + Schema 模版
VUE3_VBEN5_ANTD_SCHEMA(40), // Vue3 VBEN5 + ANTD + schema 模版 VUE3_VBEN5_ANTD_SCHEMA(40), // Vue3 VBEN5 + ANTD + schema 模版
VUE3_VBEN5_ANTD(50), // Vue3 VBEN5 + ANTD 模版
VUE3_VBEN5_ANTD_GENERAL(41), // Vue3 VBEN5 + ANTD 标准模版
; ;
/** /**

View File

@ -26,12 +26,6 @@ public class FtpFileClient extends AbstractFileClient<FtpFileClientConfig> {
@Override @Override
protected void doInit() { protected void doInit() {
// 把配置的 \ 替换成 /, 如果路径配置 \a\test, 替换成 /a/test, 替换方法已经处理 null 情况
config.setBasePath(StrUtil.replace(config.getBasePath(), StrUtil.BACKSLASH, StrUtil.SLASH));
// ftp的路径是 / 结尾
if (!config.getBasePath().endsWith(StrUtil.SLASH)) {
config.setBasePath(config.getBasePath() + StrUtil.SLASH);
}
// 初始化 Ftp 对象 // 初始化 Ftp 对象
this.ftp = new Ftp(config.getHost(), config.getPort(), config.getUsername(), config.getPassword(), this.ftp = new Ftp(config.getHost(), config.getPort(), config.getUsername(), config.getPassword(),
CharsetUtil.CHARSET_UTF_8, null, null, FtpMode.valueOf(config.getMode())); CharsetUtil.CHARSET_UTF_8, null, null, FtpMode.valueOf(config.getMode()));
@ -43,8 +37,8 @@ public class FtpFileClient extends AbstractFileClient<FtpFileClientConfig> {
String filePath = getFilePath(path); String filePath = getFilePath(path);
String fileName = FileUtil.getName(filePath); String fileName = FileUtil.getName(filePath);
String dir = StrUtil.removeSuffix(filePath, fileName); String dir = StrUtil.removeSuffix(filePath, fileName);
ftp.reconnectIfTimeout(); reconnectIfTimeout();
boolean success = ftp.upload(dir, fileName, new ByteArrayInputStream(content)); boolean success = ftp.upload(dir, fileName, new ByteArrayInputStream(content)); // 不需要主动创建目录ftp 内部已经处理见源码
if (!success) { if (!success) {
throw new FtpException(StrUtil.format("上传文件到目标目录 ({}) 失败", filePath)); throw new FtpException(StrUtil.format("上传文件到目标目录 ({}) 失败", filePath));
} }
@ -55,7 +49,7 @@ public class FtpFileClient extends AbstractFileClient<FtpFileClientConfig> {
@Override @Override
public void delete(String path) { public void delete(String path) {
String filePath = getFilePath(path); String filePath = getFilePath(path);
ftp.reconnectIfTimeout(); reconnectIfTimeout();
ftp.delFile(filePath); ftp.delFile(filePath);
} }
@ -65,13 +59,17 @@ public class FtpFileClient extends AbstractFileClient<FtpFileClientConfig> {
String fileName = FileUtil.getName(filePath); String fileName = FileUtil.getName(filePath);
String dir = StrUtil.removeSuffix(filePath, fileName); String dir = StrUtil.removeSuffix(filePath, fileName);
ByteArrayOutputStream out = new ByteArrayOutputStream(); ByteArrayOutputStream out = new ByteArrayOutputStream();
ftp.reconnectIfTimeout(); reconnectIfTimeout();
ftp.download(dir, fileName, out); ftp.download(dir, fileName, out);
return out.toByteArray(); return out.toByteArray();
} }
private String getFilePath(String path) { private String getFilePath(String path) {
return config.getBasePath() + path; return config.getBasePath() + StrUtil.SLASH + path;
}
private synchronized void reconnectIfTimeout() {
ftp.reconnectIfTimeout();
} }
} }

View File

@ -18,10 +18,6 @@ public class LocalFileClient extends AbstractFileClient<LocalFileClientConfig> {
@Override @Override
protected void doInit() { protected void doInit() {
// 补全风格例如说 Linux /Windows \
if (!config.getBasePath().endsWith(File.separator)) {
config.setBasePath(config.getBasePath() + File.separator);
}
} }
@Override @Override
@ -46,7 +42,7 @@ public class LocalFileClient extends AbstractFileClient<LocalFileClientConfig> {
} }
private String getFilePath(String path) { private String getFilePath(String path) {
return config.getBasePath() + path; return config.getBasePath() + File.separator + path;
} }
} }

View File

@ -4,29 +4,31 @@ import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil; import cn.hutool.http.HttpUtil;
import cn.iocoder.yudao.module.infra.framework.file.core.client.AbstractFileClient; import cn.iocoder.yudao.module.infra.framework.file.core.client.AbstractFileClient;
import com.amazonaws.HttpMethod; import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import com.amazonaws.auth.AWSStaticCredentialsProvider; import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials; import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import com.amazonaws.client.builder.AwsClientBuilder; import software.amazon.awssdk.core.sync.RequestBody;
import com.amazonaws.services.s3.AmazonS3Client; import software.amazon.awssdk.regions.Region;
import com.amazonaws.services.s3.AmazonS3ClientBuilder; import software.amazon.awssdk.services.s3.S3Client;
import com.amazonaws.services.s3.model.ObjectMetadata; import software.amazon.awssdk.services.s3.S3Configuration;
import com.amazonaws.services.s3.model.S3Object; import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest;
import java.io.ByteArrayInputStream; import java.net.URI;
import java.util.Date; import java.time.Duration;
import java.util.concurrent.TimeUnit;
/** /**
* 基于 S3 协议的文件客户端实现 MinIO阿里云腾讯云七牛云华为云等云服务 * 基于 S3 协议的文件客户端实现 MinIO阿里云腾讯云七牛云华为云等云服务
* <p>
* S3 协议的客户端采用亚马逊提供的 software.amazon.awssdk.s3
* *
* @author 芋道源码 * @author 芋道源码
*/ */
public class S3FileClient extends AbstractFileClient<S3FileClientConfig> { public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
private AmazonS3Client client; private S3Client client;
private S3Presigner presigner;
public S3FileClient(Long id, S3FileClientConfig config) { public S3FileClient(Long id, S3FileClientConfig config) {
super(id, config); super(id, config);
@ -38,31 +40,80 @@ public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
if (StrUtil.isEmpty(config.getDomain())) { if (StrUtil.isEmpty(config.getDomain())) {
config.setDomain(buildDomain()); config.setDomain(buildDomain());
} }
// 初始化客户端 // 初始化 S3 客户端
client = (AmazonS3Client)AmazonS3ClientBuilder.standard() Region region = Region.of("us-east-1"); // 必须填但填什么都行常见的值有 "us-east-1"不填会报错
.withCredentials(buildCredentials()) AwsCredentialsProvider credentialsProvider = StaticCredentialsProvider.create(
.withEndpointConfiguration(buildEndpointConfiguration()) AwsBasicCredentials.create(config.getAccessKey(), config.getAccessSecret()));
URI endpoint = URI.create(buildEndpoint());
S3Configuration serviceConfiguration = S3Configuration.builder() // Path-style 访问
.pathStyleAccessEnabled(Boolean.TRUE.equals(config.getEnablePathStyleAccess()))
.chunkedEncodingEnabled(false) // 禁用分块编码参见 https://t.zsxq.com/kBy57
.build();
client = S3Client.builder()
.credentialsProvider(credentialsProvider)
.region(region)
.endpointOverride(endpoint)
.serviceConfiguration(serviceConfiguration)
.build();
presigner = S3Presigner.builder()
.credentialsProvider(credentialsProvider)
.region(region)
.endpointOverride(endpoint)
.serviceConfiguration(serviceConfiguration)
.build(); .build();
} }
/** @Override
* 基于 config 秘钥构建 S3 客户端的认证信息 public String upload(byte[] content, String path, String type) {
* // 构造 PutObjectRequest
* @return S3 客户端的认证信息 PutObjectRequest putRequest = PutObjectRequest.builder()
*/ .bucket(config.getBucket())
private AWSStaticCredentialsProvider buildCredentials() { .key(path)
return new AWSStaticCredentialsProvider( .contentType(type)
new BasicAWSCredentials(config.getAccessKey(), config.getAccessSecret())); .contentLength((long) content.length)
.build();
// 上传文件
client.putObject(putRequest, RequestBody.fromBytes(content));
// 拼接返回路径
return config.getDomain() + "/" + path;
}
@Override
public void delete(String path) {
DeleteObjectRequest deleteRequest = DeleteObjectRequest.builder()
.bucket(config.getBucket())
.key(path)
.build();
client.deleteObject(deleteRequest);
}
@Override
public byte[] getContent(String path) {
GetObjectRequest getRequest = GetObjectRequest.builder()
.bucket(config.getBucket())
.key(path)
.build();
return IoUtil.readBytes(client.getObject(getRequest));
}
@Override
public FilePresignedUrlRespDTO getPresignedObjectUrl(String path) {
Duration expiration = Duration.ofHours(24);
return new FilePresignedUrlRespDTO(getPresignedUrl(path, expiration), config.getDomain() + "/" + path);
} }
/** /**
* 构建 S3 客户端的 Endpoint 配置包括 regionendpoint * 生成动态的预签名上传 URL
* *
* @return S3 客户端的 EndpointConfiguration 配置 * @param path 相对路径
* @param expiration 过期时间
* @return 生成的上传 URL
*/ */
private AwsClientBuilder.EndpointConfiguration buildEndpointConfiguration() { private String getPresignedUrl(String path, Duration expiration) {
return new AwsClientBuilder.EndpointConfiguration(config.getEndpoint(), return presigner.presignPutObject(PutObjectPresignRequest.builder()
null); // 无需设置 region .signatureDuration(expiration)
.putObjectRequest(b -> b.bucket(config.getBucket()).key(path))
.build()).url().toString();
} }
/** /**
@ -79,40 +130,17 @@ public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
return StrUtil.format("https://{}.{}", config.getBucket(), config.getEndpoint()); return StrUtil.format("https://{}.{}", config.getBucket(), config.getEndpoint());
} }
@Override /**
public String upload(byte[] content, String path, String type) throws Exception { * 节点地址补全协议头
// 元数据主要用于设置文件类型 *
ObjectMetadata objectMetadata = new ObjectMetadata(); * @return 节点地址
objectMetadata.setContentType(type); */
objectMetadata.setContentLength(content.length); // 如果不设置会有 No content length specified for stream data 警告日志 private String buildEndpoint() {
// 执行上传 // 如果已经是 http 或者 https则不进行拼接
client.putObject(config.getBucket(), if (HttpUtil.isHttp(config.getEndpoint()) || HttpUtil.isHttps(config.getEndpoint())) {
path, // 相对路径 return config.getEndpoint();
new ByteArrayInputStream(content), // 文件内容 }
objectMetadata); return StrUtil.format("https://{}", config.getEndpoint());
// 拼接返回路径
return config.getDomain() + "/" + path;
}
@Override
public void delete(String path) throws Exception {
client.deleteObject(config.getBucket(), path);
}
@Override
public byte[] getContent(String path) throws Exception {
S3Object tempS3Object = client.getObject(config.getBucket(), path);
return IoUtil.readBytes(tempS3Object.getObjectContent());
}
@Override
public FilePresignedUrlRespDTO getPresignedObjectUrl(String path) throws Exception {
// 设定过期时间为 10 分钟取值范围1 ~ 7
Date expiration = new Date(System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(10));
// 生成上传 URL
String uploadUrl = String.valueOf(client.generatePresignedUrl(config.getBucket(), path, expiration , HttpMethod.PUT));
return new FilePresignedUrlRespDTO(uploadUrl, config.getDomain() + "/" + path);
} }
} }

View File

@ -67,6 +67,12 @@ public class S3FileClientConfig implements FileClientConfig {
@NotNull(message = "accessSecret 不能为空") @NotNull(message = "accessSecret 不能为空")
private String accessSecret; private String accessSecret;
/**
* 是否启用 PathStyle 访问
*/
@NotNull(message = "enablePathStyleAccess 不能为空")
private Boolean enablePathStyleAccess;
@SuppressWarnings("RedundantIfStatement") @SuppressWarnings("RedundantIfStatement")
@AssertTrue(message = "domain 不能为空") @AssertTrue(message = "domain 不能为空")
@JsonIgnore @JsonIgnore

View File

@ -22,10 +22,6 @@ public class SftpFileClient extends AbstractFileClient<SftpFileClientConfig> {
@Override @Override
protected void doInit() { protected void doInit() {
// 补全风格例如说 Linux /Windows \
if (!config.getBasePath().endsWith(File.separator)) {
config.setBasePath(config.getBasePath() + File.separator);
}
// 初始化 Ftp 对象 // 初始化 Ftp 对象
this.sftp = new Sftp(config.getHost(), config.getPort(), config.getUsername(), config.getPassword()); this.sftp = new Sftp(config.getHost(), config.getPort(), config.getUsername(), config.getPassword());
} }
@ -35,6 +31,8 @@ public class SftpFileClient extends AbstractFileClient<SftpFileClientConfig> {
// 执行写入 // 执行写入
String filePath = getFilePath(path); String filePath = getFilePath(path);
File file = FileUtils.createTempFile(content); File file = FileUtils.createTempFile(content);
reconnectIfTimeout();
sftp.mkDirs(FileUtil.getParent(filePath, 1)); // 需要创建父目录不然会报错
sftp.upload(filePath, file); sftp.upload(filePath, file);
// 拼接返回路径 // 拼接返回路径
return super.formatFileUrl(config.getDomain(), path); return super.formatFileUrl(config.getDomain(), path);
@ -43,6 +41,7 @@ public class SftpFileClient extends AbstractFileClient<SftpFileClientConfig> {
@Override @Override
public void delete(String path) { public void delete(String path) {
String filePath = getFilePath(path); String filePath = getFilePath(path);
reconnectIfTimeout();
sftp.delFile(filePath); sftp.delFile(filePath);
} }
@ -50,12 +49,17 @@ public class SftpFileClient extends AbstractFileClient<SftpFileClientConfig> {
public byte[] getContent(String path) { public byte[] getContent(String path) {
String filePath = getFilePath(path); String filePath = getFilePath(path);
File destFile = FileUtils.createTempFile(); File destFile = FileUtils.createTempFile();
reconnectIfTimeout();
sftp.download(filePath, destFile); sftp.download(filePath, destFile);
return FileUtil.readBytes(destFile); return FileUtil.readBytes(destFile);
} }
private String getFilePath(String path) { private String getFilePath(String path) {
return config.getBasePath() + path; return config.getBasePath() + File.separator + path;
}
private synchronized void reconnectIfTimeout() {
sftp.reconnectIfTimeout();
} }
} }

View File

@ -6,7 +6,10 @@ import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
import com.alibaba.ttl.TransmittableThreadLocal; import com.alibaba.ttl.TransmittableThreadLocal;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.tika.Tika; import org.apache.tika.Tika;
import org.apache.tika.mime.MimeTypeException;
import org.apache.tika.mime.MimeTypes;
import java.io.IOException; import java.io.IOException;
@ -15,12 +18,13 @@ import java.io.IOException;
* *
* @author 芋道源码 * @author 芋道源码
*/ */
@Slf4j
public class FileTypeUtils { public class FileTypeUtils {
private static final ThreadLocal<Tika> TIKA = TransmittableThreadLocal.withInitial(Tika::new); private static final ThreadLocal<Tika> TIKA = TransmittableThreadLocal.withInitial(Tika::new);
/** /**
* 获得文件的 mineType对于docjar等文件会有误差 * 获得文件的 mineType对于 docjar 等文件会有误差
* *
* @param data 文件内容 * @param data 文件内容
* @return mineType 无法识别时会返回application/octet-stream * @return mineType 无法识别时会返回application/octet-stream
@ -31,7 +35,7 @@ public class FileTypeUtils {
} }
/** /**
* 已知文件名获取文件类型在某些情况下比通过字节数组准确例如使用jar文件时通过名字更为准确 * 已知文件名获取文件类型在某些情况下比通过字节数组准确例如使用 jar 文件时通过名字更为准确
* *
* @param name 文件名 * @param name 文件名
* @return mineType 无法识别时会返回application/octet-stream * @return mineType 无法识别时会返回application/octet-stream
@ -51,6 +55,23 @@ public class FileTypeUtils {
return TIKA.get().detect(data, name); return TIKA.get().detect(data, name);
} }
/**
* 根据 mineType 获得文件后缀
*
* 注意如果获取不到或者发生异常都返回 null
*
* @param mineType 类型
* @return 后缀例如说 .pdf
*/
public static String getExtension(String mineType) {
try {
return MimeTypes.getDefaultMimeTypes().forName(mineType).getExtension();
} catch (MimeTypeException e) {
log.warn("[getExtension][获取文件后缀({}) 失败]", mineType, e);
return null;
}
}
/** /**
* 返回附件 * 返回附件
* *

View File

@ -164,21 +164,21 @@ public class CodegenEngine {
.put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_SCHEMA.getType(), vue3Vben5AntdSchemaTemplatePath("views/modules/list_sub_erp.vue"), // 特殊主子表专属逻辑 .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_SCHEMA.getType(), vue3Vben5AntdSchemaTemplatePath("views/modules/list_sub_erp.vue"), // 特殊主子表专属逻辑
vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-list.vue")) vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-list.vue"))
// VUE3_VBEN5_ANTD // VUE3_VBEN5_ANTD
.put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD.getType(), vue3Vben5AntdGeneralTemplatePath("views/index.vue"), .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("views/index.vue"),
vue3FilePath("views/${table.moduleName}/${table.businessName}/index.vue")) vue3FilePath("views/${table.moduleName}/${table.businessName}/index.vue"))
.put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD.getType(), vue3Vben5AntdGeneralTemplatePath("views/form.vue"), .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("views/form.vue"),
vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/form.vue")) vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/form.vue"))
.put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD.getType(), vue3Vben5AntdGeneralTemplatePath("api/api.ts"), .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("api/api.ts"),
vue3FilePath("api/${table.moduleName}/${table.businessName}/index.ts")) vue3FilePath("api/${table.moduleName}/${table.businessName}/index.ts"))
.put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD.getType(), vue3Vben5AntdGeneralTemplatePath("views/modules/form_sub_normal.vue"), // 特殊主子表专属逻辑 .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("views/modules/form_sub_normal.vue"), // 特殊主子表专属逻辑
vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue")) vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue"))
.put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD.getType(), vue3Vben5AntdGeneralTemplatePath("views/modules/form_sub_inner.vue"), // 特殊主子表专属逻辑 .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("views/modules/form_sub_inner.vue"), // 特殊主子表专属逻辑
vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue")) vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue"))
.put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD.getType(), vue3Vben5AntdGeneralTemplatePath("views/modules/form_sub_erp.vue"), // 特殊主子表专属逻辑 .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("views/modules/form_sub_erp.vue"), // 特殊主子表专属逻辑
vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue")) vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue"))
.put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD.getType(), vue3Vben5AntdGeneralTemplatePath("views/modules/list_sub_inner.vue"), // 特殊主子表专属逻辑 .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("views/modules/list_sub_inner.vue"), // 特殊主子表专属逻辑
vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-list.vue")) vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-list.vue"))
.put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD.getType(), vue3Vben5AntdGeneralTemplatePath("views/modules/list_sub_erp.vue"), // 特殊主子表专属逻辑 .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("views/modules/list_sub_erp.vue"), // 特殊主子表专属逻辑
vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-list.vue")) vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-list.vue"))
.build(); .build();

View File

@ -5,6 +5,7 @@ import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FileCreateReq
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO; import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO;
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePresignedUrlRespVO; import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePresignedUrlRespVO;
import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO; import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO;
import jakarta.validation.constraints.NotEmpty;
/** /**
* 文件 Service 接口 * 文件 Service 接口
@ -24,12 +25,24 @@ public interface FileService {
/** /**
* 保存文件并返回文件的访问路径 * 保存文件并返回文件的访问路径
* *
* @param name 文件名称
* @param path 文件路径
* @param content 文件内容 * @param content 文件内容
* @param name 文件名称允许空
* @param directory 目录允许空
* @param type 文件的 MIME 类型允许空
* @return 文件路径 * @return 文件路径
*/ */
String createFile(String name, String path, byte[] content); String createFile(@NotEmpty(message = "文件内容不能为空") byte[] content,
String name, String directory, String type);
/**
* 生成文件预签名地址信息
*
* @param name 文件名
* @param directory 目录
* @return 预签名地址信息
*/
FilePresignedUrlRespVO getFilePresignedUrl(@NotEmpty(message = "文件名不能为空") String name,
String directory);
/** /**
* 创建文件 * 创建文件
@ -55,12 +68,4 @@ public interface FileService {
*/ */
byte[] getFileContent(Long configId, String path) throws Exception; byte[] getFileContent(Long configId, String path) throws Exception;
/**
* 生成文件预签名地址信息
*
* @param path 文件路径
* @return 预签名地址信息
*/
FilePresignedUrlRespVO getFilePresignedUrl(String path) throws Exception;
} }

View File

@ -1,22 +1,26 @@
package cn.iocoder.yudao.module.infra.service.file; package cn.iocoder.yudao.module.infra.service.file;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.lang.Assert; import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.io.FileUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.infra.framework.file.core.client.FileClient;
import cn.iocoder.yudao.module.infra.framework.file.core.client.s3.FilePresignedUrlRespDTO;
import cn.iocoder.yudao.module.infra.framework.file.core.utils.FileTypeUtils;
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FileCreateReqVO; import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FileCreateReqVO;
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO; import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO;
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePresignedUrlRespVO; import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePresignedUrlRespVO;
import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO; import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO;
import cn.iocoder.yudao.module.infra.dal.mysql.file.FileMapper; import cn.iocoder.yudao.module.infra.dal.mysql.file.FileMapper;
import cn.iocoder.yudao.module.infra.framework.file.core.client.FileClient;
import cn.iocoder.yudao.module.infra.framework.file.core.client.s3.FilePresignedUrlRespDTO;
import cn.iocoder.yudao.module.infra.framework.file.core.utils.FileTypeUtils;
import com.google.common.annotations.VisibleForTesting;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import static cn.hutool.core.date.DatePattern.PURE_DATE_PATTERN;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_NOT_EXISTS; import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_NOT_EXISTS;
@ -28,6 +32,20 @@ import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_NOT_EX
@Service @Service
public class FileServiceImpl implements FileService { public class FileServiceImpl implements FileService {
/**
* 上传文件的前缀是否包含日期yyyyMMdd
*
* 目的按照日期进行分目录
*/
static boolean PATH_PREFIX_DATE_ENABLE = true;
/**
* 上传文件的后缀是否包含时间戳
*
* 目的保证文件的唯一性避免覆盖
* 定制可按需调整成 UUID或者其他方式
*/
static boolean PATH_SUFFIX_TIMESTAMP_ENABLE = true;
@Resource @Resource
private FileConfigService fileConfigService; private FileConfigService fileConfigService;
@ -41,34 +59,82 @@ public class FileServiceImpl implements FileService {
@Override @Override
@SneakyThrows @SneakyThrows
public String createFile(String name, String path, byte[] content) { public String createFile(byte[] content, String name, String directory, String type) {
// 计算默认的 path // 1.1 处理 type 为空的情况
String type = FileTypeUtils.getMineType(content, name); if (StrUtil.isEmpty(type)) {
if (StrUtil.isEmpty(path)) { type = FileTypeUtils.getMineType(content, name);
path = FileUtils.generatePath(content, name);
} }
// 如果 name 为空则使用 path 填充 // 1.2 处理 name 为空的情况
if (StrUtil.isEmpty(name)) { if (StrUtil.isEmpty(name)) {
name = path; name = DigestUtil.sha256Hex(content);
}
if (StrUtil.isEmpty(FileUtil.extName(name))) {
// 如果 name 没有后缀 type则补充后缀
String extension = FileTypeUtils.getExtension(type);
if (StrUtil.isNotEmpty(extension)) {
name = name + extension;
}
} }
// 上传到文件存储器 // 2.1 生成上传的 path需要保证唯一
String path = generateUploadPath(name, directory);
// 2.2 上传到文件存储器
FileClient client = fileConfigService.getMasterFileClient(); FileClient client = fileConfigService.getMasterFileClient();
Assert.notNull(client, "客户端(master) 不能为空"); Assert.notNull(client, "客户端(master) 不能为空");
String url = client.upload(content, path, type); String url = client.upload(content, path, type);
// 保存到数据库 // 3. 保存到数据库
FileDO file = new FileDO(); fileMapper.insert(new FileDO().setConfigId(client.getId())
file.setConfigId(client.getId()); .setName(name).setPath(path).setUrl(url)
file.setName(name); .setType(type).setSize(content.length));
file.setPath(path);
file.setUrl(url);
file.setType(type);
file.setSize(content.length);
fileMapper.insert(file);
return url; return url;
} }
@VisibleForTesting
String generateUploadPath(String name, String directory) {
// 1. 生成前缀后缀
String prefix = null;
if (PATH_PREFIX_DATE_ENABLE) {
prefix = LocalDateTimeUtil.format(LocalDateTimeUtil.now(), PURE_DATE_PATTERN);
}
String suffix = null;
if (PATH_SUFFIX_TIMESTAMP_ENABLE) {
suffix = String.valueOf(System.currentTimeMillis());
}
// 2.1 先拼接 suffix 后缀
if (StrUtil.isNotEmpty(suffix)) {
String ext = FileUtil.extName(name);
if (StrUtil.isNotEmpty(ext)) {
name = FileUtil.mainName(name) + StrUtil.C_UNDERLINE + suffix + StrUtil.DOT + ext;
} else {
name = name + StrUtil.C_UNDERLINE + suffix;
}
}
// 2.2 再拼接 prefix 前缀
if (StrUtil.isNotEmpty(prefix)) {
name = prefix + StrUtil.SLASH + name;
}
// 2.3 最后拼接 directory 目录
if (StrUtil.isNotEmpty(directory)) {
name = directory + StrUtil.SLASH + name;
}
return name;
}
@Override
@SneakyThrows
public FilePresignedUrlRespVO getFilePresignedUrl(String name, String directory) {
// 1. 生成上传的 path需要保证唯一
String path = generateUploadPath(name, directory);
// 2. 获取文件预签名地址
FileClient fileClient = fileConfigService.getMasterFileClient();
FilePresignedUrlRespDTO presignedObjectUrl = fileClient.getPresignedObjectUrl(path);
return BeanUtils.toBean(presignedObjectUrl, FilePresignedUrlRespVO.class,
object -> object.setConfigId(fileClient.getId()).setPath(path));
}
@Override @Override
public Long createFile(FileCreateReqVO createReqVO) { public Long createFile(FileCreateReqVO createReqVO) {
FileDO file = BeanUtils.toBean(createReqVO, FileDO.class); FileDO file = BeanUtils.toBean(createReqVO, FileDO.class);
@ -105,12 +171,4 @@ public class FileServiceImpl implements FileService {
return client.getContent(path); return client.getContent(path);
} }
@Override
public FilePresignedUrlRespVO getFilePresignedUrl(String path) throws Exception {
FileClient fileClient = fileConfigService.getMasterFileClient();
FilePresignedUrlRespDTO presignedObjectUrl = fileClient.getPresignedObjectUrl(path);
return BeanUtils.toBean(presignedObjectUrl, FilePresignedUrlRespVO.class,
object -> object.setConfigId(fileClient.getId()));
}
} }

View File

@ -147,7 +147,7 @@ const [Modal, modalApi] = useVbenModal({
key: 'action_process_msg', key: 'action_process_msg',
}); });
} finally { } finally {
modalApi.lock(false); modalApi.unlock();
} }
}, },
async onOpenChange(isOpen: boolean) { async onOpenChange(isOpen: boolean) {
@ -165,7 +165,7 @@ const [Modal, modalApi] = useVbenModal({
try { try {
data = await get${simpleClassName}(data.id); data = await get${simpleClassName}(data.id);
} finally { } finally {
modalApi.lock(false); modalApi.unlock();
} }
} }
formData.value = data; formData.value = data;

View File

@ -24,6 +24,13 @@
}); });
const [Form, formApi] = useVbenForm({ const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-2',
labelWidth: 80,
},
layout: 'horizontal', layout: 'horizontal',
schema: use${subSimpleClassName}FormSchema(), schema: use${subSimpleClassName}FormSchema(),
showDefaultActions: false showDefaultActions: false
@ -50,7 +57,7 @@
key: 'action_process_msg', key: 'action_process_msg',
}); });
} finally { } finally {
modalApi.lock(false); modalApi.unlock();
} }
}, },
async onOpenChange(isOpen: boolean) { async onOpenChange(isOpen: boolean) {
@ -69,7 +76,7 @@
try { try {
data = await get${subSimpleClassName}(data.id); data = await get${subSimpleClassName}(data.id);
} finally { } finally {
modalApi.lock(false); modalApi.unlock();
} }
} }
// 设置到 values // 设置到 values

View File

@ -96,9 +96,16 @@ watch(
); );
#else #else
const [Form, formApi] = useVbenForm({ const [Form, formApi] = useVbenForm({
layout: 'horizontal', commonConfig: {
schema: use${subSimpleClassName}FormSchema(), componentProps: {
showDefaultActions: false class: 'w-full',
},
formItemClass: 'col-span-2',
labelWidth: 80,
},
layout: 'horizontal',
schema: use${subSimpleClassName}FormSchema(),
showDefaultActions: false
}); });
/** 暴露出表单校验方法和表单值获取方法 */ /** 暴露出表单校验方法和表单值获取方法 */

View File

@ -1,6 +1,5 @@
import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
import type { VbenFormSchema } from '#/adapter/form'; import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn } from '#/adapter/vxe-table'; import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}'; import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
import { z } from '#/adapter/form'; import { z } from '#/adapter/form';
@ -40,7 +39,6 @@ export function useFormSchema(): VbenFormSchema[] {
}); });
return handleTree(data); return handleTree(data);
}, },
class: 'w-full',
labelField: '${treeNameColumn.javaField}', labelField: '${treeNameColumn.javaField}',
valueField: 'id', valueField: 'id',
childrenField: 'children', childrenField: 'children',
@ -90,7 +88,6 @@ export function useFormSchema(): VbenFormSchema[] {
options: [], options: [],
#end #end
placeholder: '请选择${comment}', placeholder: '请选择${comment}',
class: 'w-full',
}, },
#elseif($column.htmlType == "checkbox")## 多选框 #elseif($column.htmlType == "checkbox")## 多选框
component: 'Checkbox', component: 'Checkbox',
@ -128,7 +125,6 @@ export function useFormSchema(): VbenFormSchema[] {
component: 'InputNumber', component: 'InputNumber',
componentProps: { componentProps: {
min: 0, min: 0,
class: 'w-full',
controlsPosition: 'right', controlsPosition: 'right',
placeholder: '请输入${comment}', placeholder: '请输入${comment}',
}, },
@ -326,7 +322,6 @@ export function use${subSimpleClassName}FormSchema(): VbenFormSchema[] {
options: [], options: [],
#end #end
placeholder: '请选择${comment}', placeholder: '请选择${comment}',
class: 'w-full',
}, },
#elseif($column.htmlType == "checkbox")## 多选框 #elseif($column.htmlType == "checkbox")## 多选框
component: 'Checkbox', component: 'Checkbox',
@ -364,7 +359,6 @@ export function use${subSimpleClassName}FormSchema(): VbenFormSchema[] {
component: 'InputNumber', component: 'InputNumber',
componentProps: { componentProps: {
min: 0, min: 0,
class: 'w-full',
controlsPosition: 'right', controlsPosition: 'right',
placeholder: '请输入${comment}', placeholder: '请输入${comment}',
}, },
@ -601,7 +595,6 @@ export function use${subSimpleClassName}GridColumns(
options: [], options: [],
#end #end
placeholder: '请选择${comment}', placeholder: '请选择${comment}',
class: 'w-full',
}, },
#elseif($column.htmlType == "checkbox")## 多选框 #elseif($column.htmlType == "checkbox")## 多选框
component: 'Checkbox', component: 'Checkbox',
@ -639,7 +632,6 @@ export function use${subSimpleClassName}GridColumns(
component: 'InputNumber', component: 'InputNumber',
componentProps: { componentProps: {
min: 0, min: 0,
class: 'w-full',
controlsPosition: 'right', controlsPosition: 'right',
placeholder: '请输入${comment}', placeholder: '请输入${comment}',
}, },
@ -682,4 +674,4 @@ export function use${subSimpleClassName}GridColumns(
#end #end
#end #end
#end #end

View File

@ -42,6 +42,13 @@ const getTitle = computed(() => {
#end #end
const [Form, formApi] = useVbenForm({ const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-2',
labelWidth: 80,
},
layout: 'horizontal', layout: 'horizontal',
schema: useFormSchema(), schema: useFormSchema(),
showDefaultActions: false showDefaultActions: false
@ -100,7 +107,7 @@ const [Modal, modalApi] = useVbenModal({
key: 'action_process_msg', key: 'action_process_msg',
}); });
} finally { } finally {
modalApi.lock(false); modalApi.unlock();
} }
}, },
async onOpenChange(isOpen: boolean) { async onOpenChange(isOpen: boolean) {
@ -118,7 +125,7 @@ const [Modal, modalApi] = useVbenModal({
try { try {
data = await get${simpleClassName}(data.id); data = await get${simpleClassName}(data.id);
} finally { } finally {
modalApi.lock(false); modalApi.unlock();
} }
} }
// 设置到 values // 设置到 values

View File

@ -24,6 +24,13 @@
}); });
const [Form, formApi] = useVbenForm({ const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-2',
labelWidth: 80,
},
layout: 'horizontal', layout: 'horizontal',
schema: use${subSimpleClassName}FormSchema(), schema: use${subSimpleClassName}FormSchema(),
showDefaultActions: false showDefaultActions: false
@ -50,7 +57,7 @@
key: 'action_process_msg', key: 'action_process_msg',
}); });
} finally { } finally {
modalApi.lock(false); modalApi.unlock();
} }
}, },
async onOpenChange(isOpen: boolean) { async onOpenChange(isOpen: boolean) {
@ -69,7 +76,7 @@
try { try {
data = await get${subSimpleClassName}(data.id); data = await get${subSimpleClassName}(data.id);
} finally { } finally {
modalApi.lock(false); modalApi.unlock();
} }
} }
// 设置到 values // 设置到 values

View File

@ -96,9 +96,16 @@ watch(
); );
#else #else
const [Form, formApi] = useVbenForm({ const [Form, formApi] = useVbenForm({
layout: 'horizontal', commonConfig: {
schema: use${subSimpleClassName}FormSchema(), componentProps: {
showDefaultActions: false class: 'w-full',
},
formItemClass: 'col-span-2',
labelWidth: 80,
},
layout: 'horizontal',
schema: use${subSimpleClassName}FormSchema(),
showDefaultActions: false
}); });
/** 暴露出表单校验方法和表单值获取方法 */ /** 暴露出表单校验方法和表单值获取方法 */

View File

@ -8,8 +8,23 @@ import cn.iocoder.yudao.module.infra.framework.file.core.client.ftp.FtpFileClien
import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
/**
* {@link FtpFileClient} 集成测试
*
* @author 芋道源码
*/
public class FtpFileClientTest { public class FtpFileClientTest {
// docker run -d \
// -p 2121:21 -p 30000-30009:30000-30009 \
// -e FTP_USER=foo \
// -e FTP_PASS=pass \
// -e PASV_ADDRESS=127.0.0.1 \
// -e PASV_MIN_PORT=30000 \
// -e PASV_MAX_PORT=30009 \
// -v $(pwd)/ftp-data:/home/vsftpd \
// fauria/vsftpd
@Test @Test
@Disabled @Disabled
public void test() { public void test() {
@ -17,10 +32,10 @@ public class FtpFileClientTest {
FtpFileClientConfig config = new FtpFileClientConfig(); FtpFileClientConfig config = new FtpFileClientConfig();
config.setDomain("http://127.0.0.1:48080"); config.setDomain("http://127.0.0.1:48080");
config.setBasePath("/home/ftp"); config.setBasePath("/home/ftp");
config.setHost("kanchai.club"); config.setHost("127.0.0.1");
config.setPort(221); config.setPort(2121);
config.setUsername(""); config.setUsername("foo");
config.setPassword(""); config.setPassword("pass");
config.setMode(FtpMode.Passive.name()); config.setMode(FtpMode.Passive.name());
FtpFileClient client = new FtpFileClient(0L, config); FtpFileClient client = new FtpFileClient(0L, config);
client.init(); client.init();

View File

@ -7,19 +7,29 @@ import cn.iocoder.yudao.module.infra.framework.file.core.client.sftp.SftpFileCli
import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
/**
* {@link SftpFileClient} 集成测试
*
* @author 芋道源码
*/
public class SftpFileClientTest { public class SftpFileClientTest {
// docker run -p 2222:22 -d \
// -v $(pwd)/sftp-data:/home/foo/upload \
// atmoz/sftp \
// foo:pass:1001
@Test @Test
@Disabled @Disabled
public void test() { public void test() {
// 创建客户端 // 创建客户端
SftpFileClientConfig config = new SftpFileClientConfig(); SftpFileClientConfig config = new SftpFileClientConfig();
config.setDomain("http://127.0.0.1:48080"); config.setDomain("http://127.0.0.1:48080");
config.setBasePath("/home/ftp"); config.setBasePath("/upload"); // 注意这个是相对路径不是实际 linux 上的路径
config.setHost("kanchai.club"); config.setHost("127.0.0.1");
config.setPort(222); config.setPort(2222);
config.setUsername(""); config.setUsername("foo");
config.setPassword(""); config.setPassword("pass");
SftpFileClient client = new SftpFileClient(0L, config); SftpFileClient client = new SftpFileClient(0L, config);
client.init(); client.init();
// 上传文件 // 上传文件

View File

@ -3,19 +3,20 @@ package cn.iocoder.yudao.module.infra.service.file;
import cn.hutool.core.io.resource.ResourceUtil; import cn.hutool.core.io.resource.ResourceUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
import cn.iocoder.yudao.module.infra.framework.file.core.client.FileClient;
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
import cn.iocoder.yudao.framework.test.core.util.AssertUtils; import cn.iocoder.yudao.framework.test.core.util.AssertUtils;
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO; import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO;
import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO; import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO;
import cn.iocoder.yudao.module.infra.dal.mysql.file.FileMapper; import cn.iocoder.yudao.module.infra.dal.mysql.file.FileMapper;
import cn.iocoder.yudao.module.infra.framework.file.core.client.FileClient;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
import jakarta.annotation.Resource;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.concurrent.atomic.AtomicReference;
import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildTime; import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildTime;
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
@ -29,7 +30,7 @@ import static org.mockito.Mockito.*;
public class FileServiceImplTest extends BaseDbUnitTest { public class FileServiceImplTest extends BaseDbUnitTest {
@Resource @Resource
private FileService fileService; private FileServiceImpl fileService;
@Resource @Resource
private FileMapper fileMapper; private FileMapper fileMapper;
@ -37,6 +38,12 @@ public class FileServiceImplTest extends BaseDbUnitTest {
@MockBean @MockBean
private FileConfigService fileConfigService; private FileConfigService fileConfigService;
@BeforeEach
public void setUp() {
FileServiceImpl.PATH_PREFIX_DATE_ENABLE = true;
FileServiceImpl.PATH_SUFFIX_TIMESTAMP_ENABLE = true;
}
@Test @Test
public void testGetFilePage() { public void testGetFilePage() {
// mock 数据 // mock 数据
@ -70,28 +77,69 @@ public class FileServiceImplTest extends BaseDbUnitTest {
AssertUtils.assertPojoEquals(dbFile, pageResult.getList().get(0)); AssertUtils.assertPojoEquals(dbFile, pageResult.getList().get(0));
} }
/**
* contentnamedirectorytype 都非空
*/
@Test @Test
public void testCreateFile_success() throws Exception { public void testCreateFile_success_01() throws Exception {
// 准备参数 // 准备参数
String path = randomString();
byte[] content = ResourceUtil.readBytes("file/erweima.jpg"); byte[] content = ResourceUtil.readBytes("file/erweima.jpg");
String name = "单测文件名";
String directory = randomString();
String type = "image/jpeg";
// mock Master 文件客户端 // mock Master 文件客户端
FileClient client = mock(FileClient.class); FileClient client = mock(FileClient.class);
when(fileConfigService.getMasterFileClient()).thenReturn(client); when(fileConfigService.getMasterFileClient()).thenReturn(client);
String url = randomString(); String url = randomString();
when(client.upload(same(content), same(path), eq("image/jpeg"))).thenReturn(url); AtomicReference<String> pathRef = new AtomicReference<>();
when(client.upload(same(content), argThat(path -> {
assertTrue(path.matches(directory + "/\\d{8}/" + name + "_\\d+.jpg"));
pathRef.set(path);
return true;
}), eq(type))).thenReturn(url);
when(client.getId()).thenReturn(10L); when(client.getId()).thenReturn(10L);
String name = "单测文件名";
// 调用 // 调用
String result = fileService.createFile(name, path, content); String result = fileService.createFile(content, name, directory, type);
// 断言 // 断言
assertEquals(result, url); assertEquals(result, url);
// 校验数据 // 校验数据
FileDO file = fileMapper.selectOne(FileDO::getPath, path); FileDO file = fileMapper.selectOne(FileDO::getUrl, url);
assertEquals(10L, file.getConfigId()); assertEquals(10L, file.getConfigId());
assertEquals(path, file.getPath()); assertEquals(pathRef.get(), file.getPath());
assertEquals(url, file.getUrl()); assertEquals(url, file.getUrl());
assertEquals("image/jpeg", file.getType()); assertEquals(type, file.getType());
assertEquals(content.length, file.getSize());
}
/**
* content 非空其它都空
*/
@Test
public void testCreateFile_success_02() throws Exception {
// 准备参数
byte[] content = ResourceUtil.readBytes("file/erweima.jpg");
// mock Master 文件客户端
String type = "image/jpeg";
FileClient client = mock(FileClient.class);
when(fileConfigService.getMasterFileClient()).thenReturn(client);
String url = randomString();
AtomicReference<String> pathRef = new AtomicReference<>();
when(client.upload(same(content), argThat(path -> {
assertTrue(path.matches("\\d{8}/6318848e882d8a7e7e82789d87608f684ee52d41966bfc8cad3ce15aad2b970e_\\d+\\.jpg"));
pathRef.set(path);
return true;
}), eq(type))).thenReturn(url);
when(client.getId()).thenReturn(10L);
// 调用
String result = fileService.createFile(content, null, null, null);
// 断言
assertEquals(result, url);
// 校验数据
FileDO file = fileMapper.selectOne(FileDO::getUrl, url);
assertEquals(10L, file.getConfigId());
assertEquals(pathRef.get(), file.getPath());
assertEquals(url, file.getUrl());
assertEquals(type, file.getType());
assertEquals(content.length, file.getSize()); assertEquals(content.length, file.getSize());
} }
@ -140,4 +188,122 @@ public class FileServiceImplTest extends BaseDbUnitTest {
assertSame(result, content); assertSame(result, content);
} }
@Test
public void testGenerateUploadPath_AllEnabled() {
// 准备参数
String name = "test.jpg";
String directory = "avatar";
FileServiceImpl.PATH_PREFIX_DATE_ENABLE = true;
FileServiceImpl.PATH_SUFFIX_TIMESTAMP_ENABLE = true;
// 调用
String path = fileService.generateUploadPath(name, directory);
// 断言
// 格式为avatar/yyyyMMdd/test_timestamp.jpg
assertTrue(path.startsWith(directory + "/"));
// 包含日期格式8 位数字 20240517
assertTrue(path.matches(directory + "/\\d{8}/test_\\d+\\.jpg"));
}
@Test
public void testGenerateUploadPath_PrefixEnabled_SuffixDisabled() {
// 准备参数
String name = "test.jpg";
String directory = "avatar";
FileServiceImpl.PATH_PREFIX_DATE_ENABLE = true;
FileServiceImpl.PATH_SUFFIX_TIMESTAMP_ENABLE = false;
// 调用
String path = fileService.generateUploadPath(name, directory);
// 断言
// 格式为avatar/yyyyMMdd/test.jpg
assertTrue(path.startsWith(directory + "/"));
// 包含日期格式8 位数字 20240517
assertTrue(path.matches(directory + "/\\d{8}/test\\.jpg"));
}
@Test
public void testGenerateUploadPath_PrefixDisabled_SuffixEnabled() {
// 准备参数
String name = "test.jpg";
String directory = "avatar";
FileServiceImpl.PATH_PREFIX_DATE_ENABLE = false;
FileServiceImpl.PATH_SUFFIX_TIMESTAMP_ENABLE = true;
// 调用
String path = fileService.generateUploadPath(name, directory);
// 断言
// 格式为avatar/test_timestamp.jpg
assertTrue(path.startsWith(directory + "/"));
assertTrue(path.matches(directory + "/test_\\d+\\.jpg"));
}
@Test
public void testGenerateUploadPath_AllDisabled() {
// 准备参数
String name = "test.jpg";
String directory = "avatar";
FileServiceImpl.PATH_PREFIX_DATE_ENABLE = false;
FileServiceImpl.PATH_SUFFIX_TIMESTAMP_ENABLE = false;
// 调用
String path = fileService.generateUploadPath(name, directory);
// 断言
// 格式为avatar/test.jpg
assertEquals(directory + "/" + name, path);
}
@Test
public void testGenerateUploadPath_NoExtension() {
// 准备参数
String name = "test";
String directory = "avatar";
FileServiceImpl.PATH_PREFIX_DATE_ENABLE = true;
FileServiceImpl.PATH_SUFFIX_TIMESTAMP_ENABLE = true;
// 调用
String path = fileService.generateUploadPath(name, directory);
// 断言
// 格式为avatar/yyyyMMdd/test_timestamp
assertTrue(path.startsWith(directory + "/"));
assertTrue(path.matches(directory + "/\\d{8}/test_\\d+"));
}
@Test
public void testGenerateUploadPath_DirectoryNull() {
// 准备参数
String name = "test.jpg";
String directory = null;
FileServiceImpl.PATH_PREFIX_DATE_ENABLE = true;
FileServiceImpl.PATH_SUFFIX_TIMESTAMP_ENABLE = true;
// 调用
String path = fileService.generateUploadPath(name, directory);
// 断言
// 格式为yyyyMMdd/test_timestamp.jpg
assertTrue(path.matches("\\d{8}/test_\\d+\\.jpg"));
}
@Test
public void testGenerateUploadPath_DirectoryEmpty() {
// 准备参数
String name = "test.jpg";
String directory = "";
FileServiceImpl.PATH_PREFIX_DATE_ENABLE = true;
FileServiceImpl.PATH_SUFFIX_TIMESTAMP_ENABLE = true;
// 调用
String path = fileService.generateUploadPath(name, directory);
// 断言
// 格式为yyyyMMdd/test_timestamp.jpg
assertTrue(path.matches("\\d{8}/test_\\d+\\.jpg"));
}
} }

View File

@ -5,24 +5,22 @@ import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import lombok.Data; import lombok.Data;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
@Schema(description = "管理后台 - IoT OTA 固件创建 Request VO") @Schema(description = "管理后台 - IoT OTA 固件创建 Request VO")
@Data @Data
public class IotOtaFirmwareCreateReqVO { public class IotOtaFirmwareCreateReqVO {
@Schema(description = "固件名称", requiredMode = REQUIRED, example = "智能开关固件") @Schema(description = "固件名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "智能开关固件")
@NotEmpty(message = "固件名称不能为空") @NotEmpty(message = "固件名称不能为空")
private String name; private String name;
@Schema(description = "固件描述", example = "某品牌型号固件,测试用") @Schema(description = "固件描述", example = "某品牌型号固件,测试用")
private String description; private String description;
@Schema(description = "版本号", requiredMode = REQUIRED, example = "1.0.0") @Schema(description = "版本号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1.0.0")
@NotEmpty(message = "版本号不能为空") @NotEmpty(message = "版本号不能为空")
private String version; private String version;
@Schema(description = "产品编号", requiredMode = REQUIRED, example = "1024") @Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "产品编号不能为空") @NotNull(message = "产品编号不能为空")
private String productId; private String productId;
@ -30,7 +28,7 @@ public class IotOtaFirmwareCreateReqVO {
// TODO @li是不是必传哈 // TODO @li是不是必传哈
private String signMethod; private String signMethod;
@Schema(description = "固件文件 URL", requiredMode = REQUIRED, example = "https://www.iocoder.cn/yudao-firmware.zip") @Schema(description = "固件文件 URL", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/yudao-firmware.zip")
@NotEmpty(message = "固件文件 URL 不能为空") @NotEmpty(message = "固件文件 URL 不能为空")
private String fileUrl; private String fileUrl;

View File

@ -7,8 +7,6 @@ import com.fhs.core.trans.vo.VO;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
@Data @Data
@Schema(description = "管理后台 - IoT OTA 固件 Response VO") @Schema(description = "管理后台 - IoT OTA 固件 Response VO")
public class IotOtaFirmwareRespVO implements VO { public class IotOtaFirmwareRespVO implements VO {
@ -16,12 +14,12 @@ public class IotOtaFirmwareRespVO implements VO {
/** /**
* 固件编号 * 固件编号
*/ */
@Schema(description = "固件编号", requiredMode = REQUIRED, example = "1024") @Schema(description = "固件编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long id; private Long id;
/** /**
* 固件名称 * 固件名称
*/ */
@Schema(description = "固件名称", requiredMode = REQUIRED, example = "OTA固件") @Schema(description = "固件名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "OTA固件")
private String name; private String name;
/** /**
* 固件描述 * 固件描述
@ -31,7 +29,7 @@ public class IotOtaFirmwareRespVO implements VO {
/** /**
* 版本号 * 版本号
*/ */
@Schema(description = "版本号", requiredMode = REQUIRED, example = "1.0.0") @Schema(description = "版本号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1.0.0")
private String version; private String version;
/** /**
@ -39,7 +37,7 @@ public class IotOtaFirmwareRespVO implements VO {
* <p> * <p>
* 关联 {@link cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO#getId()} * 关联 {@link cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO#getId()}
*/ */
@Schema(description = "产品编号", requiredMode = REQUIRED, example = "1024") @Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@Trans(type = TransType.SIMPLE, target = IotProductDO.class, fields = {"name"}, refs = {"productName"}) @Trans(type = TransType.SIMPLE, target = IotProductDO.class, fields = {"name"}, refs = {"productName"})
private String productId; private String productId;
/** /**
@ -47,12 +45,12 @@ public class IotOtaFirmwareRespVO implements VO {
* <p> * <p>
* 冗余 {@link cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO#getProductKey()} * 冗余 {@link cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO#getProductKey()}
*/ */
@Schema(description = "产品标识", requiredMode = REQUIRED, example = "iot-product-key") @Schema(description = "产品标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "iot-product-key")
private String productKey; private String productKey;
/** /**
* 产品名称 * 产品名称
*/ */
@Schema(description = "产品名称", requiredMode = REQUIRED, example = "OTA产品") @Schema(description = "产品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "OTA产品")
private String productName; private String productName;
/** /**
* 签名方式 * 签名方式
@ -69,12 +67,12 @@ public class IotOtaFirmwareRespVO implements VO {
/** /**
* 固件文件大小 * 固件文件大小
*/ */
@Schema(description = "固件文件大小", requiredMode = REQUIRED, example = "1024") @Schema(description = "固件文件大小", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long fileSize; private Long fileSize;
/** /**
* 固件文件 URL * 固件文件 URL
*/ */
@Schema(description = "固件文件 URL", requiredMode = REQUIRED, example = "https://www.iocoder.cn") @Schema(description = "固件文件 URL", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn")
private String fileUrl; private String fileUrl;
/** /**
* 自定义信息建议使用 JSON 格式 * 自定义信息建议使用 JSON 格式

View File

@ -5,18 +5,16 @@ import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import lombok.Data; import lombok.Data;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
@Schema(description = "管理后台 - IoT OTA 固件更新 Request VO") @Schema(description = "管理后台 - IoT OTA 固件更新 Request VO")
@Data @Data
public class IotOtaFirmwareUpdateReqVO { public class IotOtaFirmwareUpdateReqVO {
@Schema(description = "固件编号", requiredMode = REQUIRED, example = "1024") @Schema(description = "固件编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "固件编号不能为空") @NotNull(message = "固件编号不能为空")
private Long id; private Long id;
// TODO @liname 是不是可以飞必传哈 // TODO @liname 是不是可以飞必传哈
@Schema(description = "固件名称", requiredMode = REQUIRED, example = "智能开关固件") @Schema(description = "固件名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "智能开关固件")
@NotEmpty(message = "固件名称不能为空") @NotEmpty(message = "固件名称不能为空")
private String name; private String name;

View File

@ -5,8 +5,6 @@ import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import lombok.Data; import lombok.Data;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
@Data @Data
@Schema(description = "管理后台 - IoT OTA 升级记录分页 Request VO") @Schema(description = "管理后台 - IoT OTA 升级记录分页 Request VO")
public class IotOtaUpgradeRecordPageReqVO extends PageParam { public class IotOtaUpgradeRecordPageReqVO extends PageParam {
@ -17,7 +15,7 @@ public class IotOtaUpgradeRecordPageReqVO extends PageParam {
* <p> * <p>
* 该字段用于标识升级任务的唯一编号不能为空 * 该字段用于标识升级任务的唯一编号不能为空
*/ */
@Schema(description = "升级任务编号", requiredMode = REQUIRED, example = "1024") @Schema(description = "升级任务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "升级任务编号不能为空") @NotNull(message = "升级任务编号不能为空")
private Long taskId; private Long taskId;
@ -26,7 +24,7 @@ public class IotOtaUpgradeRecordPageReqVO extends PageParam {
* <p> * <p>
* 该字段用于标识设备的名称通常用于区分不同的设备 * 该字段用于标识设备的名称通常用于区分不同的设备
*/ */
@Schema(description = "设备标识", requiredMode = REQUIRED, example = "摄像头A1-1") @Schema(description = "设备标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "摄像头A1-1")
private String deviceName; private String deviceName;
} }

View File

@ -10,8 +10,6 @@ import lombok.Data;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
@Data @Data
@Schema(description = "管理后台 - IoT OTA 升级记录 Response VO") @Schema(description = "管理后台 - IoT OTA 升级记录 Response VO")
public class IotOtaUpgradeRecordRespVO { public class IotOtaUpgradeRecordRespVO {
@ -19,73 +17,73 @@ public class IotOtaUpgradeRecordRespVO {
/** /**
* 升级记录编号 * 升级记录编号
*/ */
@Schema(description = "升级记录编号", requiredMode = REQUIRED, example = "1024") @Schema(description = "升级记录编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long id; private Long id;
/** /**
* 固件编号 * 固件编号
* <p> * <p>
* 关联 {@link IotOtaFirmwareDO#getId()} * 关联 {@link IotOtaFirmwareDO#getId()}
*/ */
@Schema(description = "固件编号", requiredMode = REQUIRED, example = "1024") @Schema(description = "固件编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@Trans(type = TransType.SIMPLE, target = IotOtaFirmwareDO.class, fields = {"version"}, refs = {"firmwareVersion"}) @Trans(type = TransType.SIMPLE, target = IotOtaFirmwareDO.class, fields = {"version"}, refs = {"firmwareVersion"})
private Long firmwareId; private Long firmwareId;
/** /**
* 固件版本 * 固件版本
*/ */
@Schema(description = "固件版本", requiredMode = REQUIRED, example = "v1.0.0") @Schema(description = "固件版本", requiredMode = Schema.RequiredMode.REQUIRED, example = "v1.0.0")
private String firmwareVersion; private String firmwareVersion;
/** /**
* 任务编号 * 任务编号
* <p> * <p>
* 关联 {@link IotOtaUpgradeTaskDO#getId()} * 关联 {@link IotOtaUpgradeTaskDO#getId()}
*/ */
@Schema(description = "任务编号", requiredMode = REQUIRED, example = "1024") @Schema(description = "任务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long taskId; private Long taskId;
/** /**
* 产品标识 * 产品标识
* <p> * <p>
* 关联 {@link cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO#getId()} * 关联 {@link cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO#getId()}
*/ */
@Schema(description = "产品标识", requiredMode = REQUIRED, example = "iot") @Schema(description = "产品标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "iot")
private String productKey; private String productKey;
/** /**
* 设备名称 * 设备名称
* <p> * <p>
* 关联 {@link cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO#getId()} * 关联 {@link cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO#getId()}
*/ */
@Schema(description = "设备名称", requiredMode = REQUIRED, example = "iot") @Schema(description = "设备名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "iot")
private String deviceName; private String deviceName;
/** /**
* 设备编号 * 设备编号
* <p> * <p>
* 关联 {@link cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO#getId()} * 关联 {@link cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO#getId()}
*/ */
@Schema(description = "设备编号", requiredMode = REQUIRED, example = "1024") @Schema(description = "设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private String deviceId; private String deviceId;
/** /**
* 来源的固件编号 * 来源的固件编号
* <p> * <p>
* 关联 {@link IotDeviceDO#getFirmwareId()} * 关联 {@link IotDeviceDO#getFirmwareId()}
*/ */
@Schema(description = "来源的固件编号", requiredMode = REQUIRED, example = "1024") @Schema(description = "来源的固件编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@Trans(type = TransType.SIMPLE, target = IotOtaFirmwareDO.class, fields = {"version"}, refs = {"fromFirmwareVersion"}) @Trans(type = TransType.SIMPLE, target = IotOtaFirmwareDO.class, fields = {"version"}, refs = {"fromFirmwareVersion"})
private Long fromFirmwareId; private Long fromFirmwareId;
/** /**
* 来源的固件版本 * 来源的固件版本
*/ */
@Schema(description = "来源的固件版本", requiredMode = REQUIRED, example = "v1.0.0") @Schema(description = "来源的固件版本", requiredMode = Schema.RequiredMode.REQUIRED, example = "v1.0.0")
private String fromFirmwareVersion; private String fromFirmwareVersion;
/** /**
* 升级状态 * 升级状态
* <p> * <p>
* 关联 {@link cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeRecordStatusEnum} * 关联 {@link cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeRecordStatusEnum}
*/ */
@Schema(description = "升级状态", requiredMode = REQUIRED, allowableValues = {"0", "10", "20", "30", "40", "50"}) @Schema(description = "升级状态", requiredMode = Schema.RequiredMode.REQUIRED, allowableValues = {"0", "10", "20", "30", "40", "50"})
private Integer status; private Integer status;
/** /**
* 升级进度百分比 * 升级进度百分比
*/ */
@Schema(description = "升级进度,百分比", requiredMode = REQUIRED, example = "10") @Schema(description = "升级进度,百分比", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
private Integer progress; private Integer progress;
/** /**
* 升级进度描述 * 升级进度描述
@ -93,17 +91,17 @@ public class IotOtaUpgradeRecordRespVO {
* 注意只记录设备最后一次的升级进度描述 * 注意只记录设备最后一次的升级进度描述
* 如果想看历史记录可以查看 {@link cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceLogDO} 设备日志 * 如果想看历史记录可以查看 {@link cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceLogDO} 设备日志
*/ */
@Schema(description = "升级进度描述", requiredMode = REQUIRED, example = "10") @Schema(description = "升级进度描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
private String description; private String description;
/** /**
* 升级开始时间 * 升级开始时间
*/ */
@Schema(description = "升级开始时间", requiredMode = REQUIRED, example = "2022-07-08 07:30:00") @Schema(description = "升级开始时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2022-07-08 07:30:00")
private LocalDateTime startTime; private LocalDateTime startTime;
/** /**
* 升级结束时间 * 升级结束时间
*/ */
@Schema(description = "升级结束时间", requiredMode = REQUIRED, example = "2022-07-08 07:30:00") @Schema(description = "升级结束时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2022-07-08 07:30:00")
private LocalDateTime endTime; private LocalDateTime endTime;
} }

View File

@ -5,8 +5,6 @@ import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import lombok.Data; import lombok.Data;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
@Data @Data
@Schema(description = "管理后台 - IoT OTA 升级任务分页 Request VO") @Schema(description = "管理后台 - IoT OTA 升级任务分页 Request VO")
public class IotOtaUpgradeTaskPageReqVO extends PageParam { public class IotOtaUpgradeTaskPageReqVO extends PageParam {
@ -21,7 +19,7 @@ public class IotOtaUpgradeTaskPageReqVO extends PageParam {
* 固件编号字段用于唯一标识固件不能为空 * 固件编号字段用于唯一标识固件不能为空
*/ */
@NotNull(message = "固件编号不能为空") @NotNull(message = "固件编号不能为空")
@Schema(description = "固件编号", requiredMode = REQUIRED, example = "1024") @Schema(description = "固件编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long firmwareId; private Long firmwareId;
} }

View File

@ -9,8 +9,6 @@ import lombok.Data;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List; import java.util.List;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
@Data @Data
@Schema(description = "管理后台 - IoT OTA 升级任务 Response VO") @Schema(description = "管理后台 - IoT OTA 升级任务 Response VO")
public class IotOtaUpgradeTaskRespVO implements VO { public class IotOtaUpgradeTaskRespVO implements VO {
@ -18,12 +16,12 @@ public class IotOtaUpgradeTaskRespVO implements VO {
/** /**
* 任务编号 * 任务编号
*/ */
@Schema(description = "任务编号", requiredMode = REQUIRED, example = "1024") @Schema(description = "任务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long id; private Long id;
/** /**
* 任务名称 * 任务名称
*/ */
@Schema(description = "任务名称", requiredMode = REQUIRED, example = "升级任务") @Schema(description = "任务名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "升级任务")
private String name; private String name;
/** /**
* 任务描述 * 任务描述
@ -35,31 +33,31 @@ public class IotOtaUpgradeTaskRespVO implements VO {
* <p> * <p>
* 关联 {@link IotOtaFirmwareDO#getId()} * 关联 {@link IotOtaFirmwareDO#getId()}
*/ */
@Schema(description = "固件编号", requiredMode = REQUIRED, example = "1024") @Schema(description = "固件编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long firmwareId; private Long firmwareId;
/** /**
* 任务状态 * 任务状态
* <p> * <p>
* 关联 {@link cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeTaskStatusEnum} * 关联 {@link cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeTaskStatusEnum}
*/ */
@Schema(description = "任务状态", requiredMode = REQUIRED, allowableValues = {"10", "20", "21", "30"}) @Schema(description = "任务状态", requiredMode = Schema.RequiredMode.REQUIRED, allowableValues = {"10", "20", "21", "30"})
private Integer status; private Integer status;
/** /**
* 任务状态名称 * 任务状态名称
*/ */
@Schema(description = "任务状态名称", requiredMode = REQUIRED, example = "进行中") @Schema(description = "任务状态名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "进行中")
private String statusName; private String statusName;
/** /**
* 升级范围 * 升级范围
* <p> * <p>
* 关联 {@link cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeTaskScopeEnum} * 关联 {@link cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeTaskScopeEnum}
*/ */
@Schema(description = "升级范围", requiredMode = REQUIRED, allowableValues = {"1", "2"}) @Schema(description = "升级范围", requiredMode = Schema.RequiredMode.REQUIRED, allowableValues = {"1", "2"})
private Integer scope; private Integer scope;
/** /**
* 设备数量 * 设备数量
*/ */
@Schema(description = "设备数量", requiredMode = REQUIRED, example = "1024") @Schema(description = "设备数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long deviceCount; private Long deviceCount;
/** /**
* 选中的设备编号数组 * 选中的设备编号数组
@ -78,7 +76,7 @@ public class IotOtaUpgradeTaskRespVO implements VO {
/** /**
* 创建时间 * 创建时间
*/ */
@Schema(description = "创建时间", requiredMode = REQUIRED, example = "2022-07-08 07:30:00") @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2022-07-08 07:30:00")
private LocalDateTime createTime; private LocalDateTime createTime;
} }

View File

@ -11,8 +11,6 @@ import lombok.Data;
import java.util.List; import java.util.List;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
@Data @Data
@Schema(description = "管理后台 - IoT OTA 升级任务创建/修改 Request VO") @Schema(description = "管理后台 - IoT OTA 升级任务创建/修改 Request VO")
public class IotOtaUpgradeTaskSaveReqVO { public class IotOtaUpgradeTaskSaveReqVO {
@ -24,7 +22,7 @@ public class IotOtaUpgradeTaskSaveReqVO {
* 任务名称 * 任务名称
*/ */
@NotEmpty(message = "任务名称不能为空") @NotEmpty(message = "任务名称不能为空")
@Schema(description = "任务名称", requiredMode = REQUIRED, example = "升级任务") @Schema(description = "任务名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "升级任务")
private String name; private String name;
/** /**
@ -39,7 +37,7 @@ public class IotOtaUpgradeTaskSaveReqVO {
* 关联 {@link IotOtaFirmwareDO#getId()} * 关联 {@link IotOtaFirmwareDO#getId()}
*/ */
@NotNull(message = "固件编号不能为空") @NotNull(message = "固件编号不能为空")
@Schema(description = "固件编号", requiredMode = REQUIRED, example = "1024") @Schema(description = "固件编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long firmwareId; private Long firmwareId;
/** /**
@ -49,7 +47,7 @@ public class IotOtaUpgradeTaskSaveReqVO {
*/ */
@NotNull(message = "升级范围不能为空") @NotNull(message = "升级范围不能为空")
@InEnum(value = IotOtaUpgradeTaskScopeEnum.class) @InEnum(value = IotOtaUpgradeTaskScopeEnum.class)
@Schema(description = "升级范围", requiredMode = REQUIRED, example = "1") @Schema(description = "升级范围", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer scope; private Integer scope;
/** /**
@ -57,7 +55,7 @@ public class IotOtaUpgradeTaskSaveReqVO {
* <p> * <p>
* 关联 {@link IotDeviceDO#getId()} * 关联 {@link IotDeviceDO#getId()}
*/ */
@Schema(description = "选中的设备编号数组", requiredMode = REQUIRED, example = "[1,2,3,4]") @Schema(description = "选中的设备编号数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1,2,3,4]")
private List<Long> deviceIds; private List<Long> deviceIds;
} }

View File

@ -5,6 +5,9 @@ import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuUpdateStockReqDTO;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
/** /**
* 商品 SKU API 接口 * 商品 SKU API 接口
@ -30,6 +33,16 @@ public interface ProductSkuApi {
*/ */
List<ProductSkuRespDTO> getSkuList(Collection<Long> ids); List<ProductSkuRespDTO> getSkuList(Collection<Long> ids);
/**
* 批量查询 SKU MAP
*
* @param ids SKU 编号列表
* @return SKU MAP
*/
default Map<Long, ProductSkuRespDTO> getSkuMap(Collection<Long> ids) {
return convertMap(getSkuList(ids), ProductSkuRespDTO::getId);
}
/** /**
* 批量查询 SKU 数组 * 批量查询 SKU 数组
* *

View File

@ -30,7 +30,7 @@ public interface ProductSpuApi {
* @param ids SPU 编号列表 * @param ids SPU 编号列表
* @return SPU MAP * @return SPU MAP
*/ */
default Map<Long, ProductSpuRespDTO> getSpusMap(Collection<Long> ids) { default Map<Long, ProductSpuRespDTO> getSpuMap(Collection<Long> ids) {
return convertMap(getSpuList(ids), ProductSpuRespDTO::getId); return convertMap(getSpuList(ids), ProductSpuRespDTO::getId);
} }

View File

@ -4,17 +4,15 @@ import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
@Schema(description = "管理后台 - 商品浏览记录 Response VO") @Schema(description = "管理后台 - 商品浏览记录 Response VO")
@Data @Data
@ExcelIgnoreUnannotated @ExcelIgnoreUnannotated
public class ProductBrowseHistoryRespVO { public class ProductBrowseHistoryRespVO {
@Schema(description = "编号", requiredMode = REQUIRED, example = "1") @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long id; private Long id;
@Schema(description = "商品 SPU 编号", requiredMode = REQUIRED, example = "29502") @Schema(description = "商品 SPU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "29502")
private Long spuId; private Long spuId;
// ========== 商品相关字段 ========== // ========== 商品相关字段 ==========
@ -34,4 +32,4 @@ public class ProductBrowseHistoryRespVO {
@Schema(description = "库存", example = "100") @Schema(description = "库存", example = "100")
private Integer stock; private Integer stock;
} }

View File

@ -6,13 +6,11 @@ import lombok.Data;
import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotEmpty;
import java.util.List; import java.util.List;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
@Schema(description = "用户 APP - 商品收藏的批量 Request VO") // 用于收藏取消收藏获取收藏 @Schema(description = "用户 APP - 商品收藏的批量 Request VO") // 用于收藏取消收藏获取收藏
@Data @Data
public class AppFavoriteBatchReqVO { public class AppFavoriteBatchReqVO {
@Schema(description = "商品 SPU 编号数组", requiredMode = REQUIRED, example = "29502") @Schema(description = "商品 SPU 编号数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "29502")
@NotEmpty(message = "商品 SPU 编号数组不能为空") @NotEmpty(message = "商品 SPU 编号数组不能为空")
private List<Long> spuIds; private List<Long> spuIds;

View File

@ -5,13 +5,11 @@ import lombok.Data;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
@Schema(description = "用户 APP - 商品收藏的单个 Request VO") // 用于收藏取消收藏获取收藏 @Schema(description = "用户 APP - 商品收藏的单个 Request VO") // 用于收藏取消收藏获取收藏
@Data @Data
public class AppFavoriteReqVO { public class AppFavoriteReqVO {
@Schema(description = "商品 SPU 编号", requiredMode = REQUIRED, example = "29502") @Schema(description = "商品 SPU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "29502")
@NotNull(message = "商品 SPU 编号不能为空") @NotNull(message = "商品 SPU 编号不能为空")
private Long spuId; private Long spuId;

View File

@ -2,16 +2,15 @@ package cn.iocoder.yudao.module.product.controller.app.favorite.vo;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
@Schema(description = "用户 App - 商品收藏 Response VO") @Schema(description = "用户 App - 商品收藏 Response VO")
@Data @Data
public class AppFavoriteRespVO { public class AppFavoriteRespVO {
@Schema(description = "编号", requiredMode = REQUIRED, example = "1") @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long id; private Long id;
@Schema(description = "商品 SPU 编号", requiredMode = REQUIRED, example = "29502") @Schema(description = "商品 SPU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "29502")
private Long spuId; private Long spuId;
// ========== 商品相关字段 ========== // ========== 商品相关字段 ==========

View File

@ -6,13 +6,11 @@ import lombok.Data;
import java.util.List; import java.util.List;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
@Schema(description = "用户 APP - 删除商品浏览记录的 Request VO") @Schema(description = "用户 APP - 删除商品浏览记录的 Request VO")
@Data @Data
public class AppProductBrowseHistoryDeleteReqVO { public class AppProductBrowseHistoryDeleteReqVO {
@Schema(description = "商品 SPU 编号数组", requiredMode = REQUIRED, example = "29502") @Schema(description = "商品 SPU 编号数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "29502")
@NotEmpty(message = "商品 SPU 编号数组不能为空") @NotEmpty(message = "商品 SPU 编号数组不能为空")
private List<Long> spuIds; private List<Long> spuIds;

View File

@ -3,33 +3,31 @@ package cn.iocoder.yudao.module.product.controller.app.history.vo;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
@Schema(description = "用户 App - 商品浏览记录 Response VO") @Schema(description = "用户 App - 商品浏览记录 Response VO")
@Data @Data
public class AppProductBrowseHistoryRespVO { public class AppProductBrowseHistoryRespVO {
@Schema(description = "编号", requiredMode = REQUIRED, example = "1") @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long id; private Long id;
@Schema(description = "商品 SPU 编号", requiredMode = REQUIRED, example = "29502") @Schema(description = "商品 SPU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "29502")
private Long spuId; private Long spuId;
// ========== 商品相关字段 ========== // ========== 商品相关字段 ==========
@Schema(description = "商品 SPU 名称", requiredMode = REQUIRED, example = "赵六") @Schema(description = "商品 SPU 名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "赵六")
private String spuName; private String spuName;
@Schema(description = "商品封面图", requiredMode = REQUIRED, example = "https://www.iocoder.cn/pic.png") @Schema(description = "商品封面图", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/pic.png")
private String picUrl; private String picUrl;
@Schema(description = "商品单价", requiredMode = REQUIRED, example = "50") @Schema(description = "商品单价", requiredMode = Schema.RequiredMode.REQUIRED, example = "50")
private Integer price; private Integer price;
@Schema(description = "商品销量", requiredMode = REQUIRED, example = "60") @Schema(description = "商品销量", requiredMode = Schema.RequiredMode.REQUIRED, example = "60")
private Integer salesCount; private Integer salesCount;
@Schema(description = "库存", requiredMode = REQUIRED, example = "80") @Schema(description = "库存", requiredMode = Schema.RequiredMode.REQUIRED, example = "80")
private Integer stock; private Integer stock;
} }

View File

@ -6,6 +6,8 @@ import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.product.dal.dataobject.sku.ProductSkuDO; import cn.iocoder.yudao.module.product.dal.dataobject.sku.ProductSkuDO;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
@ -13,6 +15,9 @@ import java.util.List;
@Mapper @Mapper
public interface ProductSkuMapper extends BaseMapperX<ProductSkuDO> { public interface ProductSkuMapper extends BaseMapperX<ProductSkuDO> {
@Select("SELECT * FROM product_sku WHERE id = #{id}")
ProductSkuDO selectByIdIncludeDeleted(@Param("id") Long id);
default List<ProductSkuDO> selectListBySpuId(Long spuId) { default List<ProductSkuDO> selectListBySpuId(Long spuId) {
return selectList(ProductSkuDO::getSpuId, spuId); return selectList(ProductSkuDO::getSpuId, spuId);
} }

View File

@ -11,6 +11,8 @@ import cn.iocoder.yudao.module.product.enums.ProductConstants;
import cn.iocoder.yudao.module.product.enums.spu.ProductSpuStatusEnum; import cn.iocoder.yudao.module.product.enums.spu.ProductSpuStatusEnum;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
@ -18,6 +20,9 @@ import java.util.Set;
@Mapper @Mapper
public interface ProductSpuMapper extends BaseMapperX<ProductSpuDO> { public interface ProductSpuMapper extends BaseMapperX<ProductSpuDO> {
@Select("SELECT * FROM product_spu WHERE id = #{id}")
ProductSpuDO selectByIdIncludeDeleted(@Param("id") Long id);
/** /**
* 获取商品 SPU 分页列表数据 * 获取商品 SPU 分页列表数据
* *

View File

@ -91,7 +91,7 @@ public class ProductCommentServiceImpl implements ProductCommentService {
} }
private ProductSkuDO validateSku(Long skuId) { private ProductSkuDO validateSku(Long skuId) {
ProductSkuDO sku = productSkuService.getSku(skuId); ProductSkuDO sku = productSkuService.getSku(skuId, true);
if (sku == null) { if (sku == null) {
throw exception(SKU_NOT_EXISTS); throw exception(SKU_NOT_EXISTS);
} }
@ -99,7 +99,7 @@ public class ProductCommentServiceImpl implements ProductCommentService {
} }
private ProductSpuDO validateSpu(Long spuId) { private ProductSpuDO validateSpu(Long spuId) {
ProductSpuDO spu = productSpuService.getSpu(spuId); ProductSpuDO spu = productSpuService.getSpu(spuId, true);
if (null == spu) { if (null == spu) {
throw exception(SPU_NOT_EXISTS); throw exception(SPU_NOT_EXISTS);
} }

View File

@ -29,6 +29,15 @@ public interface ProductSkuService {
*/ */
ProductSkuDO getSku(Long id); ProductSkuDO getSku(Long id);
/**
* 获得商品 SKU 信息
*
* @param id 编号
* @param includeDeleted 是否包含已删除的
* @return 商品 SKU 信息
*/
ProductSkuDO getSku(Long id, boolean includeDeleted);
/** /**
* 获得商品 SKU 列表 * 获得商品 SKU 列表
* *

View File

@ -68,6 +68,14 @@ public class ProductSkuServiceImpl implements ProductSkuService {
return productSkuMapper.selectById(id); return productSkuMapper.selectById(id);
} }
@Override
public ProductSkuDO getSku(Long id, boolean includeDeleted) {
if (includeDeleted) {
return productSkuMapper.selectByIdIncludeDeleted(id);
}
return getSku(id);
}
@Override @Override
public List<ProductSkuDO> getSkuList(Collection<Long> ids) { public List<ProductSkuDO> getSkuList(Collection<Long> ids) {
if (CollUtil.isEmpty(ids)) { if (CollUtil.isEmpty(ids)) {

View File

@ -51,6 +51,15 @@ public interface ProductSpuService {
*/ */
ProductSpuDO getSpu(Long id); ProductSpuDO getSpu(Long id);
/**
* 获得商品 SPU
*
* @param id 编号
* @param includeDeleted 是否包含已删除的
* @return 商品 SPU
*/
ProductSpuDO getSpu(Long id, boolean includeDeleted);
/** /**
* 获得商品 SPU 列表 * 获得商品 SPU 列表
* *

View File

@ -189,6 +189,14 @@ public class ProductSpuServiceImpl implements ProductSpuService {
return productSpuMapper.selectById(id); return productSpuMapper.selectById(id);
} }
@Override
public ProductSpuDO getSpu(Long id, boolean includeDeleted) {
if (includeDeleted) {
return productSpuMapper.selectByIdIncludeDeleted(id);
}
return getSpu(id);
}
@Override @Override
public List<ProductSpuDO> getSpuList(Collection<Long> ids) { public List<ProductSpuDO> getSpuList(Collection<Long> ids) {
if (CollUtil.isEmpty(ids)) { if (CollUtil.isEmpty(ids)) {

View File

@ -124,7 +124,7 @@ public class PointActivityController {
List<PointProductDO> products = pointActivityService.getPointProductListByActivityIds( List<PointProductDO> products = pointActivityService.getPointProductListByActivityIds(
convertSet(activityList, PointActivityDO::getId)); convertSet(activityList, PointActivityDO::getId));
Map<Long, List<PointProductDO>> productsMap = convertMultiMap(products, PointProductDO::getActivityId); Map<Long, List<PointProductDO>> productsMap = convertMultiMap(products, PointProductDO::getActivityId);
Map<Long, ProductSpuRespDTO> spuMap = productSpuApi.getSpusMap( Map<Long, ProductSpuRespDTO> spuMap = productSpuApi.getSpuMap(
convertSet(activityList, PointActivityDO::getSpuId)); convertSet(activityList, PointActivityDO::getSpuId));
List<PointActivityRespVO> result = BeanUtils.toBean(activityList, PointActivityRespVO.class); List<PointActivityRespVO> result = BeanUtils.toBean(activityList, PointActivityRespVO.class);
result.forEach(activity -> { result.forEach(activity -> {

View File

@ -1,9 +1,9 @@
package cn.iocoder.yudao.module.promotion.controller.app.coupon.vo.template; package cn.iocoder.yudao.module.promotion.controller.app.coupon.vo.template;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Min;
import lombok.Data; import lombok.Data;
import jakarta.validation.constraints.Min;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List; import java.util.List;
@ -20,6 +20,9 @@ public class AppCouponTemplateRespVO {
@Schema(description = "优惠券说明", example = "优惠券使用说明") @Schema(description = "优惠券说明", example = "优惠券使用说明")
private String description; private String description;
@Schema(description = "发行总量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") // -1 - 则表示不限制发放数量
private Integer totalCount;
@Schema(description = "每人限领个数", requiredMode = Schema.RequiredMode.REQUIRED, example = "66") // -1 - 则表示不限制 @Schema(description = "每人限领个数", requiredMode = Schema.RequiredMode.REQUIRED, example = "66") // -1 - 则表示不限制
private Integer takeLimitCount; private Integer takeLimitCount;
@ -62,6 +65,9 @@ public class AppCouponTemplateRespVO {
@Schema(description = "折扣上限", example = "100") // 单位仅在 discountType PERCENT 使用 @Schema(description = "折扣上限", example = "100") // 单位仅在 discountType PERCENT 使用
private Integer discountLimitPrice; private Integer discountLimitPrice;
@Schema(description = "领取优惠券的数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Integer takeCount;
// ========== 用户相关字段 ========== // ========== 用户相关字段 ==========
@Schema(description = "是否可以领取", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") @Schema(description = "是否可以领取", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")

View File

@ -104,7 +104,7 @@ public class AppPointActivityController {
List<PointProductDO> products = pointActivityService.getPointProductListByActivityIds( List<PointProductDO> products = pointActivityService.getPointProductListByActivityIds(
convertSet(activityList, PointActivityDO::getId)); convertSet(activityList, PointActivityDO::getId));
Map<Long, List<PointProductDO>> productsMap = convertMultiMap(products, PointProductDO::getActivityId); Map<Long, List<PointProductDO>> productsMap = convertMultiMap(products, PointProductDO::getActivityId);
Map<Long, ProductSpuRespDTO> spuMap = productSpuApi.getSpusMap( Map<Long, ProductSpuRespDTO> spuMap = productSpuApi.getSpuMap(
convertSet(activityList, PointActivityDO::getSpuId)); convertSet(activityList, PointActivityDO::getSpuId));
List<AppPointActivityRespVO> result = BeanUtils.toBean(activityList, AppPointActivityRespVO.class); List<AppPointActivityRespVO> result = BeanUtils.toBean(activityList, AppPointActivityRespVO.class);
result.forEach(activity -> { result.forEach(activity -> {

View File

@ -22,6 +22,7 @@ import cn.iocoder.yudao.module.promotion.enums.coupon.CouponTemplateValidityType
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
@ -180,7 +181,7 @@ public class CouponServiceImpl implements CouponService {
* @param couponId 模版编号 * @param couponId 模版编号
* @param userId 用户编号 * @param userId 用户编号
*/ */
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW) // 每次调用开启一个新的事务避免在一个大的事务里面
public void invalidateCoupon(Long couponId, Long userId) { public void invalidateCoupon(Long couponId, Long userId) {
if (couponId == null || couponId <= 0) { if (couponId == null || couponId <= 0) {
return; return;
@ -270,13 +271,17 @@ public class CouponServiceImpl implements CouponService {
if (CollUtil.isEmpty(userIds)) { if (CollUtil.isEmpty(userIds)) {
throw exception(COUPON_TEMPLATE_USER_ALREADY_TAKE); throw exception(COUPON_TEMPLATE_USER_ALREADY_TAKE);
} }
// 校验模板 // 校验模板
if (couponTemplate == null) { if (couponTemplate == null) {
throw exception(COUPON_TEMPLATE_NOT_EXISTS); throw exception(COUPON_TEMPLATE_NOT_EXISTS);
} }
// 校验剩余数量 // 校验领取方式
if (ObjUtil.notEqual(couponTemplate.getTakeLimitCount(), CouponTemplateDO.TIME_LIMIT_COUNT_MAX) // 非不限制 if (ObjUtil.notEqual(couponTemplate.getTakeType(), takeType.getType())) {
throw exception(COUPON_TEMPLATE_CANNOT_TAKE);
}
// 校验发放数量不能过小仅在 CouponTakeTypeEnum.USER 用户领取时
if (CouponTakeTypeEnum.isUser(couponTemplate.getTakeType())
&& ObjUtil.notEqual(couponTemplate.getTakeLimitCount(), CouponTemplateDO.TIME_LIMIT_COUNT_MAX) // 非不限制
&& couponTemplate.getTakeCount() + userIds.size() > couponTemplate.getTotalCount()) { && couponTemplate.getTakeCount() + userIds.size() > couponTemplate.getTotalCount()) {
throw exception(COUPON_TEMPLATE_NOT_ENOUGH); throw exception(COUPON_TEMPLATE_NOT_ENOUGH);
} }
@ -286,10 +291,6 @@ public class CouponServiceImpl implements CouponService {
throw exception(COUPON_TEMPLATE_EXPIRED); throw exception(COUPON_TEMPLATE_EXPIRED);
} }
} }
// 校验领取方式
if (ObjectUtil.notEqual(couponTemplate.getTakeType(), takeType.getType())) {
throw exception(COUPON_TEMPLATE_CANNOT_TAKE);
}
} }
/** /**

View File

@ -1,12 +1,13 @@
package cn.iocoder.yudao.module.trade.controller.app.aftersale; package cn.iocoder.yudao.module.trade.controller.app.aftersale;
import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.trade.controller.app.aftersale.vo.AppAfterSaleCreateReqVO; import cn.iocoder.yudao.module.trade.controller.app.aftersale.vo.AppAfterSaleCreateReqVO;
import cn.iocoder.yudao.module.trade.controller.app.aftersale.vo.AppAfterSaleDeliveryReqVO; import cn.iocoder.yudao.module.trade.controller.app.aftersale.vo.AppAfterSaleDeliveryReqVO;
import cn.iocoder.yudao.module.trade.controller.app.aftersale.vo.AppAfterSalePageReqVO;
import cn.iocoder.yudao.module.trade.controller.app.aftersale.vo.AppAfterSaleRespVO; import cn.iocoder.yudao.module.trade.controller.app.aftersale.vo.AppAfterSaleRespVO;
import cn.iocoder.yudao.module.trade.convert.aftersale.AfterSaleConvert; import cn.iocoder.yudao.module.trade.dal.dataobject.aftersale.AfterSaleDO;
import cn.iocoder.yudao.module.trade.service.aftersale.AfterSaleService; import cn.iocoder.yudao.module.trade.service.aftersale.AfterSaleService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
@ -31,16 +32,17 @@ public class AppAfterSaleController {
@GetMapping(value = "/page") @GetMapping(value = "/page")
@Operation(summary = "获得售后分页") @Operation(summary = "获得售后分页")
public CommonResult<PageResult<AppAfterSaleRespVO>> getAfterSalePage(PageParam pageParam) { public CommonResult<PageResult<AppAfterSaleRespVO>> getAfterSalePage(AppAfterSalePageReqVO pageReqVO) {
return success(AfterSaleConvert.INSTANCE.convertPage02( PageResult<AfterSaleDO> pageResult = afterSaleService.getAfterSalePage(getLoginUserId(), pageReqVO);
afterSaleService.getAfterSalePage(getLoginUserId(), pageParam))); return success(BeanUtils.toBean(pageResult, AppAfterSaleRespVO.class));
} }
@GetMapping(value = "/get") @GetMapping(value = "/get")
@Operation(summary = "获得售后订单") @Operation(summary = "获得售后订单")
@Parameter(name = "id", description = "售后编号", required = true, example = "1") @Parameter(name = "id", description = "售后编号", required = true, example = "1")
public CommonResult<AppAfterSaleRespVO> getAfterSale(@RequestParam("id") Long id) { public CommonResult<AppAfterSaleRespVO> getAfterSale(@RequestParam("id") Long id) {
return success(AfterSaleConvert.INSTANCE.convert(afterSaleService.getAfterSale(getLoginUserId(), id))); AfterSaleDO afterSale = afterSaleService.getAfterSale(getLoginUserId(), id);
return success(BeanUtils.toBean(afterSale, AppAfterSaleRespVO.class));
} }
@PostMapping(value = "/create") @PostMapping(value = "/create")

View File

@ -0,0 +1,20 @@
package cn.iocoder.yudao.module.trade.controller.app.aftersale.vo;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import java.util.Set;
@Schema(description = "用户 App - 交易售后分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class AppAfterSalePageReqVO extends PageParam {
@Schema(description = "售后状态", example = "10, 20")
private Set<Integer> statuses;
}

View File

@ -3,8 +3,6 @@ package cn.iocoder.yudao.module.trade.controller.app.base.spu;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import java.util.List;
/** /**
* 商品 SPU 基础 Response VO * 商品 SPU 基础 Response VO
* *
@ -25,4 +23,10 @@ public class AppProductSpuBaseRespVO {
@Schema(description = "商品分类编号", example = "1") @Schema(description = "商品分类编号", example = "1")
private Long categoryId; private Long categoryId;
@Schema(description = "商品库存", requiredMode = Schema.RequiredMode.REQUIRED, example = "10000")
private Integer stock;
@Schema(description = "商品状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer status;
} }

View File

@ -1,8 +1,12 @@
package cn.iocoder.yudao.module.trade.controller.app.delivery.vo.pickup; package cn.iocoder.yudao.module.trade.controller.app.delivery.vo.pickup;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data; import lombok.Data;
import java.time.LocalTime;
@Schema(description = "用户 App - 自提门店 Response VO") @Schema(description = "用户 App - 自提门店 Response VO")
@Data @Data
public class AppDeliveryPickUpStoreRespVO { public class AppDeliveryPickUpStoreRespVO {
@ -28,6 +32,16 @@ public class AppDeliveryPickUpStoreRespVO {
@Schema(description = "门店详细地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "复旦大学路 188 号") @Schema(description = "门店详细地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "复旦大学路 188 号")
private String detailAddress; private String detailAddress;
@Schema(description = "营业开始时间", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "营业开始时间不能为空")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "HH:mm")
private LocalTime openingTime;
@Schema(description = "营业结束时间", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "营业结束时间不能为空")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "HH:mm")
private LocalTime closingTime;
@Schema(description = "纬度", requiredMode = Schema.RequiredMode.REQUIRED, example = "5.88") @Schema(description = "纬度", requiredMode = Schema.RequiredMode.REQUIRED, example = "5.88")
private Double latitude; private Double latitude;

View File

@ -11,7 +11,6 @@ import cn.iocoder.yudao.module.trade.controller.admin.base.member.user.MemberUse
import cn.iocoder.yudao.module.trade.controller.admin.base.product.property.ProductPropertyValueDetailRespVO; import cn.iocoder.yudao.module.trade.controller.admin.base.product.property.ProductPropertyValueDetailRespVO;
import cn.iocoder.yudao.module.trade.controller.admin.order.vo.TradeOrderBaseVO; import cn.iocoder.yudao.module.trade.controller.admin.order.vo.TradeOrderBaseVO;
import cn.iocoder.yudao.module.trade.controller.app.aftersale.vo.AppAfterSaleCreateReqVO; import cn.iocoder.yudao.module.trade.controller.app.aftersale.vo.AppAfterSaleCreateReqVO;
import cn.iocoder.yudao.module.trade.controller.app.aftersale.vo.AppAfterSaleRespVO;
import cn.iocoder.yudao.module.trade.dal.dataobject.aftersale.AfterSaleDO; import cn.iocoder.yudao.module.trade.dal.dataobject.aftersale.AfterSaleDO;
import cn.iocoder.yudao.module.trade.dal.dataobject.aftersale.AfterSaleLogDO; import cn.iocoder.yudao.module.trade.dal.dataobject.aftersale.AfterSaleLogDO;
import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO; import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO;
@ -63,10 +62,6 @@ public interface AfterSaleConvert {
ProductPropertyValueDetailRespVO convert(ProductPropertyValueDetailRespDTO bean); ProductPropertyValueDetailRespVO convert(ProductPropertyValueDetailRespDTO bean);
AppAfterSaleRespVO convert(AfterSaleDO bean);
PageResult<AppAfterSaleRespVO> convertPage02(PageResult<AfterSaleDO> page);
default AfterSaleDetailRespVO convert(AfterSaleDO afterSale, TradeOrderDO order, TradeOrderItemDO orderItem, default AfterSaleDetailRespVO convert(AfterSaleDO afterSale, TradeOrderDO order, TradeOrderItemDO orderItem,
MemberUserRespDTO user, List<AfterSaleLogDO> logs) { MemberUserRespDTO user, List<AfterSaleLogDO> logs) {
AfterSaleDetailRespVO respVO = convert02(afterSale); AfterSaleDetailRespVO respVO = convert02(afterSale);

View File

@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.trade.convert.cart; package cn.iocoder.yudao.module.trade.convert.cart;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO; import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO;
import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO; import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO;
import cn.iocoder.yudao.module.product.enums.spu.ProductSpuStatusEnum; import cn.iocoder.yudao.module.product.enums.spu.ProductSpuStatusEnum;
@ -33,21 +34,18 @@ public interface TradeCartConvert {
cartVO.setId(cart.getId()).setCount(cart.getCount()).setSelected(cart.getSelected()); cartVO.setId(cart.getId()).setCount(cart.getCount()).setSelected(cart.getSelected());
ProductSpuRespDTO spu = spuMap.get(cart.getSpuId()); ProductSpuRespDTO spu = spuMap.get(cart.getSpuId());
ProductSkuRespDTO sku = skuMap.get(cart.getSkuId()); ProductSkuRespDTO sku = skuMap.get(cart.getSkuId());
cartVO.setSpu(convert(spu)).setSku(convert(sku)); cartVO.setSpu(BeanUtils.toBean(spu, AppProductSpuBaseRespVO.class))
.setSku(BeanUtils.toBean(sku, AppProductSkuBaseRespVO.class));
// 如果 SPU 不存在或者下架或者库存不足说明是无效的 // 如果 SPU 不存在或者下架或者库存不足说明是无效的
if (spu == null if (spu == null
|| !ProductSpuStatusEnum.isEnable(spu.getStatus()) || !ProductSpuStatusEnum.isEnable(spu.getStatus())
|| spu.getStock() <= 0) { || spu.getStock() <= 0) {
cartVO.setSelected(false); // 强制设置成不可选中
invalidList.add(cartVO); invalidList.add(cartVO);
} else { } else {
// 虽然 SKU 可能也会不存在但是可以通过购物车重新选择
validList.add(cartVO); validList.add(cartVO);
} }
}); });
return new AppCartListRespVO().setValidList(validList).setInvalidList(invalidList); return new AppCartListRespVO().setValidList(validList).setInvalidList(invalidList);
} }
AppProductSpuBaseRespVO convert(ProductSpuRespDTO spu);
AppProductSkuBaseRespVO convert(ProductSkuRespDTO sku);
} }

View File

@ -1,10 +1,10 @@
package cn.iocoder.yudao.module.trade.dal.mysql.aftersale; package cn.iocoder.yudao.module.trade.dal.mysql.aftersale;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.trade.controller.admin.aftersale.vo.AfterSalePageReqVO; import cn.iocoder.yudao.module.trade.controller.admin.aftersale.vo.AfterSalePageReqVO;
import cn.iocoder.yudao.module.trade.controller.app.aftersale.vo.AppAfterSalePageReqVO;
import cn.iocoder.yudao.module.trade.dal.dataobject.aftersale.AfterSaleDO; import cn.iocoder.yudao.module.trade.dal.dataobject.aftersale.AfterSaleDO;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
@ -27,9 +27,10 @@ public interface AfterSaleMapper extends BaseMapperX<AfterSaleDO> {
.orderByDesc(AfterSaleDO::getId)); .orderByDesc(AfterSaleDO::getId));
} }
default PageResult<AfterSaleDO> selectPage(Long userId, PageParam pageParam) { default PageResult<AfterSaleDO> selectPage(Long userId, AppAfterSalePageReqVO pageReqVO) {
return selectPage(pageParam, new LambdaQueryWrapperX<AfterSaleDO>() return selectPage(pageReqVO, new LambdaQueryWrapperX<AfterSaleDO>()
.eqIfPresent(AfterSaleDO::getUserId, userId) .eq(AfterSaleDO::getUserId, userId)
.inIfPresent(AfterSaleDO::getStatus, pageReqVO.getStatuses())
.orderByDesc(AfterSaleDO::getId)); .orderByDesc(AfterSaleDO::getId));
} }

View File

@ -1,12 +1,12 @@
package cn.iocoder.yudao.module.trade.service.aftersale; package cn.iocoder.yudao.module.trade.service.aftersale;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.trade.controller.admin.aftersale.vo.AfterSaleDisagreeReqVO; import cn.iocoder.yudao.module.trade.controller.admin.aftersale.vo.AfterSaleDisagreeReqVO;
import cn.iocoder.yudao.module.trade.controller.admin.aftersale.vo.AfterSalePageReqVO; import cn.iocoder.yudao.module.trade.controller.admin.aftersale.vo.AfterSalePageReqVO;
import cn.iocoder.yudao.module.trade.controller.admin.aftersale.vo.AfterSaleRefuseReqVO; import cn.iocoder.yudao.module.trade.controller.admin.aftersale.vo.AfterSaleRefuseReqVO;
import cn.iocoder.yudao.module.trade.controller.app.aftersale.vo.AppAfterSaleCreateReqVO; import cn.iocoder.yudao.module.trade.controller.app.aftersale.vo.AppAfterSaleCreateReqVO;
import cn.iocoder.yudao.module.trade.controller.app.aftersale.vo.AppAfterSaleDeliveryReqVO; import cn.iocoder.yudao.module.trade.controller.app.aftersale.vo.AppAfterSaleDeliveryReqVO;
import cn.iocoder.yudao.module.trade.controller.app.aftersale.vo.AppAfterSalePageReqVO;
import cn.iocoder.yudao.module.trade.dal.dataobject.aftersale.AfterSaleDO; import cn.iocoder.yudao.module.trade.dal.dataobject.aftersale.AfterSaleDO;
/** /**
@ -28,10 +28,10 @@ public interface AfterSaleService {
* 会员获得售后订单分页 * 会员获得售后订单分页
* *
* @param userId 用户编号 * @param userId 用户编号
* @param pageParam 分页参数 * @param pageReqVO 分页参数
* @return 售后订单分页 * @return 售后订单分页
*/ */
PageResult<AfterSaleDO> getAfterSalePage(Long userId, PageParam pageParam); PageResult<AfterSaleDO> getAfterSalePage(Long userId, AppAfterSalePageReqVO pageReqVO);
/** /**
* 会员获得售后单 * 会员获得售后单

View File

@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.trade.service.aftersale;
import cn.hutool.core.map.MapUtil; import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
import cn.iocoder.yudao.module.pay.api.refund.PayRefundApi; import cn.iocoder.yudao.module.pay.api.refund.PayRefundApi;
@ -16,6 +15,7 @@ import cn.iocoder.yudao.module.trade.controller.admin.aftersale.vo.AfterSalePage
import cn.iocoder.yudao.module.trade.controller.admin.aftersale.vo.AfterSaleRefuseReqVO; import cn.iocoder.yudao.module.trade.controller.admin.aftersale.vo.AfterSaleRefuseReqVO;
import cn.iocoder.yudao.module.trade.controller.app.aftersale.vo.AppAfterSaleCreateReqVO; import cn.iocoder.yudao.module.trade.controller.app.aftersale.vo.AppAfterSaleCreateReqVO;
import cn.iocoder.yudao.module.trade.controller.app.aftersale.vo.AppAfterSaleDeliveryReqVO; import cn.iocoder.yudao.module.trade.controller.app.aftersale.vo.AppAfterSaleDeliveryReqVO;
import cn.iocoder.yudao.module.trade.controller.app.aftersale.vo.AppAfterSalePageReqVO;
import cn.iocoder.yudao.module.trade.convert.aftersale.AfterSaleConvert; import cn.iocoder.yudao.module.trade.convert.aftersale.AfterSaleConvert;
import cn.iocoder.yudao.module.trade.dal.dataobject.aftersale.AfterSaleDO; import cn.iocoder.yudao.module.trade.dal.dataobject.aftersale.AfterSaleDO;
import cn.iocoder.yudao.module.trade.dal.dataobject.delivery.DeliveryExpressDO; import cn.iocoder.yudao.module.trade.dal.dataobject.delivery.DeliveryExpressDO;
@ -36,6 +36,7 @@ import cn.iocoder.yudao.module.trade.framework.order.config.TradeOrderProperties
import cn.iocoder.yudao.module.trade.service.delivery.DeliveryExpressService; import cn.iocoder.yudao.module.trade.service.delivery.DeliveryExpressService;
import cn.iocoder.yudao.module.trade.service.order.TradeOrderQueryService; import cn.iocoder.yudao.module.trade.service.order.TradeOrderQueryService;
import cn.iocoder.yudao.module.trade.service.order.TradeOrderUpdateService; import cn.iocoder.yudao.module.trade.service.order.TradeOrderUpdateService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -44,7 +45,6 @@ import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import jakarta.annotation.Resource;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@ -87,8 +87,8 @@ public class AfterSaleServiceImpl implements AfterSaleService {
} }
@Override @Override
public PageResult<AfterSaleDO> getAfterSalePage(Long userId, PageParam pageParam) { public PageResult<AfterSaleDO> getAfterSalePage(Long userId, AppAfterSalePageReqVO pageReqVO) {
return tradeAfterSaleMapper.selectPage(userId, pageParam); return tradeAfterSaleMapper.selectPage(userId, pageReqVO);
} }
@Override @Override
@ -386,7 +386,7 @@ public class AfterSaleServiceImpl implements AfterSaleService {
public void afterCommit() { public void afterCommit() {
// 创建退款单 // 创建退款单
PayRefundCreateReqDTO createReqDTO = AfterSaleConvert.INSTANCE.convert(userIp, afterSale, tradeOrderProperties) PayRefundCreateReqDTO createReqDTO = AfterSaleConvert.INSTANCE.convert(userIp, afterSale, tradeOrderProperties)
.setReason(StrUtil.format("退款【{}】", afterSale.getSpuName()));; .setReason(StrUtil.format("退款【{}】", afterSale.getSpuName()));
Long payRefundId = payRefundApi.createRefund(createReqDTO); Long payRefundId = payRefundApi.createRefund(createReqDTO);
// 更新售后单的退款单号 // 更新售后单的退款单号
tradeAfterSaleMapper.updateById(new AfterSaleDO().setId(afterSale.getId()).setPayRefundId(payRefundId)); tradeAfterSaleMapper.updateById(new AfterSaleDO().setId(afterSale.getId()).setPayRefundId(payRefundId));

View File

@ -135,7 +135,7 @@ public class BrokerageWithdrawServiceImpl implements BrokerageWithdrawService {
private Long createPayTransfer(BrokerageWithdrawDO withdraw) { private Long createPayTransfer(BrokerageWithdrawDO withdraw) {
// 1.1 获取微信 openid // 1.1 获取微信 openid
SocialUserRespDTO socialUser = socialUserApi.getSocialUserByUserId( SocialUserRespDTO socialUser = socialUserApi.getSocialUserByUserId(
UserTypeEnum.MEMBER.getValue(), withdraw.getUserId(), SocialTypeEnum.WECHAT_MINI_APP.getType()); UserTypeEnum.MEMBER.getValue(), withdraw.getUserId(), SocialTypeEnum.WECHAT_MINI_PROGRAM.getType());
// TODO @luchi这里需要校验非空如果空的话要有业务异常哈 // TODO @luchi这里需要校验非空如果空的话要有业务异常哈
// 1.2 构建请求 // 1.2 构建请求
PayTransferCreateReqDTO payTransferCreateReqDTO = new PayTransferCreateReqDTO() PayTransferCreateReqDTO payTransferCreateReqDTO = new PayTransferCreateReqDTO()

View File

@ -545,6 +545,14 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
if (ObjectUtil.notEqual(order.getStatus(), TradeOrderStatusEnum.UNPAID.getStatus())) { if (ObjectUtil.notEqual(order.getStatus(), TradeOrderStatusEnum.UNPAID.getStatus())) {
throw exception(ORDER_CANCEL_FAIL_STATUS_NOT_UNPAID); throw exception(ORDER_CANCEL_FAIL_STATUS_NOT_UNPAID);
} }
// 1.3 校验是否支持延迟不允许取消
if (TradeOrderStatusEnum.isUnpaid(order.getStatus())) {
PayOrderRespDTO payOrder = payOrderApi.getOrder(order.getPayOrderId());
if (payOrder != null && PayOrderStatusEnum.isSuccess(payOrder.getStatus())) {
log.warn("[cancelOrderByMember][order({}) 支付单已支付(支付回调延迟),不支持取消]", order.getId());
throw exception(ORDER_CANCEL_FAIL_STATUS_NOT_UNPAID);
}
}
// 2. 取消订单 // 2. 取消订单
cancelOrder0(order, TradeOrderCancelTypeEnum.MEMBER_CANCEL); cancelOrder0(order, TradeOrderCancelTypeEnum.MEMBER_CANCEL);
@ -581,6 +589,15 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
@TradeOrderLog(operateType = TradeOrderOperateTypeEnum.SYSTEM_CANCEL) @TradeOrderLog(operateType = TradeOrderOperateTypeEnum.SYSTEM_CANCEL)
public void cancelOrderBySystem(TradeOrderDO order) { public void cancelOrderBySystem(TradeOrderDO order) {
// 校验是否支持延迟不允许取消
if (TradeOrderStatusEnum.isUnpaid(order.getStatus())) {
PayOrderRespDTO payOrder = payOrderApi.getOrder(order.getPayOrderId());
if (payOrder != null && PayOrderStatusEnum.isSuccess(payOrder.getStatus())) {
log.warn("[cancelOrderBySystem][order({}) 支付单已支付(支付回调延迟),不支持取消]", order.getId());
return;
}
}
cancelOrder0(order, TradeOrderCancelTypeEnum.PAY_TIMEOUT); cancelOrder0(order, TradeOrderCancelTypeEnum.PAY_TIMEOUT);
} }
@ -895,12 +912,11 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
if (order == null) { if (order == null) {
throw exception(ORDER_NOT_FOUND); throw exception(ORDER_NOT_FOUND);
} }
// 1.3 校验订单是否支付 // 1.3 校验订单是否支付
if (!order.getPayStatus()) { if (!order.getPayStatus()) {
throw exception(ORDER_CANCEL_PAID_FAIL, "已支付"); throw exception(ORDER_CANCEL_PAID_FAIL, "已支付");
} }
// 1.3 校验订单是否未退款 // 1.4 校验订单是否未退款
if (ObjUtil.notEqual(TradeOrderRefundStatusEnum.NONE.getStatus(), order.getRefundStatus())) { if (ObjUtil.notEqual(TradeOrderRefundStatusEnum.NONE.getStatus(), order.getRefundStatus())) {
throw exception(ORDER_CANCEL_PAID_FAIL, "未退款"); throw exception(ORDER_CANCEL_PAID_FAIL, "未退款");
} }

View File

@ -20,6 +20,7 @@ import org.springframework.stereotype.Component;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import java.util.List; import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
@ -101,13 +102,17 @@ public class TradeBrokerageOrderHandler implements TradeOrderHandler {
protected void addBrokerage(Long userId, List<TradeOrderItemDO> orderItems) { protected void addBrokerage(Long userId, List<TradeOrderItemDO> orderItems) {
MemberUserRespDTO user = memberUserApi.getUser(userId); MemberUserRespDTO user = memberUserApi.getUser(userId);
Assert.notNull(user); Assert.notNull(user);
ProductSpuRespDTO spu = productSpuApi.getSpu(orderItems.get(0).getSpuId()); Map<Long, ProductSpuRespDTO> spusMap = productSpuApi.getSpuMap(convertList(orderItems, TradeOrderItemDO::getSpuId));
Assert.notNull(spu); Map<Long, ProductSkuRespDTO> skusMap = productSkuApi.getSkuMap(convertList(orderItems, TradeOrderItemDO::getSkuId));
ProductSkuRespDTO sku = productSkuApi.getSku(orderItems.get(0).getSkuId());
// 每一个订单项都会去生成分销记录 // 每一个订单项都会去生成分销记录
List<BrokerageAddReqBO> addList = convertList(orderItems, List<BrokerageAddReqBO> addList = convertList(orderItems, item -> {
item -> TradeOrderConvert.INSTANCE.convert(user, item, spu, sku)); ProductSpuRespDTO spu = spusMap.get(item.getSpuId());
Assert.notNull(spu);
ProductSkuRespDTO sku = skusMap.get(item.getSkuId());
Assert.notNull(sku);
return TradeOrderConvert.INSTANCE.convert(user, item, spu, sku);
});
brokerageRecordService.addBrokerage(userId, BrokerageRecordBizTypeEnum.ORDER, addList); brokerageRecordService.addBrokerage(userId, BrokerageRecordBizTypeEnum.ORDER, addList);
} }

View File

@ -1,7 +1,6 @@
package cn.iocoder.yudao.module.member.service.auth; package cn.iocoder.yudao.module.member.service.auth;
import cn.hutool.core.lang.Assert; import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ObjectUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.enums.TerminalEnum; import cn.iocoder.yudao.framework.common.enums.TerminalEnum;
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
@ -27,11 +26,11 @@ import cn.iocoder.yudao.module.system.enums.logger.LoginResultEnum;
import cn.iocoder.yudao.module.system.enums.oauth2.OAuth2ClientConstants; import cn.iocoder.yudao.module.system.enums.oauth2.OAuth2ClientConstants;
import cn.iocoder.yudao.module.system.enums.sms.SmsSceneEnum; import cn.iocoder.yudao.module.system.enums.sms.SmsSceneEnum;
import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum; import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import jakarta.annotation.Resource;
import java.util.Objects; import java.util.Objects;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@ -147,7 +146,7 @@ public class MemberAuthServiceImpl implements MemberAuthService {
// 绑定社交用户 // 绑定社交用户
String openid = socialUserApi.bindSocialUser(new SocialUserBindReqDTO(user.getId(), getUserType().getValue(), String openid = socialUserApi.bindSocialUser(new SocialUserBindReqDTO(user.getId(), getUserType().getValue(),
SocialTypeEnum.WECHAT_MINI_APP.getType(), reqVO.getLoginCode(), reqVO.getState())); SocialTypeEnum.WECHAT_MINI_PROGRAM.getType(), reqVO.getLoginCode(), reqVO.getState()));
// 创建 Token 令牌记录登录日志 // 创建 Token 令牌记录登录日志
return createTokenAfterLoginSuccess(user, user.getMobile(), LoginLogTypeEnum.LOGIN_SOCIAL, openid); return createTokenAfterLoginSuccess(user, user.getMobile(), LoginLogTypeEnum.LOGIN_SOCIAL, openid);

View File

@ -5,12 +5,12 @@ import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.util.collection.ArrayUtils; import cn.iocoder.yudao.framework.common.util.collection.ArrayUtils;
import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration; import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration;
import cn.iocoder.yudao.framework.test.core.ut.BaseDbAndRedisUnitTest; import cn.iocoder.yudao.framework.test.core.ut.BaseDbAndRedisUnitTest;
import cn.iocoder.yudao.module.infra.api.file.FileApi;
import cn.iocoder.yudao.module.member.controller.app.user.vo.AppMemberUserUpdateMobileReqVO; import cn.iocoder.yudao.module.member.controller.app.user.vo.AppMemberUserUpdateMobileReqVO;
import cn.iocoder.yudao.module.member.dal.dataobject.user.MemberUserDO; import cn.iocoder.yudao.module.member.dal.dataobject.user.MemberUserDO;
import cn.iocoder.yudao.module.member.dal.mysql.user.MemberUserMapper; import cn.iocoder.yudao.module.member.dal.mysql.user.MemberUserMapper;
import cn.iocoder.yudao.module.member.service.auth.MemberAuthServiceImpl; import cn.iocoder.yudao.module.member.service.auth.MemberAuthServiceImpl;
import cn.iocoder.yudao.module.system.api.sms.SmsCodeApi; import cn.iocoder.yudao.module.system.api.sms.SmsCodeApi;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.mock.mockito.MockBean;
@ -18,7 +18,6 @@ import org.springframework.context.annotation.Import;
import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import jakarta.annotation.Resource;
import java.util.function.Consumer; import java.util.function.Consumer;
import static cn.hutool.core.util.RandomUtil.randomEle; import static cn.hutool.core.util.RandomUtil.randomEle;
@ -53,8 +52,6 @@ public class MemberUserServiceImplTest extends BaseDbAndRedisUnitTest {
@MockBean @MockBean
private SmsCodeApi smsCodeApi; private SmsCodeApi smsCodeApi;
@MockBean
private FileApi fileApi;
// TODO 芋艿后续重构这个单测 // TODO 芋艿后续重构这个单测
// @Test // @Test
@ -72,25 +69,6 @@ public class MemberUserServiceImplTest extends BaseDbAndRedisUnitTest {
// String nickname = memberUserService.getUser(userDO.getId()).getNickname(); // String nickname = memberUserService.getUser(userDO.getId()).getNickname();
// // 断言 // // 断言
// assertEquals(newNickName,nickname); // assertEquals(newNickName,nickname);
// }
//
// @Test
// public void testUpdateAvatar_success() throws Exception {
// // mock 数据
// MemberUserDO dbUser = randomUserDO();
// userMapper.insert(dbUser);
//
// // 准备参数
// Long userId = dbUser.getId();
// byte[] avatarFileBytes = randomBytes(10);
// ByteArrayInputStream avatarFile = new ByteArrayInputStream(avatarFileBytes);
// // mock 方法
// String avatar = randomString();
// when(fileApi.createFile(eq(avatarFileBytes))).thenReturn(avatar);
// // 调用
// String str = memberUserService.updateUserAvatar(userId, avatarFile);
// // 断言
// assertEquals(avatar, str);
// } // }
@Test @Test

View File

@ -218,7 +218,7 @@ public class MpMaterialServiceImpl implements MpMaterialService {
private String uploadFile(String mediaId, File file) { private String uploadFile(String mediaId, File file) {
String path = mediaId + "." + FileTypeUtil.getType(file); String path = mediaId + "." + FileTypeUtil.getType(file);
return fileApi.createFile(path, FileUtil.readBytes(file)); return fileApi.createFile(FileUtil.readBytes(file), path);
} }
} }

View File

@ -64,6 +64,10 @@
<groupId>org.jeecgframework.jimureport</groupId> <groupId>org.jeecgframework.jimureport</groupId>
<artifactId>jimureport-spring-boot3-starter-fastjson2</artifactId> <artifactId>jimureport-spring-boot3-starter-fastjson2</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.jeecgframework.jimureport</groupId>
<artifactId>jimubi-spring-boot3-starter</artifactId>
</dependency>
<dependency> <dependency>
<groupId>cn.iocoder.boot</groupId> <groupId>cn.iocoder.boot</groupId>

View File

@ -1,13 +1,15 @@
package cn.iocoder.yudao.module.report.framework.jmreport.config; package cn.iocoder.yudao.module.report.framework.jmreport.config;
import cn.iocoder.yudao.framework.security.config.SecurityProperties; import cn.iocoder.yudao.framework.security.config.SecurityProperties;
import cn.iocoder.yudao.module.system.api.oauth2.OAuth2TokenApi; import cn.iocoder.yudao.module.report.framework.jmreport.core.service.JmOnlDragExternalServiceImpl;
import cn.iocoder.yudao.module.report.framework.jmreport.core.service.JmReportTokenServiceImpl; import cn.iocoder.yudao.module.report.framework.jmreport.core.service.JmReportTokenServiceImpl;
import cn.iocoder.yudao.module.system.api.oauth2.OAuth2TokenApi;
import cn.iocoder.yudao.module.system.api.permission.PermissionApi; import cn.iocoder.yudao.module.system.api.permission.PermissionApi;
import org.jeecg.modules.jmreport.api.JmReportTokenServiceI; import org.jeecg.modules.jmreport.api.JmReportTokenServiceI;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
/** /**
* 积木报表的配置类 * 积木报表的配置类
@ -19,11 +21,16 @@ import org.springframework.context.annotation.Configuration;
public class JmReportConfiguration { public class JmReportConfiguration {
@Bean @Bean
@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
public JmReportTokenServiceI jmReportTokenService(OAuth2TokenApi oAuth2TokenApi, public JmReportTokenServiceI jmReportTokenService(OAuth2TokenApi oAuth2TokenApi,
PermissionApi permissionApi, PermissionApi permissionApi,
SecurityProperties securityProperties) { SecurityProperties securityProperties) {
return new JmReportTokenServiceImpl(oAuth2TokenApi, permissionApi, securityProperties); return new JmReportTokenServiceImpl(oAuth2TokenApi, permissionApi, securityProperties);
} }
@Bean // 暂时注释可以按需实现后打开
@Primary
public JmOnlDragExternalServiceImpl jmOnlDragExternalService2() {
return new JmOnlDragExternalServiceImpl();
}
} }

View File

@ -0,0 +1,68 @@
package cn.iocoder.yudao.module.report.framework.jmreport.core.service;
import com.alibaba.fastjson.JSONObject;
import lombok.RequiredArgsConstructor;
import org.jeecg.modules.drag.service.IOnlDragExternalService;
import org.jeecg.modules.drag.vo.DragDictModel;
import org.jeecg.modules.drag.vo.DragLogDTO;
import java.util.List;
import java.util.Map;
/**
* {@link IOnlDragExternalService} 实现类提供积木仪表盘的查询等功能
*
* 实现可参考
* 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>
* 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>
*
* @author 芋道源码
*/
@RequiredArgsConstructor
public class JmOnlDragExternalServiceImpl implements IOnlDragExternalService {
// ========== DictItem 相关 ==========
@Override
public Map<String, List<DragDictModel>> getManyDictItems(List<String> codeList, List<JSONObject> tableDictList) {
return IOnlDragExternalService.super.getManyDictItems(codeList, tableDictList);
}
@Override
public List<DragDictModel> getDictItems(String dictCode) {
return IOnlDragExternalService.super.getDictItems(dictCode);
}
@Override
public List<DragDictModel> getTableDictItems(String dictTable, String dictText, String dictCode) {
return IOnlDragExternalService.super.getTableDictItems(dictTable, dictText, dictCode);
}
@Override
public List<DragDictModel> getCategoryTreeDictItems(List<String> ids) {
return IOnlDragExternalService.super.getCategoryTreeDictItems(ids);
}
@Override
public List<DragDictModel> getUserDictItems(List<String> ids) {
return IOnlDragExternalService.super.getUserDictItems(ids);
}
@Override
public List<DragDictModel> getDeptsDictItems(List<String> ids) {
return IOnlDragExternalService.super.getDeptsDictItems(ids);
}
// ========== Log 相关 ==========
@Override
public void addLog(DragLogDTO dto) {
IOnlDragExternalService.super.addLog(dto);
}
@Override
public void addLog(String logMsg, int logType, int operateType) {
IOnlDragExternalService.super.addLog(logMsg, logType, operateType);
}
}

View File

@ -18,7 +18,11 @@ public class SecurityConfiguration {
@Override @Override
public void customize(AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry registry) { public void customize(AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry registry) {
registry.requestMatchers("/jmreport/**").permitAll(); // 积木报表 // 积木报表
registry.requestMatchers("/jmreport/**").permitAll();
// 积木仪表盘
registry.requestMatchers("/drag/**").permitAll();
registry.requestMatchers("/jimubi/**").permitAll();
} }
}; };

View File

@ -45,6 +45,7 @@ public interface ErrorCodeConstants {
ErrorCode USER_COUNT_MAX = new ErrorCode(1_002_003_008, "创建用户失败,原因:超过租户最大租户配额({})"); ErrorCode USER_COUNT_MAX = new ErrorCode(1_002_003_008, "创建用户失败,原因:超过租户最大租户配额({})");
ErrorCode USER_IMPORT_INIT_PASSWORD = new ErrorCode(1_002_003_009, "初始密码不能为空"); ErrorCode USER_IMPORT_INIT_PASSWORD = new ErrorCode(1_002_003_009, "初始密码不能为空");
ErrorCode USER_MOBILE_NOT_EXISTS = new ErrorCode(1_002_003_010, "该手机号尚未注册"); ErrorCode USER_MOBILE_NOT_EXISTS = new ErrorCode(1_002_003_010, "该手机号尚未注册");
ErrorCode USER_REGISTER_DISABLED = new ErrorCode(1_002_003_011, "注册功能已关闭");
// ========== 部门模块 1-002-004-000 ========== // ========== 部门模块 1-002-004-000 ==========
ErrorCode DEPT_NAME_DUPLICATE = new ErrorCode(1_002_004_000, "已经存在该名字的部门"); ErrorCode DEPT_NAME_DUPLICATE = new ErrorCode(1_002_004_000, "已经存在该名字的部门");

View File

@ -52,7 +52,7 @@ public enum SocialTypeEnum implements ArrayValuable<Integer> {
* *
* @see <a href="https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html">接入文档</a> * @see <a href="https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html">接入文档</a>
*/ */
WECHAT_MINI_APP(34, "WECHAT_MINI_APP"), WECHAT_MINI_PROGRAM(34, "WECHAT_MINI_PROGRAM"),
; ;
public static final Integer[] ARRAYS = Arrays.stream(values()).map(SocialTypeEnum::getType).toArray(Integer[]::new); public static final Integer[] ARRAYS = Arrays.stream(values()).map(SocialTypeEnum::getType).toArray(Integer[]::new);

View File

@ -97,8 +97,12 @@
<!-- 三方云服务相关 --> <!-- 三方云服务相关 -->
<dependency> <dependency>
<groupId>com.xingyuv</groupId> <groupId>me.zhyd.oauth</groupId>
<artifactId>spring-boot-starter-justauth</artifactId> <!-- 社交登陆(例如说,个人微信、企业微信等等) --> <artifactId>JustAuth</artifactId> <!-- 社交登陆(例如说,个人微信、企业微信等等) -->
</dependency>
<dependency>
<groupId>com.xkcoding.justauth</groupId>
<artifactId>justauth-spring-boot-starter</artifactId>
</dependency> </dependency>
<dependency> <dependency>
@ -111,8 +115,8 @@
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.xingyuv</groupId> <groupId>com.anji-plus</groupId>
<artifactId>spring-boot-starter-captcha-plus</artifactId> <!-- 验证码,一般用于登录使用 --> <artifactId>captcha-spring-boot-starter</artifactId> <!-- 验证码,一般用于登录使用 -->
</dependency> </dependency>
<dependency> <dependency>

View File

@ -84,7 +84,7 @@ public class SocialClientApiImpl implements SocialClientApi {
// 2. 获得社交用户 // 2. 获得社交用户
SocialUserRespDTO socialUser = socialUserService.getSocialUserByUserId(reqDTO.getUserType(), reqDTO.getUserId(), SocialUserRespDTO socialUser = socialUserService.getSocialUserByUserId(reqDTO.getUserType(), reqDTO.getUserId(),
SocialTypeEnum.WECHAT_MINI_APP.getType()); SocialTypeEnum.WECHAT_MINI_PROGRAM.getType());
if (StrUtil.isBlankIfStr(socialUser.getOpenid())) { if (StrUtil.isBlankIfStr(socialUser.getOpenid())) {
log.warn("[sendWxaSubscribeMessage][reqDTO({}) 发送订阅消息失败,原因:会员 openid 缺失]", reqDTO); log.warn("[sendWxaSubscribeMessage][reqDTO({}) 发送订阅消息失败,原因:会员 openid 缺失]", reqDTO);
return; return;

View File

@ -2,20 +2,19 @@ package cn.iocoder.yudao.module.system.controller.admin.captcha;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
import com.xingyuv.captcha.model.common.ResponseModel; import com.anji.captcha.model.common.ResponseModel;
import com.xingyuv.captcha.model.vo.CaptchaVO; import com.anji.captcha.model.vo.CaptchaVO;
import com.xingyuv.captcha.service.CaptchaService; import com.anji.captcha.service.CaptchaService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.annotation.security.PermitAll;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import jakarta.annotation.Resource;
import jakarta.annotation.security.PermitAll;
import jakarta.servlet.http.HttpServletRequest;
@Tag(name = "管理后台 - 验证码") @Tag(name = "管理后台 - 验证码")
@RestController("adminCaptchaController") @RestController("adminCaptchaController")
@RequestMapping("/system/captcha") @RequestMapping("/system/captcha")

View File

@ -23,14 +23,11 @@ import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.List; import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_IS_EMPTY;
@Tag(name = "管理后台 - 用户个人中心") @Tag(name = "管理后台 - 用户个人中心")
@RestController @RestController
@ -79,16 +76,4 @@ public class UserProfileController {
return success(true); return success(true);
} }
@Deprecated // TODO @芋艿逐步替换到 updateUserProfile 接口
@RequestMapping(value = "/update-avatar",
method = {RequestMethod.POST, RequestMethod.PUT}) // 解决 uni-app 不支持 Put 上传文件的问题
@Operation(summary = "上传用户个人头像")
public CommonResult<String> updateUserAvatar(@RequestParam("avatarFile") MultipartFile file) throws Exception {
if (file.isEmpty()) {
throw exception(FILE_IS_EMPTY);
}
String avatar = userService.updateUserAvatar(getLoginUserId(), file.getInputStream());
return success(avatar);
}
} }

View File

@ -7,8 +7,8 @@ import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum;
import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import com.xingyuv.jushauth.config.AuthConfig;
import lombok.*; import lombok.*;
import me.zhyd.oauth.config.AuthConfig;
/** /**
* 社交客户端 DO * 社交客户端 DO

View File

@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.system.dal.mysql.mail;
import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX;
import cn.iocoder.yudao.module.system.controller.admin.mail.vo.account.MailAccountPageReqVO; import cn.iocoder.yudao.module.system.controller.admin.mail.vo.account.MailAccountPageReqVO;
import cn.iocoder.yudao.module.system.dal.dataobject.mail.MailAccountDO; import cn.iocoder.yudao.module.system.dal.dataobject.mail.MailAccountDO;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
@ -14,7 +13,8 @@ public interface MailAccountMapper extends BaseMapperX<MailAccountDO> {
default PageResult<MailAccountDO> selectPage(MailAccountPageReqVO pageReqVO) { default PageResult<MailAccountDO> selectPage(MailAccountPageReqVO pageReqVO) {
return selectPage(pageReqVO, new LambdaQueryWrapperX<MailAccountDO>() return selectPage(pageReqVO, new LambdaQueryWrapperX<MailAccountDO>()
.likeIfPresent(MailAccountDO::getMail, pageReqVO.getMail()) .likeIfPresent(MailAccountDO::getMail, pageReqVO.getMail())
.likeIfPresent(MailAccountDO::getUsername , pageReqVO.getUsername())); .likeIfPresent(MailAccountDO::getUsername , pageReqVO.getUsername())
.orderByDesc(MailAccountDO::getId));
} }
} }

View File

@ -3,14 +3,9 @@ package cn.iocoder.yudao.module.system.dal.mysql.mail;
import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX;
import cn.iocoder.yudao.module.system.controller.admin.mail.vo.template.MailTemplatePageReqVO; import cn.iocoder.yudao.module.system.controller.admin.mail.vo.template.MailTemplatePageReqVO;
import cn.iocoder.yudao.module.system.dal.dataobject.mail.MailTemplateDO; import cn.iocoder.yudao.module.system.dal.dataobject.mail.MailTemplateDO;
import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsTemplateDO;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.Date;
@Mapper @Mapper
public interface MailTemplateMapper extends BaseMapperX<MailTemplateDO> { public interface MailTemplateMapper extends BaseMapperX<MailTemplateDO> {
@ -21,7 +16,8 @@ public interface MailTemplateMapper extends BaseMapperX<MailTemplateDO> {
.likeIfPresent(MailTemplateDO::getCode, pageReqVO.getCode()) .likeIfPresent(MailTemplateDO::getCode, pageReqVO.getCode())
.likeIfPresent(MailTemplateDO::getName, pageReqVO.getName()) .likeIfPresent(MailTemplateDO::getName, pageReqVO.getName())
.eqIfPresent(MailTemplateDO::getAccountId, pageReqVO.getAccountId()) .eqIfPresent(MailTemplateDO::getAccountId, pageReqVO.getAccountId())
.betweenIfPresent(MailTemplateDO::getCreateTime, pageReqVO.getCreateTime())); .betweenIfPresent(MailTemplateDO::getCreateTime, pageReqVO.getCreateTime())
.orderByDesc(MailTemplateDO::getId));
} }
default Long selectCountByAccountId(Long accountId) { default Long selectCountByAccountId(Long accountId) {

View File

@ -1,11 +1,14 @@
package cn.iocoder.yudao.module.system.framework.captcha.config; package cn.iocoder.yudao.module.system.framework.captcha.config;
import cn.iocoder.yudao.module.system.framework.captcha.core.RedisCaptchaServiceImpl; import cn.iocoder.yudao.module.system.framework.captcha.core.RedisCaptchaServiceImpl;
import com.xingyuv.captcha.properties.AjCaptchaProperties; import com.anji.captcha.config.AjCaptchaAutoConfiguration;
import com.xingyuv.captcha.service.CaptchaCacheService; import com.anji.captcha.properties.AjCaptchaProperties;
import com.xingyuv.captcha.service.impl.CaptchaServiceFactory; import com.anji.captcha.service.CaptchaCacheService;
import com.anji.captcha.service.impl.CaptchaServiceFactory;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.StringRedisTemplate;
/** /**
@ -14,9 +17,11 @@ import org.springframework.data.redis.core.StringRedisTemplate;
* @author 芋道源码 * @author 芋道源码
*/ */
@Configuration(proxyBeanMethods = false) @Configuration(proxyBeanMethods = false)
@ImportAutoConfiguration(AjCaptchaAutoConfiguration.class) // 目的解决 aj-captcha 针对 SpringBoot 3.X 自动配置不生效的问题
public class YudaoCaptchaConfiguration { public class YudaoCaptchaConfiguration {
@Bean @Bean(name = "AjCaptchaCacheService")
@Primary
public CaptchaCacheService captchaCacheService(AjCaptchaProperties config, public CaptchaCacheService captchaCacheService(AjCaptchaProperties config,
StringRedisTemplate stringRedisTemplate) { StringRedisTemplate stringRedisTemplate) {
CaptchaCacheService captchaCacheService = CaptchaServiceFactory.getCache(config.getCacheType().name()); CaptchaCacheService captchaCacheService = CaptchaServiceFactory.getCache(config.getCacheType().name());

View File

@ -1,6 +1,6 @@
package cn.iocoder.yudao.module.system.framework.captcha.core; package cn.iocoder.yudao.module.system.framework.captcha.core;
import com.xingyuv.captcha.service.CaptchaCacheService; import com.anji.captcha.service.CaptchaCacheService;
import lombok.Setter; import lombok.Setter;
import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.StringRedisTemplate;
@ -28,7 +28,7 @@ public class RedisCaptchaServiceImpl implements CaptchaCacheService {
@Override @Override
public boolean exists(String key) { public boolean exists(String key) {
return Boolean.TRUE.equals(stringRedisTemplate.hasKey(key)); return stringRedisTemplate.hasKey(key);
} }
@Override @Override

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