mirror of
https://gitee.com/czh-dev/upload-hub
synced 2026-09-08 00:47:02 +08:00
feat:存储配置添加测试连接功能
This commit is contained in:
@@ -5,6 +5,7 @@
|
|||||||
## 演示
|
## 演示
|
||||||

|

|
||||||
|
|
||||||
|
|
||||||
## 项目简介
|
## 项目简介
|
||||||
|
|
||||||
本项目是一个基于前后端分离架构的文件上传系统,支持多种存储方式,包括本地存储、阿里云 OSS 和 MinIO。用户可以通过直观的界面上传文件并管理已上传的文件。
|
本项目是一个基于前后端分离架构的文件上传系统,支持多种存储方式,包括本地存储、阿里云 OSS 和 MinIO。用户可以通过直观的界面上传文件并管理已上传的文件。
|
||||||
@@ -24,26 +25,47 @@
|
|||||||
- MinIO(分布式对象存储)
|
- MinIO(分布式对象存储)
|
||||||
- 敬请期待。。。
|
- 敬请期待。。。
|
||||||
|
|
||||||
|
### 功能特性
|
||||||
|
|
||||||
## 开发者的话
|
- 断点续传
|
||||||
|
- 文件分片
|
||||||
|
- 文件校验
|
||||||
|
- 文件秒传
|
||||||
|
- 多文件上传
|
||||||
|
- 支持多种存储方式切换
|
||||||
|
- 文件预览(图片、视频、PDF)
|
||||||
|
- 存储配置连接测试
|
||||||
|
|
||||||
尊敬的读者:
|
|
||||||
|
|
||||||
首先,请允许我向您致以诚挚的问候,并对项目中可能存在的不足表达由衷的歉意。作为一名主要专注于后端开发的工程师,我在前端设计与用户界面的呈现上仍处于学习与成长的阶段。因此,您在体验本项目时,可能会注意到页面布局不够精致、样式设计略显简朴、交互体验有待流畅,或是响应式设计尚未完美适配等问题。这些瑕疵,或许无法完全展现我对这个项目的完整构想,也可能在某种程度上影响您的使用体验。
|
|
||||||
|
|
||||||
我深知,一个优雅、直观的用户界面是产品体验不可或缺的一部分。为此,我正全力以赴提升前端开发技能,并积极探索更优的解决方案,以弥补现阶段的不足,逐步提升页面的美观度与实用性。
|
## 预览
|
||||||
|
### 首页
|
||||||
|

|
||||||
|
|
||||||
您的每一条意见与建议,对我来说都是弥足珍贵的财富。如果您在使用过程中有任何想法或改进建议,欢迎随时与我联系(邮箱:chenzhihua0123@gmail.com 或 GitHub Issues)。我承诺,将认真倾听您的需求,并尽我所能优化这个项目,力求为您带来一个更加美观、友好且高效的文件上传工具。
|
### 已上传文件 页面
|
||||||
|

|
||||||
|
|
||||||
感谢您的耐心与包容。正是因为有您的支持,我才有动力不断前行。我坚信,随着时间的推移,我的技能会逐步精进,项目的品质也将日益提升。希望我们能一起见证这份作品从青涩走向成熟的旅程,每一个小小的进步都将成为我们共同的成就。
|
### 上传设置
|
||||||
|

|
||||||
|
|
||||||
再次感谢您的理解与陪伴,期待与您携手共创更美好的体验!
|
### 存储配置
|
||||||
|

|
||||||
|
|
||||||
此致,
|
### 文件预览
|
||||||
|
|
||||||
|
- 图片
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- 视频
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- PDF
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
czh-dev
|
|
||||||
|
|
||||||
2025年3月
|
|
||||||
|
|
||||||
|
|
||||||
## 快速开始
|
## 快速开始
|
||||||
@@ -223,30 +245,6 @@ logging:
|
|||||||
cn.czh.mapper: debug
|
cn.czh.mapper: debug
|
||||||
```
|
```
|
||||||
|
|
||||||
## 功能特性
|
|
||||||
|
|
||||||
- 断点续传
|
|
||||||
- 文件分片
|
|
||||||
- 文件校验
|
|
||||||
- 文件秒传
|
|
||||||
- 多文件上传
|
|
||||||
- 支持多种存储方式切换
|
|
||||||
- 文件预览和管理
|
|
||||||
- 分页加载已上传文件
|
|
||||||
|
|
||||||
## 预览
|
|
||||||
### 首页
|
|
||||||

|
|
||||||
|
|
||||||
### 已上传文件 页面
|
|
||||||

|
|
||||||
|
|
||||||
### 上传设置
|
|
||||||

|
|
||||||
|
|
||||||
### 存储配置
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
## 贡献
|
## 贡献
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 170 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.7 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 248 KiB |
@@ -2,18 +2,15 @@ package cn.czh.advice;
|
|||||||
|
|
||||||
import cn.czh.base.BusinessException;
|
import cn.czh.base.BusinessException;
|
||||||
import cn.czh.base.Result;
|
import cn.czh.base.Result;
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.validation.FieldError;
|
import org.springframework.validation.FieldError;
|
||||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
|
||||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
|
|
||||||
@RestControllerAdvice
|
@RestControllerAdvice
|
||||||
public class GlobalExceptionHandler {
|
public class GlobalExceptionHandler {
|
||||||
|
|
||||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
|
||||||
public Result<?> handleValidationException(MethodArgumentNotValidException ex) {
|
public Result<?> handleValidationException(MethodArgumentNotValidException ex) {
|
||||||
FieldError fieldError = ex.getBindingResult().getFieldErrors().get(0);
|
FieldError fieldError = ex.getBindingResult().getFieldErrors().get(0);
|
||||||
String errorMessage = fieldError.getDefaultMessage();
|
String errorMessage = fieldError.getDefaultMessage();
|
||||||
@@ -21,7 +18,6 @@ public class GlobalExceptionHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ExceptionHandler(BusinessException.class)
|
@ExceptionHandler(BusinessException.class)
|
||||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
|
||||||
public Result<?> handleBusinessException(BusinessException e) {
|
public Result<?> handleBusinessException(BusinessException e) {
|
||||||
return Result.error(500, e.getMessage());
|
return Result.error(500, e.getMessage());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,4 +26,9 @@ public class ConfigController {
|
|||||||
return Result.success();
|
return Result.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/test")
|
||||||
|
public Result<?> testConfig(@Valid @RequestBody StorageConfig config) {
|
||||||
|
storageConfigService.testStorageConfig(config);
|
||||||
|
return Result.success();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,4 +14,9 @@ public interface IStorageConfigService {
|
|||||||
*/
|
*/
|
||||||
void updateStorageConfig(StorageConfig config);
|
void updateStorageConfig(StorageConfig config);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试存储配置
|
||||||
|
*/
|
||||||
|
void testStorageConfig(StorageConfig config);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+93
-2
@@ -4,18 +4,26 @@ import cn.czh.base.BusinessException;
|
|||||||
import cn.czh.context.StorageConfigUpdateEvent;
|
import cn.czh.context.StorageConfigUpdateEvent;
|
||||||
import cn.czh.entity.StorageConfig;
|
import cn.czh.entity.StorageConfig;
|
||||||
import cn.czh.mapper.StorageConfigMapper;
|
import cn.czh.mapper.StorageConfigMapper;
|
||||||
import cn.czh.mapper.UploadFileMapper;
|
|
||||||
import cn.czh.service.IStorageConfigService;
|
import cn.czh.service.IStorageConfigService;
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import com.aliyun.oss.OSS;
|
||||||
|
import com.aliyun.oss.OSSClientBuilder;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import io.minio.BucketExistsArgs;
|
||||||
|
import io.minio.MinioClient;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.context.ApplicationEventPublisher;
|
import org.springframework.context.ApplicationEventPublisher;
|
||||||
import org.springframework.stereotype.Service;
|
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.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
public class StorageConfigServiceImpl extends ServiceImpl<StorageConfigMapper, StorageConfig> implements IStorageConfigService {
|
public class StorageConfigServiceImpl extends ServiceImpl<StorageConfigMapper, StorageConfig> implements IStorageConfigService {
|
||||||
|
|
||||||
@@ -45,4 +53,87 @@ public class StorageConfigServiceImpl extends ServiceImpl<StorageConfigMapper, S
|
|||||||
eventPublisher.publishEvent(new StorageConfigUpdateEvent(this, config)); // 发布事件
|
eventPublisher.publishEvent(new StorageConfigUpdateEvent(this, config)); // 发布事件
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void testStorageConfig(StorageConfig config) {
|
||||||
|
switch (config.getType().toLowerCase()) {
|
||||||
|
case StorageConfig.LOCAL:
|
||||||
|
testLocalStorage(config);
|
||||||
|
break;
|
||||||
|
case StorageConfig.MINIO:
|
||||||
|
testMinioStorage(config);
|
||||||
|
break;
|
||||||
|
case StorageConfig.OSS:
|
||||||
|
testOSSStorage(config);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("不支持的存储类型: " + config.getType());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void testLocalStorage(StorageConfig config) {
|
||||||
|
Path path = Paths.get(config.getBucket());
|
||||||
|
|
||||||
|
if (!Files.exists(path)) {
|
||||||
|
try {
|
||||||
|
Files.createDirectories(path);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new BusinessException("无法创建目录: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Files.isDirectory(path)) {
|
||||||
|
throw new BusinessException("指定路径不是目录: " + config.getBucket());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Files.isWritable(path)) {
|
||||||
|
throw new BusinessException("路径不可写: " + config.getBucket());
|
||||||
|
}
|
||||||
|
|
||||||
|
Path testFile = path.resolve("test.txt");
|
||||||
|
try {
|
||||||
|
Files.write(testFile, "test".getBytes());
|
||||||
|
Files.delete(testFile);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new BusinessException("读写测试失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void testMinioStorage(StorageConfig config) {
|
||||||
|
if (config.getAccessKey() == null || config.getSecretKey() == null) {
|
||||||
|
throw new BusinessException("MinIO 需要提供 AccessKey 和 SecretKey");
|
||||||
|
}
|
||||||
|
|
||||||
|
MinioClient minioClient = MinioClient.builder()
|
||||||
|
.endpoint(config.getEndpoint())
|
||||||
|
.credentials(config.getAccessKey(), config.getSecretKey())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
try {
|
||||||
|
BucketExistsArgs args = BucketExistsArgs.builder().bucket(config.getBucket()).build();
|
||||||
|
boolean exists = minioClient.bucketExists(args);
|
||||||
|
if (!exists) {
|
||||||
|
throw new BusinessException("Bucket 不存在: " + config.getBucket());
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new BusinessException("MinIO 测试失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void testOSSStorage(StorageConfig config) {
|
||||||
|
if (config.getAccessKey() == null || config.getSecretKey() == null) {
|
||||||
|
throw new BusinessException("OSS 需要提供 AccessKey 和 SecretKey");
|
||||||
|
}
|
||||||
|
|
||||||
|
OSS ossClient = new OSSClientBuilder()
|
||||||
|
.build(config.getEndpoint(), config.getAccessKey(), config.getSecretKey());
|
||||||
|
|
||||||
|
try {
|
||||||
|
ossClient.listBuckets();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new BusinessException("OSS 测试失败: " + e.getMessage());
|
||||||
|
} finally {
|
||||||
|
ossClient.shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,15 +27,18 @@
|
|||||||
<input v-model="config.bucket" type="text" placeholder="请输入Bucket" />
|
<input v-model="config.bucket" type="text" placeholder="请输入Bucket" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button type="button" @click="saveConfig">保存</button>
|
<button type="button" @click="testConfig" :disabled="testing">
|
||||||
<button type="button" @click="close">取消</button>
|
{{ testing ? '测试中...' : '测试连接' }}
|
||||||
|
</button>
|
||||||
|
<button type="button" @click="saveConfig" :disabled="testing">保存</button>
|
||||||
|
<button type="button" @click="close" :disabled="testing">取消</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { getStorageConfig, setStorageConfig } from '@/utils/api';
|
import { getStorageConfig, setStorageConfig, testStorageConfig } from '@/utils/api';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'StorageConfigModal',
|
name: 'StorageConfigModal',
|
||||||
@@ -51,7 +54,8 @@ export default {
|
|||||||
accessKey: '',
|
accessKey: '',
|
||||||
secretKey: '',
|
secretKey: '',
|
||||||
bucket: ''
|
bucket: ''
|
||||||
}
|
},
|
||||||
|
testing: false
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -81,6 +85,22 @@ export default {
|
|||||||
this.$message.error('保存存储配置失败');
|
this.$message.error('保存存储配置失败');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async testConfig() {
|
||||||
|
this.testing = true;
|
||||||
|
try {
|
||||||
|
const response = await testStorageConfig(this.config);
|
||||||
|
if (response.code === 200) {
|
||||||
|
this.$message.success('存储配置测试成功!');
|
||||||
|
} else {
|
||||||
|
this.$message.error(`${response.msg || '未知错误'}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('存储配置测试失败:', error);
|
||||||
|
this.$message.error(`${error.message || '服务器错误'}`);
|
||||||
|
} finally {
|
||||||
|
this.testing = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
close() {
|
close() {
|
||||||
this.$emit('close');
|
this.$emit('close');
|
||||||
},
|
},
|
||||||
@@ -162,6 +182,11 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.form-actions button:first-child {
|
.form-actions button:first-child {
|
||||||
|
background: #ff9800;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions button:nth-child(2) {
|
||||||
background: #1976d2;
|
background: #1976d2;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
@@ -171,4 +196,9 @@ export default {
|
|||||||
color: #1976d2;
|
color: #1976d2;
|
||||||
border: 1px solid #d0e4fc;
|
border: 1px solid #d0e4fc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-actions button:disabled {
|
||||||
|
background: #cccccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -160,6 +160,26 @@ const setStorageConfig = ({ id, type, endpoint, accessKey, secretKey, bucket })
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试存储配置
|
||||||
|
* @param {Object} params 参数对象
|
||||||
|
* @param {string} params.type 类型
|
||||||
|
* @param {string} params.endpoint endpoint
|
||||||
|
* @param {string} params.accessKey accessKey
|
||||||
|
* @param {string} params.secretKey secretKey
|
||||||
|
* @param {string} params.bucket bucket
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const testStorageConfig = ({ type, endpoint, accessKey, secretKey, bucket }) => {
|
||||||
|
return http.post('/config/test', {
|
||||||
|
type,
|
||||||
|
endpoint,
|
||||||
|
accessKey,
|
||||||
|
secretKey,
|
||||||
|
bucket
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
getUploadProgress,
|
getUploadProgress,
|
||||||
createMultipartUpload,
|
createMultipartUpload,
|
||||||
@@ -170,5 +190,6 @@ export {
|
|||||||
pageFiles,
|
pageFiles,
|
||||||
getStorageConfig,
|
getStorageConfig,
|
||||||
setStorageConfig,
|
setStorageConfig,
|
||||||
|
testStorageConfig,
|
||||||
httpExtra
|
httpExtra
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user