diff --git a/Readme.md b/Readme.md
index 384ac95..9a2088e 100644
--- a/Readme.md
+++ b/Readme.md
@@ -6,6 +6,7 @@
| 1.0.0 | 2025-06-05 | Luke.Ye | 添加修订记录(新增token校验接口) |
| 1.0.1 | 2025-06-06 | Luke.Ye | 重构登录Token校验逻辑, |
| 1.0.2 | 2025-06-09 | Luke.Ye | 添加用户角色相关逻辑,重构代码 |
+| 1.0.3 | 2025-06-16 | Luke.Ye | 新增文档上传至对象存储接口 |
diff --git a/pom.xml b/pom.xml
index 1f70706..2f2ce0a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -44,6 +44,8 @@
3.5.5
1.70
+
+ 8.5.3
@@ -213,6 +215,13 @@
${org.bouncycastle.version}
+
+ io.minio
+ minio
+ ${minio.version}
+
+
+
@@ -357,6 +366,11 @@
bcprov-jdk15to18
+
+ io.minio
+ minio
+
+
diff --git a/src/main/java/com/knowledge/base/infrastructure/config/ObjectStorageProperties.java b/src/main/java/com/knowledge/base/infrastructure/config/ObjectStorageProperties.java
new file mode 100644
index 0000000..7f7038a
--- /dev/null
+++ b/src/main/java/com/knowledge/base/infrastructure/config/ObjectStorageProperties.java
@@ -0,0 +1,12 @@
+package com.knowledge.base.infrastructure.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+@ConfigurationProperties(prefix = "object.storage")
+@Data
+public class ObjectStorageProperties {
+ private boolean useUuidPrefix = true;
+}
\ No newline at end of file
diff --git a/src/main/java/com/knowledge/base/infrastructure/north/controller/FileWriteController.java b/src/main/java/com/knowledge/base/infrastructure/north/controller/FileWriteController.java
index 3d14422..c09dd86 100644
--- a/src/main/java/com/knowledge/base/infrastructure/north/controller/FileWriteController.java
+++ b/src/main/java/com/knowledge/base/infrastructure/north/controller/FileWriteController.java
@@ -1,8 +1,11 @@
package com.knowledge.base.infrastructure.north.controller;
+import cn.hutool.core.lang.UUID;
import com.knowledge.base.application.service.UserAppService;
import com.knowledge.base.domain.doc.service.iface.DocumentImporter;
import com.knowledge.base.infrastructure.cache.FileCacheService;
+import com.knowledge.base.infrastructure.config.ObjectStorageProperties;
+import com.knowledge.base.infrastructure.south.ObjectStorageGateway;
import com.knowledge.base.infrastructure.util.SafeIdUtil;
import lombok.RequiredArgsConstructor;
import org.elasticsearch.action.delete.DeleteRequest;
@@ -14,7 +17,10 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+import java.nio.file.Paths;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -32,6 +38,9 @@ public class FileWriteController {
private final UserAppService userAppService;
+ private final ObjectStorageGateway objectStorageGateway;
+ private final ObjectStorageProperties objectStorageProperties;
+
private List importers;
@PostMapping("/addUserFileAuth")
@@ -119,4 +128,53 @@ public class FileWriteController {
return ResponseEntity.status(500).body("全量清空失败: " + e.getMessage());
}
}
+
+ @PostMapping("/upload-files")
+ public ResponseEntity> uploadFolderToOS(
+ @RequestParam("files") MultipartFile[] files,
+ @RequestParam("bucket") String bucket
+ ) {
+ try {
+ objectStorageGateway.ensureBucketExists(bucket);
+
+ Map filePathMap = new LinkedHashMap<>();
+
+ for (MultipartFile file : files) {
+ String originalName = file.getOriginalFilename();
+ if (originalName == null || originalName.isBlank()) {
+ originalName = file.getName();
+ }
+
+ // 路径前缀
+ String folderPrefix = "default/";
+ String safeName = originalName.replace("\\", "/");
+
+ // 是否启用 UUID 前缀
+ if (objectStorageProperties.isUseUuidPrefix()) {
+ String uuid = UUID.randomUUID().toString().replace("-", "").substring(0, 8);
+ safeName = folderPrefix + uuid + "_" + Paths.get(safeName).getFileName().toString();
+ } else {
+ safeName = folderPrefix + Paths.get(safeName).getFileName().toString();
+ }
+
+ // 上传到 MinIO
+ objectStorageGateway.uploadFile(bucket, safeName, file);
+
+ // 使用原始文件名作为 key,MinIO 对象路径作为 value
+ String fileKey = Paths.get(originalName).getFileName().toString();
+ filePathMap.put(fileKey, String.format("http://s3.wisdompulse.cn/%s/%s", bucket, safeName));
+ }
+
+ return ResponseEntity.ok(Map.of(
+ "msg", "上传成功",
+ "count", filePathMap.size(),
+ "files", filePathMap
+ ));
+ } catch (Exception e) {
+ logger.error("上传失败", e);
+ return ResponseEntity.status(500).body("上传失败: " + e.getMessage());
+ }
+ }
+
+
}
diff --git a/src/main/java/com/knowledge/base/infrastructure/south/MinioOSGatewayImpl.java b/src/main/java/com/knowledge/base/infrastructure/south/MinioOSGatewayImpl.java
new file mode 100644
index 0000000..be2be35
--- /dev/null
+++ b/src/main/java/com/knowledge/base/infrastructure/south/MinioOSGatewayImpl.java
@@ -0,0 +1,49 @@
+package com.knowledge.base.infrastructure.south;
+
+import io.minio.*;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.util.Optional;
+
+@Component
+@Slf4j
+public class MinioOSGatewayImpl implements ObjectStorageGateway {
+
+ private final MinioClient minioClient;
+
+ public MinioOSGatewayImpl(
+ @Value("${minio.endpoint:http://s3.wisdompulse.cn}") String endpoint,
+ @Value("${minio.access-key:lukeye}") String accessKey,
+ @Value("${minio.secret-key:lukeye@6}") String secretKey
+ ) {
+ this.minioClient = MinioClient.builder()
+ .endpoint(endpoint)
+ .credentials(accessKey, secretKey)
+ .build();
+ }
+
+ @Override
+ public void ensureBucketExists(String bucket) throws Exception {
+ boolean exists = minioClient.bucketExists(
+ BucketExistsArgs.builder().bucket(bucket).build()
+ );
+ if (!exists) {
+ minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucket).build());
+ log.info("已创建 bucket: {}", bucket);
+ }
+ }
+
+ @Override
+ public void uploadFile(String bucket, String objectPath, MultipartFile file) throws Exception {
+ minioClient.putObject(PutObjectArgs.builder()
+ .bucket(bucket)
+ .object(objectPath)
+ .stream(file.getInputStream(), file.getSize(), -1)
+ .contentType(Optional.ofNullable(file.getContentType()).orElse("application/octet-stream"))
+ .build());
+ log.info("上传成功: {}/{}", bucket, objectPath);
+ }
+}
diff --git a/src/main/java/com/knowledge/base/infrastructure/south/ObjectStorageGateway.java b/src/main/java/com/knowledge/base/infrastructure/south/ObjectStorageGateway.java
new file mode 100644
index 0000000..e8e2cf4
--- /dev/null
+++ b/src/main/java/com/knowledge/base/infrastructure/south/ObjectStorageGateway.java
@@ -0,0 +1,16 @@
+package com.knowledge.base.infrastructure.south;
+
+import org.springframework.web.multipart.MultipartFile;
+
+public interface ObjectStorageGateway {
+
+ /**
+ * 确保 bucket 存在,不存在则创建
+ */
+ void ensureBucketExists(String bucket) throws Exception;
+
+ /**
+ * 上传文件到指定 bucket 的路径
+ */
+ void uploadFile(String bucket, String objectPath, MultipartFile file) throws Exception;
+}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 6dffe6c..84adbaf 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -26,3 +26,6 @@ import.schedule.cron=0 0 * * * *
# Redis 开关控制
knowledge.base.redis.enable=false
+object.storage.use-uuid-prefix=true
+spring.servlet.multipart.max-file-size=200MB
+spring.servlet.multipart.max-request-size=500MB