mirror of
				https://gitee.com/hhyykk/ipms-sjy.git
				synced 2025-11-04 04:08:43 +08:00 
			
		
		
		
	Merge remote-tracking branch 'origin/master' into feature/bpm-back
This commit is contained in:
		@@ -116,7 +116,7 @@ ps:核心功能已经实现,正在对接微信小程序中...
 | 
			
		||||
|     | 表单构建     | 拖动表单元素生成相应的 HTML 代码,支持导出 JSON、Vue 文件         |
 | 
			
		||||
| 🚀  | 配置管理     | 对系统动态配置常用参数,支持 SpringBoot 加载                 |
 | 
			
		||||
| ⭐️  | 定时任务     | 在线(添加、修改、删除)任务调度包含执行结果日志                     |
 | 
			
		||||
| 🚀  | 文件服务     | 支持本地文件存储,同时支持兼容 Amazon S3 协议的云服务、开源组件        | 
 | 
			
		||||
| 🚀  | 文件服务     | 支持将文件存储到 S3(MinIO、阿里云、腾讯云、七牛云)、本地、FTP、数据库等      | 
 | 
			
		||||
| 🚀  | API 日志   | 包括 RESTful API 访问日志、异常日志两部分,方便排查 API 相关的问题   |
 | 
			
		||||
|     | MySQL 监控 | 监视当前系统数据库连接池状态,可进行分析SQL找出系统性能瓶颈              |
 | 
			
		||||
|     | Redis 监控 | 监控 Redis 数据库的使用情况,使用的 Redis Key 管理           |
 | 
			
		||||
@@ -218,7 +218,7 @@ ps:核心功能已经实现,正在对接微信小程序中...
 | 
			
		||||
|---------------|----------------------------------------------------------------------|--------------------------------------------------------------------|------------------------------------------------------------------|
 | 
			
		||||
| 代码生成          |      |    | -                                                                |
 | 
			
		||||
| 文档            |      |  | -                                                                |
 | 
			
		||||
| 文件 & 配置       |      |    | -                                                                |
 | 
			
		||||
| 文件 & 配置       |  |      |    |
 | 
			
		||||
| 定时任务          |      |    | -                                                                |
 | 
			
		||||
| API 日志        |      |    | -                                                                |
 | 
			
		||||
| MySQL & Redis |    |  | -                                                                |
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								pom.xml
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								pom.xml
									
									
									
									
									
								
							@@ -25,7 +25,7 @@
 | 
			
		||||
    <url>https://github.com/YunaiV/ruoyi-vue-pro</url>
 | 
			
		||||
 | 
			
		||||
    <properties>
 | 
			
		||||
        <revision>1.6.0-snapshot</revision>
 | 
			
		||||
        <revision>1.6.1-snapshot</revision>
 | 
			
		||||
        <!-- Maven 相关 -->
 | 
			
		||||
        <java.version>1.8</java.version>
 | 
			
		||||
        <maven.compiler.source>${java.version}</maven.compiler.source>
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -14,7 +14,7 @@
 | 
			
		||||
    <url>https://github.com/YunaiV/ruoyi-vue-pro</url>
 | 
			
		||||
 | 
			
		||||
    <properties>
 | 
			
		||||
        <revision>1.6.0-snapshot</revision>
 | 
			
		||||
        <revision>1.6.1-snapshot</revision>
 | 
			
		||||
        <!-- 统一依赖管理 -->
 | 
			
		||||
        <spring.boot.version>2.5.10</spring.boot.version>
 | 
			
		||||
        <!-- Web 相关 -->
 | 
			
		||||
@@ -28,7 +28,7 @@
 | 
			
		||||
        <dynamic-datasource.version>3.5.0</dynamic-datasource.version>
 | 
			
		||||
        <redisson.version>3.16.6</redisson.version>
 | 
			
		||||
        <!-- Config 配置中心相关 -->
 | 
			
		||||
        <apollo.version>1.7.0</apollo.version>
 | 
			
		||||
        <apollo.version>1.9.2</apollo.version>
 | 
			
		||||
        <!-- Job 定时任务相关 -->
 | 
			
		||||
        <!-- 服务保障相关 -->
 | 
			
		||||
        <lock4j.version>2.2.0</lock4j.version>
 | 
			
		||||
@@ -52,8 +52,12 @@
 | 
			
		||||
        <velocity.version>2.2</velocity.version>
 | 
			
		||||
        <screw.version>1.0.5</screw.version>
 | 
			
		||||
        <guava.version>30.1.1-jre</guava.version>
 | 
			
		||||
        <guice.version>5.1.0</guice.version>
 | 
			
		||||
        <transmittable-thread-local.version>2.12.2</transmittable-thread-local.version>
 | 
			
		||||
        <commons-net.version>3.8.0</commons-net.version>
 | 
			
		||||
        <jsch.version>0.1.55</jsch.version>
 | 
			
		||||
        <!-- 三方云服务相关 -->
 | 
			
		||||
        <minio.version>8.2.2</minio.version>
 | 
			
		||||
        <aliyun-java-sdk-core.version>4.5.25</aliyun-java-sdk-core.version>
 | 
			
		||||
        <aliyun-java-sdk-dysmsapi.version>2.1.0</aliyun-java-sdk-dysmsapi.version>
 | 
			
		||||
        <yunpian-java-sdk.version>1.2.7</yunpian-java-sdk.version>
 | 
			
		||||
@@ -487,13 +491,40 @@
 | 
			
		||||
                <version>${guava.version}</version>
 | 
			
		||||
            </dependency>
 | 
			
		||||
 | 
			
		||||
            <dependency>
 | 
			
		||||
                <groupId>com.google.inject</groupId>
 | 
			
		||||
                <artifactId>guice</artifactId>
 | 
			
		||||
                <version>${guice.version}</version>
 | 
			
		||||
            </dependency>
 | 
			
		||||
 | 
			
		||||
            <dependency>
 | 
			
		||||
                <groupId>com.alibaba</groupId>
 | 
			
		||||
                <artifactId>transmittable-thread-local</artifactId> <!-- 解决 ThreadLocal 父子线程的传值问题 -->
 | 
			
		||||
                <version>${transmittable-thread-local.version}</version>
 | 
			
		||||
            </dependency>
 | 
			
		||||
 | 
			
		||||
            <dependency>
 | 
			
		||||
                <groupId>commons-net</groupId>
 | 
			
		||||
                <artifactId>commons-net</artifactId> <!-- 解决 ftp 连接 -->
 | 
			
		||||
                <version>${commons-net.version}</version>
 | 
			
		||||
            </dependency>
 | 
			
		||||
            <dependency>
 | 
			
		||||
                <groupId>com.jcraft</groupId>
 | 
			
		||||
                <artifactId>jsch</artifactId> <!-- 解决 sftp 连接 -->
 | 
			
		||||
                <version>${jsch.version}</version>
 | 
			
		||||
            </dependency>
 | 
			
		||||
 | 
			
		||||
            <!-- 三方云服务相关 -->
 | 
			
		||||
            <dependency>
 | 
			
		||||
                <groupId>cn.iocoder.boot</groupId>
 | 
			
		||||
                <artifactId>yudao-spring-boot-starter-file</artifactId>
 | 
			
		||||
                <version>${revision}</version>
 | 
			
		||||
            </dependency>
 | 
			
		||||
            <dependency>
 | 
			
		||||
                <groupId>io.minio</groupId>
 | 
			
		||||
                <artifactId>minio</artifactId>
 | 
			
		||||
                <version>${minio.version}</version>
 | 
			
		||||
            </dependency>
 | 
			
		||||
 | 
			
		||||
            <!-- SMS SDK begin -->
 | 
			
		||||
            <dependency>
 | 
			
		||||
 
 | 
			
		||||
@@ -16,6 +16,7 @@
 | 
			
		||||
        <module>yudao-spring-boot-starter-web</module>
 | 
			
		||||
        <module>yudao-spring-boot-starter-security</module>
 | 
			
		||||
 | 
			
		||||
        <module>yudao-spring-boot-starter-file</module>
 | 
			
		||||
        <module>yudao-spring-boot-starter-monitor</module>
 | 
			
		||||
        <module>yudao-spring-boot-starter-protection</module>
 | 
			
		||||
        <module>yudao-spring-boot-starter-config</module>
 | 
			
		||||
 
 | 
			
		||||
@@ -22,13 +22,40 @@ public class FileUtils {
 | 
			
		||||
     */
 | 
			
		||||
    @SneakyThrows
 | 
			
		||||
    public static File createTempFile(String data) {
 | 
			
		||||
        // 创建文件,通过 UUID 保证唯一
 | 
			
		||||
        File file = File.createTempFile(IdUtil.simpleUUID(), null);
 | 
			
		||||
        // 标记 JVM 退出时,自动删除
 | 
			
		||||
        file.deleteOnExit();
 | 
			
		||||
        File file = createTempFile();
 | 
			
		||||
        // 写入内容
 | 
			
		||||
        FileUtil.writeUtf8String(data, file);
 | 
			
		||||
        return file;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 创建临时文件
 | 
			
		||||
     * 该文件会在 JVM 退出时,进行删除
 | 
			
		||||
     *
 | 
			
		||||
     * @param data 文件内容
 | 
			
		||||
     * @return 文件
 | 
			
		||||
     */
 | 
			
		||||
    @SneakyThrows
 | 
			
		||||
    public static File createTempFile(byte[] data) {
 | 
			
		||||
        File file = createTempFile();
 | 
			
		||||
        // 写入内容
 | 
			
		||||
        FileUtil.writeBytes(data, file);
 | 
			
		||||
        return file;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 创建临时文件,无内容
 | 
			
		||||
     * 该文件会在 JVM 退出时,进行删除
 | 
			
		||||
     *
 | 
			
		||||
     * @return 文件
 | 
			
		||||
     */
 | 
			
		||||
    @SneakyThrows
 | 
			
		||||
    public static File createTempFile() {
 | 
			
		||||
        // 创建文件,通过 UUID 保证唯一
 | 
			
		||||
        File file = File.createTempFile(IdUtil.simpleUUID(), null);
 | 
			
		||||
        // 标记 JVM 退出时,自动删除
 | 
			
		||||
        file.deleteOnExit();
 | 
			
		||||
        return file;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@ package cn.iocoder.yudao.framework.common.util.json;
 | 
			
		||||
 | 
			
		||||
import cn.hutool.core.util.ArrayUtil;
 | 
			
		||||
import cn.hutool.core.util.StrUtil;
 | 
			
		||||
import cn.hutool.json.JSONUtil;
 | 
			
		||||
import com.fasterxml.jackson.core.type.TypeReference;
 | 
			
		||||
import com.fasterxml.jackson.databind.JsonNode;
 | 
			
		||||
import com.fasterxml.jackson.databind.ObjectMapper;
 | 
			
		||||
@@ -55,7 +56,6 @@ public class JsonUtils {
 | 
			
		||||
        if (StrUtil.isEmpty(text)) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            return objectMapper.readValue(text, clazz);
 | 
			
		||||
        } catch (IOException e) {
 | 
			
		||||
@@ -64,11 +64,26 @@ public class JsonUtils {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 将字符串解析成指定类型的对象
 | 
			
		||||
     * 使用 {@link #parseObject(String, Class)} 时,在@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) 的场景下,
 | 
			
		||||
     * 如果 text 没有 class 属性,则会报错。此时,使用这个方法,可以解决。
 | 
			
		||||
     *
 | 
			
		||||
     * @param text 字符串
 | 
			
		||||
     * @param clazz 类型
 | 
			
		||||
     * @return 对象
 | 
			
		||||
     */
 | 
			
		||||
    public static <T> T parseObject2(String text, Class<T> clazz) {
 | 
			
		||||
        if (StrUtil.isEmpty(text)) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
        return JSONUtil.toBean(text, clazz);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static <T> T parseObject(byte[] bytes, Class<T> clazz) {
 | 
			
		||||
        if (ArrayUtil.isEmpty(bytes)) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            return objectMapper.readValue(bytes, clazz);
 | 
			
		||||
        } catch (IOException e) {
 | 
			
		||||
@@ -90,7 +105,6 @@ public class JsonUtils {
 | 
			
		||||
        if (StrUtil.isEmpty(text)) {
 | 
			
		||||
            return new ArrayList<>();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            return objectMapper.readValue(text, objectMapper.getTypeFactory().constructCollectionType(List.class, clazz));
 | 
			
		||||
        } catch (IOException e) {
 | 
			
		||||
 
 | 
			
		||||
@@ -39,8 +39,8 @@ public class ValidationUtils {
 | 
			
		||||
                && PATTERN_XML_NCNAME.matcher(str).matches();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void validate(Validator validator, Object reqVO, Class<?>... groups) {
 | 
			
		||||
        Set<ConstraintViolation<Object>> constraintViolations = validator.validate(reqVO, groups);
 | 
			
		||||
    public static void validate(Validator validator, Object object, Class<?>... groups) {
 | 
			
		||||
        Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups);
 | 
			
		||||
        if (CollUtil.isNotEmpty(constraintViolations)) {
 | 
			
		||||
            throw new ConstraintViolationException(constraintViolations);
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,7 @@ import org.springframework.context.annotation.Configuration;
 | 
			
		||||
 *
 | 
			
		||||
 * @author 芋道源码
 | 
			
		||||
 */
 | 
			
		||||
@Configuration
 | 
			
		||||
@EnableConfigurationProperties(PayProperties.class)
 | 
			
		||||
public class YudaoPayAutoConfiguration {
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
package cn.iocoder.yudao.framework.pay.core.client.impl;
 | 
			
		||||
 | 
			
		||||
import cn.hutool.extra.validation.ValidationUtil;
 | 
			
		||||
import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
 | 
			
		||||
import cn.iocoder.yudao.framework.pay.core.client.AbstractPayCodeMapping;
 | 
			
		||||
import cn.iocoder.yudao.framework.pay.core.client.PayClient;
 | 
			
		||||
import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig;
 | 
			
		||||
@@ -11,7 +10,6 @@ import cn.iocoder.yudao.framework.pay.core.client.dto.PayRefundUnifiedReqDTO;
 | 
			
		||||
import cn.iocoder.yudao.framework.pay.core.client.dto.PayRefundUnifiedRespDTO;
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
 | 
			
		||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 | 
			
		||||
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -26,7 +24,6 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
 | 
			
		||||
     * 渠道编号
 | 
			
		||||
     */
 | 
			
		||||
    private final Long channelId;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 渠道编码
 | 
			
		||||
     */
 | 
			
		||||
@@ -40,10 +37,6 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
 | 
			
		||||
     */
 | 
			
		||||
    protected Config config;
 | 
			
		||||
 | 
			
		||||
    protected Double calculateAmount(Long amount) {
 | 
			
		||||
        return amount / 100.0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public AbstractPayClient(Long channelId, String channelCode, Config config, AbstractPayCodeMapping codeMapping) {
 | 
			
		||||
        this.channelId = channelId;
 | 
			
		||||
        this.channelCode = channelCode;
 | 
			
		||||
@@ -75,6 +68,10 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
 | 
			
		||||
        this.init();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected Double calculateAmount(Long amount) {
 | 
			
		||||
        return amount / 100.0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public Long getId() {
 | 
			
		||||
        return channelId;
 | 
			
		||||
@@ -96,12 +93,9 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
 | 
			
		||||
        return result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    protected abstract PayCommonResult<?> doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO)
 | 
			
		||||
            throws Throwable;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public PayCommonResult<PayRefundUnifiedRespDTO> unifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
 | 
			
		||||
        PayCommonResult<PayRefundUnifiedRespDTO> resp;
 | 
			
		||||
@@ -115,7 +109,6 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
 | 
			
		||||
        return resp;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    protected abstract PayCommonResult<PayRefundUnifiedRespDTO> doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -27,11 +27,11 @@ public class PayClientFactoryImpl implements PayClientFactory {
 | 
			
		||||
     * 支付客户端 Map
 | 
			
		||||
     * key:渠道编号
 | 
			
		||||
     */
 | 
			
		||||
    private final ConcurrentMap<Long, AbstractPayClient<?>> channelIdClients = new ConcurrentHashMap<>();
 | 
			
		||||
    private final ConcurrentMap<Long, AbstractPayClient<?>> clients = new ConcurrentHashMap<>();
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public PayClient getPayClient(Long channelId) {
 | 
			
		||||
        AbstractPayClient<?> client = channelIdClients.get(channelId);
 | 
			
		||||
        AbstractPayClient<?> client = clients.get(channelId);
 | 
			
		||||
        if (client == null) {
 | 
			
		||||
            log.error("[getPayClient][渠道编号({}) 找不到客户端]", channelId);
 | 
			
		||||
        }
 | 
			
		||||
@@ -42,11 +42,11 @@ public class PayClientFactoryImpl implements PayClientFactory {
 | 
			
		||||
    @SuppressWarnings("unchecked")
 | 
			
		||||
    public <Config extends PayClientConfig> void createOrUpdatePayClient(Long channelId, String channelCode,
 | 
			
		||||
                                                                         Config config) {
 | 
			
		||||
        AbstractPayClient<Config> client = (AbstractPayClient<Config>) channelIdClients.get(channelId);
 | 
			
		||||
        AbstractPayClient<Config> client = (AbstractPayClient<Config>) clients.get(channelId);
 | 
			
		||||
        if (client == null) {
 | 
			
		||||
            client = this.createPayClient(channelId, channelCode, config);
 | 
			
		||||
            client.init();
 | 
			
		||||
            channelIdClients.put(client.getId(), client);
 | 
			
		||||
            clients.put(client.getId(), client);
 | 
			
		||||
        } else {
 | 
			
		||||
            client.refresh(config);
 | 
			
		||||
        }
 | 
			
		||||
@@ -69,7 +69,7 @@ public class PayClientFactoryImpl implements PayClientFactory {
 | 
			
		||||
            case ALIPAY_PC: return (AbstractPayClient<Config>) new AlipayQrPayClient(channelId, (AlipayPayClientConfig) config);
 | 
			
		||||
        }
 | 
			
		||||
        // 创建失败,错误日志 + 抛出异常
 | 
			
		||||
        log.error("[createSmsClient][配置({}) 找不到合适的客户端实现]", config);
 | 
			
		||||
        log.error("[createPayClient][配置({}) 找不到合适的客户端实现]", config);
 | 
			
		||||
        throw new IllegalArgumentException(String.format("配置(%s) 找不到合适的客户端实现", config));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -56,5 +56,4 @@ public enum PayChannelEnum {
 | 
			
		||||
        return ArrayUtil.firstMatch(o -> o.getCode().equals(code), values());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										78
									
								
								yudao-framework/yudao-spring-boot-starter-file/pom.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								yudao-framework/yudao-spring-boot-starter-file/pom.xml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,78 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
 | 
			
		||||
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 | 
			
		||||
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 | 
			
		||||
    <parent>
 | 
			
		||||
        <artifactId>yudao-framework</artifactId>
 | 
			
		||||
        <groupId>cn.iocoder.boot</groupId>
 | 
			
		||||
        <version>${revision}</version>
 | 
			
		||||
    </parent>
 | 
			
		||||
    <modelVersion>4.0.0</modelVersion>
 | 
			
		||||
    <artifactId>yudao-spring-boot-starter-file</artifactId>
 | 
			
		||||
 | 
			
		||||
    <name>${project.artifactId}</name>
 | 
			
		||||
    <description>文件客户端,支持多种存储器
 | 
			
		||||
        1. file:本地磁盘
 | 
			
		||||
        2. ftp:FTP 服务器
 | 
			
		||||
        2. sftp:SFTP 服务器
 | 
			
		||||
        4. db:数据库
 | 
			
		||||
        5. s3:支持 S3 协议的云存储服务,例如说 MinIO、阿里云、华为云、腾讯云、七牛云等等
 | 
			
		||||
    </description>
 | 
			
		||||
    <url>https://github.com/YunaiV/ruoyi-vue-pro</url>
 | 
			
		||||
 | 
			
		||||
    <dependencies>
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>cn.iocoder.boot</groupId>
 | 
			
		||||
            <artifactId>yudao-common</artifactId>
 | 
			
		||||
        </dependency>
 | 
			
		||||
 | 
			
		||||
        <!-- Spring 核心 -->
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>org.springframework.boot</groupId>
 | 
			
		||||
            <artifactId>spring-boot-starter</artifactId>
 | 
			
		||||
        </dependency>
 | 
			
		||||
 | 
			
		||||
        <!-- 工具类相关 -->
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>org.springframework.boot</groupId>
 | 
			
		||||
            <artifactId>spring-boot-starter-validation</artifactId>
 | 
			
		||||
        </dependency>
 | 
			
		||||
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>org.slf4j</groupId>
 | 
			
		||||
            <artifactId>slf4j-api</artifactId>
 | 
			
		||||
        </dependency>
 | 
			
		||||
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>com.fasterxml.jackson.core</groupId>
 | 
			
		||||
            <artifactId>jackson-databind</artifactId>
 | 
			
		||||
        </dependency>
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>com.fasterxml.jackson.core</groupId>
 | 
			
		||||
            <artifactId>jackson-core</artifactId>
 | 
			
		||||
        </dependency>
 | 
			
		||||
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>commons-net</groupId>
 | 
			
		||||
            <artifactId>commons-net</artifactId> <!-- 解决 ftp 连接 -->
 | 
			
		||||
        </dependency>
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>com.jcraft</groupId>
 | 
			
		||||
            <artifactId>jsch</artifactId> <!-- 解决 sftp 连接 -->
 | 
			
		||||
        </dependency>
 | 
			
		||||
 | 
			
		||||
        <!-- 三方云服务相关 -->
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>io.minio</groupId>
 | 
			
		||||
            <artifactId>minio</artifactId>
 | 
			
		||||
        </dependency>
 | 
			
		||||
 | 
			
		||||
        <!-- Test 测试相关 -->
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>cn.iocoder.boot</groupId>
 | 
			
		||||
            <artifactId>yudao-spring-boot-starter-test</artifactId>
 | 
			
		||||
            <scope>test</scope>
 | 
			
		||||
        </dependency>
 | 
			
		||||
    </dependencies>
 | 
			
		||||
 | 
			
		||||
</project>
 | 
			
		||||
@@ -0,0 +1,21 @@
 | 
			
		||||
package cn.iocoder.yudao.framework.file.config;
 | 
			
		||||
 | 
			
		||||
import cn.iocoder.yudao.framework.file.core.client.FileClientFactory;
 | 
			
		||||
import cn.iocoder.yudao.framework.file.core.client.FileClientFactoryImpl;
 | 
			
		||||
import org.springframework.context.annotation.Bean;
 | 
			
		||||
import org.springframework.context.annotation.Configuration;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 文件配置类
 | 
			
		||||
 *
 | 
			
		||||
 * @author 芋道源码
 | 
			
		||||
 */
 | 
			
		||||
@Configuration
 | 
			
		||||
public class YudaoFileAutoConfiguration {
 | 
			
		||||
 | 
			
		||||
    @Bean
 | 
			
		||||
    public FileClientFactory fileClientFactory() {
 | 
			
		||||
        return new FileClientFactoryImpl();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,69 @@
 | 
			
		||||
package cn.iocoder.yudao.framework.file.core.client;
 | 
			
		||||
 | 
			
		||||
import cn.hutool.core.util.StrUtil;
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 文件客户端的抽象类,提供模板方法,减少子类的冗余代码
 | 
			
		||||
 *
 | 
			
		||||
 * @author 芋道源码
 | 
			
		||||
 */
 | 
			
		||||
@Slf4j
 | 
			
		||||
public abstract class AbstractFileClient<Config extends FileClientConfig> implements FileClient {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 配置编号
 | 
			
		||||
     */
 | 
			
		||||
    private final Long id;
 | 
			
		||||
    /**
 | 
			
		||||
     * 文件配置
 | 
			
		||||
     */
 | 
			
		||||
    protected Config config;
 | 
			
		||||
 | 
			
		||||
    public AbstractFileClient(Long id, Config config) {
 | 
			
		||||
        this.id = id;
 | 
			
		||||
        this.config = config;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 初始化
 | 
			
		||||
     */
 | 
			
		||||
    public final void init() {
 | 
			
		||||
        doInit();
 | 
			
		||||
        log.info("[init][配置({}) 初始化完成]", config);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 自定义初始化
 | 
			
		||||
     */
 | 
			
		||||
    protected abstract void doInit();
 | 
			
		||||
 | 
			
		||||
    public final void refresh(Config config) {
 | 
			
		||||
        // 判断是否更新
 | 
			
		||||
        if (config.equals(this.config)) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        log.info("[refresh][配置({})发生变化,重新初始化]", config);
 | 
			
		||||
        this.config = config;
 | 
			
		||||
        // 初始化
 | 
			
		||||
        this.init();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public Long getId() {
 | 
			
		||||
        return id;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 格式化文件的 URL 访问地址
 | 
			
		||||
     * 使用场景:local、ftp、db,通过 FileController 的 getFile 来获取文件内容
 | 
			
		||||
     *
 | 
			
		||||
     * @param domain 自定义域名
 | 
			
		||||
     * @param path 文件路径
 | 
			
		||||
     * @return URL 访问地址
 | 
			
		||||
     */
 | 
			
		||||
    protected String formatFileUrl(String domain, String path) {
 | 
			
		||||
        return StrUtil.format("{}/admin-api/infra/file/{}/get/{}", domain, getId(), path);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,43 @@
 | 
			
		||||
package cn.iocoder.yudao.framework.file.core.client;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 文件客户端
 | 
			
		||||
 *
 | 
			
		||||
 * @author 芋道源码
 | 
			
		||||
 */
 | 
			
		||||
public interface FileClient {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获得客户端编号
 | 
			
		||||
     *
 | 
			
		||||
     * @return 客户端编号
 | 
			
		||||
     */
 | 
			
		||||
    Long getId();
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 上传文件
 | 
			
		||||
     *
 | 
			
		||||
     * @param content 文件流
 | 
			
		||||
     * @param path 相对路径
 | 
			
		||||
     * @return 完整路径,即 HTTP 访问地址
 | 
			
		||||
     * @throws Exception 上传文件时,抛出 Exception 异常
 | 
			
		||||
     */
 | 
			
		||||
    String upload(byte[] content, String path) throws  Exception;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 删除文件
 | 
			
		||||
     *
 | 
			
		||||
     * @param path 相对路径
 | 
			
		||||
     * @throws Exception 删除文件时,抛出 Exception 异常
 | 
			
		||||
     */
 | 
			
		||||
    void delete(String path) throws Exception;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获得文件的内容
 | 
			
		||||
     *
 | 
			
		||||
     * @param path 相对路径
 | 
			
		||||
     * @return 文件的内容
 | 
			
		||||
     */
 | 
			
		||||
    byte[] getContent(String path) throws Exception;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,16 @@
 | 
			
		||||
package cn.iocoder.yudao.framework.file.core.client;
 | 
			
		||||
 | 
			
		||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 文件客户端的配置
 | 
			
		||||
 * 不同实现的客户端,需要不同的配置,通过子类来定义
 | 
			
		||||
 *
 | 
			
		||||
 * @author 芋道源码
 | 
			
		||||
 */
 | 
			
		||||
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
 | 
			
		||||
// @JsonTypeInfo 注解的作用,Jackson 多态
 | 
			
		||||
// 1. 序列化到时数据库时,增加 @class 属性。
 | 
			
		||||
// 2. 反序列化到内存对象时,通过 @class 属性,可以创建出正确的类型
 | 
			
		||||
public interface FileClientConfig {
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,22 @@
 | 
			
		||||
package cn.iocoder.yudao.framework.file.core.client;
 | 
			
		||||
 | 
			
		||||
public interface FileClientFactory {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获得文件客户端
 | 
			
		||||
     *
 | 
			
		||||
     * @param configId 配置编号
 | 
			
		||||
     * @return 文件客户端
 | 
			
		||||
     */
 | 
			
		||||
    FileClient getFileClient(Long configId);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 创建文件客户端
 | 
			
		||||
     *
 | 
			
		||||
     * @param configId 配置编号
 | 
			
		||||
     * @param storage 存储器的枚举 {@link cn.iocoder.yudao.framework.file.core.enums.FileStorageEnum}
 | 
			
		||||
     * @param config 文件配置
 | 
			
		||||
     */
 | 
			
		||||
    <Config extends FileClientConfig> void createOrUpdateFileClient(Long configId, Integer storage, Config config);
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,56 @@
 | 
			
		||||
package cn.iocoder.yudao.framework.file.core.client;
 | 
			
		||||
 | 
			
		||||
import cn.hutool.core.lang.Assert;
 | 
			
		||||
import cn.hutool.core.util.ReflectUtil;
 | 
			
		||||
import cn.iocoder.yudao.framework.file.core.enums.FileStorageEnum;
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
 | 
			
		||||
import java.util.concurrent.ConcurrentHashMap;
 | 
			
		||||
import java.util.concurrent.ConcurrentMap;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 文件客户端的工厂实现类
 | 
			
		||||
 *
 | 
			
		||||
 * @author 芋道源码
 | 
			
		||||
 */
 | 
			
		||||
@Slf4j
 | 
			
		||||
public class FileClientFactoryImpl implements FileClientFactory {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 文件客户端 Map
 | 
			
		||||
     * key:配置编号
 | 
			
		||||
     */
 | 
			
		||||
    private final ConcurrentMap<Long, AbstractFileClient<?>> clients = new ConcurrentHashMap<>();
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public FileClient getFileClient(Long configId) {
 | 
			
		||||
        AbstractFileClient<?> client = clients.get(configId);
 | 
			
		||||
        if (client == null) {
 | 
			
		||||
            log.error("[getFileClient][配置编号({}) 找不到客户端]", configId);
 | 
			
		||||
        }
 | 
			
		||||
        return client;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    @SuppressWarnings("unchecked")
 | 
			
		||||
    public <Config extends FileClientConfig> void createOrUpdateFileClient(Long configId, Integer storage, Config config) {
 | 
			
		||||
        AbstractFileClient<Config> client = (AbstractFileClient<Config>) clients.get(configId);
 | 
			
		||||
        if (client == null) {
 | 
			
		||||
            client = this.createFileClient(configId, storage, config);
 | 
			
		||||
            client.init();
 | 
			
		||||
            clients.put(client.getId(), client);
 | 
			
		||||
        } else {
 | 
			
		||||
            client.refresh(config);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @SuppressWarnings("unchecked")
 | 
			
		||||
    private <Config extends FileClientConfig> AbstractFileClient<Config> createFileClient(
 | 
			
		||||
            Long configId, Integer storage, Config config) {
 | 
			
		||||
        FileStorageEnum storageEnum = FileStorageEnum.getByStorage(storage);
 | 
			
		||||
        Assert.notNull(storageEnum, String.format("文件配置(%s) 为空", storageEnum));
 | 
			
		||||
        // 创建客户端
 | 
			
		||||
        return (AbstractFileClient<Config>) ReflectUtil.newInstance(storageEnum.getClientClass(), configId, config);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,48 @@
 | 
			
		||||
package cn.iocoder.yudao.framework.file.core.client.db;
 | 
			
		||||
 | 
			
		||||
import cn.hutool.extra.spring.SpringUtil;
 | 
			
		||||
import cn.iocoder.yudao.framework.file.core.client.AbstractFileClient;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 基于 DB 存储的文件客户端的配置类
 | 
			
		||||
 *
 | 
			
		||||
 * @author 芋道源码
 | 
			
		||||
 */
 | 
			
		||||
public class DBFileClient extends AbstractFileClient<DBFileClientConfig> {
 | 
			
		||||
 | 
			
		||||
    private DBFileContentFrameworkDAO dao;
 | 
			
		||||
 | 
			
		||||
    public DBFileClient(Long id, DBFileClientConfig config) {
 | 
			
		||||
        super(id, config);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void doInit() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public String upload(byte[] content, String path) {
 | 
			
		||||
        getDao().insert(getId(), path, content);
 | 
			
		||||
        // 拼接返回路径
 | 
			
		||||
        return super.formatFileUrl(config.getDomain(), path);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void delete(String path) {
 | 
			
		||||
        getDao().delete(getId(), path);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public byte[] getContent(String path) {
 | 
			
		||||
        return getDao().selectContent(getId(), path);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private DBFileContentFrameworkDAO getDao() {
 | 
			
		||||
        // 延迟获取,因为 SpringUtil 初始化太慢
 | 
			
		||||
        if (dao == null) {
 | 
			
		||||
            dao = SpringUtil.getBean(DBFileContentFrameworkDAO.class);
 | 
			
		||||
        }
 | 
			
		||||
        return dao;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,24 @@
 | 
			
		||||
package cn.iocoder.yudao.framework.file.core.client.db;
 | 
			
		||||
 | 
			
		||||
import cn.iocoder.yudao.framework.file.core.client.FileClientConfig;
 | 
			
		||||
import lombok.Data;
 | 
			
		||||
import org.hibernate.validator.constraints.URL;
 | 
			
		||||
 | 
			
		||||
import javax.validation.constraints.NotEmpty;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 基于 DB 存储的文件客户端的配置类
 | 
			
		||||
 *
 | 
			
		||||
 * @author 芋道源码
 | 
			
		||||
 */
 | 
			
		||||
@Data
 | 
			
		||||
public class DBFileClientConfig implements FileClientConfig {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 自定义域名
 | 
			
		||||
     */
 | 
			
		||||
    @NotEmpty(message = "domain 不能为空")
 | 
			
		||||
    @URL(message = "domain 必须是 URL 格式")
 | 
			
		||||
    private String domain;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,36 @@
 | 
			
		||||
package cn.iocoder.yudao.framework.file.core.client.db;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 文件内容 Framework DAO 接口
 | 
			
		||||
 *
 | 
			
		||||
 * @author 芋道源码
 | 
			
		||||
 */
 | 
			
		||||
public interface DBFileContentFrameworkDAO {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 插入文件内容
 | 
			
		||||
     *
 | 
			
		||||
     * @param configId 配置编号
 | 
			
		||||
     * @param path 路径
 | 
			
		||||
     * @param content 内容
 | 
			
		||||
     */
 | 
			
		||||
    void insert(Long configId, String path, byte[] content);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 删除文件内容
 | 
			
		||||
     *
 | 
			
		||||
     * @param configId 配置编号
 | 
			
		||||
     * @param path 路径
 | 
			
		||||
     */
 | 
			
		||||
    void delete(Long configId, String path);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获得文件内容
 | 
			
		||||
     *
 | 
			
		||||
     * @param configId 配置编号
 | 
			
		||||
     * @param path 路径
 | 
			
		||||
     * @return 内容
 | 
			
		||||
     */
 | 
			
		||||
    byte[] selectContent(Long configId, String path);
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,73 @@
 | 
			
		||||
package cn.iocoder.yudao.framework.file.core.client.ftp;
 | 
			
		||||
 | 
			
		||||
import cn.hutool.core.io.FileUtil;
 | 
			
		||||
import cn.hutool.core.util.CharsetUtil;
 | 
			
		||||
import cn.hutool.core.util.StrUtil;
 | 
			
		||||
import cn.hutool.extra.ftp.Ftp;
 | 
			
		||||
import cn.hutool.extra.ftp.FtpException;
 | 
			
		||||
import cn.hutool.extra.ftp.FtpMode;
 | 
			
		||||
import cn.iocoder.yudao.framework.file.core.client.AbstractFileClient;
 | 
			
		||||
 | 
			
		||||
import java.io.ByteArrayInputStream;
 | 
			
		||||
import java.io.ByteArrayOutputStream;
 | 
			
		||||
import java.io.File;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Ftp 文件客户端
 | 
			
		||||
 *
 | 
			
		||||
 * @author 芋道源码
 | 
			
		||||
 */
 | 
			
		||||
public class FtpFileClient extends AbstractFileClient<FtpFileClientConfig> {
 | 
			
		||||
 | 
			
		||||
    private Ftp ftp;
 | 
			
		||||
 | 
			
		||||
    public FtpFileClient(Long id, FtpFileClientConfig config) {
 | 
			
		||||
        super(id, config);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void doInit() {
 | 
			
		||||
        // 补全风格。例如说 Linux 是 /,Windows 是 \
 | 
			
		||||
        if (!config.getBasePath().endsWith(File.separator)) {
 | 
			
		||||
            config.setBasePath(config.getBasePath() + File.separator);
 | 
			
		||||
        }
 | 
			
		||||
        // 初始化 Ftp 对象
 | 
			
		||||
        this.ftp = new Ftp(config.getHost(), config.getPort(), config.getUsername(), config.getPassword(),
 | 
			
		||||
                CharsetUtil.CHARSET_UTF_8, null, null, FtpMode.valueOf(config.getMode()));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public String upload(byte[] content, String path) {
 | 
			
		||||
        // 执行写入
 | 
			
		||||
        String filePath = getFilePath(path);
 | 
			
		||||
        String fileName = FileUtil.getName(filePath);
 | 
			
		||||
        String dir = StrUtil.removeSuffix(filePath, fileName);
 | 
			
		||||
        boolean success = ftp.upload(dir, fileName, new ByteArrayInputStream(content));
 | 
			
		||||
        if (!success) {
 | 
			
		||||
            throw new FtpException(StrUtil.format("上海文件到目标目录 ({}) 失败", filePath));
 | 
			
		||||
        }
 | 
			
		||||
        // 拼接返回路径
 | 
			
		||||
        return super.formatFileUrl(config.getDomain(), path);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void delete(String path) {
 | 
			
		||||
        String filePath = getFilePath(path);
 | 
			
		||||
        ftp.delFile(filePath);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public byte[] getContent(String path) {
 | 
			
		||||
        String filePath = getFilePath(path);
 | 
			
		||||
        String fileName = FileUtil.getName(filePath);
 | 
			
		||||
        String dir = StrUtil.removeSuffix(path, fileName);
 | 
			
		||||
        ByteArrayOutputStream out = new ByteArrayOutputStream();
 | 
			
		||||
        ftp.download(dir, fileName, out);
 | 
			
		||||
        return out.toByteArray();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private String getFilePath(String path) {
 | 
			
		||||
        return config.getBasePath() + path;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,59 @@
 | 
			
		||||
package cn.iocoder.yudao.framework.file.core.client.ftp;
 | 
			
		||||
 | 
			
		||||
import cn.iocoder.yudao.framework.file.core.client.FileClientConfig;
 | 
			
		||||
import lombok.Data;
 | 
			
		||||
import org.hibernate.validator.constraints.URL;
 | 
			
		||||
 | 
			
		||||
import javax.validation.constraints.NotEmpty;
 | 
			
		||||
import javax.validation.constraints.NotNull;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Ftp 文件客户端的配置类
 | 
			
		||||
 *
 | 
			
		||||
 * @author 芋道源码
 | 
			
		||||
 */
 | 
			
		||||
@Data
 | 
			
		||||
public class FtpFileClientConfig implements FileClientConfig {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 基础路径
 | 
			
		||||
     */
 | 
			
		||||
    @NotEmpty(message = "基础路径不能为空")
 | 
			
		||||
    private String basePath;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 自定义域名
 | 
			
		||||
     */
 | 
			
		||||
    @NotEmpty(message = "domain 不能为空")
 | 
			
		||||
    @URL(message = "domain 必须是 URL 格式")
 | 
			
		||||
    private String domain;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 主机地址
 | 
			
		||||
     */
 | 
			
		||||
    @NotEmpty(message = "host 不能为空")
 | 
			
		||||
    private String host;
 | 
			
		||||
    /**
 | 
			
		||||
     * 主机端口
 | 
			
		||||
     */
 | 
			
		||||
    @NotNull(message = "port 不能为空")
 | 
			
		||||
    private Integer port;
 | 
			
		||||
    /**
 | 
			
		||||
     * 用户名
 | 
			
		||||
     */
 | 
			
		||||
    @NotEmpty(message = "用户名不能为空")
 | 
			
		||||
    private String username;
 | 
			
		||||
    /**
 | 
			
		||||
     * 密码
 | 
			
		||||
     */
 | 
			
		||||
    @NotEmpty(message = "密码不能为空")
 | 
			
		||||
    private String password;
 | 
			
		||||
    /**
 | 
			
		||||
     * 连接模式
 | 
			
		||||
     *
 | 
			
		||||
     * 使用 {@link  cn.hutool.extra.ftp.FtpMode} 对应的字符串
 | 
			
		||||
     */
 | 
			
		||||
    @NotEmpty(message = "连接模式不能为空")
 | 
			
		||||
    private String mode;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,52 @@
 | 
			
		||||
package cn.iocoder.yudao.framework.file.core.client.local;
 | 
			
		||||
 | 
			
		||||
import cn.hutool.core.io.FileUtil;
 | 
			
		||||
import cn.iocoder.yudao.framework.file.core.client.AbstractFileClient;
 | 
			
		||||
 | 
			
		||||
import java.io.File;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 本地文件客户端
 | 
			
		||||
 *
 | 
			
		||||
 * @author 芋道源码
 | 
			
		||||
 */
 | 
			
		||||
public class LocalFileClient extends AbstractFileClient<LocalFileClientConfig> {
 | 
			
		||||
 | 
			
		||||
    public LocalFileClient(Long id, LocalFileClientConfig config) {
 | 
			
		||||
        super(id, config);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void doInit() {
 | 
			
		||||
        // 补全风格。例如说 Linux 是 /,Windows 是 \
 | 
			
		||||
        if (!config.getBasePath().endsWith(File.separator)) {
 | 
			
		||||
            config.setBasePath(config.getBasePath() + File.separator);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public String upload(byte[] content, String path) {
 | 
			
		||||
        // 执行写入
 | 
			
		||||
        String filePath = getFilePath(path);
 | 
			
		||||
        FileUtil.writeBytes(content, filePath);
 | 
			
		||||
        // 拼接返回路径
 | 
			
		||||
        return super.formatFileUrl(config.getDomain(), path);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void delete(String path) {
 | 
			
		||||
        String filePath = getFilePath(path);
 | 
			
		||||
        FileUtil.del(filePath);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public byte[] getContent(String path) {
 | 
			
		||||
        String filePath = getFilePath(path);
 | 
			
		||||
        return FileUtil.readBytes(filePath);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private String getFilePath(String path) {
 | 
			
		||||
        return config.getBasePath() + path;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,30 @@
 | 
			
		||||
package cn.iocoder.yudao.framework.file.core.client.local;
 | 
			
		||||
 | 
			
		||||
import cn.iocoder.yudao.framework.file.core.client.FileClientConfig;
 | 
			
		||||
import lombok.Data;
 | 
			
		||||
import org.hibernate.validator.constraints.URL;
 | 
			
		||||
 | 
			
		||||
import javax.validation.constraints.NotEmpty;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 本地文件客户端的配置类
 | 
			
		||||
 *
 | 
			
		||||
 * @author 芋道源码
 | 
			
		||||
 */
 | 
			
		||||
@Data
 | 
			
		||||
public class LocalFileClientConfig implements FileClientConfig {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 基础路径
 | 
			
		||||
     */
 | 
			
		||||
    @NotEmpty(message = "基础路径不能为空")
 | 
			
		||||
    private String basePath;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 自定义域名
 | 
			
		||||
     */
 | 
			
		||||
    @NotEmpty(message = "domain 不能为空")
 | 
			
		||||
    @URL(message = "domain 必须是 URL 格式")
 | 
			
		||||
    private String domain;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,112 @@
 | 
			
		||||
package cn.iocoder.yudao.framework.file.core.client.s3;
 | 
			
		||||
 | 
			
		||||
import cn.hutool.core.io.IoUtil;
 | 
			
		||||
import cn.hutool.core.util.StrUtil;
 | 
			
		||||
import cn.hutool.http.HttpUtil;
 | 
			
		||||
import cn.iocoder.yudao.framework.file.core.client.AbstractFileClient;
 | 
			
		||||
import io.minio.*;
 | 
			
		||||
 | 
			
		||||
import java.io.ByteArrayInputStream;
 | 
			
		||||
 | 
			
		||||
import static cn.iocoder.yudao.framework.file.core.client.s3.S3FileClientConfig.ENDPOINT_ALIYUN;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 基于 S3 协议的文件客户端,实现 MinIO、阿里云、腾讯云、七牛云、华为云等云服务
 | 
			
		||||
 *
 | 
			
		||||
 * S3 协议的客户端,采用亚马逊提供的 software.amazon.awssdk.s3 库
 | 
			
		||||
 *
 | 
			
		||||
 * @author 芋道源码
 | 
			
		||||
 */
 | 
			
		||||
public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
 | 
			
		||||
 | 
			
		||||
    private MinioClient client;
 | 
			
		||||
 | 
			
		||||
    public S3FileClient(Long id, S3FileClientConfig config) {
 | 
			
		||||
        super(id, config);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void doInit() {
 | 
			
		||||
        // 补全 domain
 | 
			
		||||
        if (StrUtil.isEmpty(config.getDomain())) {
 | 
			
		||||
            config.setDomain(buildDomain());
 | 
			
		||||
        }
 | 
			
		||||
        // 初始化客户端
 | 
			
		||||
        client = MinioClient.builder()
 | 
			
		||||
                .endpoint(buildEndpointURL()) // Endpoint URL
 | 
			
		||||
                .region(buildRegion()) // Region
 | 
			
		||||
                .credentials(config.getAccessKey(), config.getAccessSecret()) // 认证密钥
 | 
			
		||||
                .build();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 基于 endpoint 构建调用云服务的 URL 地址
 | 
			
		||||
     *
 | 
			
		||||
     * @return URI 地址
 | 
			
		||||
     */
 | 
			
		||||
    private String buildEndpointURL() {
 | 
			
		||||
        // 如果已经是 http 或者 https,则不进行拼接.主要适配 MinIO
 | 
			
		||||
        if (HttpUtil.isHttp(config.getEndpoint()) || HttpUtil.isHttps(config.getEndpoint())) {
 | 
			
		||||
            return config.getEndpoint();
 | 
			
		||||
        }
 | 
			
		||||
        return StrUtil.format("https://{}", config.getEndpoint());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 基于 bucket + endpoint 构建访问的 Domain 地址
 | 
			
		||||
     *
 | 
			
		||||
     * @return Domain 地址
 | 
			
		||||
     */
 | 
			
		||||
    private String buildDomain() {
 | 
			
		||||
        // 如果已经是 http 或者 https,则不进行拼接.主要适配 MinIO
 | 
			
		||||
        if (HttpUtil.isHttp(config.getEndpoint()) || HttpUtil.isHttps(config.getEndpoint())) {
 | 
			
		||||
            return StrUtil.format("{}/{}", config.getEndpoint(), config.getBucket());
 | 
			
		||||
        }
 | 
			
		||||
        // 阿里云、腾讯云、华为云都适合。七牛云比较特殊,必须有自定义域名
 | 
			
		||||
        return StrUtil.format("https://{}.{}", config.getBucket(), config.getEndpoint());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 基于 bucket 构建 region 地区
 | 
			
		||||
     *
 | 
			
		||||
     * @return region 地区
 | 
			
		||||
     */
 | 
			
		||||
    private String buildRegion() {
 | 
			
		||||
        // 阿里云必须有 region,否则会报错
 | 
			
		||||
        if (config.getEndpoint().contains(ENDPOINT_ALIYUN)) {
 | 
			
		||||
            return StrUtil.subBefore(config.getEndpoint(), '.', false)
 | 
			
		||||
                    .replaceAll("-internal", ""); // 去除内网 Endpoint 的后缀
 | 
			
		||||
        }
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public String upload(byte[] content, String path) throws Exception {
 | 
			
		||||
        // 执行上传
 | 
			
		||||
        client.putObject(PutObjectArgs.builder()
 | 
			
		||||
                .bucket(config.getBucket()) // bucket 必须传递
 | 
			
		||||
                .object(path) // 相对路径作为 key
 | 
			
		||||
                .stream(new ByteArrayInputStream(content), content.length, -1) // 文件内容
 | 
			
		||||
                .build());
 | 
			
		||||
        // 拼接返回路径
 | 
			
		||||
        return config.getDomain() + "/" + path;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void delete(String path) throws Exception {
 | 
			
		||||
        client.removeObject(RemoveObjectArgs.builder()
 | 
			
		||||
                .bucket(config.getBucket()) // bucket 必须传递
 | 
			
		||||
                .object(path) // 相对路径作为 key
 | 
			
		||||
                .build());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public byte[] getContent(String path) throws Exception {
 | 
			
		||||
        GetObjectResponse response = client.getObject(GetObjectArgs.builder()
 | 
			
		||||
                .bucket(config.getBucket()) // bucket 必须传递
 | 
			
		||||
                .object(path) // 相对路径作为 key
 | 
			
		||||
                .build());
 | 
			
		||||
        return IoUtil.readBytes(response);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,76 @@
 | 
			
		||||
package cn.iocoder.yudao.framework.file.core.client.s3;
 | 
			
		||||
 | 
			
		||||
import cn.hutool.core.util.StrUtil;
 | 
			
		||||
import cn.iocoder.yudao.framework.file.core.client.FileClientConfig;
 | 
			
		||||
import com.fasterxml.jackson.annotation.JsonIgnore;
 | 
			
		||||
import lombok.Data;
 | 
			
		||||
import org.hibernate.validator.constraints.URL;
 | 
			
		||||
 | 
			
		||||
import javax.validation.constraints.AssertTrue;
 | 
			
		||||
import javax.validation.constraints.NotNull;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * S3 文件客户端的配置类
 | 
			
		||||
 *
 | 
			
		||||
 * @author 芋道源码
 | 
			
		||||
 */
 | 
			
		||||
@Data
 | 
			
		||||
public class S3FileClientConfig implements FileClientConfig {
 | 
			
		||||
 | 
			
		||||
    public static final String ENDPOINT_QINIU = "qiniucs.com";
 | 
			
		||||
    public static final String ENDPOINT_ALIYUN = "aliyuncs.com";
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 节点地址
 | 
			
		||||
     * 1. MinIO:https://www.iocoder.cn/Spring-Boot/MinIO 。例如说,http://127.0.0.1:9000
 | 
			
		||||
     * 2. 阿里云:https://help.aliyun.com/document_detail/31837.html
 | 
			
		||||
     * 3. 腾讯云:https://cloud.tencent.com/document/product/436/6224
 | 
			
		||||
     * 4. 七牛云:https://developer.qiniu.com/kodo/4088/s3-access-domainname
 | 
			
		||||
     * 5. 华为云:https://developer.huaweicloud.com/endpoint?OBS
 | 
			
		||||
     */
 | 
			
		||||
    @NotNull(message = "endpoint 不能为空")
 | 
			
		||||
    private String endpoint;
 | 
			
		||||
    /**
 | 
			
		||||
     * 自定义域名
 | 
			
		||||
     * 1. MinIO:通过 Nginx 配置
 | 
			
		||||
     * 2. 阿里云:https://help.aliyun.com/document_detail/31836.html
 | 
			
		||||
     * 3. 腾讯云:https://cloud.tencent.com/document/product/436/11142
 | 
			
		||||
     * 4. 七牛云:https://developer.qiniu.com/kodo/8556/set-the-custom-source-domain-name
 | 
			
		||||
     * 5. 华为云:https://support.huaweicloud.com/usermanual-obs/obs_03_0032.html
 | 
			
		||||
     */
 | 
			
		||||
    @URL(message = "domain 必须是 URL 格式")
 | 
			
		||||
    private String domain;
 | 
			
		||||
    /**
 | 
			
		||||
     * 存储 Bucket
 | 
			
		||||
     */
 | 
			
		||||
    @NotNull(message = "bucket 不能为空")
 | 
			
		||||
    private String bucket;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 访问 Key
 | 
			
		||||
     * 1. MinIO:https://www.iocoder.cn/Spring-Boot/MinIO
 | 
			
		||||
     * 2. 阿里云:https://ram.console.aliyun.com/manage/ak
 | 
			
		||||
     * 3. 腾讯云:https://console.cloud.tencent.com/cam/capi
 | 
			
		||||
     * 4. 七牛云:https://portal.qiniu.com/user/key
 | 
			
		||||
     * 5. 华为云:https://support.huaweicloud.com/qs-obs/obs_qs_0005.html
 | 
			
		||||
     */
 | 
			
		||||
    @NotNull(message = "accessKey 不能为空")
 | 
			
		||||
    private String accessKey;
 | 
			
		||||
    /**
 | 
			
		||||
     * 访问 Secret
 | 
			
		||||
     */
 | 
			
		||||
    @NotNull(message = "accessSecret 不能为空")
 | 
			
		||||
    private String accessSecret;
 | 
			
		||||
 | 
			
		||||
    @SuppressWarnings("RedundantIfStatement")
 | 
			
		||||
    @AssertTrue(message = "domain 不能为空")
 | 
			
		||||
    @JsonIgnore
 | 
			
		||||
    public boolean isDomainValid() {
 | 
			
		||||
        // 如果是七牛,必须带有 domain
 | 
			
		||||
        if (StrUtil.contains(endpoint, ENDPOINT_QINIU) && StrUtil.isEmpty(domain)) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,61 @@
 | 
			
		||||
package cn.iocoder.yudao.framework.file.core.client.sftp;
 | 
			
		||||
 | 
			
		||||
import cn.hutool.core.io.FileUtil;
 | 
			
		||||
import cn.hutool.extra.ssh.Sftp;
 | 
			
		||||
import cn.iocoder.yudao.framework.common.util.io.FileUtils;
 | 
			
		||||
import cn.iocoder.yudao.framework.file.core.client.AbstractFileClient;
 | 
			
		||||
 | 
			
		||||
import java.io.File;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Sftp 文件客户端
 | 
			
		||||
 *
 | 
			
		||||
 * @author 芋道源码
 | 
			
		||||
 */
 | 
			
		||||
public class SftpFileClient extends AbstractFileClient<SftpFileClientConfig> {
 | 
			
		||||
 | 
			
		||||
    private Sftp sftp;
 | 
			
		||||
 | 
			
		||||
    public SftpFileClient(Long id, SftpFileClientConfig config) {
 | 
			
		||||
        super(id, config);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void doInit() {
 | 
			
		||||
        // 补全风格。例如说 Linux 是 /,Windows 是 \
 | 
			
		||||
        if (!config.getBasePath().endsWith(File.separator)) {
 | 
			
		||||
            config.setBasePath(config.getBasePath() + File.separator);
 | 
			
		||||
        }
 | 
			
		||||
        // 初始化 Ftp 对象
 | 
			
		||||
        this.sftp = new Sftp(config.getHost(), config.getPort(), config.getUsername(), config.getPassword());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public String upload(byte[] content, String path) {
 | 
			
		||||
        // 执行写入
 | 
			
		||||
        String filePath = getFilePath(path);
 | 
			
		||||
        File file = FileUtils.createTempFile(content);
 | 
			
		||||
        sftp.upload(filePath, file);
 | 
			
		||||
        // 拼接返回路径
 | 
			
		||||
        return super.formatFileUrl(config.getDomain(), path);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void delete(String path) {
 | 
			
		||||
        String filePath = getFilePath(path);
 | 
			
		||||
        sftp.delFile(filePath);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public byte[] getContent(String path) {
 | 
			
		||||
        String filePath = getFilePath(path);
 | 
			
		||||
        File destFile = FileUtils.createTempFile();
 | 
			
		||||
        sftp.download(filePath, destFile);
 | 
			
		||||
        return FileUtil.readBytes(destFile);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private String getFilePath(String path) {
 | 
			
		||||
        return config.getBasePath() + path;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,52 @@
 | 
			
		||||
package cn.iocoder.yudao.framework.file.core.client.sftp;
 | 
			
		||||
 | 
			
		||||
import cn.iocoder.yudao.framework.file.core.client.FileClientConfig;
 | 
			
		||||
import lombok.Data;
 | 
			
		||||
import org.hibernate.validator.constraints.URL;
 | 
			
		||||
 | 
			
		||||
import javax.validation.constraints.NotEmpty;
 | 
			
		||||
import javax.validation.constraints.NotNull;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Sftp 文件客户端的配置类
 | 
			
		||||
 *
 | 
			
		||||
 * @author 芋道源码
 | 
			
		||||
 */
 | 
			
		||||
@Data
 | 
			
		||||
public class SftpFileClientConfig implements FileClientConfig {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 基础路径
 | 
			
		||||
     */
 | 
			
		||||
    @NotEmpty(message = "基础路径不能为空")
 | 
			
		||||
    private String basePath;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 自定义域名
 | 
			
		||||
     */
 | 
			
		||||
    @NotEmpty(message = "domain 不能为空")
 | 
			
		||||
    @URL(message = "domain 必须是 URL 格式")
 | 
			
		||||
    private String domain;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 主机地址
 | 
			
		||||
     */
 | 
			
		||||
    @NotEmpty(message = "host 不能为空")
 | 
			
		||||
    private String host;
 | 
			
		||||
    /**
 | 
			
		||||
     * 主机端口
 | 
			
		||||
     */
 | 
			
		||||
    @NotNull(message = "port 不能为空")
 | 
			
		||||
    private Integer port;
 | 
			
		||||
    /**
 | 
			
		||||
     * 用户名
 | 
			
		||||
     */
 | 
			
		||||
    @NotEmpty(message = "用户名不能为空")
 | 
			
		||||
    private String username;
 | 
			
		||||
    /**
 | 
			
		||||
     * 密码
 | 
			
		||||
     */
 | 
			
		||||
    @NotEmpty(message = "密码不能为空")
 | 
			
		||||
    private String password;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,55 @@
 | 
			
		||||
package cn.iocoder.yudao.framework.file.core.enums;
 | 
			
		||||
 | 
			
		||||
import cn.hutool.core.util.ArrayUtil;
 | 
			
		||||
import cn.iocoder.yudao.framework.file.core.client.FileClient;
 | 
			
		||||
import cn.iocoder.yudao.framework.file.core.client.FileClientConfig;
 | 
			
		||||
import cn.iocoder.yudao.framework.file.core.client.db.DBFileClient;
 | 
			
		||||
import cn.iocoder.yudao.framework.file.core.client.db.DBFileClientConfig;
 | 
			
		||||
import cn.iocoder.yudao.framework.file.core.client.ftp.FtpFileClient;
 | 
			
		||||
import cn.iocoder.yudao.framework.file.core.client.ftp.FtpFileClientConfig;
 | 
			
		||||
import cn.iocoder.yudao.framework.file.core.client.local.LocalFileClient;
 | 
			
		||||
import cn.iocoder.yudao.framework.file.core.client.local.LocalFileClientConfig;
 | 
			
		||||
import cn.iocoder.yudao.framework.file.core.client.s3.S3FileClient;
 | 
			
		||||
import cn.iocoder.yudao.framework.file.core.client.s3.S3FileClientConfig;
 | 
			
		||||
import cn.iocoder.yudao.framework.file.core.client.sftp.SftpFileClient;
 | 
			
		||||
import cn.iocoder.yudao.framework.file.core.client.sftp.SftpFileClientConfig;
 | 
			
		||||
import lombok.AllArgsConstructor;
 | 
			
		||||
import lombok.Getter;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 文件存储器枚举
 | 
			
		||||
 *
 | 
			
		||||
 * @author 芋道源码
 | 
			
		||||
 */
 | 
			
		||||
@AllArgsConstructor
 | 
			
		||||
@Getter
 | 
			
		||||
public enum FileStorageEnum {
 | 
			
		||||
 | 
			
		||||
    DB(1, DBFileClientConfig.class, DBFileClient.class),
 | 
			
		||||
 | 
			
		||||
    LOCAL(10, LocalFileClientConfig.class, LocalFileClient.class),
 | 
			
		||||
    FTP(11, FtpFileClientConfig.class, FtpFileClient.class),
 | 
			
		||||
    SFTP(12, SftpFileClientConfig.class, SftpFileClient.class),
 | 
			
		||||
 | 
			
		||||
    S3(20, S3FileClientConfig.class, S3FileClient.class),
 | 
			
		||||
    ;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 存储器
 | 
			
		||||
     */
 | 
			
		||||
    private final Integer storage;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 配置类
 | 
			
		||||
     */
 | 
			
		||||
    private final Class<? extends FileClientConfig> configClass;
 | 
			
		||||
    /**
 | 
			
		||||
     * 客户端类
 | 
			
		||||
     */
 | 
			
		||||
    private final Class<? extends FileClient> clientClass;
 | 
			
		||||
 | 
			
		||||
    public static FileStorageEnum getByStorage(Integer storage) {
 | 
			
		||||
        return ArrayUtil.firstMatch(o -> o.getStorage().equals(storage), values());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,2 @@
 | 
			
		||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
 | 
			
		||||
  cn.iocoder.yudao.framework.file.config.YudaoFileAutoConfiguration
 | 
			
		||||
@@ -0,0 +1,4 @@
 | 
			
		||||
/**
 | 
			
		||||
 * 占位,避免 package 无法提交到 Git 仓库
 | 
			
		||||
 */
 | 
			
		||||
package cn.iocoder.yudao.framework.file.config;
 | 
			
		||||
@@ -0,0 +1,39 @@
 | 
			
		||||
package cn.iocoder.yudao.framework.file.core.client.ftp;
 | 
			
		||||
 | 
			
		||||
import cn.hutool.core.io.resource.ResourceUtil;
 | 
			
		||||
import cn.hutool.core.util.IdUtil;
 | 
			
		||||
import cn.hutool.extra.ftp.FtpMode;
 | 
			
		||||
import org.junit.jupiter.api.Disabled;
 | 
			
		||||
import org.junit.jupiter.api.Test;
 | 
			
		||||
 | 
			
		||||
public class FtpFileClientTest {
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    @Disabled
 | 
			
		||||
    public void test() {
 | 
			
		||||
        // 创建客户端
 | 
			
		||||
        FtpFileClientConfig config = new FtpFileClientConfig();
 | 
			
		||||
        config.setDomain("http://127.0.0.1:48080");
 | 
			
		||||
        config.setBasePath("/home/ftp");
 | 
			
		||||
        config.setHost("kanchai.club");
 | 
			
		||||
        config.setPort(221);
 | 
			
		||||
        config.setUsername("");
 | 
			
		||||
        config.setPassword("");
 | 
			
		||||
        config.setMode(FtpMode.Passive.name());
 | 
			
		||||
        FtpFileClient client = new FtpFileClient(0L, config);
 | 
			
		||||
        client.init();
 | 
			
		||||
        // 上传文件
 | 
			
		||||
        String path = IdUtil.fastSimpleUUID() + ".jpg";
 | 
			
		||||
        byte[] content = ResourceUtil.readBytes("file/erweima.jpg");
 | 
			
		||||
        String fullPath = client.upload(content, path);
 | 
			
		||||
        System.out.println("访问地址:" + fullPath);
 | 
			
		||||
        if (false) {
 | 
			
		||||
            byte[] bytes = client.getContent(path);
 | 
			
		||||
            System.out.println("文件内容:" + bytes);
 | 
			
		||||
        }
 | 
			
		||||
        if (false) {
 | 
			
		||||
            client.delete(path);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,27 @@
 | 
			
		||||
package cn.iocoder.yudao.framework.file.core.client.local;
 | 
			
		||||
 | 
			
		||||
import cn.hutool.core.io.resource.ResourceUtil;
 | 
			
		||||
import cn.hutool.core.util.IdUtil;
 | 
			
		||||
import org.junit.jupiter.api.Disabled;
 | 
			
		||||
import org.junit.jupiter.api.Test;
 | 
			
		||||
 | 
			
		||||
public class LocalFileClientTest {
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    @Disabled
 | 
			
		||||
    public void test() {
 | 
			
		||||
        // 创建客户端
 | 
			
		||||
        LocalFileClientConfig config = new LocalFileClientConfig();
 | 
			
		||||
        config.setDomain("http://127.0.0.1:48080");
 | 
			
		||||
        config.setBasePath("/Users/yunai/file_test");
 | 
			
		||||
        LocalFileClient client = new LocalFileClient(0L, config);
 | 
			
		||||
        client.init();
 | 
			
		||||
        // 上传文件
 | 
			
		||||
        String path = IdUtil.fastSimpleUUID() + ".jpg";
 | 
			
		||||
        byte[] content = ResourceUtil.readBytes("file/erweima.jpg");
 | 
			
		||||
        String fullPath = client.upload(content, path);
 | 
			
		||||
        System.out.println("访问地址:" + fullPath);
 | 
			
		||||
        client.delete(path);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,117 @@
 | 
			
		||||
package cn.iocoder.yudao.framework.file.core.client.s3;
 | 
			
		||||
 | 
			
		||||
import cn.hutool.core.io.resource.ResourceUtil;
 | 
			
		||||
import cn.hutool.core.util.IdUtil;
 | 
			
		||||
import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
 | 
			
		||||
import org.junit.jupiter.api.Disabled;
 | 
			
		||||
import org.junit.jupiter.api.Test;
 | 
			
		||||
 | 
			
		||||
import javax.validation.Validation;
 | 
			
		||||
 | 
			
		||||
public class S3FileClientTest {
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    @Disabled // MinIO,如果要集成测试,可以注释本行
 | 
			
		||||
    public void testMinIO() throws Exception {
 | 
			
		||||
        S3FileClientConfig config = new S3FileClientConfig();
 | 
			
		||||
        // 配置成你自己的
 | 
			
		||||
        config.setAccessKey("admin");
 | 
			
		||||
        config.setAccessSecret("password");
 | 
			
		||||
        config.setBucket("yudaoyuanma");
 | 
			
		||||
        config.setDomain(null);
 | 
			
		||||
        // 默认 9000 endpoint
 | 
			
		||||
        config.setEndpoint("http://127.0.0.1:9000");
 | 
			
		||||
 | 
			
		||||
        // 执行上传
 | 
			
		||||
        testExecuteUpload(config);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    @Disabled // 阿里云 OSS,如果要集成测试,可以注释本行
 | 
			
		||||
    public void testAliyun() throws Exception {
 | 
			
		||||
        S3FileClientConfig config = new S3FileClientConfig();
 | 
			
		||||
        // 配置成你自己的
 | 
			
		||||
        config.setAccessKey(System.getenv("ALIYUN_ACCESS_KEY"));
 | 
			
		||||
        config.setAccessSecret(System.getenv("ALIYUN_SECRET_KEY"));
 | 
			
		||||
        config.setBucket("yunai-aoteman");
 | 
			
		||||
        config.setDomain(null); // 如果有自定义域名,则可以设置。http://ali-oss.iocoder.cn
 | 
			
		||||
        // 默认北京的 endpoint
 | 
			
		||||
        config.setEndpoint("oss-cn-beijing.aliyuncs.com");
 | 
			
		||||
 | 
			
		||||
        // 执行上传
 | 
			
		||||
        testExecuteUpload(config);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    @Disabled // 腾讯云 COS,如果要集成测试,可以注释本行
 | 
			
		||||
    public void testQCloud() throws Exception {
 | 
			
		||||
        S3FileClientConfig config = new S3FileClientConfig();
 | 
			
		||||
        // 配置成你自己的
 | 
			
		||||
        config.setAccessKey(System.getenv("QCLOUD_ACCESS_KEY"));
 | 
			
		||||
        config.setAccessSecret(System.getenv("QCLOUD_SECRET_KEY"));
 | 
			
		||||
        config.setBucket("aoteman-1255880240");
 | 
			
		||||
        config.setDomain(null); // 如果有自定义域名,则可以设置。http://tengxun-oss.iocoder.cn
 | 
			
		||||
        // 默认上海的 endpoint
 | 
			
		||||
        config.setEndpoint("cos.ap-shanghai.myqcloud.com");
 | 
			
		||||
 | 
			
		||||
        // 执行上传
 | 
			
		||||
        testExecuteUpload(config);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    @Disabled // 七牛云存储,如果要集成测试,可以注释本行
 | 
			
		||||
    public void testQiniu() throws Exception {
 | 
			
		||||
        S3FileClientConfig config = new S3FileClientConfig();
 | 
			
		||||
        // 配置成你自己的
 | 
			
		||||
//        config.setAccessKey(System.getenv("QINIU_ACCESS_KEY"));
 | 
			
		||||
//        config.setAccessSecret(System.getenv("QINIU_SECRET_KEY"));
 | 
			
		||||
        config.setAccessKey("b7yvuhBSAGjmtPhMFcn9iMOxUOY_I06cA_p0ZUx8");
 | 
			
		||||
        config.setAccessSecret("kXM1l5ia1RvSX3QaOEcwI3RLz3Y2rmNszWonKZtP");
 | 
			
		||||
        config.setBucket("ruoyi-vue-pro");
 | 
			
		||||
        config.setDomain("http://test.yudao.iocoder.cn"); // 如果有自定义域名,则可以设置。http://static.yudao.iocoder.cn
 | 
			
		||||
        // 默认上海的 endpoint
 | 
			
		||||
        config.setEndpoint("s3-cn-south-1.qiniucs.com");
 | 
			
		||||
 | 
			
		||||
        // 执行上传
 | 
			
		||||
        testExecuteUpload(config);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    @Disabled // 华为云存储,如果要集成测试,可以注释本行
 | 
			
		||||
    public void testHuaweiCloud() throws Exception {
 | 
			
		||||
        S3FileClientConfig config = new S3FileClientConfig();
 | 
			
		||||
        // 配置成你自己的
 | 
			
		||||
//        config.setAccessKey(System.getenv("HUAWEI_CLOUD_ACCESS_KEY"));
 | 
			
		||||
//        config.setAccessSecret(System.getenv("HUAWEI_CLOUD_SECRET_KEY"));
 | 
			
		||||
        config.setBucket("yudao");
 | 
			
		||||
        config.setDomain(null); // 如果有自定义域名,则可以设置。
 | 
			
		||||
        // 默认上海的 endpoint
 | 
			
		||||
        config.setEndpoint("obs.cn-east-3.myhuaweicloud.com");
 | 
			
		||||
 | 
			
		||||
        // 执行上传
 | 
			
		||||
        testExecuteUpload(config);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void testExecuteUpload(S3FileClientConfig config) throws Exception {
 | 
			
		||||
        // 校验配置
 | 
			
		||||
        ValidationUtils.validate(Validation.buildDefaultValidatorFactory().getValidator(), config);
 | 
			
		||||
        // 创建 Client
 | 
			
		||||
        S3FileClient client = new S3FileClient(0L, config);
 | 
			
		||||
        client.init();
 | 
			
		||||
        // 上传文件
 | 
			
		||||
        String path = IdUtil.fastSimpleUUID() + ".jpg";
 | 
			
		||||
        byte[] content = ResourceUtil.readBytes("file/erweima.jpg");
 | 
			
		||||
        String fullPath = client.upload(content, path);
 | 
			
		||||
        System.out.println("访问地址:" + fullPath);
 | 
			
		||||
        // 读取文件
 | 
			
		||||
        if (true) {
 | 
			
		||||
            byte[] bytes = client.getContent(path);
 | 
			
		||||
            System.out.println("文件内容:" + bytes.length);
 | 
			
		||||
        }
 | 
			
		||||
        // 删除文件
 | 
			
		||||
        if (false) {
 | 
			
		||||
            client.delete(path);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,37 @@
 | 
			
		||||
package cn.iocoder.yudao.framework.file.core.client.sftp;
 | 
			
		||||
 | 
			
		||||
import cn.hutool.core.io.resource.ResourceUtil;
 | 
			
		||||
import cn.hutool.core.util.IdUtil;
 | 
			
		||||
import org.junit.jupiter.api.Disabled;
 | 
			
		||||
import org.junit.jupiter.api.Test;
 | 
			
		||||
 | 
			
		||||
public class SftpFileClientTest {
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    @Disabled
 | 
			
		||||
    public void test() {
 | 
			
		||||
        // 创建客户端
 | 
			
		||||
        SftpFileClientConfig config = new SftpFileClientConfig();
 | 
			
		||||
        config.setDomain("http://127.0.0.1:48080");
 | 
			
		||||
        config.setBasePath("/home/ftp");
 | 
			
		||||
        config.setHost("kanchai.club");
 | 
			
		||||
        config.setPort(222);
 | 
			
		||||
        config.setUsername("");
 | 
			
		||||
        config.setPassword("");
 | 
			
		||||
        SftpFileClient client = new SftpFileClient(0L, config);
 | 
			
		||||
        client.init();
 | 
			
		||||
        // 上传文件
 | 
			
		||||
        String path = IdUtil.fastSimpleUUID() + ".jpg";
 | 
			
		||||
        byte[] content = ResourceUtil.readBytes("file/erweima.jpg");
 | 
			
		||||
        String fullPath = client.upload(content, path);
 | 
			
		||||
        System.out.println("访问地址:" + fullPath);
 | 
			
		||||
        if (false) {
 | 
			
		||||
            byte[] bytes = client.getContent(path);
 | 
			
		||||
            System.out.println("文件内容:" + bytes);
 | 
			
		||||
        }
 | 
			
		||||
        if (false) {
 | 
			
		||||
            client.delete(path);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,4 @@
 | 
			
		||||
/**
 | 
			
		||||
 * 占位,避免 package 无法提交到 Git 仓库
 | 
			
		||||
 */
 | 
			
		||||
package cn.iocoder.yudao.framework.file.core.enums;
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB  | 
@@ -80,4 +80,7 @@ public interface BaseMapperX<T> extends BaseMapper<T> {
 | 
			
		||||
        entities.forEach(this::insert);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    default void updateBatch(T update) {
 | 
			
		||||
        update(update, new QueryWrapper<>());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -22,7 +22,6 @@ public interface BpmMessageService {
 | 
			
		||||
     */
 | 
			
		||||
    void sendMessageWhenProcessInstanceApprove(@Valid BpmMessageSendWhenProcessInstanceApproveReqDTO reqDTO);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 发送流程实例被不通过的消息
 | 
			
		||||
     *
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,6 @@ import cn.hutool.core.util.RandomUtil;
 | 
			
		||||
import cn.hutool.core.util.StrUtil;
 | 
			
		||||
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
 | 
			
		||||
import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission;
 | 
			
		||||
import cn.iocoder.yudao.framework.datapermission.core.dept.rule.DeptDataPermissionRule;
 | 
			
		||||
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmTaskAssignRuleDO;
 | 
			
		||||
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmUserGroupDO;
 | 
			
		||||
import cn.iocoder.yudao.module.bpm.enums.definition.BpmTaskAssignRuleTypeEnum;
 | 
			
		||||
@@ -69,11 +68,13 @@ public class BpmUserTaskActivityBehavior extends UserTaskActivityBehavior {
 | 
			
		||||
    public BpmUserTaskActivityBehavior(UserTask userTask) {
 | 
			
		||||
        super(userTask);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void setScripts(List<BpmTaskAssignScript> scripts) {
 | 
			
		||||
        this.scriptMap = convertMap(scripts, script -> script.getEnum().getId());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    @DataPermission(enable = false) // 不需要处理数据权限, 不然会有问题,查询不到数据
 | 
			
		||||
    protected void handleAssignments(TaskService taskService, String assignee, String owner, List<String> candidateUsers, List<String> candidateGroups, TaskEntity task, ExpressionManager expressionManager, DelegateExecution execution, ProcessEngineConfigurationImpl processEngineConfiguration) {
 | 
			
		||||
        // 第一步,获得任务的规则
 | 
			
		||||
        BpmTaskAssignRuleDO rule = getTaskRule(task);
 | 
			
		||||
@@ -98,7 +99,6 @@ public class BpmUserTaskActivityBehavior extends UserTaskActivityBehavior {
 | 
			
		||||
        return taskRules.get(0);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @VisibleForTesting
 | 
			
		||||
    Set<Long> calculateTaskCandidateUsers(TaskEntity task, BpmTaskAssignRuleDO rule) {
 | 
			
		||||
        Set<Long> assigneeUserIds = null;
 | 
			
		||||
        if (Objects.equals(BpmTaskAssignRuleTypeEnum.ROLE.getType(), rule.getType())) {
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,6 @@ import java.util.Set;
 | 
			
		||||
@Component
 | 
			
		||||
public class BpmTaskAssignLeaderX2Script extends BpmTaskAssignLeaderAbstractScript {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    @DataPermission(enable = false) // 不需要处理数据权限, 不然会有问题,查询不到数据
 | 
			
		||||
    public Set<Long> calculateTaskCandidateUsers(TaskEntity task) {
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,7 @@ public interface FileApi {
 | 
			
		||||
     * @param content 文件内容
 | 
			
		||||
     * @return 文件路径
 | 
			
		||||
     */
 | 
			
		||||
   default String createFile(byte[] content) {
 | 
			
		||||
   default String createFile(byte[] content) throws Exception {
 | 
			
		||||
       return createFile(IdUtil.fastUUID(), content);
 | 
			
		||||
   }
 | 
			
		||||
 | 
			
		||||
@@ -26,6 +26,6 @@ public interface FileApi {
 | 
			
		||||
     * @param content 文件内容
 | 
			
		||||
     * @return 文件路径
 | 
			
		||||
     */
 | 
			
		||||
    String createFile(String path, byte[] content);
 | 
			
		||||
    String createFile(String path, byte[] content) throws Exception;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -42,8 +42,11 @@ public interface ErrorCodeConstants {
 | 
			
		||||
    ErrorCode CODEGEN_SYNC_COLUMNS_NULL = new ErrorCode(1003001006, "同步的字段不存在");
 | 
			
		||||
    ErrorCode CODEGEN_SYNC_NONE_CHANGE = new ErrorCode(1003001007, "同步失败,不存在改变");
 | 
			
		||||
 | 
			
		||||
    // ========== 字典类型(测试) 1003000000 ==========
 | 
			
		||||
    ErrorCode TEST_DEMO_NOT_EXISTS = new ErrorCode(1003000000, "测试示例不存在");
 | 
			
		||||
    // ========== 字典类型(测试)1001005000 ==========
 | 
			
		||||
    ErrorCode TEST_DEMO_NOT_EXISTS = new ErrorCode(1001005000, "测试示例不存在");
 | 
			
		||||
 | 
			
		||||
    // ========== 文件配置 1001006000 ==========
 | 
			
		||||
    ErrorCode FILE_CONFIG_NOT_EXISTS = new ErrorCode(1001006000, "文件配置不存在");
 | 
			
		||||
    ErrorCode FILE_CONFIG_DELETE_FAIL_MASTER = new ErrorCode(1001006001, "该文件配置不允许删除,原因:它是主配置,删除会导致无法上传文件");
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -109,6 +109,13 @@
 | 
			
		||||
            <groupId>de.codecentric</groupId>
 | 
			
		||||
            <artifactId>spring-boot-admin-starter-server</artifactId> <!-- 实现 Spring Boot Admin Server 服务端 -->
 | 
			
		||||
        </dependency>
 | 
			
		||||
 | 
			
		||||
        <!-- 三方云服务相关 -->
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>cn.iocoder.boot</groupId>
 | 
			
		||||
            <artifactId>yudao-spring-boot-starter-file</artifactId>
 | 
			
		||||
        </dependency>
 | 
			
		||||
 | 
			
		||||
    </dependencies>
 | 
			
		||||
 | 
			
		||||
</project>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,5 @@
 | 
			
		||||
package cn.iocoder.yudao.module.infra.api.file;
 | 
			
		||||
 | 
			
		||||
import cn.iocoder.yudao.module.infra.api.file.FileApi;
 | 
			
		||||
import cn.iocoder.yudao.module.infra.service.file.FileService;
 | 
			
		||||
import org.springframework.stereotype.Service;
 | 
			
		||||
import org.springframework.validation.annotation.Validated;
 | 
			
		||||
@@ -20,7 +19,7 @@ public class FileApiImpl implements FileApi {
 | 
			
		||||
    private FileService fileService;
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public String createFile(String path, byte[] content) {
 | 
			
		||||
    public String createFile(String path, byte[] content) throws Exception {
 | 
			
		||||
        return fileService.createFile(path, content);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,45 @@
 | 
			
		||||
### 请求 /infra/file-config/create 接口 => 成功
 | 
			
		||||
POST {{baseUrl}}/infra/file-config/create
 | 
			
		||||
Content-Type: application/json
 | 
			
		||||
tenant-id: {{adminTenentId}}
 | 
			
		||||
Authorization: Bearer {{token}}
 | 
			
		||||
 | 
			
		||||
{
 | 
			
		||||
  "name": "S3 - 七牛云",
 | 
			
		||||
  "remark": "",
 | 
			
		||||
  "storage": 20,
 | 
			
		||||
  "config": {
 | 
			
		||||
    "accessKey": "b7yvuhBSAGjmtPhMFcn9iMOxUOY_I06cA_p0ZUx8",
 | 
			
		||||
    "accessSecret": "kXM1l5ia1RvSX3QaOEcwI3RLz3Y2rmNszWonKZtP",
 | 
			
		||||
    "bucket": "ruoyi-vue-pro",
 | 
			
		||||
    "endpoint": "s3-cn-south-1.qiniucs.com",
 | 
			
		||||
    "domain": "http://test.yudao.iocoder.cn",
 | 
			
		||||
    "region": "oss-cn-beijing"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
### 请求 /infra/file-config/update 接口 => 成功
 | 
			
		||||
PUT {{baseUrl}}/infra/file-config/update
 | 
			
		||||
Content-Type: application/json
 | 
			
		||||
tenant-id: {{adminTenentId}}
 | 
			
		||||
Authorization: Bearer {{token}}
 | 
			
		||||
 | 
			
		||||
{
 | 
			
		||||
  "id": 2,
 | 
			
		||||
  "name": "S3 - 七牛云",
 | 
			
		||||
  "remark": "",
 | 
			
		||||
  "config": {
 | 
			
		||||
    "accessKey": "b7yvuhBSAGjmtPhMFcn9iMOxUOY_I06cA_p0ZUx8",
 | 
			
		||||
    "accessSecret": "kXM1l5ia1RvSX3QaOEcwI3RLz3Y2rmNszWonKZtP",
 | 
			
		||||
    "bucket": "ruoyi-vue-pro",
 | 
			
		||||
    "endpoint": "s3-cn-south-1.qiniucs.com",
 | 
			
		||||
    "domain": "http://test.yudao.iocoder.cn",
 | 
			
		||||
    "region": "oss-cn-beijing"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
### 请求 /infra/file-config/test 接口 => 成功
 | 
			
		||||
GET {{baseUrl}}/infra/file-config/test?id=2
 | 
			
		||||
Content-Type: application/json
 | 
			
		||||
tenant-id: {{adminTenentId}}
 | 
			
		||||
Authorization: Bearer {{token}}
 | 
			
		||||
@@ -0,0 +1,89 @@
 | 
			
		||||
package cn.iocoder.yudao.module.infra.controller.admin.file;
 | 
			
		||||
 | 
			
		||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 | 
			
		||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
 | 
			
		||||
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.config.FileConfigCreateReqVO;
 | 
			
		||||
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.config.FileConfigPageReqVO;
 | 
			
		||||
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.config.FileConfigRespVO;
 | 
			
		||||
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.config.FileConfigUpdateReqVO;
 | 
			
		||||
import cn.iocoder.yudao.module.infra.convert.file.FileConfigConvert;
 | 
			
		||||
import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileConfigDO;
 | 
			
		||||
import cn.iocoder.yudao.module.infra.service.file.FileConfigService;
 | 
			
		||||
import io.swagger.annotations.Api;
 | 
			
		||||
import io.swagger.annotations.ApiImplicitParam;
 | 
			
		||||
import io.swagger.annotations.ApiOperation;
 | 
			
		||||
import org.springframework.security.access.prepost.PreAuthorize;
 | 
			
		||||
import org.springframework.validation.annotation.Validated;
 | 
			
		||||
import org.springframework.web.bind.annotation.*;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Resource;
 | 
			
		||||
import javax.validation.Valid;
 | 
			
		||||
 | 
			
		||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 | 
			
		||||
 | 
			
		||||
@Api(tags = "管理后台 - 文件配置")
 | 
			
		||||
@RestController
 | 
			
		||||
@RequestMapping("/infra/file-config")
 | 
			
		||||
@Validated
 | 
			
		||||
public class FileConfigController {
 | 
			
		||||
 | 
			
		||||
    @Resource
 | 
			
		||||
    private FileConfigService fileConfigService;
 | 
			
		||||
 | 
			
		||||
    @PostMapping("/create")
 | 
			
		||||
    @ApiOperation("创建文件配置")
 | 
			
		||||
    @PreAuthorize("@ss.hasPermission('infra:file-config:create')")
 | 
			
		||||
    public CommonResult<Long> createFileConfig(@Valid @RequestBody FileConfigCreateReqVO createReqVO) {
 | 
			
		||||
        return success(fileConfigService.createFileConfig(createReqVO));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @PutMapping("/update")
 | 
			
		||||
    @ApiOperation("更新文件配置")
 | 
			
		||||
    @PreAuthorize("@ss.hasPermission('infra:file-config:update')")
 | 
			
		||||
    public CommonResult<Boolean> updateFileConfig(@Valid @RequestBody FileConfigUpdateReqVO updateReqVO) {
 | 
			
		||||
        fileConfigService.updateFileConfig(updateReqVO);
 | 
			
		||||
        return success(true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @PutMapping("/update-master")
 | 
			
		||||
    @ApiOperation("更新文件配置为 Master")
 | 
			
		||||
    @PreAuthorize("@ss.hasPermission('infra:file-config:update')")
 | 
			
		||||
    public CommonResult<Boolean> updateFileConfigMaster(@RequestParam("id") Long id) {
 | 
			
		||||
        fileConfigService.updateFileConfigMaster(id);
 | 
			
		||||
        return success(true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @DeleteMapping("/delete")
 | 
			
		||||
    @ApiOperation("删除文件配置")
 | 
			
		||||
    @ApiImplicitParam(name = "id", value = "编号", required = true, dataTypeClass = Long.class)
 | 
			
		||||
    @PreAuthorize("@ss.hasPermission('infra:file-config:delete')")
 | 
			
		||||
    public CommonResult<Boolean> deleteFileConfig(@RequestParam("id") Long id) {
 | 
			
		||||
        fileConfigService.deleteFileConfig(id);
 | 
			
		||||
        return success(true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @GetMapping("/get")
 | 
			
		||||
    @ApiOperation("获得文件配置")
 | 
			
		||||
    @ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class)
 | 
			
		||||
    @PreAuthorize("@ss.hasPermission('infra:file-config:query')")
 | 
			
		||||
    public CommonResult<FileConfigRespVO> getFileConfig(@RequestParam("id") Long id) {
 | 
			
		||||
        FileConfigDO fileConfig = fileConfigService.getFileConfig(id);
 | 
			
		||||
        return success(FileConfigConvert.INSTANCE.convert(fileConfig));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @GetMapping("/page")
 | 
			
		||||
    @ApiOperation("获得文件配置分页")
 | 
			
		||||
    @PreAuthorize("@ss.hasPermission('infra:file-config:query')")
 | 
			
		||||
    public CommonResult<PageResult<FileConfigRespVO>> getFileConfigPage(@Valid FileConfigPageReqVO pageVO) {
 | 
			
		||||
        PageResult<FileConfigDO> pageResult = fileConfigService.getFileConfigPage(pageVO);
 | 
			
		||||
        return success(FileConfigConvert.INSTANCE.convertPage(pageResult));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @GetMapping("/test")
 | 
			
		||||
    @ApiOperation("测试文件配置是否正确")
 | 
			
		||||
    @PreAuthorize("@ss.hasPermission('infra:file-config:query')")
 | 
			
		||||
    public CommonResult<String> testFileConfig(@RequestParam("id") Long id) throws Exception {
 | 
			
		||||
        String url = fileConfigService.testFileConfig(id);
 | 
			
		||||
        return success(url);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -4,8 +4,8 @@ import cn.hutool.core.io.IoUtil;
 | 
			
		||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 | 
			
		||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
 | 
			
		||||
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
 | 
			
		||||
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.FilePageReqVO;
 | 
			
		||||
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.FileRespVO;
 | 
			
		||||
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO;
 | 
			
		||||
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FileRespVO;
 | 
			
		||||
import cn.iocoder.yudao.module.infra.convert.file.FileConvert;
 | 
			
		||||
import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO;
 | 
			
		||||
import cn.iocoder.yudao.module.infra.service.file.FileService;
 | 
			
		||||
@@ -23,7 +23,6 @@ import org.springframework.web.multipart.MultipartFile;
 | 
			
		||||
import javax.annotation.Resource;
 | 
			
		||||
import javax.servlet.http.HttpServletResponse;
 | 
			
		||||
import javax.validation.Valid;
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
 | 
			
		||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 | 
			
		||||
 | 
			
		||||
@@ -44,30 +43,35 @@ public class FileController {
 | 
			
		||||
            @ApiImplicitParam(name = "path", value = "文件路径", example = "yudaoyuanma.png", dataTypeClass = String.class)
 | 
			
		||||
    })
 | 
			
		||||
    public CommonResult<String> uploadFile(@RequestParam("file") MultipartFile file,
 | 
			
		||||
                                           @RequestParam("path") String path) throws IOException {
 | 
			
		||||
                                           @RequestParam("path") String path) throws Exception {
 | 
			
		||||
        return success(fileService.createFile(path, IoUtil.readBytes(file.getInputStream())));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @DeleteMapping("/delete")
 | 
			
		||||
    @ApiOperation("删除文件")
 | 
			
		||||
    @ApiImplicitParam(name = "id", value = "编号", required = true, dataTypeClass = String.class)
 | 
			
		||||
    @ApiImplicitParam(name = "id", value = "编号", required = true, dataTypeClass = Long.class)
 | 
			
		||||
    @PreAuthorize("@ss.hasPermission('infra:file:delete')")
 | 
			
		||||
    public CommonResult<Boolean> deleteFile(@RequestParam("id") String id) {
 | 
			
		||||
    public CommonResult<Boolean> deleteFile(@RequestParam("id") Long id) throws Exception {
 | 
			
		||||
        fileService.deleteFile(id);
 | 
			
		||||
        return success(true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @GetMapping("/get/{path}")
 | 
			
		||||
    @GetMapping("/{configId}/get/{path}")
 | 
			
		||||
    @ApiOperation("下载文件")
 | 
			
		||||
    @ApiImplicitParam(name = "path", value = "文件附件", required = true, dataTypeClass = MultipartFile.class)
 | 
			
		||||
    public void getFile(HttpServletResponse response, @PathVariable("path") String path) throws IOException {
 | 
			
		||||
        FileDO file = fileService.getFile(path);
 | 
			
		||||
        if (file == null) {
 | 
			
		||||
            log.warn("[getFile][path({}) 文件不存在]", path);
 | 
			
		||||
    @ApiImplicitParams({
 | 
			
		||||
            @ApiImplicitParam(name = "configId", value = "配置编号",  required = true, dataTypeClass = Long.class),
 | 
			
		||||
            @ApiImplicitParam(name = "path", value = "文件路径", required = true, dataTypeClass = String.class)
 | 
			
		||||
    })
 | 
			
		||||
    public void getFileContent(HttpServletResponse response,
 | 
			
		||||
                               @PathVariable("configId") Long configId,
 | 
			
		||||
                               @PathVariable("path") String path) throws Exception {
 | 
			
		||||
        byte[] content = fileService.getFileContent(configId, path);
 | 
			
		||||
        if (content == null) {
 | 
			
		||||
            log.warn("[getFileContent][configId({}) path({}) 文件不存在]", configId, path);
 | 
			
		||||
            response.setStatus(HttpStatus.NOT_FOUND.value());
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        ServletUtils.writeAttachment(response, path, file.getContent());
 | 
			
		||||
        ServletUtils.writeAttachment(response, path, content);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @GetMapping("/page")
 | 
			
		||||
 
 | 
			
		||||
@@ -1,22 +0,0 @@
 | 
			
		||||
package cn.iocoder.yudao.module.infra.controller.admin.file.vo;
 | 
			
		||||
 | 
			
		||||
import io.swagger.annotations.ApiModel;
 | 
			
		||||
import io.swagger.annotations.ApiModelProperty;
 | 
			
		||||
import lombok.Data;
 | 
			
		||||
 | 
			
		||||
import java.util.Date;
 | 
			
		||||
 | 
			
		||||
@ApiModel(value = "管理后台 - 文件 Response VO", description = "不返回 content 字段,太大")
 | 
			
		||||
@Data
 | 
			
		||||
public class FileRespVO {
 | 
			
		||||
 | 
			
		||||
    @ApiModelProperty(value = "文件路径", required = true, example = "yudao.jpg")
 | 
			
		||||
    private String id;
 | 
			
		||||
 | 
			
		||||
    @ApiModelProperty(value = "文件类型", required = true, example = "jpg")
 | 
			
		||||
    private String type;
 | 
			
		||||
 | 
			
		||||
    @ApiModelProperty(value = "创建时间", required = true)
 | 
			
		||||
    private Date createTime;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,22 @@
 | 
			
		||||
package cn.iocoder.yudao.module.infra.controller.admin.file.vo.config;
 | 
			
		||||
 | 
			
		||||
import io.swagger.annotations.ApiModelProperty;
 | 
			
		||||
import lombok.Data;
 | 
			
		||||
 | 
			
		||||
import javax.validation.constraints.NotNull;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
* 文件配置 Base VO,提供给添加、修改、详细的子 VO 使用
 | 
			
		||||
* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
 | 
			
		||||
*/
 | 
			
		||||
@Data
 | 
			
		||||
public class FileConfigBaseVO {
 | 
			
		||||
 | 
			
		||||
    @ApiModelProperty(value = "配置名", required = true, example = "S3 - 阿里云")
 | 
			
		||||
    @NotNull(message = "配置名不能为空")
 | 
			
		||||
    private String name;
 | 
			
		||||
 | 
			
		||||
    @ApiModelProperty(value = "备注", example = "我是备注")
 | 
			
		||||
    private String remark;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,26 @@
 | 
			
		||||
package cn.iocoder.yudao.module.infra.controller.admin.file.vo.config;
 | 
			
		||||
 | 
			
		||||
import io.swagger.annotations.ApiModel;
 | 
			
		||||
import io.swagger.annotations.ApiModelProperty;
 | 
			
		||||
import lombok.Data;
 | 
			
		||||
import lombok.EqualsAndHashCode;
 | 
			
		||||
import lombok.ToString;
 | 
			
		||||
 | 
			
		||||
import javax.validation.constraints.NotNull;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
 | 
			
		||||
@ApiModel("管理后台 - 文件配置创建 Request VO")
 | 
			
		||||
@Data
 | 
			
		||||
@EqualsAndHashCode(callSuper = true)
 | 
			
		||||
@ToString(callSuper = true)
 | 
			
		||||
public class FileConfigCreateReqVO extends FileConfigBaseVO {
 | 
			
		||||
 | 
			
		||||
    @ApiModelProperty(value = "存储器", required = true, example = "1", notes = "参见 FileStorageEnum 枚举类")
 | 
			
		||||
    @NotNull(message = "存储器不能为空")
 | 
			
		||||
    private Integer storage;
 | 
			
		||||
 | 
			
		||||
    @ApiModelProperty(value = "存储配置", required = true, notes = "配置是动态参数,所以使用 Map 接收")
 | 
			
		||||
    @NotNull(message = "存储配置不能为空")
 | 
			
		||||
    private Map<String, Object> config;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,35 @@
 | 
			
		||||
package cn.iocoder.yudao.module.infra.controller.admin.file.vo.config;
 | 
			
		||||
 | 
			
		||||
import cn.iocoder.yudao.framework.common.pojo.PageParam;
 | 
			
		||||
import io.swagger.annotations.ApiModel;
 | 
			
		||||
import io.swagger.annotations.ApiModelProperty;
 | 
			
		||||
import lombok.Data;
 | 
			
		||||
import lombok.EqualsAndHashCode;
 | 
			
		||||
import lombok.ToString;
 | 
			
		||||
import org.springframework.format.annotation.DateTimeFormat;
 | 
			
		||||
 | 
			
		||||
import java.util.Date;
 | 
			
		||||
 | 
			
		||||
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
 | 
			
		||||
 | 
			
		||||
@ApiModel("管理后台 - 文件配置分页 Request VO")
 | 
			
		||||
@Data
 | 
			
		||||
@EqualsAndHashCode(callSuper = true)
 | 
			
		||||
@ToString(callSuper = true)
 | 
			
		||||
public class FileConfigPageReqVO extends PageParam {
 | 
			
		||||
 | 
			
		||||
    @ApiModelProperty(value = "配置名", example = "S3 - 阿里云")
 | 
			
		||||
    private String name;
 | 
			
		||||
 | 
			
		||||
    @ApiModelProperty(value = "存储器", example = "1")
 | 
			
		||||
    private Integer storage;
 | 
			
		||||
 | 
			
		||||
    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
 | 
			
		||||
    @ApiModelProperty(value = "开始创建时间")
 | 
			
		||||
    private Date beginCreateTime;
 | 
			
		||||
 | 
			
		||||
    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
 | 
			
		||||
    @ApiModelProperty(value = "结束创建时间")
 | 
			
		||||
    private Date endCreateTime;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,36 @@
 | 
			
		||||
package cn.iocoder.yudao.module.infra.controller.admin.file.vo.config;
 | 
			
		||||
 | 
			
		||||
import cn.iocoder.yudao.framework.file.core.client.FileClientConfig;
 | 
			
		||||
import io.swagger.annotations.ApiModel;
 | 
			
		||||
import io.swagger.annotations.ApiModelProperty;
 | 
			
		||||
import lombok.Data;
 | 
			
		||||
import lombok.EqualsAndHashCode;
 | 
			
		||||
import lombok.ToString;
 | 
			
		||||
 | 
			
		||||
import javax.validation.constraints.NotNull;
 | 
			
		||||
import java.util.Date;
 | 
			
		||||
 | 
			
		||||
@ApiModel("管理后台 - 文件配置 Response VO")
 | 
			
		||||
@Data
 | 
			
		||||
@EqualsAndHashCode(callSuper = true)
 | 
			
		||||
@ToString(callSuper = true)
 | 
			
		||||
public class FileConfigRespVO extends FileConfigBaseVO {
 | 
			
		||||
 | 
			
		||||
    @ApiModelProperty(value = "编号", required = true, example = "1")
 | 
			
		||||
    private Long id;
 | 
			
		||||
 | 
			
		||||
    @ApiModelProperty(value = "存储器", required = true, example = "1", notes = "参见 FileStorageEnum 枚举类")
 | 
			
		||||
    @NotNull(message = "存储器不能为空")
 | 
			
		||||
    private Integer storage;
 | 
			
		||||
 | 
			
		||||
    @ApiModelProperty(value = "是否为主配置", required = true, example = "true")
 | 
			
		||||
    @NotNull(message = "是否为主配置不能为空")
 | 
			
		||||
    private Boolean master;
 | 
			
		||||
 | 
			
		||||
    @ApiModelProperty(value = "存储配置", required = true)
 | 
			
		||||
    private FileClientConfig config;
 | 
			
		||||
 | 
			
		||||
    @ApiModelProperty(value = "创建时间", required = true)
 | 
			
		||||
    private Date createTime;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,26 @@
 | 
			
		||||
package cn.iocoder.yudao.module.infra.controller.admin.file.vo.config;
 | 
			
		||||
 | 
			
		||||
import io.swagger.annotations.ApiModel;
 | 
			
		||||
import io.swagger.annotations.ApiModelProperty;
 | 
			
		||||
import lombok.Data;
 | 
			
		||||
import lombok.EqualsAndHashCode;
 | 
			
		||||
import lombok.ToString;
 | 
			
		||||
 | 
			
		||||
import javax.validation.constraints.NotNull;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
 | 
			
		||||
@ApiModel("管理后台 - 文件配置更新 Request VO")
 | 
			
		||||
@Data
 | 
			
		||||
@EqualsAndHashCode(callSuper = true)
 | 
			
		||||
@ToString(callSuper = true)
 | 
			
		||||
public class FileConfigUpdateReqVO extends FileConfigBaseVO {
 | 
			
		||||
 | 
			
		||||
    @ApiModelProperty(value = "编号", required = true, example = "1")
 | 
			
		||||
    @NotNull(message = "编号不能为空")
 | 
			
		||||
    private Long id;
 | 
			
		||||
 | 
			
		||||
    @ApiModelProperty(value = "存储配置", required = true, notes = "配置是动态参数,所以使用 Map 接收")
 | 
			
		||||
    @NotNull(message = "存储配置不能为空")
 | 
			
		||||
    private Map<String, Object> config;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
package cn.iocoder.yudao.module.infra.controller.admin.file.vo;
 | 
			
		||||
package cn.iocoder.yudao.module.infra.controller.admin.file.vo.file;
 | 
			
		||||
 | 
			
		||||
import cn.iocoder.yudao.framework.common.pojo.PageParam;
 | 
			
		||||
import io.swagger.annotations.ApiModel;
 | 
			
		||||
@@ -19,7 +19,7 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_
 | 
			
		||||
public class FilePageReqVO extends PageParam {
 | 
			
		||||
 | 
			
		||||
    @ApiModelProperty(value = "文件路径", example = "yudao", notes = "模糊匹配")
 | 
			
		||||
    private String id;
 | 
			
		||||
    private String path;
 | 
			
		||||
 | 
			
		||||
    @ApiModelProperty(value = "文件类型", example = "jpg", notes = "模糊匹配")
 | 
			
		||||
    private String type;
 | 
			
		||||
@@ -0,0 +1,31 @@
 | 
			
		||||
package cn.iocoder.yudao.module.infra.controller.admin.file.vo.file;
 | 
			
		||||
 | 
			
		||||
import io.swagger.annotations.ApiModel;
 | 
			
		||||
import io.swagger.annotations.ApiModelProperty;
 | 
			
		||||
import lombok.Data;
 | 
			
		||||
 | 
			
		||||
import java.util.Date;
 | 
			
		||||
 | 
			
		||||
@ApiModel(value = "管理后台 - 文件 Response VO", description = "不返回 content 字段,太大")
 | 
			
		||||
@Data
 | 
			
		||||
public class FileRespVO {
 | 
			
		||||
 | 
			
		||||
    @ApiModelProperty(value = "文件编号", required = true, example = "1024")
 | 
			
		||||
    private Long id;
 | 
			
		||||
 | 
			
		||||
    @ApiModelProperty(value = "文件路径", required = true, example = "yudao.jpg")
 | 
			
		||||
    private String path;
 | 
			
		||||
 | 
			
		||||
    @ApiModelProperty(value = "文件 URL", required = true, example = "https://www.iocoder.cn/yudao.jpg")
 | 
			
		||||
    private String url;
 | 
			
		||||
 | 
			
		||||
    @ApiModelProperty(value = "文件类型", example = "jpg")
 | 
			
		||||
    private String type;
 | 
			
		||||
 | 
			
		||||
    @ApiModelProperty(value = "文件大小", example = "2048", required = true)
 | 
			
		||||
    private Integer size;
 | 
			
		||||
 | 
			
		||||
    @ApiModelProperty(value = "创建时间", required = true)
 | 
			
		||||
    private Date createTime;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,36 @@
 | 
			
		||||
package cn.iocoder.yudao.module.infra.convert.file;
 | 
			
		||||
 | 
			
		||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
 | 
			
		||||
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.config.FileConfigCreateReqVO;
 | 
			
		||||
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.config.FileConfigRespVO;
 | 
			
		||||
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.config.FileConfigUpdateReqVO;
 | 
			
		||||
import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileConfigDO;
 | 
			
		||||
import org.mapstruct.Mapper;
 | 
			
		||||
import org.mapstruct.Mapping;
 | 
			
		||||
import org.mapstruct.factory.Mappers;
 | 
			
		||||
 | 
			
		||||
import java.util.List;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 文件配置 Convert
 | 
			
		||||
 *
 | 
			
		||||
 * @author 芋道源码
 | 
			
		||||
 */
 | 
			
		||||
@Mapper
 | 
			
		||||
public interface FileConfigConvert {
 | 
			
		||||
 | 
			
		||||
    FileConfigConvert INSTANCE = Mappers.getMapper(FileConfigConvert.class);
 | 
			
		||||
 | 
			
		||||
    @Mapping(target = "config", ignore = true)
 | 
			
		||||
    FileConfigDO convert(FileConfigCreateReqVO bean);
 | 
			
		||||
 | 
			
		||||
    @Mapping(target = "config", ignore = true)
 | 
			
		||||
    FileConfigDO convert(FileConfigUpdateReqVO bean);
 | 
			
		||||
 | 
			
		||||
    FileConfigRespVO convert(FileConfigDO bean);
 | 
			
		||||
 | 
			
		||||
    List<FileConfigRespVO> convertList(List<FileConfigDO> list);
 | 
			
		||||
 | 
			
		||||
    PageResult<FileConfigRespVO> convertPage(PageResult<FileConfigDO> page);
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
package cn.iocoder.yudao.module.infra.convert.file;
 | 
			
		||||
 | 
			
		||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
 | 
			
		||||
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.FileRespVO;
 | 
			
		||||
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FileRespVO;
 | 
			
		||||
import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO;
 | 
			
		||||
import org.mapstruct.Mapper;
 | 
			
		||||
import org.mapstruct.factory.Mappers;
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,56 @@
 | 
			
		||||
package cn.iocoder.yudao.module.infra.dal.dataobject.file;
 | 
			
		||||
 | 
			
		||||
import cn.iocoder.yudao.framework.file.core.client.FileClientConfig;
 | 
			
		||||
import cn.iocoder.yudao.framework.file.core.enums.FileStorageEnum;
 | 
			
		||||
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
 | 
			
		||||
import com.baomidou.mybatisplus.annotation.TableField;
 | 
			
		||||
import com.baomidou.mybatisplus.annotation.TableName;
 | 
			
		||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
 | 
			
		||||
import lombok.*;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 文件配置表
 | 
			
		||||
 *
 | 
			
		||||
 * @author 芋道源码
 | 
			
		||||
 */
 | 
			
		||||
@Data
 | 
			
		||||
@TableName(value = "infra_file_config", autoResultMap = true)
 | 
			
		||||
@EqualsAndHashCode(callSuper = true)
 | 
			
		||||
@ToString(callSuper = true)
 | 
			
		||||
@Builder
 | 
			
		||||
@NoArgsConstructor
 | 
			
		||||
@AllArgsConstructor
 | 
			
		||||
public class FileConfigDO extends BaseDO {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 配置编号,数据库自增
 | 
			
		||||
     */
 | 
			
		||||
    private Long id;
 | 
			
		||||
    /**
 | 
			
		||||
     * 配置名
 | 
			
		||||
     */
 | 
			
		||||
    private String name;
 | 
			
		||||
    /**
 | 
			
		||||
     * 存储器
 | 
			
		||||
     *
 | 
			
		||||
     * 枚举 {@link FileStorageEnum}
 | 
			
		||||
     */
 | 
			
		||||
    private Integer storage;
 | 
			
		||||
    /**
 | 
			
		||||
     * 备注
 | 
			
		||||
     */
 | 
			
		||||
    private String remark;
 | 
			
		||||
    /**
 | 
			
		||||
     * 是否为主配置
 | 
			
		||||
     *
 | 
			
		||||
     * 由于我们可以配置多个文件配置,默认情况下,使用主配置进行文件的上传
 | 
			
		||||
     */
 | 
			
		||||
    private Boolean master;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 支付渠道配置
 | 
			
		||||
     */
 | 
			
		||||
    @TableField(typeHandler = JacksonTypeHandler.class)
 | 
			
		||||
    private FileClientConfig config;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,45 @@
 | 
			
		||||
package cn.iocoder.yudao.module.infra.dal.dataobject.file;
 | 
			
		||||
 | 
			
		||||
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
 | 
			
		||||
import com.baomidou.mybatisplus.annotation.IdType;
 | 
			
		||||
import com.baomidou.mybatisplus.annotation.TableId;
 | 
			
		||||
import com.baomidou.mybatisplus.annotation.TableName;
 | 
			
		||||
import lombok.*;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 文件内容表
 | 
			
		||||
 *
 | 
			
		||||
 * 专门用于存储 {@link cn.iocoder.yudao.framework.file.core.client.db.DBFileClient} 的文件内容
 | 
			
		||||
 *
 | 
			
		||||
 * @author 芋道源码
 | 
			
		||||
 */
 | 
			
		||||
@Data
 | 
			
		||||
@TableName("infra_file_content")
 | 
			
		||||
@EqualsAndHashCode(callSuper = true)
 | 
			
		||||
@ToString(callSuper = true)
 | 
			
		||||
@Builder
 | 
			
		||||
@NoArgsConstructor
 | 
			
		||||
@AllArgsConstructor
 | 
			
		||||
public class FileContentDO extends BaseDO {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 编号,数据库自增
 | 
			
		||||
     */
 | 
			
		||||
    @TableId(type = IdType.INPUT)
 | 
			
		||||
    private String id;
 | 
			
		||||
    /**
 | 
			
		||||
     * 配置编号
 | 
			
		||||
     *
 | 
			
		||||
     * 关联 {@link FileConfigDO#getId()}
 | 
			
		||||
     */
 | 
			
		||||
    private Long configId;
 | 
			
		||||
    /**
 | 
			
		||||
     * 路径,即文件名
 | 
			
		||||
     */
 | 
			
		||||
    private String path;
 | 
			
		||||
    /**
 | 
			
		||||
     * 文件内容
 | 
			
		||||
     */
 | 
			
		||||
    private byte[] content;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -1,9 +1,7 @@
 | 
			
		||||
package cn.iocoder.yudao.module.infra.dal.dataobject.file;
 | 
			
		||||
 | 
			
		||||
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
 | 
			
		||||
import com.baomidou.mybatisplus.annotation.IdType;
 | 
			
		||||
import com.baomidou.mybatisplus.annotation.TableField;
 | 
			
		||||
import com.baomidou.mybatisplus.annotation.TableId;
 | 
			
		||||
import com.baomidou.mybatisplus.annotation.TableName;
 | 
			
		||||
import lombok.*;
 | 
			
		||||
 | 
			
		||||
@@ -11,6 +9,7 @@ import java.io.InputStream;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 文件表
 | 
			
		||||
 * 每次文件上传,都会记录一条记录到该表中
 | 
			
		||||
 *
 | 
			
		||||
 * @author 芋道源码
 | 
			
		||||
 */
 | 
			
		||||
@@ -24,10 +23,23 @@ import java.io.InputStream;
 | 
			
		||||
public class FileDO extends BaseDO {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 文件路径
 | 
			
		||||
     * 编号,数据库自增
 | 
			
		||||
     */
 | 
			
		||||
    @TableId(type = IdType.INPUT)
 | 
			
		||||
    private String id;
 | 
			
		||||
    private Long id;
 | 
			
		||||
    /**
 | 
			
		||||
     * 配置编号
 | 
			
		||||
     *
 | 
			
		||||
     * 关联 {@link FileConfigDO#getId()}
 | 
			
		||||
     */
 | 
			
		||||
    private Long configId;
 | 
			
		||||
    /**
 | 
			
		||||
     * 路径,即文件名
 | 
			
		||||
     */
 | 
			
		||||
    private String path;
 | 
			
		||||
    /**
 | 
			
		||||
     * 访问地址
 | 
			
		||||
     */
 | 
			
		||||
    private String url;
 | 
			
		||||
    /**
 | 
			
		||||
     * 文件类型
 | 
			
		||||
     *
 | 
			
		||||
@@ -36,8 +48,8 @@ public class FileDO extends BaseDO {
 | 
			
		||||
    @TableField(value = "`type`")
 | 
			
		||||
    private String type;
 | 
			
		||||
    /**
 | 
			
		||||
     * 文件内容
 | 
			
		||||
     * 文件大小
 | 
			
		||||
     */
 | 
			
		||||
    private byte[] content;
 | 
			
		||||
    private Integer size;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,32 @@
 | 
			
		||||
package cn.iocoder.yudao.module.infra.dal.mysql.file;
 | 
			
		||||
 | 
			
		||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
 | 
			
		||||
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
 | 
			
		||||
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
 | 
			
		||||
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.config.FileConfigPageReqVO;
 | 
			
		||||
import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileConfigDO;
 | 
			
		||||
import org.apache.ibatis.annotations.Mapper;
 | 
			
		||||
import org.apache.ibatis.annotations.Select;
 | 
			
		||||
 | 
			
		||||
import java.util.Date;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 文件配置 Mapper
 | 
			
		||||
 *
 | 
			
		||||
 * @author 芋道源码
 | 
			
		||||
 */
 | 
			
		||||
@Mapper
 | 
			
		||||
public interface FileConfigMapper extends BaseMapperX<FileConfigDO> {
 | 
			
		||||
 | 
			
		||||
    default PageResult<FileConfigDO> selectPage(FileConfigPageReqVO reqVO) {
 | 
			
		||||
        return selectPage(reqVO, new LambdaQueryWrapperX<FileConfigDO>()
 | 
			
		||||
                .likeIfPresent(FileConfigDO::getName, reqVO.getName())
 | 
			
		||||
                .eqIfPresent(FileConfigDO::getStorage, reqVO.getStorage())
 | 
			
		||||
                .betweenIfPresent(FileConfigDO::getCreateTime, reqVO.getBeginCreateTime(), reqVO.getEndCreateTime())
 | 
			
		||||
                .orderByDesc(FileConfigDO::getId));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Select("SELECT id FROM infra_file_config WHERE update_time > #{maxUpdateTime} LIMIT 1")
 | 
			
		||||
    Long selectExistsByUpdateTimeAfter(Date maxUpdateTime);
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,41 @@
 | 
			
		||||
package cn.iocoder.yudao.module.infra.dal.mysql.file;
 | 
			
		||||
 | 
			
		||||
import cn.iocoder.yudao.framework.file.core.client.db.DBFileContentFrameworkDAO;
 | 
			
		||||
import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileContentDO;
 | 
			
		||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 | 
			
		||||
import org.springframework.stereotype.Repository;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Resource;
 | 
			
		||||
 | 
			
		||||
@Repository
 | 
			
		||||
public class FileContentDAOImpl implements DBFileContentFrameworkDAO {
 | 
			
		||||
 | 
			
		||||
    @Resource
 | 
			
		||||
    private FileContentMapper fileContentMapper;
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void insert(Long configId, String path, byte[] content) {
 | 
			
		||||
        FileContentDO entity = new FileContentDO().setConfigId(configId)
 | 
			
		||||
                .setPath(path).setContent(content);
 | 
			
		||||
        fileContentMapper.insert(entity);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void delete(Long configId, String path) {
 | 
			
		||||
        fileContentMapper.delete(buildQuery(configId, path));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public byte[] selectContent(Long configId, String path) {
 | 
			
		||||
        FileContentDO fileContentDO = fileContentMapper.selectOne(
 | 
			
		||||
                buildQuery(configId, path).select(FileContentDO::getContent));
 | 
			
		||||
        return fileContentDO != null ? fileContentDO.getContent() : null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private LambdaQueryWrapper<FileContentDO> buildQuery(Long configId, String path) {
 | 
			
		||||
        return new LambdaQueryWrapper<FileContentDO>()
 | 
			
		||||
                .eq(FileContentDO::getConfigId, configId)
 | 
			
		||||
                .eq(FileContentDO::getPath, path);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,9 @@
 | 
			
		||||
package cn.iocoder.yudao.module.infra.dal.mysql.file;
 | 
			
		||||
 | 
			
		||||
import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileContentDO;
 | 
			
		||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 | 
			
		||||
import org.apache.ibatis.annotations.Mapper;
 | 
			
		||||
 | 
			
		||||
@Mapper
 | 
			
		||||
public interface FileContentMapper extends BaseMapper<FileContentDO> {
 | 
			
		||||
}
 | 
			
		||||
@@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.infra.dal.mysql.file;
 | 
			
		||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
 | 
			
		||||
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
 | 
			
		||||
import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX;
 | 
			
		||||
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.FilePageReqVO;
 | 
			
		||||
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO;
 | 
			
		||||
import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO;
 | 
			
		||||
import org.apache.ibatis.annotations.Mapper;
 | 
			
		||||
 | 
			
		||||
@@ -17,25 +17,10 @@ public interface FileMapper extends BaseMapperX<FileDO> {
 | 
			
		||||
 | 
			
		||||
    default PageResult<FileDO> selectPage(FilePageReqVO reqVO) {
 | 
			
		||||
        return selectPage(reqVO, new QueryWrapperX<FileDO>()
 | 
			
		||||
                .likeIfPresent("id", reqVO.getId())
 | 
			
		||||
                .likeIfPresent("path", reqVO.getPath())
 | 
			
		||||
                .likeIfPresent("type", reqVO.getType())
 | 
			
		||||
                .betweenIfPresent("create_time", reqVO.getBeginCreateTime(), reqVO.getEndCreateTime())
 | 
			
		||||
                .orderByDesc("create_time"));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    default Long selectCountById(String id) {
 | 
			
		||||
        return selectCount(FileDO::getId, id);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 基于 Path 获取文件
 | 
			
		||||
     * 实际上,是基于 ID 查询
 | 
			
		||||
     *
 | 
			
		||||
     * @param path 路径
 | 
			
		||||
     * @return 文件
 | 
			
		||||
     */
 | 
			
		||||
    default FileDO selectByPath(String path) {
 | 
			
		||||
        return selectById(path);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +0,0 @@
 | 
			
		||||
package cn.iocoder.yudao.module.infra.framework.file.config;
 | 
			
		||||
 | 
			
		||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
 | 
			
		||||
import org.springframework.context.annotation.Configuration;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 文件 配置类
 | 
			
		||||
 */
 | 
			
		||||
@Configuration
 | 
			
		||||
@EnableConfigurationProperties(FileProperties.class)
 | 
			
		||||
public class FileConfiguration {
 | 
			
		||||
}
 | 
			
		||||
@@ -1,22 +0,0 @@
 | 
			
		||||
package cn.iocoder.yudao.module.infra.framework.file.config;
 | 
			
		||||
 | 
			
		||||
import lombok.Data;
 | 
			
		||||
import org.springframework.boot.context.properties.ConfigurationProperties;
 | 
			
		||||
import org.springframework.validation.annotation.Validated;
 | 
			
		||||
 | 
			
		||||
import javax.validation.constraints.NotNull;
 | 
			
		||||
 | 
			
		||||
@ConfigurationProperties(prefix = "yudao.file")
 | 
			
		||||
@Validated
 | 
			
		||||
@Data
 | 
			
		||||
public class FileProperties {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 对应 FileController 的 getFile 方法
 | 
			
		||||
     */
 | 
			
		||||
    @NotNull(message = "基础文件路径不能为空")
 | 
			
		||||
    private String basePath;
 | 
			
		||||
 | 
			
		||||
    // TODO 七牛、等等
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -1,16 +0,0 @@
 | 
			
		||||
/**
 | 
			
		||||
 * 文件的存储,推荐使用七牛、阿里云、华为云、腾讯云等文件服务
 | 
			
		||||
 *
 | 
			
		||||
 * 在不采用云服务的情况下,我们有几种技术选型:
 | 
			
		||||
 * 方案 1. 使用自建的文件服务,例如说 minIO、FastDFS 等等
 | 
			
		||||
 * 方案 2. 使用服务器的文件系统存储
 | 
			
		||||
 * 方案 3. 使用数据库进行存储
 | 
			
		||||
 *
 | 
			
		||||
 * 如果考虑额外在搭建服务,推荐方案 1。
 | 
			
		||||
 * 对于方案 2 来说,如果要实现文件存储的高可用,需要多台服务器之间做实时同步,可以基于 rsync + inotify 来做
 | 
			
		||||
 * 对于方案 3 的话,实现起来最简单,但是数据库本身不适合存储海量的文件
 | 
			
		||||
 *
 | 
			
		||||
 * 综合考虑,暂时使用方案 3 的方式,比较适合这样一个 all in one 的项目。
 | 
			
		||||
 * 随着文件的量级大了之后,还是推荐采用云服务。
 | 
			
		||||
 */
 | 
			
		||||
package cn.iocoder.yudao.module.infra.framework.file;
 | 
			
		||||
@@ -36,7 +36,7 @@ public class SecurityConfiguration {
 | 
			
		||||
                registry.antMatchers(adminSeverContextPath).anonymous()
 | 
			
		||||
                        .antMatchers(adminSeverContextPath + "/**").anonymous();
 | 
			
		||||
                // 文件的获取接口,可匿名访问
 | 
			
		||||
                registry.antMatchers(buildAdminApi("/infra/file/get/**"), buildAppApi("/infra/file/get/**")).anonymous();
 | 
			
		||||
                registry.antMatchers(buildAdminApi("/infra/file/*/get/**"), buildAppApi("/infra/file/get/**")).permitAll();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        };
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,29 @@
 | 
			
		||||
package cn.iocoder.yudao.module.infra.mq.consumer.file;
 | 
			
		||||
 | 
			
		||||
import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessageListener;
 | 
			
		||||
import cn.iocoder.yudao.module.infra.mq.message.file.FileConfigRefreshMessage;
 | 
			
		||||
import cn.iocoder.yudao.module.infra.service.file.FileConfigService;
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
import org.springframework.stereotype.Component;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Resource;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 针对 {@link FileConfigRefreshMessage} 的消费者
 | 
			
		||||
 *
 | 
			
		||||
 * @author 芋道源码
 | 
			
		||||
 */
 | 
			
		||||
@Component
 | 
			
		||||
@Slf4j
 | 
			
		||||
public class FileConfigRefreshConsumer extends AbstractChannelMessageListener<FileConfigRefreshMessage> {
 | 
			
		||||
 | 
			
		||||
    @Resource
 | 
			
		||||
    private FileConfigService fileConfigService;
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onMessage(FileConfigRefreshMessage message) {
 | 
			
		||||
        log.info("[onMessage][收到 FileConfig 刷新消息]");
 | 
			
		||||
        fileConfigService.initFileClients();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -1 +0,0 @@
 | 
			
		||||
package cn.iocoder.yudao.module.infra.mq.consumer;
 | 
			
		||||
@@ -0,0 +1,17 @@
 | 
			
		||||
package cn.iocoder.yudao.module.infra.mq.message.file;
 | 
			
		||||
 | 
			
		||||
import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessage;
 | 
			
		||||
import lombok.Data;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 文件配置数据刷新 Message
 | 
			
		||||
 */
 | 
			
		||||
@Data
 | 
			
		||||
public class FileConfigRefreshMessage extends AbstractChannelMessage {
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public String getChannel() {
 | 
			
		||||
        return "infra.file-config.refresh";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -1 +0,0 @@
 | 
			
		||||
package cn.iocoder.yudao.module.infra.mq.message;
 | 
			
		||||
@@ -0,0 +1,26 @@
 | 
			
		||||
package cn.iocoder.yudao.module.infra.mq.producer.file;
 | 
			
		||||
 | 
			
		||||
import cn.iocoder.yudao.framework.mq.core.RedisMQTemplate;
 | 
			
		||||
import cn.iocoder.yudao.module.infra.mq.message.file.FileConfigRefreshMessage;
 | 
			
		||||
import org.springframework.stereotype.Component;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Resource;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 文件配置相关消息的 Producer
 | 
			
		||||
 */
 | 
			
		||||
@Component
 | 
			
		||||
public class FileConfigProducer {
 | 
			
		||||
 | 
			
		||||
    @Resource
 | 
			
		||||
    private RedisMQTemplate redisMQTemplate;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 发送 {@link FileConfigRefreshMessage} 消息
 | 
			
		||||
     */
 | 
			
		||||
    public void sendFileConfigRefreshMessage() {
 | 
			
		||||
        FileConfigRefreshMessage message = new FileConfigRefreshMessage();
 | 
			
		||||
        redisMQTemplate.send(message);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -1 +0,0 @@
 | 
			
		||||
package cn.iocoder.yudao.module.infra.mq.producer;
 | 
			
		||||
@@ -15,7 +15,6 @@ import cn.iocoder.yudao.module.infra.dal.mysql.codegen.CodegenTableMapper;
 | 
			
		||||
import cn.iocoder.yudao.module.infra.dal.mysql.codegen.SchemaColumnMapper;
 | 
			
		||||
import cn.iocoder.yudao.module.infra.dal.mysql.codegen.SchemaTableMapper;
 | 
			
		||||
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenImportTypeEnum;
 | 
			
		||||
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenSceneEnum;
 | 
			
		||||
import cn.iocoder.yudao.module.infra.framework.codegen.config.CodegenProperties;
 | 
			
		||||
import cn.iocoder.yudao.module.infra.service.codegen.inner.CodegenBuilder;
 | 
			
		||||
import cn.iocoder.yudao.module.infra.service.codegen.inner.CodegenEngine;
 | 
			
		||||
@@ -26,7 +25,10 @@ import org.springframework.stereotype.Service;
 | 
			
		||||
import org.springframework.transaction.annotation.Transactional;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Resource;
 | 
			
		||||
import java.util.*;
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.Set;
 | 
			
		||||
import java.util.stream.Collectors;
 | 
			
		||||
 | 
			
		||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 | 
			
		||||
@@ -80,11 +82,8 @@ public class CodegenServiceImpl implements CodegenService {
 | 
			
		||||
        table.setAuthor(userApi.getUser(userId).getNickname());
 | 
			
		||||
        codegenTableMapper.insert(table);
 | 
			
		||||
        // 构建 CodegenColumnDO 数组,插入到 DB 中
 | 
			
		||||
        List<CodegenColumnDO> columns = codegenBuilder.buildColumns(schemaColumns);
 | 
			
		||||
        columns.forEach(column -> {
 | 
			
		||||
            column.setTableId(table.getId());
 | 
			
		||||
            codegenColumnMapper.insert(column); // TODO 批量插入
 | 
			
		||||
        });
 | 
			
		||||
        List<CodegenColumnDO> columns = codegenBuilder.buildColumns(table.getId(), schemaColumns);
 | 
			
		||||
        codegenColumnMapper.insertBatch(columns);
 | 
			
		||||
        return table.getId();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -197,11 +196,8 @@ public class CodegenServiceImpl implements CodegenService {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 插入新增的字段
 | 
			
		||||
        List<CodegenColumnDO> columns = codegenBuilder.buildColumns(schemaColumns);
 | 
			
		||||
        columns.forEach(column -> {
 | 
			
		||||
            column.setTableId(tableId);
 | 
			
		||||
            codegenColumnMapper.insert(column); // TODO 批量插入
 | 
			
		||||
        });
 | 
			
		||||
        List<CodegenColumnDO> columns = codegenBuilder.buildColumns(tableId, schemaColumns);
 | 
			
		||||
        codegenColumnMapper.insertBatch(columns);
 | 
			
		||||
        // 删除不存在的字段
 | 
			
		||||
        if (CollUtil.isNotEmpty(deleteColumnIds)) {
 | 
			
		||||
            codegenColumnMapper.deleteBatchIds(deleteColumnIds);
 | 
			
		||||
 
 | 
			
		||||
@@ -133,9 +133,12 @@ public class CodegenBuilder {
 | 
			
		||||
        table.setTemplateType(CodegenTemplateTypeEnum.CRUD.getType());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public List<CodegenColumnDO> buildColumns(List<SchemaColumnDO> schemaColumns) {
 | 
			
		||||
    public List<CodegenColumnDO> buildColumns(Long tableId, List<SchemaColumnDO> schemaColumns) {
 | 
			
		||||
        List<CodegenColumnDO> columns = CodegenConvert.INSTANCE.convertList(schemaColumns);
 | 
			
		||||
        columns.forEach(this::initColumnDefault);
 | 
			
		||||
        for (CodegenColumnDO column : columns) {
 | 
			
		||||
            column.setTableId(tableId);
 | 
			
		||||
            initColumnDefault(column);
 | 
			
		||||
        }
 | 
			
		||||
        return columns;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,102 @@
 | 
			
		||||
package cn.iocoder.yudao.module.infra.service.file;
 | 
			
		||||
 | 
			
		||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
 | 
			
		||||
import cn.iocoder.yudao.framework.file.core.client.FileClient;
 | 
			
		||||
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.config.FileConfigCreateReqVO;
 | 
			
		||||
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.config.FileConfigPageReqVO;
 | 
			
		||||
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.config.FileConfigUpdateReqVO;
 | 
			
		||||
import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileConfigDO;
 | 
			
		||||
 | 
			
		||||
import javax.validation.Valid;
 | 
			
		||||
import java.util.Collection;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 文件配置 Service 接口
 | 
			
		||||
 *
 | 
			
		||||
 * @author 芋道源码
 | 
			
		||||
 */
 | 
			
		||||
public interface FileConfigService {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 初始化文件客户端
 | 
			
		||||
     */
 | 
			
		||||
    void initFileClients();
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 创建文件配置
 | 
			
		||||
     *
 | 
			
		||||
     * @param createReqVO 创建信息
 | 
			
		||||
     * @return 编号
 | 
			
		||||
     */
 | 
			
		||||
    Long createFileConfig(@Valid FileConfigCreateReqVO createReqVO);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 更新文件配置
 | 
			
		||||
     *
 | 
			
		||||
     * @param updateReqVO 更新信息
 | 
			
		||||
     */
 | 
			
		||||
    void updateFileConfig(@Valid FileConfigUpdateReqVO updateReqVO);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 更新文件配置为 Master
 | 
			
		||||
     *
 | 
			
		||||
     * @param id 编号
 | 
			
		||||
     */
 | 
			
		||||
    void updateFileConfigMaster(Long id);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 删除文件配置
 | 
			
		||||
     *
 | 
			
		||||
     * @param id 编号
 | 
			
		||||
     */
 | 
			
		||||
    void deleteFileConfig(Long id);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获得文件配置
 | 
			
		||||
     *
 | 
			
		||||
     * @param id 编号
 | 
			
		||||
     * @return 文件配置
 | 
			
		||||
     */
 | 
			
		||||
    FileConfigDO getFileConfig(Long id);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获得文件配置列表
 | 
			
		||||
     *
 | 
			
		||||
     * @param ids 编号
 | 
			
		||||
     * @return 文件配置列表
 | 
			
		||||
     */
 | 
			
		||||
    List<FileConfigDO> getFileConfigList(Collection<Long> ids);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获得文件配置分页
 | 
			
		||||
     *
 | 
			
		||||
     * @param pageReqVO 分页查询
 | 
			
		||||
     * @return 文件配置分页
 | 
			
		||||
     */
 | 
			
		||||
    PageResult<FileConfigDO> getFileConfigPage(FileConfigPageReqVO pageReqVO);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 测试文件配置是否正确,通过上传文件
 | 
			
		||||
     *
 | 
			
		||||
     * @param id 编号
 | 
			
		||||
     * @return 文件 URL
 | 
			
		||||
     */
 | 
			
		||||
    String testFileConfig(Long id) throws Exception;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获得指定编号的文件客户端
 | 
			
		||||
     *
 | 
			
		||||
     * @param id 配置编号
 | 
			
		||||
     * @return 文件客户端
 | 
			
		||||
     */
 | 
			
		||||
    FileClient getFileClient(Long id);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获得 Master 文件客户端
 | 
			
		||||
     *
 | 
			
		||||
     * @return 文件客户端
 | 
			
		||||
     */
 | 
			
		||||
    FileClient getMasterFileClient();
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,241 @@
 | 
			
		||||
package cn.iocoder.yudao.module.infra.service.file;
 | 
			
		||||
 | 
			
		||||
import cn.hutool.core.collection.CollUtil;
 | 
			
		||||
import cn.hutool.core.io.resource.ResourceUtil;
 | 
			
		||||
import cn.hutool.core.util.IdUtil;
 | 
			
		||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
 | 
			
		||||
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
 | 
			
		||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
 | 
			
		||||
import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
 | 
			
		||||
import cn.iocoder.yudao.framework.file.core.client.FileClient;
 | 
			
		||||
import cn.iocoder.yudao.framework.file.core.client.FileClientConfig;
 | 
			
		||||
import cn.iocoder.yudao.framework.file.core.client.FileClientFactory;
 | 
			
		||||
import cn.iocoder.yudao.framework.file.core.enums.FileStorageEnum;
 | 
			
		||||
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.config.FileConfigCreateReqVO;
 | 
			
		||||
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.config.FileConfigPageReqVO;
 | 
			
		||||
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.config.FileConfigUpdateReqVO;
 | 
			
		||||
import cn.iocoder.yudao.module.infra.convert.file.FileConfigConvert;
 | 
			
		||||
import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileConfigDO;
 | 
			
		||||
import cn.iocoder.yudao.module.infra.dal.mysql.file.FileConfigMapper;
 | 
			
		||||
import cn.iocoder.yudao.module.infra.mq.producer.file.FileConfigProducer;
 | 
			
		||||
import lombok.Getter;
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
import org.springframework.context.annotation.Lazy;
 | 
			
		||||
import org.springframework.scheduling.annotation.Scheduled;
 | 
			
		||||
import org.springframework.stereotype.Service;
 | 
			
		||||
import org.springframework.transaction.annotation.Transactional;
 | 
			
		||||
import org.springframework.transaction.support.TransactionSynchronization;
 | 
			
		||||
import org.springframework.transaction.support.TransactionSynchronizationManager;
 | 
			
		||||
import org.springframework.validation.annotation.Validated;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.PostConstruct;
 | 
			
		||||
import javax.annotation.Resource;
 | 
			
		||||
import javax.validation.Validator;
 | 
			
		||||
import java.util.Collection;
 | 
			
		||||
import java.util.Date;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
 | 
			
		||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 | 
			
		||||
import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_CONFIG_DELETE_FAIL_MASTER;
 | 
			
		||||
import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_CONFIG_NOT_EXISTS;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 文件配置 Service 实现类
 | 
			
		||||
 *
 | 
			
		||||
 * @author 芋道源码
 | 
			
		||||
 */
 | 
			
		||||
@Service
 | 
			
		||||
@Validated
 | 
			
		||||
@Slf4j
 | 
			
		||||
public class FileConfigServiceImpl implements FileConfigService {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 定时执行 {@link #schedulePeriodicRefresh()} 的周期
 | 
			
		||||
     * 因为已经通过 Redis Pub/Sub 机制,所以频率不需要高
 | 
			
		||||
     */
 | 
			
		||||
    private static final long SCHEDULER_PERIOD = 5 * 60 * 1000L;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 缓存菜单的最大更新时间,用于后续的增量轮询,判断是否有更新
 | 
			
		||||
     */
 | 
			
		||||
    @Getter
 | 
			
		||||
    private volatile Date maxUpdateTime;
 | 
			
		||||
 | 
			
		||||
    @Resource
 | 
			
		||||
    private FileClientFactory fileClientFactory;
 | 
			
		||||
    /**
 | 
			
		||||
     * Master FileClient 对象,有且仅有一个,即 {@link FileConfigDO#getMaster()} 对应的
 | 
			
		||||
     */
 | 
			
		||||
    @Getter
 | 
			
		||||
    private FileClient masterFileClient;
 | 
			
		||||
 | 
			
		||||
    @Resource
 | 
			
		||||
    private FileConfigMapper fileConfigMapper;
 | 
			
		||||
 | 
			
		||||
    @Resource
 | 
			
		||||
    private FileConfigProducer fileConfigProducer;
 | 
			
		||||
 | 
			
		||||
    @Resource
 | 
			
		||||
    private Validator validator;
 | 
			
		||||
 | 
			
		||||
    @Resource
 | 
			
		||||
    @Lazy // 注入自己,所以延迟加载
 | 
			
		||||
    private FileConfigService self;
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    @PostConstruct
 | 
			
		||||
    public void initFileClients() {
 | 
			
		||||
        // 获取文件配置,如果有更新
 | 
			
		||||
        List<FileConfigDO> configs = loadFileConfigIfUpdate(maxUpdateTime);
 | 
			
		||||
        if (CollUtil.isEmpty(configs)) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 创建或更新支付 Client
 | 
			
		||||
        configs.forEach(config -> {
 | 
			
		||||
            fileClientFactory.createOrUpdateFileClient(config.getId(), config.getStorage(), config.getConfig());
 | 
			
		||||
            // 如果是 master,进行设置
 | 
			
		||||
            if (Boolean.TRUE.equals(config.getMaster())) {
 | 
			
		||||
                masterFileClient = fileClientFactory.getFileClient(config.getId());
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // 写入缓存
 | 
			
		||||
        maxUpdateTime = CollectionUtils.getMaxValue(configs, FileConfigDO::getUpdateTime);
 | 
			
		||||
        log.info("[initFileClients][初始化 FileConfig 数量为 {}]", configs.size());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Scheduled(fixedDelay = SCHEDULER_PERIOD, initialDelay = SCHEDULER_PERIOD)
 | 
			
		||||
    public void schedulePeriodicRefresh() {
 | 
			
		||||
        self.initFileClients();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 如果文件配置发生变化,从数据库中获取最新的全量文件配置。
 | 
			
		||||
     * 如果未发生变化,则返回空
 | 
			
		||||
     *
 | 
			
		||||
     * @param maxUpdateTime 当前文件配置的最大更新时间
 | 
			
		||||
     * @return 文件配置列表
 | 
			
		||||
     */
 | 
			
		||||
    private List<FileConfigDO> loadFileConfigIfUpdate(Date maxUpdateTime) {
 | 
			
		||||
        // 第一步,判断是否要更新。
 | 
			
		||||
        if (maxUpdateTime == null) { // 如果更新时间为空,说明 DB 一定有新数据
 | 
			
		||||
            log.info("[loadFileConfigIfUpdate][首次加载全量文件配置]");
 | 
			
		||||
        } else { // 判断数据库中是否有更新的文件配置
 | 
			
		||||
            if (fileConfigMapper.selectExistsByUpdateTimeAfter(maxUpdateTime) == null) {
 | 
			
		||||
                return null;
 | 
			
		||||
            }
 | 
			
		||||
            log.info("[loadFileConfigIfUpdate][增量加载全量文件配置]");
 | 
			
		||||
        }
 | 
			
		||||
        // 第二步,如果有更新,则从数据库加载所有文件配置
 | 
			
		||||
        return fileConfigMapper.selectList();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public Long createFileConfig(FileConfigCreateReqVO createReqVO) {
 | 
			
		||||
        // 插入
 | 
			
		||||
        FileConfigDO fileConfig = FileConfigConvert.INSTANCE.convert(createReqVO)
 | 
			
		||||
                .setConfig(parseClientConfig(createReqVO.getStorage(), createReqVO.getConfig()))
 | 
			
		||||
                .setMaster(false); // 默认非 master
 | 
			
		||||
        fileConfigMapper.insert(fileConfig);
 | 
			
		||||
        // 发送刷新配置的消息
 | 
			
		||||
        fileConfigProducer.sendFileConfigRefreshMessage();
 | 
			
		||||
        // 返回
 | 
			
		||||
        return fileConfig.getId();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void updateFileConfig(FileConfigUpdateReqVO updateReqVO) {
 | 
			
		||||
        // 校验存在
 | 
			
		||||
        FileConfigDO config = this.validateFileConfigExists(updateReqVO.getId());
 | 
			
		||||
        // 更新
 | 
			
		||||
        FileConfigDO updateObj = FileConfigConvert.INSTANCE.convert(updateReqVO)
 | 
			
		||||
                .setConfig(parseClientConfig(config.getStorage(), updateReqVO.getConfig()));
 | 
			
		||||
        fileConfigMapper.updateById(updateObj);
 | 
			
		||||
        // 发送刷新配置的消息
 | 
			
		||||
        fileConfigProducer.sendFileConfigRefreshMessage();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    @Transactional(rollbackFor = Exception.class)
 | 
			
		||||
    public void updateFileConfigMaster(Long id) {
 | 
			
		||||
        // 校验存在
 | 
			
		||||
        this.validateFileConfigExists(id);
 | 
			
		||||
        // 更新其它为非 master
 | 
			
		||||
        fileConfigMapper.updateBatch(new FileConfigDO().setMaster(false));
 | 
			
		||||
        // 更新
 | 
			
		||||
        fileConfigMapper.updateById(new FileConfigDO().setId(id).setMaster(true));
 | 
			
		||||
        // 发送刷新配置的消息
 | 
			
		||||
        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
 | 
			
		||||
 | 
			
		||||
            @Override
 | 
			
		||||
            public void afterCommit() {
 | 
			
		||||
                fileConfigProducer.sendFileConfigRefreshMessage();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private FileClientConfig parseClientConfig(Integer storage, Map<String, Object> config) {
 | 
			
		||||
        // 获取配置类
 | 
			
		||||
        Class<? extends FileClientConfig> configClass = FileStorageEnum.getByStorage(storage)
 | 
			
		||||
                .getConfigClass();
 | 
			
		||||
        FileClientConfig clientConfig = JsonUtils.parseObject2(JsonUtils.toJsonString(config), configClass);
 | 
			
		||||
        // 参数校验
 | 
			
		||||
        ValidationUtils.validate(validator, clientConfig);
 | 
			
		||||
        // 设置参数
 | 
			
		||||
        return clientConfig;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void deleteFileConfig(Long id) {
 | 
			
		||||
        // 校验存在
 | 
			
		||||
        FileConfigDO config = this.validateFileConfigExists(id);
 | 
			
		||||
        if (Boolean.TRUE.equals(config.getMaster())) {
 | 
			
		||||
             throw exception(FILE_CONFIG_DELETE_FAIL_MASTER);
 | 
			
		||||
        }
 | 
			
		||||
        // 删除
 | 
			
		||||
        fileConfigMapper.deleteById(id);
 | 
			
		||||
        // 发送刷新配置的消息
 | 
			
		||||
        fileConfigProducer.sendFileConfigRefreshMessage();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private FileConfigDO validateFileConfigExists(Long id) {
 | 
			
		||||
        FileConfigDO config = fileConfigMapper.selectById(id);
 | 
			
		||||
        if (config == null) {
 | 
			
		||||
            throw exception(FILE_CONFIG_NOT_EXISTS);
 | 
			
		||||
        }
 | 
			
		||||
        return config;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public FileConfigDO getFileConfig(Long id) {
 | 
			
		||||
        return fileConfigMapper.selectById(id);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public List<FileConfigDO> getFileConfigList(Collection<Long> ids) {
 | 
			
		||||
        return fileConfigMapper.selectBatchIds(ids);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public PageResult<FileConfigDO> getFileConfigPage(FileConfigPageReqVO pageReqVO) {
 | 
			
		||||
        return fileConfigMapper.selectPage(pageReqVO);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public String testFileConfig(Long id) throws Exception {
 | 
			
		||||
        // 校验存在
 | 
			
		||||
        this.validateFileConfigExists(id);
 | 
			
		||||
        // 上传文件
 | 
			
		||||
        byte[] content = ResourceUtil.readBytes("file/erweima.jpg");
 | 
			
		||||
        return fileClientFactory.getFileClient(id).upload(content, IdUtil.fastSimpleUUID() + ".jpg");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public FileClient getFileClient(Long id) {
 | 
			
		||||
        return fileClientFactory.getFileClient(id);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
package cn.iocoder.yudao.module.infra.service.file;
 | 
			
		||||
 | 
			
		||||
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.FilePageReqVO;
 | 
			
		||||
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO;
 | 
			
		||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
 | 
			
		||||
import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO;
 | 
			
		||||
 | 
			
		||||
@@ -26,21 +26,22 @@ public interface FileService {
 | 
			
		||||
     * @param content 文件内容
 | 
			
		||||
     * @return 文件路径
 | 
			
		||||
     */
 | 
			
		||||
    String createFile(String path, byte[] content);
 | 
			
		||||
    String createFile(String path, byte[] content) throws Exception;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 删除文件
 | 
			
		||||
     *
 | 
			
		||||
     * @param id 编号
 | 
			
		||||
     */
 | 
			
		||||
    void deleteFile(String id);
 | 
			
		||||
    void deleteFile(Long id) throws Exception;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获得文件
 | 
			
		||||
     * 获得文件内容
 | 
			
		||||
     *
 | 
			
		||||
     * @param configId 配置编号
 | 
			
		||||
     * @param path 文件路径
 | 
			
		||||
     * @return 文件
 | 
			
		||||
     * @return 文件内容
 | 
			
		||||
     */
 | 
			
		||||
    FileDO getFile(String path);
 | 
			
		||||
    byte[] getFileContent(Long configId, String path) throws Exception;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,18 +1,19 @@
 | 
			
		||||
package cn.iocoder.yudao.module.infra.service.file;
 | 
			
		||||
 | 
			
		||||
import cn.hutool.core.io.FileTypeUtil;
 | 
			
		||||
import cn.hutool.core.lang.Assert;
 | 
			
		||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
 | 
			
		||||
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.FilePageReqVO;
 | 
			
		||||
import cn.iocoder.yudao.framework.file.core.client.FileClient;
 | 
			
		||||
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.mysql.file.FileMapper;
 | 
			
		||||
import cn.iocoder.yudao.module.infra.framework.file.config.FileProperties;
 | 
			
		||||
import org.springframework.stereotype.Service;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Resource;
 | 
			
		||||
import java.io.ByteArrayInputStream;
 | 
			
		||||
 | 
			
		||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 | 
			
		||||
import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*;
 | 
			
		||||
import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_NOT_EXISTS;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 文件 Service 实现类
 | 
			
		||||
@@ -23,10 +24,10 @@ import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*;
 | 
			
		||||
public class FileServiceImpl implements FileService {
 | 
			
		||||
 | 
			
		||||
    @Resource
 | 
			
		||||
    private FileMapper fileMapper;
 | 
			
		||||
    private FileConfigService fileConfigService;
 | 
			
		||||
 | 
			
		||||
    @Resource
 | 
			
		||||
    private FileProperties fileProperties;
 | 
			
		||||
    private FileMapper fileMapper;
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public PageResult<FileDO> getFilePage(FilePageReqVO pageReqVO) {
 | 
			
		||||
@@ -34,37 +35,50 @@ public class FileServiceImpl implements FileService {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public String createFile(String path, byte[] content) {
 | 
			
		||||
        if (fileMapper.selectCountById(path) > 0) {
 | 
			
		||||
            throw exception(FILE_PATH_EXISTS);
 | 
			
		||||
        }
 | 
			
		||||
    public String createFile(String path, byte[] content) throws Exception {
 | 
			
		||||
        // 上传到文件存储器
 | 
			
		||||
        FileClient client = fileConfigService.getMasterFileClient();
 | 
			
		||||
        Assert.notNull(client, "客户端(master) 不能为空");
 | 
			
		||||
        String url = client.upload(content, path);
 | 
			
		||||
 | 
			
		||||
        // 保存到数据库
 | 
			
		||||
        FileDO file = new FileDO();
 | 
			
		||||
        file.setId(path);
 | 
			
		||||
        file.setConfigId(client.getId());
 | 
			
		||||
        file.setPath(path);
 | 
			
		||||
        file.setUrl(url);
 | 
			
		||||
        file.setType(FileTypeUtil.getType(new ByteArrayInputStream(content)));
 | 
			
		||||
        file.setContent(content);
 | 
			
		||||
        file.setSize(content.length);
 | 
			
		||||
        fileMapper.insert(file);
 | 
			
		||||
        // 拼接路径返回
 | 
			
		||||
        return fileProperties.getBasePath() + path;
 | 
			
		||||
        return url;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void deleteFile(String id) {
 | 
			
		||||
    public void deleteFile(Long id) throws Exception {
 | 
			
		||||
        // 校验存在
 | 
			
		||||
        this.validateFileExists(id);
 | 
			
		||||
        // 更新
 | 
			
		||||
        FileDO file = this.validateFileExists(id);
 | 
			
		||||
 | 
			
		||||
        // 从文件存储器中删除
 | 
			
		||||
        FileClient client = fileConfigService.getFileClient(file.getConfigId());
 | 
			
		||||
        Assert.notNull(client, "客户端({}) 不能为空", file.getConfigId());
 | 
			
		||||
        client.delete(file.getPath());
 | 
			
		||||
 | 
			
		||||
        // 删除记录
 | 
			
		||||
        fileMapper.deleteById(id);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void validateFileExists(String id) {
 | 
			
		||||
        if (fileMapper.selectById(id) == null) {
 | 
			
		||||
    private FileDO validateFileExists(Long id) {
 | 
			
		||||
        FileDO fileDO = fileMapper.selectById(id);
 | 
			
		||||
        if (fileDO == null) {
 | 
			
		||||
            throw exception(FILE_NOT_EXISTS);
 | 
			
		||||
        }
 | 
			
		||||
        return fileDO;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public FileDO getFile(String path) {
 | 
			
		||||
        return fileMapper.selectByPath(path);
 | 
			
		||||
    public byte[] getFileContent(Long configId, String path) throws Exception {
 | 
			
		||||
        FileClient client = fileConfigService.getFileClient(configId);
 | 
			
		||||
        Assert.notNull(client, "客户端({}) 不能为空", configId);
 | 
			
		||||
        return client.getContent(path);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@
 | 
			
		||||
  <div class="app-container">
 | 
			
		||||
 | 
			
		||||
    <!-- 搜索工作栏 -->
 | 
			
		||||
    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
 | 
			
		||||
    <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
 | 
			
		||||
#foreach($column in $columns)
 | 
			
		||||
#if ($column.listOperation)
 | 
			
		||||
    #set ($dictType=$column.dictType)
 | 
			
		||||
@@ -11,7 +11,7 @@
 | 
			
		||||
    #set ($comment=$column.columnComment)
 | 
			
		||||
#if ($column.htmlType == "input")
 | 
			
		||||
      <el-form-item label="${comment}" prop="${javaField}">
 | 
			
		||||
        <el-input v-model="queryParams.${javaField}" placeholder="请输入${comment}" clearable size="small" @keyup.enter.native="handleQuery"/>
 | 
			
		||||
        <el-input v-model="queryParams.${javaField}" placeholder="请输入${comment}" clearable @keyup.enter.native="handleQuery"/>
 | 
			
		||||
      </el-form-item>
 | 
			
		||||
#elseif ($column.htmlType == "select" || $column.htmlType == "radio")
 | 
			
		||||
      <el-form-item label="${comment}" prop="${javaField}">
 | 
			
		||||
@@ -27,11 +27,11 @@
 | 
			
		||||
#elseif($column.htmlType == "datetime")
 | 
			
		||||
    #if ($column.listOperationCondition != "BETWEEN")## 非范围
 | 
			
		||||
      <el-form-item label="${comment}" prop="${javaField}">
 | 
			
		||||
        <el-date-picker clearable size="small" v-model="queryParams.${javaField}" type="date" value-format="yyyy-MM-dd" placeholder="选择${comment}" />
 | 
			
		||||
        <el-date-picker clearable v-model="queryParams.${javaField}" type="date" value-format="yyyy-MM-dd" placeholder="选择${comment}" />
 | 
			
		||||
      </el-form-item>
 | 
			
		||||
    #else## 范围
 | 
			
		||||
      <el-form-item label="${comment}">
 | 
			
		||||
        <el-date-picker v-model="dateRange${AttrName}" size="small" style="width: 240px" value-format="yyyy-MM-dd"
 | 
			
		||||
        <el-date-picker v-model="dateRange${AttrName}" style="width: 240px" value-format="yyyy-MM-dd"
 | 
			
		||||
                        type="daterange" range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期" />
 | 
			
		||||
      </el-form-item>
 | 
			
		||||
    #end
 | 
			
		||||
@@ -39,8 +39,8 @@
 | 
			
		||||
#end
 | 
			
		||||
#end
 | 
			
		||||
      <el-form-item>
 | 
			
		||||
        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
 | 
			
		||||
        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
 | 
			
		||||
        <el-button type="primary" icon="el-icon-search" @click="handleQuery">搜索</el-button>
 | 
			
		||||
        <el-button icon="el-icon-refresh" @click="resetQuery">重置</el-button>
 | 
			
		||||
      </el-form-item>
 | 
			
		||||
    </el-form>
 | 
			
		||||
 | 
			
		||||
@@ -160,7 +160,7 @@
 | 
			
		||||
        </el-form-item>
 | 
			
		||||
#elseif($column.htmlType == "datetime")## 时间框
 | 
			
		||||
        <el-form-item label="${comment}" prop="${javaField}">
 | 
			
		||||
          <el-date-picker clearable size="small" v-model="form.${javaField}" type="date" value-format="yyyy-MM-dd" placeholder="选择${comment}" />
 | 
			
		||||
          <el-date-picker clearable v-model="form.${javaField}" type="date" value-format="yyyy-MM-dd" placeholder="选择${comment}" />
 | 
			
		||||
        </el-form-item>
 | 
			
		||||
#elseif($column.htmlType == "textarea")## 文本框
 | 
			
		||||
        <el-form-item label="${comment}" prop="${javaField}">
 | 
			
		||||
 
 | 
			
		||||
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 18 KiB  | 
@@ -0,0 +1,256 @@
 | 
			
		||||
package cn.iocoder.yudao.module.infra.service.file;
 | 
			
		||||
 | 
			
		||||
import cn.hutool.core.map.MapUtil;
 | 
			
		||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
 | 
			
		||||
import cn.iocoder.yudao.framework.file.core.client.FileClient;
 | 
			
		||||
import cn.iocoder.yudao.framework.file.core.client.FileClientConfig;
 | 
			
		||||
import cn.iocoder.yudao.framework.file.core.client.FileClientFactory;
 | 
			
		||||
import cn.iocoder.yudao.framework.file.core.client.local.LocalFileClientConfig;
 | 
			
		||||
import cn.iocoder.yudao.framework.file.core.enums.FileStorageEnum;
 | 
			
		||||
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.config.FileConfigCreateReqVO;
 | 
			
		||||
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.config.FileConfigPageReqVO;
 | 
			
		||||
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.config.FileConfigUpdateReqVO;
 | 
			
		||||
import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileConfigDO;
 | 
			
		||||
import cn.iocoder.yudao.module.infra.dal.mysql.file.FileConfigMapper;
 | 
			
		||||
import cn.iocoder.yudao.module.infra.mq.producer.file.FileConfigProducer;
 | 
			
		||||
import cn.iocoder.yudao.module.infra.test.BaseDbUnitTest;
 | 
			
		||||
import lombok.Data;
 | 
			
		||||
import org.junit.jupiter.api.Test;
 | 
			
		||||
import org.springframework.boot.test.mock.mockito.MockBean;
 | 
			
		||||
import org.springframework.context.annotation.Import;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Resource;
 | 
			
		||||
import javax.validation.Validator;
 | 
			
		||||
import java.io.Serializable;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
 | 
			
		||||
import static cn.hutool.core.util.RandomUtil.randomEle;
 | 
			
		||||
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.buildTime;
 | 
			
		||||
import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId;
 | 
			
		||||
import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.max;
 | 
			
		||||
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals;
 | 
			
		||||
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
 | 
			
		||||
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
 | 
			
		||||
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
 | 
			
		||||
import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_CONFIG_DELETE_FAIL_MASTER;
 | 
			
		||||
import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_CONFIG_NOT_EXISTS;
 | 
			
		||||
import static org.junit.jupiter.api.Assertions.*;
 | 
			
		||||
import static org.mockito.ArgumentMatchers.eq;
 | 
			
		||||
import static org.mockito.Mockito.*;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
* {@link FileConfigServiceImpl} 的单元测试类
 | 
			
		||||
*
 | 
			
		||||
* @author 芋道源码
 | 
			
		||||
*/
 | 
			
		||||
@Import(FileConfigServiceImpl.class)
 | 
			
		||||
public class FileConfigServiceImplTest extends BaseDbUnitTest {
 | 
			
		||||
 | 
			
		||||
    @Resource
 | 
			
		||||
    private FileConfigServiceImpl fileConfigService;
 | 
			
		||||
 | 
			
		||||
    @Resource
 | 
			
		||||
    private FileConfigMapper fileConfigMapper;
 | 
			
		||||
 | 
			
		||||
    @MockBean
 | 
			
		||||
    private FileConfigProducer fileConfigProducer;
 | 
			
		||||
    @MockBean
 | 
			
		||||
    private Validator validator;
 | 
			
		||||
    @MockBean
 | 
			
		||||
    private FileClientFactory fileClientFactory;
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testInitLocalCache() {
 | 
			
		||||
        // mock 数据
 | 
			
		||||
        FileConfigDO configDO1 = randomFileConfigDO().setId(1L).setMaster(true);
 | 
			
		||||
        fileConfigMapper.insert(configDO1);
 | 
			
		||||
        FileConfigDO configDO2 = randomFileConfigDO().setId(2L).setMaster(false);
 | 
			
		||||
        fileConfigMapper.insert(configDO2);
 | 
			
		||||
        // mock fileClientFactory 获得 master
 | 
			
		||||
        FileClient masterFileClient = mock(FileClient.class);
 | 
			
		||||
        when(fileClientFactory.getFileClient(eq(1L))).thenReturn(masterFileClient);
 | 
			
		||||
 | 
			
		||||
        // 调用
 | 
			
		||||
        fileConfigService.initFileClients();
 | 
			
		||||
        // 断言 fileClientFactory 调用
 | 
			
		||||
        verify(fileClientFactory).createOrUpdateFileClient(eq(1L),
 | 
			
		||||
                eq(configDO1.getStorage()), eq(configDO1.getConfig()));
 | 
			
		||||
        verify(fileClientFactory).createOrUpdateFileClient(eq(2L),
 | 
			
		||||
                eq(configDO2.getStorage()), eq(configDO2.getConfig()));
 | 
			
		||||
        assertSame(masterFileClient, fileConfigService.getMasterFileClient());
 | 
			
		||||
        // 断言 maxUpdateTime 缓存
 | 
			
		||||
        assertEquals(max(configDO1.getUpdateTime(), configDO2.getUpdateTime()),
 | 
			
		||||
                fileConfigService.getMaxUpdateTime());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testCreateFileConfig_success() {
 | 
			
		||||
        // 准备参数
 | 
			
		||||
        Map<String, Object> config = MapUtil.<String, Object>builder().put("basePath", "/yunai")
 | 
			
		||||
                .put("domain", "https://www.iocoder.cn").build();
 | 
			
		||||
        FileConfigCreateReqVO reqVO = randomPojo(FileConfigCreateReqVO.class,
 | 
			
		||||
                o -> o.setStorage(FileStorageEnum.LOCAL.getStorage()).setConfig(config));
 | 
			
		||||
 | 
			
		||||
        // 调用
 | 
			
		||||
        Long fileConfigId = fileConfigService.createFileConfig(reqVO);
 | 
			
		||||
        // 断言
 | 
			
		||||
        assertNotNull(fileConfigId);
 | 
			
		||||
        // 校验记录的属性是否正确
 | 
			
		||||
        FileConfigDO fileConfig = fileConfigMapper.selectById(fileConfigId);
 | 
			
		||||
        assertPojoEquals(reqVO, fileConfig, "config");
 | 
			
		||||
        assertFalse(fileConfig.getMaster());
 | 
			
		||||
        assertEquals("/yunai", ((LocalFileClientConfig) fileConfig.getConfig()).getBasePath());
 | 
			
		||||
        assertEquals("https://www.iocoder.cn", ((LocalFileClientConfig) fileConfig.getConfig()).getDomain());
 | 
			
		||||
        // verify 调用
 | 
			
		||||
        verify(fileConfigProducer).sendFileConfigRefreshMessage();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testUpdateFileConfig_success() {
 | 
			
		||||
        // mock 数据
 | 
			
		||||
        FileConfigDO dbFileConfig = randomPojo(FileConfigDO.class, o -> o.setStorage(FileStorageEnum.LOCAL.getStorage())
 | 
			
		||||
                .setConfig(new LocalFileClientConfig().setBasePath("/yunai").setDomain("https://www.iocoder.cn")));
 | 
			
		||||
        fileConfigMapper.insert(dbFileConfig);// @Sql: 先插入出一条存在的数据
 | 
			
		||||
        // 准备参数
 | 
			
		||||
        FileConfigUpdateReqVO reqVO = randomPojo(FileConfigUpdateReqVO.class, o -> {
 | 
			
		||||
            o.setId(dbFileConfig.getId()); // 设置更新的 ID
 | 
			
		||||
            Map<String, Object> config = MapUtil.<String, Object>builder().put("basePath", "/yunai2")
 | 
			
		||||
                    .put("domain", "https://doc.iocoder.cn").build();
 | 
			
		||||
            o.setConfig(config);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // 调用
 | 
			
		||||
        fileConfigService.updateFileConfig(reqVO);
 | 
			
		||||
        // 校验是否更新正确
 | 
			
		||||
        FileConfigDO fileConfig = fileConfigMapper.selectById(reqVO.getId()); // 获取最新的
 | 
			
		||||
        assertPojoEquals(reqVO, fileConfig, "config");
 | 
			
		||||
        assertEquals("/yunai2", ((LocalFileClientConfig) fileConfig.getConfig()).getBasePath());
 | 
			
		||||
        assertEquals("https://doc.iocoder.cn", ((LocalFileClientConfig) fileConfig.getConfig()).getDomain());
 | 
			
		||||
        // verify 调用
 | 
			
		||||
        verify(fileConfigProducer).sendFileConfigRefreshMessage();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testUpdateFileConfig_notExists() {
 | 
			
		||||
        // 准备参数
 | 
			
		||||
        FileConfigUpdateReqVO reqVO = randomPojo(FileConfigUpdateReqVO.class);
 | 
			
		||||
 | 
			
		||||
        // 调用, 并断言异常
 | 
			
		||||
        assertServiceException(() -> fileConfigService.updateFileConfig(reqVO), FILE_CONFIG_NOT_EXISTS);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testUpdateFileConfigMaster_success() {
 | 
			
		||||
        // mock 数据
 | 
			
		||||
        FileConfigDO dbFileConfig = randomFileConfigDO().setMaster(false);
 | 
			
		||||
        fileConfigMapper.insert(dbFileConfig);// @Sql: 先插入出一条存在的数据
 | 
			
		||||
        FileConfigDO masterFileConfig = randomFileConfigDO().setMaster(true);
 | 
			
		||||
        fileConfigMapper.insert(masterFileConfig);// @Sql: 先插入出一条存在的数据
 | 
			
		||||
 | 
			
		||||
        // 调用
 | 
			
		||||
        fileConfigService.updateFileConfigMaster(dbFileConfig.getId());
 | 
			
		||||
        // 断言数据
 | 
			
		||||
        assertTrue(fileConfigMapper.selectById(dbFileConfig.getId()).getMaster());
 | 
			
		||||
        assertFalse(fileConfigMapper.selectById(masterFileConfig.getId()).getMaster());
 | 
			
		||||
        // verify 调用
 | 
			
		||||
        verify(fileConfigProducer).sendFileConfigRefreshMessage();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testUpdateFileConfigMaster_notExists() {
 | 
			
		||||
        // 调用, 并断言异常
 | 
			
		||||
        assertServiceException(() -> fileConfigService.updateFileConfigMaster(randomLongId()), FILE_CONFIG_NOT_EXISTS);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testDeleteFileConfig_success() {
 | 
			
		||||
        // mock 数据
 | 
			
		||||
        FileConfigDO dbFileConfig = randomFileConfigDO().setMaster(false);
 | 
			
		||||
        fileConfigMapper.insert(dbFileConfig);// @Sql: 先插入出一条存在的数据
 | 
			
		||||
        // 准备参数
 | 
			
		||||
        Long id = dbFileConfig.getId();
 | 
			
		||||
 | 
			
		||||
        // 调用
 | 
			
		||||
        fileConfigService.deleteFileConfig(id);
 | 
			
		||||
       // 校验数据不存在了
 | 
			
		||||
       assertNull(fileConfigMapper.selectById(id));
 | 
			
		||||
        // verify 调用
 | 
			
		||||
        verify(fileConfigProducer).sendFileConfigRefreshMessage();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testDeleteFileConfig_notExists() {
 | 
			
		||||
        // 准备参数
 | 
			
		||||
        Long id = randomLongId();
 | 
			
		||||
 | 
			
		||||
        // 调用, 并断言异常
 | 
			
		||||
        assertServiceException(() -> fileConfigService.deleteFileConfig(id), FILE_CONFIG_NOT_EXISTS);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testDeleteFileConfig_master() {
 | 
			
		||||
        // mock 数据
 | 
			
		||||
        FileConfigDO dbFileConfig = randomFileConfigDO().setMaster(true);
 | 
			
		||||
        fileConfigMapper.insert(dbFileConfig);// @Sql: 先插入出一条存在的数据
 | 
			
		||||
        // 准备参数
 | 
			
		||||
        Long id = dbFileConfig.getId();
 | 
			
		||||
 | 
			
		||||
        // 调用, 并断言异常
 | 
			
		||||
        assertServiceException(() -> fileConfigService.deleteFileConfig(id), FILE_CONFIG_DELETE_FAIL_MASTER);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testGetFileConfigPage() {
 | 
			
		||||
       // mock 数据
 | 
			
		||||
       FileConfigDO dbFileConfig = randomFileConfigDO().setName("芋道源码")
 | 
			
		||||
               .setStorage(FileStorageEnum.LOCAL.getStorage());
 | 
			
		||||
       dbFileConfig.setCreateTime(buildTime(2022, 11, 11));// 等会查询到
 | 
			
		||||
       fileConfigMapper.insert(dbFileConfig);
 | 
			
		||||
       // 测试 name 不匹配
 | 
			
		||||
       fileConfigMapper.insert(cloneIgnoreId(dbFileConfig, o -> o.setName("源码")));
 | 
			
		||||
       // 测试 storage 不匹配
 | 
			
		||||
       fileConfigMapper.insert(cloneIgnoreId(dbFileConfig, o -> o.setStorage(FileStorageEnum.DB.getStorage())));
 | 
			
		||||
       // 测试 createTime 不匹配
 | 
			
		||||
       fileConfigMapper.insert(cloneIgnoreId(dbFileConfig, o -> o.setCreateTime(buildTime(2022, 12, 12))));
 | 
			
		||||
       // 准备参数
 | 
			
		||||
       FileConfigPageReqVO reqVO = new FileConfigPageReqVO();
 | 
			
		||||
       reqVO.setName("芋道");
 | 
			
		||||
       reqVO.setStorage(FileStorageEnum.LOCAL.getStorage());
 | 
			
		||||
       reqVO.setBeginCreateTime(buildTime(2022, 11, 10));
 | 
			
		||||
       reqVO.setEndCreateTime(buildTime(2022, 11, 12));
 | 
			
		||||
 | 
			
		||||
       // 调用
 | 
			
		||||
       PageResult<FileConfigDO> pageResult = fileConfigService.getFileConfigPage(reqVO);
 | 
			
		||||
       // 断言
 | 
			
		||||
       assertEquals(1, pageResult.getTotal());
 | 
			
		||||
       assertEquals(1, pageResult.getList().size());
 | 
			
		||||
       assertPojoEquals(dbFileConfig, pageResult.getList().get(0));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testFileConfig() throws Exception {
 | 
			
		||||
        // mock 数据
 | 
			
		||||
        FileConfigDO dbFileConfig = randomFileConfigDO().setMaster(false);
 | 
			
		||||
        fileConfigMapper.insert(dbFileConfig);// @Sql: 先插入出一条存在的数据
 | 
			
		||||
        // 准备参数
 | 
			
		||||
        Long id = dbFileConfig.getId();
 | 
			
		||||
        // mock 获得 Client
 | 
			
		||||
        FileClient fileClient = mock(FileClient.class);
 | 
			
		||||
        when(fileClientFactory.getFileClient(eq(id))).thenReturn(fileClient);
 | 
			
		||||
        when(fileClient.upload(any(), any())).thenReturn("https://www.iocoder.cn");
 | 
			
		||||
 | 
			
		||||
        // 调用,并断言
 | 
			
		||||
        assertEquals("https://www.iocoder.cn", fileConfigService.testFileConfig(id));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private FileConfigDO randomFileConfigDO() {
 | 
			
		||||
        return randomPojo(FileConfigDO.class).setStorage(randomEle(FileStorageEnum.values()).getStorage())
 | 
			
		||||
                .setConfig(new EmptyFileClientConfig());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Data
 | 
			
		||||
    public static class EmptyFileClientConfig implements FileClientConfig, Serializable {
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -3,11 +3,11 @@ package cn.iocoder.yudao.module.infra.service.file;
 | 
			
		||||
import cn.hutool.core.io.resource.ResourceUtil;
 | 
			
		||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
 | 
			
		||||
import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
 | 
			
		||||
import cn.iocoder.yudao.framework.file.core.client.FileClient;
 | 
			
		||||
import cn.iocoder.yudao.framework.test.core.util.AssertUtils;
 | 
			
		||||
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.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.mysql.file.FileMapper;
 | 
			
		||||
import cn.iocoder.yudao.module.infra.framework.file.config.FileProperties;
 | 
			
		||||
import cn.iocoder.yudao.module.infra.test.BaseDbUnitTest;
 | 
			
		||||
import org.junit.jupiter.api.Test;
 | 
			
		||||
import org.springframework.boot.test.mock.mockito.MockBean;
 | 
			
		||||
@@ -17,47 +17,46 @@ import javax.annotation.Resource;
 | 
			
		||||
 | 
			
		||||
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.buildTime;
 | 
			
		||||
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
 | 
			
		||||
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
 | 
			
		||||
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString;
 | 
			
		||||
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
 | 
			
		||||
import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*;
 | 
			
		||||
import static org.junit.jupiter.api.Assertions.*;
 | 
			
		||||
import static org.mockito.ArgumentMatchers.same;
 | 
			
		||||
import static org.mockito.Mockito.*;
 | 
			
		||||
 | 
			
		||||
@Import({FileServiceImpl.class, FileProperties.class})
 | 
			
		||||
@Import({FileServiceImpl.class})
 | 
			
		||||
public class FileServiceTest extends BaseDbUnitTest {
 | 
			
		||||
 | 
			
		||||
    @Resource
 | 
			
		||||
    private FileService fileService;
 | 
			
		||||
 | 
			
		||||
    @MockBean
 | 
			
		||||
    private FileProperties fileProperties;
 | 
			
		||||
 | 
			
		||||
    @Resource
 | 
			
		||||
    private FileMapper fileMapper;
 | 
			
		||||
 | 
			
		||||
    @MockBean
 | 
			
		||||
    private FileConfigService fileConfigService;
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testGetFilePage() {
 | 
			
		||||
        // mock 数据
 | 
			
		||||
        FileDO dbFile = randomPojo(FileDO.class, o -> { // 等会查询到
 | 
			
		||||
            o.setId("yunai");
 | 
			
		||||
            o.setPath("yunai");
 | 
			
		||||
            o.setType("jpg");
 | 
			
		||||
            o.setCreateTime(buildTime(2021, 1, 15));
 | 
			
		||||
        });
 | 
			
		||||
        fileMapper.insert(dbFile);
 | 
			
		||||
        // 测试 id 不匹配
 | 
			
		||||
        fileMapper.insert(ObjectUtils.cloneIgnoreId(dbFile, o -> o.setId("tudou")));
 | 
			
		||||
        // 测试 path 不匹配
 | 
			
		||||
        fileMapper.insert(ObjectUtils.cloneIgnoreId(dbFile, o -> o.setPath("tudou")));
 | 
			
		||||
        // 测试 type 不匹配
 | 
			
		||||
        fileMapper.insert(ObjectUtils.cloneIgnoreId(dbFile, o -> {
 | 
			
		||||
            o.setId("yunai02");
 | 
			
		||||
            o.setType("png");
 | 
			
		||||
        }));
 | 
			
		||||
        // 测试 createTime 不匹配
 | 
			
		||||
        fileMapper.insert(ObjectUtils.cloneIgnoreId(dbFile, o -> {
 | 
			
		||||
            o.setId("yunai03");
 | 
			
		||||
            o.setCreateTime(buildTime(2020, 1, 15));
 | 
			
		||||
        }));
 | 
			
		||||
        // 准备参数
 | 
			
		||||
        FilePageReqVO reqVO = new FilePageReqVO();
 | 
			
		||||
        reqVO.setId("yunai");
 | 
			
		||||
        reqVO.setPath("yunai");
 | 
			
		||||
        reqVO.setType("jp");
 | 
			
		||||
        reqVO.setBeginCreateTime(buildTime(2021, 1, 10));
 | 
			
		||||
        reqVO.setEndCreateTime(buildTime(2021, 1, 20));
 | 
			
		||||
@@ -67,60 +66,76 @@ public class FileServiceTest extends BaseDbUnitTest {
 | 
			
		||||
        // 断言
 | 
			
		||||
        assertEquals(1, pageResult.getTotal());
 | 
			
		||||
        assertEquals(1, pageResult.getList().size());
 | 
			
		||||
        AssertUtils.assertPojoEquals(dbFile, pageResult.getList().get(0), "content");
 | 
			
		||||
        AssertUtils.assertPojoEquals(dbFile, pageResult.getList().get(0));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testCreateFile_success() {
 | 
			
		||||
    public void testCreateFile_success() throws Exception {
 | 
			
		||||
        // 准备参数
 | 
			
		||||
        String path = randomString();
 | 
			
		||||
        byte[] content = ResourceUtil.readBytes("file/erweima.jpg");
 | 
			
		||||
        // mock Master 文件客户端
 | 
			
		||||
        FileClient client = mock(FileClient.class);
 | 
			
		||||
        when(fileConfigService.getMasterFileClient()).thenReturn(client);
 | 
			
		||||
        String url = randomString();
 | 
			
		||||
        when(client.upload(same(content), same(path))).thenReturn(url);
 | 
			
		||||
        when(client.getId()).thenReturn(10L);
 | 
			
		||||
 | 
			
		||||
        // 调用
 | 
			
		||||
        String url = fileService.createFile(path, content);
 | 
			
		||||
        String result = fileService.createFile(path, content);
 | 
			
		||||
        // 断言
 | 
			
		||||
        assertEquals(fileProperties.getBasePath() + path, url);
 | 
			
		||||
        assertEquals(result, url);
 | 
			
		||||
        // 校验数据
 | 
			
		||||
        FileDO file = fileMapper.selectById(path);
 | 
			
		||||
        assertEquals(path, file.getId());
 | 
			
		||||
        FileDO file = fileMapper.selectOne(FileDO::getPath, path);
 | 
			
		||||
        assertEquals(10L, file.getConfigId());
 | 
			
		||||
        assertEquals(path, file.getPath());
 | 
			
		||||
        assertEquals(url, file.getUrl());
 | 
			
		||||
        assertEquals("jpg", file.getType());
 | 
			
		||||
        assertArrayEquals(content, file.getContent());
 | 
			
		||||
        assertEquals(content.length, file.getSize());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testCreateFile_exists() {
 | 
			
		||||
    public void testDeleteFile_success() throws Exception {
 | 
			
		||||
        // mock 数据
 | 
			
		||||
        FileDO dbFile = randomPojo(FileDO.class);
 | 
			
		||||
        fileMapper.insert(dbFile);
 | 
			
		||||
        // 准备参数
 | 
			
		||||
        String path = dbFile.getId(); // 模拟已存在
 | 
			
		||||
        byte[] content = ResourceUtil.readBytes("file/erweima.jpg");
 | 
			
		||||
 | 
			
		||||
        // 调用,并断言异常
 | 
			
		||||
        assertServiceException(() -> fileService.createFile(path, content), FILE_PATH_EXISTS);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testDeleteFile_success() {
 | 
			
		||||
        // mock 数据
 | 
			
		||||
        FileDO dbFile = randomPojo(FileDO.class);
 | 
			
		||||
        FileDO dbFile = randomPojo(FileDO.class, o -> o.setConfigId(10L).setPath("tudou.jpg"));
 | 
			
		||||
        fileMapper.insert(dbFile);// @Sql: 先插入出一条存在的数据
 | 
			
		||||
        // mock Master 文件客户端
 | 
			
		||||
        FileClient client = mock(FileClient.class);
 | 
			
		||||
        when(fileConfigService.getFileClient(eq(10L))).thenReturn(client);
 | 
			
		||||
        // 准备参数
 | 
			
		||||
        String id = dbFile.getId();
 | 
			
		||||
        Long id = dbFile.getId();
 | 
			
		||||
 | 
			
		||||
        // 调用
 | 
			
		||||
        fileService.deleteFile(id);
 | 
			
		||||
        // 校验数据不存在了
 | 
			
		||||
        assertNull(fileMapper.selectById(id));
 | 
			
		||||
        // 校验调用
 | 
			
		||||
        verify(client).delete(eq("tudou.jpg"));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testDeleteFile_notExists() {
 | 
			
		||||
        // 准备参数
 | 
			
		||||
        String id = randomString();
 | 
			
		||||
        Long id = randomLongId();
 | 
			
		||||
 | 
			
		||||
        // 调用, 并断言异常
 | 
			
		||||
        assertServiceException(() -> fileService.deleteFile(id), FILE_NOT_EXISTS);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testGetFileContent() throws Exception {
 | 
			
		||||
        // 准备参数
 | 
			
		||||
        Long configId = 10L;
 | 
			
		||||
        String path = "tudou.jpg";
 | 
			
		||||
        // mock 方法
 | 
			
		||||
        FileClient client = mock(FileClient.class);
 | 
			
		||||
        when(fileConfigService.getFileClient(eq(10L))).thenReturn(client);
 | 
			
		||||
        byte[] content = new byte[]{};
 | 
			
		||||
        when(client.getContent(eq("tudou.jpg"))).thenReturn(content);
 | 
			
		||||
 | 
			
		||||
        // 调用
 | 
			
		||||
        byte[] result = fileService.getFileContent(configId, path);
 | 
			
		||||
        // 断言
 | 
			
		||||
        assertSame(result, content);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -26,8 +26,9 @@ spring:
 | 
			
		||||
    port: 16379 # 端口(单元测试,使用 16379 端口)
 | 
			
		||||
    database: 0 # 数据库索引
 | 
			
		||||
 | 
			
		||||
mybatis:
 | 
			
		||||
mybatis-plus:
 | 
			
		||||
  lazy-initialization: true # 单元测试,设置 MyBatis Mapper 延迟加载,加速每个单元测试
 | 
			
		||||
  type-aliases-package: ${yudao.info.base-package}.module.*.dal.dataobject
 | 
			
		||||
 | 
			
		||||
--- #################### 定时任务相关配置 ####################
 | 
			
		||||
 | 
			
		||||
@@ -46,4 +47,4 @@ mybatis:
 | 
			
		||||
# 芋道配置项,设置当前项目所有自定义的配置
 | 
			
		||||
yudao:
 | 
			
		||||
  info:
 | 
			
		||||
    base-package: cn.iocoder.yudao.module
 | 
			
		||||
    base-package: cn.iocoder.yudao
 | 
			
		||||
 
 | 
			
		||||
@@ -8,3 +8,4 @@ DELETE FROM "infra_api_access_log";
 | 
			
		||||
DELETE FROM "infra_file";
 | 
			
		||||
DELETE FROM "infra_api_error_log";
 | 
			
		||||
DELETE FROM "infra_test_demo";
 | 
			
		||||
DELETE FROM "infra_file_config";
 | 
			
		||||
 
 | 
			
		||||
@@ -16,10 +16,28 @@ CREATE TABLE IF NOT EXISTS "infra_config" (
 | 
			
		||||
    PRIMARY KEY ("id")
 | 
			
		||||
) COMMENT '参数配置表';
 | 
			
		||||
 | 
			
		||||
CREATE TABLE IF NOT EXISTS "infra_file_config" (
 | 
			
		||||
    "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
 | 
			
		||||
    "name" varchar(63) NOT NULL,
 | 
			
		||||
    "storage" tinyint NOT NULL,
 | 
			
		||||
    "remark" varchar(255),
 | 
			
		||||
    "master" bit(1) NOT NULL,
 | 
			
		||||
    "config" varchar(4096) NOT NULL,
 | 
			
		||||
    "creator" varchar(64) DEFAULT '',
 | 
			
		||||
    "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
 | 
			
		||||
    "updater" varchar(64) DEFAULT '',
 | 
			
		||||
    "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
 | 
			
		||||
    "deleted" bit NOT NULL DEFAULT FALSE,
 | 
			
		||||
    PRIMARY KEY ("id")
 | 
			
		||||
) COMMENT '文件配置表';
 | 
			
		||||
 | 
			
		||||
CREATE TABLE IF NOT EXISTS "infra_file" (
 | 
			
		||||
    "id" varchar(188) NOT NULL,
 | 
			
		||||
    "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
 | 
			
		||||
    "config_id" bigint NOT NULL,
 | 
			
		||||
    "path" varchar(512),
 | 
			
		||||
    "url" varchar(1024),
 | 
			
		||||
    "type" varchar(63) DEFAULT NULL,
 | 
			
		||||
    "content" blob NOT NULL,
 | 
			
		||||
    "size" bigint NOT NULL,
 | 
			
		||||
    "creator" varchar(64) DEFAULT '',
 | 
			
		||||
    "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
 | 
			
		||||
    "updater" varchar(64) DEFAULT '',
 | 
			
		||||
 
 | 
			
		||||
@@ -16,9 +16,8 @@ import org.springframework.web.multipart.MultipartFile;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Resource;
 | 
			
		||||
import javax.validation.Valid;
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
 | 
			
		||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.*;
 | 
			
		||||
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.security.core.util.SecurityFrameworkUtils.getLoginUserId;
 | 
			
		||||
import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_IS_EMPTY;
 | 
			
		||||
@@ -44,7 +43,7 @@ public class AppUserController {
 | 
			
		||||
    @PutMapping("/update-avatar")
 | 
			
		||||
    @ApiOperation("修改用户头像")
 | 
			
		||||
    @PreAuthenticated
 | 
			
		||||
    public CommonResult<String> updateUserAvatar(@RequestParam("avatarFile") MultipartFile file) throws IOException {
 | 
			
		||||
    public CommonResult<String> updateUserAvatar(@RequestParam("avatarFile") MultipartFile file) throws Exception {
 | 
			
		||||
        if (file.isEmpty()) {
 | 
			
		||||
            throw exception(FILE_IS_EMPTY);
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -60,7 +60,7 @@ public interface MemberUserService {
 | 
			
		||||
     * @param inputStream 头像文件
 | 
			
		||||
     * @return 头像url
 | 
			
		||||
     */
 | 
			
		||||
    String updateUserAvatar(Long userId, InputStream inputStream);
 | 
			
		||||
    String updateUserAvatar(Long userId, InputStream inputStream) throws Exception;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 修改手机
 | 
			
		||||
 
 | 
			
		||||
@@ -100,7 +100,7 @@ public class MemberUserServiceImpl implements MemberUserService {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public String updateUserAvatar(Long userId, InputStream avatarFile) {
 | 
			
		||||
    public String updateUserAvatar(Long userId, InputStream avatarFile) throws Exception {
 | 
			
		||||
        this.checkUserExists(userId);
 | 
			
		||||
        // 创建文件
 | 
			
		||||
        String avatar = fileApi.createFile(IoUtil.readBytes(avatarFile));
 | 
			
		||||
 
 | 
			
		||||
@@ -74,7 +74,7 @@ public class MemberUserServiceImplTest extends BaseDbAndRedisUnitTest {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testUpdateAvatar_success(){
 | 
			
		||||
    public void testUpdateAvatar_success() throws Exception {
 | 
			
		||||
        // mock 数据
 | 
			
		||||
        MemberUserDO dbUser = randomUserDO();
 | 
			
		||||
        userMapper.insert(dbUser);
 | 
			
		||||
 
 | 
			
		||||
@@ -127,6 +127,7 @@ public class PayChannelServiceImpl implements PayChannelService {
 | 
			
		||||
        PayChannelDO channel = PayChannelConvert.INSTANCE.convert(reqVO);
 | 
			
		||||
        settingConfigAndCheckParam(channel, reqVO.getConfig());
 | 
			
		||||
        channelMapper.insert(channel);
 | 
			
		||||
        // TODO 芋艿:缺少刷新本地缓存的机制
 | 
			
		||||
        return channel.getId();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -138,6 +139,7 @@ public class PayChannelServiceImpl implements PayChannelService {
 | 
			
		||||
        PayChannelDO channel = PayChannelConvert.INSTANCE.convert(updateReqVO);
 | 
			
		||||
        settingConfigAndCheckParam(channel, updateReqVO.getConfig());
 | 
			
		||||
        channelMapper.updateById(channel);
 | 
			
		||||
        // TODO 芋艿:缺少刷新本地缓存的机制
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
@@ -146,6 +148,7 @@ public class PayChannelServiceImpl implements PayChannelService {
 | 
			
		||||
        this.validateChannelExists(id);
 | 
			
		||||
        // 删除
 | 
			
		||||
        channelMapper.deleteById(id);
 | 
			
		||||
        // TODO 芋艿:缺少刷新本地缓存的机制
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void validateChannelExists(Long id) {
 | 
			
		||||
@@ -224,6 +227,7 @@ public class PayChannelServiceImpl implements PayChannelService {
 | 
			
		||||
        if (ObjectUtil.isNull(payClass)) {
 | 
			
		||||
            throw exception(CHANNEL_NOT_EXISTS);
 | 
			
		||||
        }
 | 
			
		||||
        // TODO @芋艿:不要使用 hutool 的 json 工具,用项目的
 | 
			
		||||
        PayClientConfig config = JSONUtil.toBean(configStr, payClass);
 | 
			
		||||
 | 
			
		||||
        // 验证参数
 | 
			
		||||
 
 | 
			
		||||
@@ -14,16 +14,16 @@ public interface DictTypeConstants {
 | 
			
		||||
 | 
			
		||||
    String USER_SEX = "system_user_sex"; // 用户性别
 | 
			
		||||
 | 
			
		||||
    String OPERATE_TYPE = "sys_operate_type"; // 操作类型
 | 
			
		||||
    String OPERATE_TYPE = "system_operate_type"; // 操作类型
 | 
			
		||||
 | 
			
		||||
    String LOGIN_TYPE = "sys_login_type"; // 登录日志的类型
 | 
			
		||||
    String LOGIN_RESULT = "sys_login_result"; // 登录结果
 | 
			
		||||
    String LOGIN_TYPE = "system_login_type"; // 登录日志的类型
 | 
			
		||||
    String LOGIN_RESULT = "system_login_result"; // 登录结果
 | 
			
		||||
 | 
			
		||||
    String ERROR_CODE_TYPE = "system_error_code_type"; // 错误码的类型枚举
 | 
			
		||||
 | 
			
		||||
    String SMS_CHANNEL_CODE = "sys_sms_channel_code"; // 短信渠道编码
 | 
			
		||||
    String SMS_TEMPLATE_TYPE = "sys_sms_template_type"; // 短信模板类型
 | 
			
		||||
    String SMS_SEND_STATUS = "sys_sms_send_status"; // 短信发送状态
 | 
			
		||||
    String SMS_RECEIVE_STATUS = "sys_sms_receive_status"; // 短信接收状态
 | 
			
		||||
    String SMS_CHANNEL_CODE = "system_sms_channel_code"; // 短信渠道编码
 | 
			
		||||
    String SMS_TEMPLATE_TYPE = "system_sms_template_type"; // 短信模板类型
 | 
			
		||||
    String SMS_SEND_STATUS = "system_sms_send_status"; // 短信发送状态
 | 
			
		||||
    String SMS_RECEIVE_STATUS = "system_sms_receive_status"; // 短信接收状态
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -29,7 +29,6 @@ import org.springframework.web.multipart.MultipartFile;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Resource;
 | 
			
		||||
import javax.validation.Valid;
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
 | 
			
		||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 | 
			
		||||
@@ -99,7 +98,7 @@ public class UserProfileController {
 | 
			
		||||
 | 
			
		||||
    @PutMapping("/update-avatar")
 | 
			
		||||
    @ApiOperation("上传用户个人头像")
 | 
			
		||||
    public CommonResult<String> updateUserAvatar(@RequestParam("avatarFile") MultipartFile file) throws IOException {
 | 
			
		||||
    public CommonResult<String> updateUserAvatar(@RequestParam("avatarFile") MultipartFile file) throws Exception {
 | 
			
		||||
        if (file.isEmpty()) {
 | 
			
		||||
            throw ServiceExceptionUtil.exception(FILE_IS_EMPTY);
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -30,7 +30,7 @@ public interface OperateLogConvert {
 | 
			
		||||
    default List<OperateLogExcelVO> convertList(List<OperateLogDO> list, Map<Long, AdminUserDO> userMap) {
 | 
			
		||||
        return list.stream().map(operateLog -> {
 | 
			
		||||
            OperateLogExcelVO excelVO = convert02(operateLog);
 | 
			
		||||
            MapUtils.findAndThen(userMap, operateLog.getId(), user -> excelVO.setUserNickname(user.getNickname()));
 | 
			
		||||
            MapUtils.findAndThen(userMap, operateLog.getUserId(), user -> excelVO.setUserNickname(user.getNickname()));
 | 
			
		||||
            excelVO.setSuccessStr(SUCCESS.getCode().equals(operateLog.getResultCode()) ? "成功" : "失败");
 | 
			
		||||
            return excelVO;
 | 
			
		||||
        }).collect(Collectors.toList());
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,12 @@
 | 
			
		||||
package cn.iocoder.yudao.module.system.dal.mysql.permission;
 | 
			
		||||
 | 
			
		||||
import cn.iocoder.yudao.framework.mybatis.core.enums.SqlConstants;
 | 
			
		||||
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
 | 
			
		||||
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
 | 
			
		||||
import cn.iocoder.yudao.module.system.controller.admin.permission.vo.menu.MenuListReqVO;
 | 
			
		||||
import cn.iocoder.yudao.module.system.dal.dataobject.permission.MenuDO;
 | 
			
		||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 | 
			
		||||
import org.apache.ibatis.annotations.Mapper;
 | 
			
		||||
import org.apache.ibatis.annotations.Select;
 | 
			
		||||
 | 
			
		||||
import java.util.Date;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
@@ -28,9 +28,7 @@ public interface MenuMapper extends BaseMapperX<MenuDO> {
 | 
			
		||||
                .eqIfPresent(MenuDO::getStatus, reqVO.getStatus()));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    default boolean selectExistsByUpdateTimeAfter(Date maxUpdateTime) {
 | 
			
		||||
        return selectOne(new LambdaQueryWrapper<MenuDO>().select(MenuDO::getId)
 | 
			
		||||
                .gt(MenuDO::getUpdateTime, maxUpdateTime).last(SqlConstants.LIMIT1)) != null;
 | 
			
		||||
    }
 | 
			
		||||
    @Select("SELECT id FROM system_menu WHERE update_time > #{maxUpdateTime} LIMIT 1")
 | 
			
		||||
    MenuDO selectExistsByUpdateTimeAfter(Date maxUpdateTime);
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -7,9 +7,8 @@ import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX;
 | 
			
		||||
import cn.iocoder.yudao.module.system.controller.admin.permission.vo.role.RoleExportReqVO;
 | 
			
		||||
import cn.iocoder.yudao.module.system.controller.admin.permission.vo.role.RolePageReqVO;
 | 
			
		||||
import cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleDO;
 | 
			
		||||
import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
 | 
			
		||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 | 
			
		||||
import org.apache.ibatis.annotations.Mapper;
 | 
			
		||||
import org.apache.ibatis.annotations.Select;
 | 
			
		||||
import org.springframework.lang.Nullable;
 | 
			
		||||
 | 
			
		||||
import java.util.Collection;
 | 
			
		||||
@@ -45,10 +44,7 @@ public interface RoleMapper extends BaseMapperX<RoleDO> {
 | 
			
		||||
        return selectList(new LambdaQueryWrapperX<RoleDO>().inIfPresent(RoleDO::getStatus, statuses));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @InterceptorIgnore(tenantLine = "true") // 该方法忽略多租户。原因:该方法被异步 task 调用,此时获取不到租户编号
 | 
			
		||||
    default boolean selectExistsByUpdateTimeAfter(Date maxUpdateTime) {
 | 
			
		||||
        return selectOne(new QueryWrapper<RoleDO>().select("id")
 | 
			
		||||
                .gt("update_time", maxUpdateTime).last("LIMIT 1")) != null;
 | 
			
		||||
    }
 | 
			
		||||
    @Select("SELECT id FROM system_role WHERE update_time > #{maxUpdateTime} LIMIT 1")
 | 
			
		||||
    RoleDO selectExistsByUpdateTimeAfter(Date maxUpdateTime);
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user