mirror of
https://gitee.com/huangge1199_admin/vue-pro.git
synced 2025-01-19 11:40:05 +08:00
!98 文件存储的功能,支持将文件存储到 S3(MinIO、阿里云、腾讯云、七牛云)、本地、FTP、FTP、数据库等
Merge pull request !98 from 芋道源码/feature/1.6.1
This commit is contained in:
commit
d2075d5c18
@ -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:核心功能已经实现,正在对接微信小程序中...
|
||||
|---------------|----------------------------------------------------------------------|--------------------------------------------------------------------|------------------------------------------------------------------|
|
||||
| 代码生成 | ![代码生成](https://static.iocoder.cn/images/ruoyi-vue-pro/代码生成.jpg) | ![生成效果](https://static.iocoder.cn/images/ruoyi-vue-pro/生成效果.jpg) | - |
|
||||
| 文档 | ![系统接口](https://static.iocoder.cn/images/ruoyi-vue-pro/系统接口.jpg) | ![数据库文档](https://static.iocoder.cn/images/ruoyi-vue-pro/数据库文档.jpg) | - |
|
||||
| 文件 & 配置 | ![文件管理](https://static.iocoder.cn/images/ruoyi-vue-pro/文件管理.jpg) | ![配置管理](https://static.iocoder.cn/images/ruoyi-vue-pro/配置管理.jpg) | - |
|
||||
| 文件 & 配置 | ![文件配置](https://static.iocoder.cn/images/ruoyi-vue-pro/文件配置.jpg) | ![文件管理](https://static.iocoder.cn/images/ruoyi-vue-pro/文件管理2.jpg) | ![配置管理](https://static.iocoder.cn/images/ruoyi-vue-pro/配置管理.jpg) |
|
||||
| 定时任务 | ![定时任务](https://static.iocoder.cn/images/ruoyi-vue-pro/定时任务.jpg) | ![任务日志](https://static.iocoder.cn/images/ruoyi-vue-pro/任务日志.jpg) | - |
|
||||
| API 日志 | ![访问日志](https://static.iocoder.cn/images/ruoyi-vue-pro/访问日志.jpg) | ![错误日志](https://static.iocoder.cn/images/ruoyi-vue-pro/错误日志.jpg) | - |
|
||||
| MySQL & Redis | ![MySQL](https://static.iocoder.cn/images/ruoyi-vue-pro/MySQL.jpg) | ![Redis](https://static.iocoder.cn/images/ruoyi-vue-pro/Redis.jpg) | - |
|
||||
|
File diff suppressed because one or more lines are too long
@ -53,7 +53,10 @@
|
||||
<screw.version>1.0.5</screw.version>
|
||||
<guava.version>30.1.1-jre</guava.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>
|
||||
<!-- 三方云服务相关 -->
|
||||
<s3.version>2.17.147</s3.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>
|
||||
@ -493,7 +496,28 @@
|
||||
<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>software.amazon.awssdk</groupId>
|
||||
<artifactId>s3</artifactId>
|
||||
<version>${s3.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>software.amazon.awssdk</groupId>
|
||||
<artifactId>s3</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,41 @@
|
||||
package cn.iocoder.yudao.framework.file.core.client;
|
||||
|
||||
/**
|
||||
* 文件客户端
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public interface FileClient {
|
||||
|
||||
/**
|
||||
* 获得客户端编号
|
||||
*
|
||||
* @return 客户端编号
|
||||
*/
|
||||
Long getId();
|
||||
|
||||
/**
|
||||
* 上传文件
|
||||
*
|
||||
* @param content 文件流
|
||||
* @param path 相对路径
|
||||
* @return 完整路径,即 HTTP 访问地址
|
||||
*/
|
||||
String upload(byte[] content, String path);
|
||||
|
||||
/**
|
||||
* 删除文件
|
||||
*
|
||||
* @param path 相对路径
|
||||
*/
|
||||
void delete(String path);
|
||||
|
||||
/**
|
||||
* 获得文件的内容
|
||||
*
|
||||
* @param path 相对路径
|
||||
* @return 文件的内容
|
||||
*/
|
||||
byte[] getContent(String path);
|
||||
|
||||
}
|
@ -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,103 @@
|
||||
package cn.iocoder.yudao.framework.file.core.client.s3;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.framework.file.core.client.AbstractFileClient;
|
||||
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
|
||||
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
|
||||
import software.amazon.awssdk.core.sync.RequestBody;
|
||||
import software.amazon.awssdk.regions.Region;
|
||||
import software.amazon.awssdk.services.s3.S3Client;
|
||||
import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
|
||||
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
|
||||
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
import static cn.iocoder.yudao.framework.file.core.client.s3.S3FileClientConfig.ENDPOINT_QINIU;
|
||||
|
||||
/**
|
||||
* 基于 S3 协议的文件客户端,实现 MinIO、阿里云、腾讯云、七牛云、华为云等云服务
|
||||
*
|
||||
* S3 协议的客户端,采用亚马逊提供的 software.amazon.awssdk.s3 库
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
|
||||
|
||||
private S3Client client;
|
||||
|
||||
public S3FileClient(Long id, S3FileClientConfig config) {
|
||||
super(id, config);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doInit() {
|
||||
// 补全 domain
|
||||
if (StrUtil.isEmpty(config.getDomain())) {
|
||||
config.setDomain(createDomain());
|
||||
}
|
||||
// 初始化客户端
|
||||
client = S3Client.builder()
|
||||
.serviceConfiguration(sb -> sb.pathStyleAccessEnabled(false) // 关闭路径风格
|
||||
.chunkedEncodingEnabled(false)) // 禁用 chunk
|
||||
.endpointOverride(createURI()) // 上传地址
|
||||
.region(Region.of(config.getRegion())) // Region
|
||||
.credentialsProvider(StaticCredentialsProvider.create( // 认证密钥
|
||||
AwsBasicCredentials.create(config.getAccessKey(), config.getAccessSecret())))
|
||||
.overrideConfiguration(cb -> cb.addExecutionInterceptor(new S3ModifyPathInterceptor(config.getBucket())))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于 endpoint 构建调用云服务的 URI 地址
|
||||
*
|
||||
* @return URI 地址
|
||||
*/
|
||||
private URI createURI() {
|
||||
String uri;
|
||||
// 如果是七牛,无需拼接 bucket
|
||||
if (config.getEndpoint().contains(ENDPOINT_QINIU)) {
|
||||
uri = StrUtil.format("https://{}", config.getEndpoint());
|
||||
} else {
|
||||
uri = StrUtil.format("https://{}.{}", config.getBucket(), config.getEndpoint());
|
||||
}
|
||||
return URI.create(uri);
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于 bucket + endpoint 构建访问的 Domain 地址
|
||||
*
|
||||
* @return Domain 地址
|
||||
*/
|
||||
private String createDomain() {
|
||||
return StrUtil.format("https://{}.{}", config.getBucket(), config.getEndpoint());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String upload(byte[] content, String path) {
|
||||
// 执行上传
|
||||
PutObjectRequest.Builder request = PutObjectRequest.builder()
|
||||
.bucket(config.getBucket()) // bucket 必须传递
|
||||
.key(path); // 相对路径作为 key
|
||||
client.putObject(request.build(), RequestBody.fromBytes(content));
|
||||
// 拼接返回路径
|
||||
return config.getDomain() + "/" + path;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String path) {
|
||||
DeleteObjectRequest.Builder request = DeleteObjectRequest.builder()
|
||||
.bucket(config.getBucket()) // bucket 必须传递
|
||||
.key(path); // 相对路径作为 key
|
||||
client.deleteObject(request.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getContent(String path) {
|
||||
GetObjectRequest.Builder request = GetObjectRequest.builder()
|
||||
.bucket(config.getBucket()) // bucket 必须传递
|
||||
.key(path); // 相对路径作为 key
|
||||
return client.getObjectAsBytes(request.build()).asByteArray();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
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";
|
||||
|
||||
/**
|
||||
* 节点地址
|
||||
* 1. MinIO:
|
||||
* 2. 阿里云:https://help.aliyun.com/document_detail/31837.html
|
||||
* 3. 腾讯云:
|
||||
* 4. 七牛云:https://developer.qiniu.com/kodo/4088/s3-access-domainname
|
||||
* 5. 华为云:
|
||||
*/
|
||||
@NotNull(message = "endpoint 不能为空")
|
||||
private String endpoint;
|
||||
/**
|
||||
* 自定义域名
|
||||
* 1. MinIO:
|
||||
* 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. 华为云:
|
||||
*/
|
||||
@URL(message = "domain 必须是 URL 格式")
|
||||
private String domain;
|
||||
/**
|
||||
* 区域
|
||||
* 1. MinIO:
|
||||
* 2. 阿里云:https://help.aliyun.com/document_detail/31837.html
|
||||
* 3. 腾讯云:
|
||||
* 4. 七牛云:https://developer.qiniu.com/kodo/4088/s3-access-domainname
|
||||
* 5. 华为云:
|
||||
*/
|
||||
@NotNull(message = "region 不能为空")
|
||||
private String region;
|
||||
/**
|
||||
* 存储 Bucket
|
||||
*/
|
||||
@NotNull(message = "bucket 不能为空")
|
||||
private String bucket;
|
||||
|
||||
/**
|
||||
* 访问 Key
|
||||
* 1. MinIO:
|
||||
* 2. 阿里云:
|
||||
* 3. 腾讯云:https://console.cloud.tencent.com/cam/capi
|
||||
* 4. 七牛云:https://portal.qiniu.com/user/key
|
||||
* 5. 华为云:
|
||||
*/
|
||||
@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,36 @@
|
||||
package cn.iocoder.yudao.framework.file.core.client.s3;
|
||||
|
||||
import software.amazon.awssdk.core.interceptor.Context;
|
||||
import software.amazon.awssdk.core.interceptor.ExecutionAttributes;
|
||||
import software.amazon.awssdk.core.interceptor.ExecutionInterceptor;
|
||||
import software.amazon.awssdk.http.SdkHttpRequest;
|
||||
|
||||
/**
|
||||
* S3 修改路径的拦截器,移除多余的 Bucket 前缀。
|
||||
* 如果不使用该拦截器,希望上传的路径是 /tudou.jpg 时,会被添加成 /bucket/tudou.jpg
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class S3ModifyPathInterceptor implements ExecutionInterceptor {
|
||||
|
||||
private final String bucket;
|
||||
|
||||
public S3ModifyPathInterceptor(String bucket) {
|
||||
this.bucket = "/" + bucket;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SdkHttpRequest modifyHttpRequest(Context.ModifyHttpRequest context, ExecutionAttributes executionAttributes) {
|
||||
SdkHttpRequest request = context.httpRequest();
|
||||
SdkHttpRequest.Builder rb = SdkHttpRequest.builder().protocol(request.protocol()).host(request.host()).port(request.port())
|
||||
.method(request.method()).headers(request.headers()).rawQueryParameters(request.rawQueryParameters());
|
||||
// 移除 path 前的 bucket 路径
|
||||
if (request.encodedPath().startsWith(bucket)) {
|
||||
rb.encodedPath(request.encodedPath().substring(bucket.length()));
|
||||
} else {
|
||||
rb.encodedPath(request.encodedPath());
|
||||
}
|
||||
return rb.build();
|
||||
}
|
||||
|
||||
}
|
@ -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,37 @@
|
||||
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.Test;
|
||||
|
||||
public class FtpFileClientTest {
|
||||
|
||||
@Test
|
||||
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,25 @@
|
||||
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.Test;
|
||||
|
||||
public class LocalFileClientTest {
|
||||
|
||||
@Test
|
||||
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,90 @@
|
||||
package cn.iocoder.yudao.framework.file.core.client.s3;
|
||||
|
||||
import cn.hutool.core.io.resource.ResourceUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
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 // 阿里云 OSS,如果要集成测试,可以注释本行
|
||||
public void testAliyun() {
|
||||
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() {
|
||||
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");
|
||||
config.setRegion("ap-shanghai");
|
||||
|
||||
// 执行上传
|
||||
testExecuteUpload(config);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled // 七牛云存储,如果要集成测试,可以注释本行
|
||||
public void testQiniu() {
|
||||
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);
|
||||
}
|
||||
|
||||
private void testExecuteUpload(S3FileClientConfig config) {
|
||||
// 补全配置
|
||||
if (config.getRegion() == null) {
|
||||
config.setRegion(StrUtil.subBefore(config.getEndpoint(), '.', false));
|
||||
}
|
||||
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 (false) {
|
||||
byte[] bytes = client.getContent(path);
|
||||
System.out.println("文件内容:" + bytes);
|
||||
}
|
||||
// 删除文件
|
||||
if (false) {
|
||||
client.delete(path);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
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.Test;
|
||||
|
||||
public class SftpFileClientTest {
|
||||
|
||||
@Test
|
||||
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<>());
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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) {
|
||||
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;
|
||||
@ -50,24 +50,29 @@ public class FileController {
|
||||
|
||||
@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) {
|
||||
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 IOException {
|
||||
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;
|
||||
@ -81,10 +83,7 @@ public class CodegenServiceImpl implements CodegenService {
|
||||
codegenTableMapper.insert(table);
|
||||
// 构建 CodegenColumnDO 数组,插入到 DB 中
|
||||
List<CodegenColumnDO> columns = codegenBuilder.buildColumns(schemaColumns);
|
||||
columns.forEach(column -> {
|
||||
column.setTableId(table.getId());
|
||||
codegenColumnMapper.insert(column); // TODO 批量插入
|
||||
});
|
||||
codegenColumnMapper.insertBatch(columns);
|
||||
return table.getId();
|
||||
}
|
||||
|
||||
@ -198,10 +197,7 @@ public class CodegenServiceImpl implements CodegenService {
|
||||
|
||||
// 插入新增的字段
|
||||
List<CodegenColumnDO> columns = codegenBuilder.buildColumns(schemaColumns);
|
||||
columns.forEach(column -> {
|
||||
column.setTableId(tableId);
|
||||
codegenColumnMapper.insert(column); // TODO 批量插入
|
||||
});
|
||||
codegenColumnMapper.insertBatch(columns);
|
||||
// 删除不存在的字段
|
||||
if (CollUtil.isNotEmpty(deleteColumnIds)) {
|
||||
codegenColumnMapper.deleteBatchIds(deleteColumnIds);
|
||||
|
@ -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);
|
||||
|
||||
/**
|
||||
* 获得指定编号的文件客户端
|
||||
*
|
||||
* @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) {
|
||||
// 校验存在
|
||||
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;
|
||||
|
||||
@ -33,14 +33,15 @@ public interface FileService {
|
||||
*
|
||||
* @param id 编号
|
||||
*/
|
||||
void deleteFile(String id);
|
||||
void deleteFile(Long id);
|
||||
|
||||
/**
|
||||
* 获得文件
|
||||
* 获得文件内容
|
||||
*
|
||||
* @param configId 配置编号
|
||||
* @param path 文件路径
|
||||
* @return 文件
|
||||
* @return 文件内容
|
||||
*/
|
||||
FileDO getFile(String path);
|
||||
byte[] getFileContent(Long configId, String path);
|
||||
|
||||
}
|
||||
|
@ -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) {
|
||||
@ -35,36 +36,49 @@ public class FileServiceImpl implements FileService {
|
||||
|
||||
@Override
|
||||
public String createFile(String path, byte[] content) {
|
||||
if (fileMapper.selectCountById(path) > 0) {
|
||||
throw exception(FILE_PATH_EXISTS);
|
||||
}
|
||||
// 上传到文件存储器
|
||||
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) {
|
||||
// 校验存在
|
||||
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) {
|
||||
FileClient client = fileConfigService.getFileClient(configId);
|
||||
Assert.notNull(client, "客户端({}) 不能为空", configId);
|
||||
return client.getContent(path);
|
||||
}
|
||||
|
||||
}
|
||||
|
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() {
|
||||
// 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,7 +66,7 @@ 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
|
||||
@ -75,52 +74,68 @@ public class FileServiceTest extends BaseDbUnitTest {
|
||||
// 准备参数
|
||||
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());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateFile_exists() {
|
||||
// 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);
|
||||
assertEquals(content.length, file.getSize());
|
||||
}
|
||||
|
||||
@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() {
|
||||
// 准备参数
|
||||
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 '',
|
||||
|
@ -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);
|
||||
|
||||
// 验证参数
|
||||
|
@ -25,8 +25,7 @@ public interface RoleMenuMapper extends BaseMapperX<RoleMenuDO> {
|
||||
entity.setMenuId(menuId);
|
||||
return entity;
|
||||
}).collect(Collectors.toList());
|
||||
// TODO 芋艿,mybatis plus 增加批量插入的功能
|
||||
list.forEach(this::insert);
|
||||
insertBatch(list);
|
||||
}
|
||||
|
||||
default void deleteListByRoleIdAndMenuIds(Long roleId, Collection<Long> menuIds) {
|
||||
|
@ -172,8 +172,6 @@ yudao:
|
||||
session-timeout: 30m
|
||||
mock-enable: true
|
||||
mock-secret: test
|
||||
file:
|
||||
base-path: http://api-dashboard.yudao.iocoder.cn${yudao.web.admin-api.prefix}/infra/file/get/
|
||||
xss:
|
||||
enable: false
|
||||
exclude-urls: # 如下两个 url,仅仅是为了演示,去掉配置也没关系
|
||||
|
@ -184,8 +184,6 @@ yudao:
|
||||
session-timeout: 1d
|
||||
mock-enable: true
|
||||
mock-secret: test
|
||||
file:
|
||||
base-path: http://127.0.0.1:${server.port}${yudao.web.admin-api.prefix}/infra/file/get/
|
||||
xss:
|
||||
enable: false
|
||||
exclude-urls: # 如下两个 url,仅仅是为了演示,去掉配置也没关系
|
||||
|
@ -92,7 +92,7 @@ yudao:
|
||||
ignore-urls:
|
||||
- /admin-api/system/tenant/get-id-by-name # 基于名字获取租户,不许带租户编号
|
||||
- /admin-api/system/captcha/get-image # 获取图片验证码,和租户无关
|
||||
- /admin-api/infra/file/get/* # 获取图片,和租户无关
|
||||
- /admin-api/infra/file/*/get/** # 获取图片,和租户无关
|
||||
- /admin-api/system/sms/callback/* # 短信回调接口,无法带上租户编号
|
||||
ignore-tables:
|
||||
- system_tenant
|
||||
@ -110,7 +110,9 @@ yudao:
|
||||
- tables
|
||||
- columns
|
||||
- infra_config
|
||||
- infra_file_config
|
||||
- infra_file
|
||||
- infra_file_content
|
||||
- infra_job
|
||||
- infra_job_log
|
||||
- infra_job_log
|
||||
|
59
yudao-ui-admin/src/api/infra/fileConfig.js
Executable file
59
yudao-ui-admin/src/api/infra/fileConfig.js
Executable file
@ -0,0 +1,59 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 创建文件配置
|
||||
export function createFileConfig(data) {
|
||||
return request({
|
||||
url: '/infra/file-config/create',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 更新文件配置
|
||||
export function updateFileConfig(data) {
|
||||
return request({
|
||||
url: '/infra/file-config/update',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 更新文件配置为主配置
|
||||
export function updateFileConfigMaster(id) {
|
||||
return request({
|
||||
url: '/infra/file-config/update-master?id=' + id,
|
||||
method: 'put'
|
||||
})
|
||||
}
|
||||
|
||||
// 删除文件配置
|
||||
export function deleteFileConfig(id) {
|
||||
return request({
|
||||
url: '/infra/file-config/delete?id=' + id,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
// 获得文件配置
|
||||
export function getFileConfig(id) {
|
||||
return request({
|
||||
url: '/infra/file-config/get?id=' + id,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 获得文件配置分页
|
||||
export function getFileConfigPage(query) {
|
||||
return request({
|
||||
url: '/infra/file-config/page',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
export function testFileConfig(id) {
|
||||
return request({
|
||||
url: '/infra/file-config/test?id=' + id,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
@ -17,7 +17,7 @@ export default {
|
||||
name: "DictTag",
|
||||
props: {
|
||||
type: String,
|
||||
value: [Number, String, Array],
|
||||
value: [Number, String, Boolean, Array],
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -25,6 +25,7 @@ export const DICT_TYPE = {
|
||||
SYSTEM_ERROR_CODE_TYPE: 'system_error_code_type',
|
||||
|
||||
// ========== INFRA 模块 ==========
|
||||
INFRA_BOOLEAN_STRING: 'infra_boolean_string',
|
||||
INFRA_REDIS_TIMEOUT_TYPE: 'infra_redis_timeout_type',
|
||||
INFRA_JOB_STATUS: 'infra_job_status',
|
||||
INFRA_JOB_LOG_STATUS: 'infra_job_log_status',
|
||||
@ -32,6 +33,7 @@ export const DICT_TYPE = {
|
||||
INFRA_CONFIG_TYPE: 'infra_config_type',
|
||||
INFRA_CODEGEN_TEMPLATE_TYPE: 'infra_codegen_template_type',
|
||||
INFRA_CODEGEN_SCENE: 'infra_codegen_scene',
|
||||
INFRA_FILE_STORAGE: 'infra_file_storage',
|
||||
|
||||
// ========== BPM 模块 ==========
|
||||
BPM_MODEL_CATEGORY: 'bpm_model_category',
|
||||
|
@ -3,8 +3,8 @@
|
||||
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
|
||||
<el-form-item label="文件路径" prop="id">
|
||||
<el-input v-model="queryParams.id" placeholder="请输入文件路径" clearable size="small" @keyup.enter.native="handleQuery"/>
|
||||
<el-form-item label="文件路径" prop="path">
|
||||
<el-input v-model="queryParams.path" placeholder="请输入文件路径" clearable size="small" @keyup.enter.native="handleQuery"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="文件类型" prop="type">
|
||||
<el-select v-model="queryParams.type" placeholder="请选择文件类型" clearable size="small">
|
||||
@ -31,21 +31,23 @@
|
||||
|
||||
<!-- 列表 -->
|
||||
<el-table v-loading="loading" :data="list">
|
||||
<el-table-column label="文件路径" align="center" prop="id" width="300" />
|
||||
<el-table-column label="文件名" align="center" prop="path" />
|
||||
<el-table-column label="URL" align="center" prop="url" />
|
||||
<el-table-column label="文件大小" align="center" prop="size" width="120" :formatter="sizeFormat" />
|
||||
<el-table-column label="文件类型" align="center" prop="type" width="80" />
|
||||
<el-table-column label="文件内容" align="center" prop="content">
|
||||
<template slot-scope="scope">
|
||||
<img v-if="scope.row.type === 'jpg' || scope.row.type === 'png' || scope.row.type === 'gif'"
|
||||
width="200px" :src="getFileUrl + scope.row.id">
|
||||
<i v-else>非图片,无法预览</i>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
|
||||
<!-- <el-table-column label="文件内容" align="center" prop="content">-->
|
||||
<!-- <template slot-scope="scope">-->
|
||||
<!-- <img v-if="scope.row.type === 'jpg' || scope.row.type === 'png' || scope.row.type === 'gif'"-->
|
||||
<!-- width="200px" :src="getFileUrl + scope.row.id">-->
|
||||
<!-- <i v-else>非图片,无法预览</i>-->
|
||||
<!-- </template>-->
|
||||
<!-- </el-table-column>-->
|
||||
<el-table-column label="上传时间" align="center" prop="createTime" width="180">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ parseTime(scope.row.createTime) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="100">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
|
||||
v-hasPermi="['infra:file:delete']">删除</el-button>
|
||||
@ -102,7 +104,7 @@ export default {
|
||||
queryParams: {
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
id: null,
|
||||
path: null,
|
||||
type: null,
|
||||
},
|
||||
// 用户导入参数
|
||||
@ -193,6 +195,15 @@ export default {
|
||||
this.$modal.msgSuccess("删除成功");
|
||||
}).catch(() => {});
|
||||
},
|
||||
// 用户昵称展示
|
||||
sizeFormat(row, column) {
|
||||
const unitArr = ["Bytes","KB","MB","GB","TB","PB","EB","ZB","YB"];
|
||||
const srcSize = parseFloat(row.size);
|
||||
const index = Math.floor(Math.log(srcSize) / Math.log(1024));
|
||||
let size =srcSize/Math.pow(1024,index);
|
||||
size = size.toFixed(2);//保留的小数位数
|
||||
return size + ' ' + unitArr[index];
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
313
yudao-ui-admin/src/views/infra/fileConfig/index.vue
Executable file
313
yudao-ui-admin/src/views/infra/fileConfig/index.vue
Executable file
@ -0,0 +1,313 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
|
||||
<el-form-item label="配置名" prop="name">
|
||||
<el-input v-model="queryParams.name" placeholder="请输入配置名" clearable size="small" @keyup.enter.native="handleQuery"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="存储器" prop="storage">
|
||||
<el-select v-model="queryParams.storage" placeholder="请选择存储器" clearable size="small">
|
||||
<el-option v-for="dict in this.getDictDatas(DICT_TYPE.INFRA_FILE_STORAGE)"
|
||||
:key="dict.value" :label="dict.label" :value="dict.value"/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="创建时间">
|
||||
<el-date-picker v-model="dateRangeCreateTime" size="small" style="width: 240px" value-format="yyyy-MM-dd"
|
||||
type="daterange" range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期" />
|
||||
</el-form-item>
|
||||
<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-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 操作工具栏 -->
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd"
|
||||
v-hasPermi="['infra:file-config:create']">新增</el-button>
|
||||
</el-col>
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||
</el-row>
|
||||
|
||||
<!-- 列表 -->
|
||||
<el-table v-loading="loading" :data="list">
|
||||
<el-table-column label="编号" align="center" prop="id" />
|
||||
<el-table-column label="配置名" align="center" prop="name" />
|
||||
<el-table-column label="存储器" align="center" prop="storage">
|
||||
<template slot-scope="scope">
|
||||
<dict-tag :type="DICT_TYPE.INFRA_FILE_STORAGE" :value="scope.row.storage" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" align="center" prop="remark" />
|
||||
<el-table-column label="主配置" align="center" prop="primary">
|
||||
<template slot-scope="scope">
|
||||
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.master" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ parseTime(scope.row.createTime) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)"
|
||||
v-hasPermi="['infra:file-config:update']">修改</el-button>
|
||||
<el-button size="mini" type="text" icon="el-icon-attract" @click="handleMaster(scope.row)"
|
||||
:disabled="scope.row.master" v-hasPermi="['infra:file-config:update']">主配置</el-button>
|
||||
<el-button size="mini" type="text" icon="el-icon-share" @click="handleTest(scope.row)">测试</el-button>
|
||||
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
|
||||
v-hasPermi="['infra:file-config:delete']">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!-- 分页组件 -->
|
||||
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
|
||||
@pagination="getList"/>
|
||||
|
||||
<!-- 对话框(添加 / 修改) -->
|
||||
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
|
||||
<el-form ref="form" :model="form" :rules="rules" label-width="120px">
|
||||
<el-form-item label="配置名" prop="name">
|
||||
<el-input v-model="form.name" placeholder="请输入配置名" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="form.remark" placeholder="请输入备注" />
|
||||
</el-form-item>
|
||||
<el-form-item label="存储器" prop="storage">
|
||||
<el-select v-model="form.storage" placeholder="请选择存储器" :disabled="form.id">
|
||||
<el-option v-for="dict in this.getDictDatas(DICT_TYPE.INFRA_FILE_STORAGE)"
|
||||
:key="dict.value" :label="dict.label" :value="parseInt(dict.value)" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<!-- DB -->
|
||||
<!-- Local / FTP / SFTP -->
|
||||
<el-form-item v-if="form.storage >= 10 && form.storage <= 12" label="基础路径" prop="config.basePath">
|
||||
<el-input v-model="form.config.basePath" placeholder="请输入基础路径" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="form.storage >= 11 && form.storage <= 12" label="主机地址" prop="config.host">
|
||||
<el-input v-model="form.config.host" placeholder="请输入主机地址" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="form.storage >= 11 && form.storage <= 12" label="主机端口" prop="config.port">
|
||||
<el-input-number min="0" v-model="form.config.port" placeholder="请输入主机端口" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="form.storage >= 11 && form.storage <= 12" label="用户名" prop="config.username">
|
||||
<el-input v-model="form.config.username" placeholder="请输入密码" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="form.storage >= 11 && form.storage <= 12" label="密码" prop="config.password">
|
||||
<el-input v-model="form.config.password" placeholder="请输入密码" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="form.storage === 11" label="连接模式" prop="config.mode">
|
||||
<el-radio-group v-model="form.config.mode">
|
||||
<el-radio key="Active" label="Active">主动模式</el-radio>
|
||||
<el-radio key="Passive" label="Passive">主动模式</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<!-- S3 -->
|
||||
<el-form-item v-if="form.storage === 20" label="节点地址" prop="config.endpoint">
|
||||
<el-input v-model="form.config.endpoint" placeholder="请输入节点地址" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="form.storage === 20" label="区域" prop="config.region">
|
||||
<el-input v-model="form.config.region" placeholder="请输入区域" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="form.storage === 20" label="存储 bucket" prop="config.bucket">
|
||||
<el-input v-model="form.config.bucket" placeholder="请输入 bucket" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="form.storage === 20" label="accessKey" prop="config.accessKey">
|
||||
<el-input v-model="form.config.accessKey" placeholder="请输入 accessKey" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="form.storage === 20" label="accessSecret" prop="config.accessSecret">
|
||||
<el-input v-model="form.config.accessSecret" placeholder="请输入 accessSecret" />
|
||||
</el-form-item>
|
||||
<!-- 通用 -->
|
||||
<el-form-item v-if="form.storage === 20" label="自定义域名"> <!-- 无需参数校验,所以去掉 prop -->
|
||||
<el-input v-model="form.config.domain" placeholder="请输入自定义域名" />
|
||||
</el-form-item>
|
||||
<el-form-item v-else-if="form.storage" label="自定义域名" prop="config.domain">
|
||||
<el-input v-model="form.config.domain" placeholder="请输入自定义域名" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="cancel">取 消</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
createFileConfig,
|
||||
updateFileConfig,
|
||||
deleteFileConfig,
|
||||
getFileConfig,
|
||||
getFileConfigPage,
|
||||
testFileConfig, updateFileConfigMaster
|
||||
} from "@/api/infra/fileConfig";
|
||||
|
||||
export default {
|
||||
name: "FileConfig",
|
||||
components: {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// 遮罩层
|
||||
loading: true,
|
||||
// 显示搜索条件
|
||||
showSearch: true,
|
||||
// 总条数
|
||||
total: 0,
|
||||
// 文件配置列表
|
||||
list: [],
|
||||
// 弹出层标题
|
||||
title: "",
|
||||
// 是否显示弹出层
|
||||
open: false,
|
||||
dateRangeCreateTime: [],
|
||||
// 查询参数
|
||||
queryParams: {
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
name: null,
|
||||
storage: null,
|
||||
},
|
||||
// 表单参数
|
||||
form: {
|
||||
storage: undefined,
|
||||
config: {}
|
||||
},
|
||||
// 表单校验
|
||||
rules: {
|
||||
name: [{ required: true, message: "配置名不能为空", trigger: "blur" }],
|
||||
storage: [{ required: true, message: "存储器不能为空", trigger: "change" }],
|
||||
config: {
|
||||
basePath: [{ required: true, message: "基础路径不能为空", trigger: "blur" }],
|
||||
host: [{ required: true, message: "主机地址不能为空", trigger: "blur" }],
|
||||
port: [{ required: true, message: "主机端口不能为空", trigger: "blur" }],
|
||||
username: [{ required: true, message: "用户名不能为空", trigger: "blur" }],
|
||||
password: [{ required: true, message: "密码不能为空", trigger: "blur" }],
|
||||
mode: [{ required: true, message: "连接模式不能为空", trigger: "change" }],
|
||||
endpoint: [{ required: true, message: "节点地址不能为空", trigger: "blur" }],
|
||||
region: [{ required: true, message: "区域名不能为空", trigger: "blur" }],
|
||||
bucket: [{ required: true, message: "存储 bucket 不能为空", trigger: "blur" }],
|
||||
accessKey: [{ required: true, message: "accessKey 不能为空", trigger: "blur" }],
|
||||
accessSecret: [{ required: true, message: "accessSecret 不能为空", trigger: "blur" }],
|
||||
domain: [{ required: true, message: "自定义域名不能为空", trigger: "blur" }],
|
||||
},
|
||||
}
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.getList();
|
||||
},
|
||||
methods: {
|
||||
/** 查询列表 */
|
||||
getList() {
|
||||
this.loading = true;
|
||||
// 处理查询参数
|
||||
let params = {...this.queryParams};
|
||||
this.addBeginAndEndTime(params, this.dateRangeCreateTime, 'createTime');
|
||||
// 执行查询
|
||||
getFileConfigPage(params).then(response => {
|
||||
this.list = response.data.list;
|
||||
this.total = response.data.total;
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
/** 取消按钮 */
|
||||
cancel() {
|
||||
this.open = false;
|
||||
this.reset();
|
||||
},
|
||||
/** 表单重置 */
|
||||
reset() {
|
||||
this.form = {
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
storage: undefined,
|
||||
remark: undefined,
|
||||
config: {},
|
||||
};
|
||||
this.resetForm("form");
|
||||
},
|
||||
/** 搜索按钮操作 */
|
||||
handleQuery() {
|
||||
this.queryParams.pageNo = 1;
|
||||
this.getList();
|
||||
},
|
||||
/** 重置按钮操作 */
|
||||
resetQuery() {
|
||||
this.dateRangeCreateTime = [];
|
||||
this.resetForm("queryForm");
|
||||
this.handleQuery();
|
||||
},
|
||||
/** 新增按钮操作 */
|
||||
handleAdd() {
|
||||
this.reset();
|
||||
this.open = true;
|
||||
this.title = "添加文件配置";
|
||||
},
|
||||
/** 修改按钮操作 */
|
||||
handleUpdate(row) {
|
||||
this.reset();
|
||||
const id = row.id;
|
||||
getFileConfig(id).then(response => {
|
||||
this.form = response.data;
|
||||
this.open = true;
|
||||
this.title = "修改文件配置";
|
||||
});
|
||||
},
|
||||
/** 提交按钮 */
|
||||
submitForm() {
|
||||
this.$refs["form"].validate(valid => {
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
// 修改的提交
|
||||
if (this.form.id != null) {
|
||||
updateFileConfig(this.form).then(response => {
|
||||
this.$modal.msgSuccess("修改成功");
|
||||
this.open = false;
|
||||
this.getList();
|
||||
});
|
||||
return;
|
||||
}
|
||||
// 添加的提交
|
||||
createFileConfig(this.form).then(response => {
|
||||
this.$modal.msgSuccess("新增成功");
|
||||
this.open = false;
|
||||
this.getList();
|
||||
});
|
||||
});
|
||||
},
|
||||
/** 删除按钮操作 */
|
||||
handleDelete(row) {
|
||||
const id = row.id;
|
||||
this.$modal.confirm('是否确认删除文件配置编号为"' + id + '"的数据项?').then(function() {
|
||||
return deleteFileConfig(id);
|
||||
}).then(() => {
|
||||
this.getList();
|
||||
this.$modal.msgSuccess("删除成功");
|
||||
}).catch(() => {});
|
||||
},
|
||||
/** 主配置按钮操作 */
|
||||
handleMaster(row) {
|
||||
const id = row.id;
|
||||
this.$modal.confirm('是否确认修改配置编号为"' + id + '"的数据项为主配置?').then(function() {
|
||||
return updateFileConfigMaster(id);
|
||||
}).then(() => {
|
||||
this.getList();
|
||||
this.$modal.msgSuccess("修改成功");
|
||||
}).catch(() => {});
|
||||
},
|
||||
/** 测试按钮操作 */
|
||||
handleTest(row) {
|
||||
testFileConfig(row.id).then((response) => {
|
||||
this.$modal.alert("测试通过,上传文件成功!访问地址:" + response.data);
|
||||
}).catch(() => {});
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
Loading…
Reference in New Issue
Block a user