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