瀏覽代碼

fix(backend): 文件存储重构 — 可配置上传目录、MIME类型校验、delete方法

• FileStorageService: 方法 rename upload→store,增加 MIME 类型白名单校验
• FileStorageService: 增加 delete() 方法用于删除已上传文件
• WebConfig: addResourceHandler 使用 upload.base-dir 替代 System.getProperty
• application.yml: 添加 upload.base-dir 和 upload.allowed-types 配置

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Xiaogang Liao 2 月之前
父節點
當前提交
d37d4b11c8

+ 5 - 1
cfc-backend/src/main/java/com/etotem/cfc/config/WebConfig.java

@@ -1,6 +1,7 @@
 package com.etotem.cfc.config;
 
 import javax.annotation.Resource;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.core.Ordered;
@@ -26,6 +27,9 @@ public class WebConfig implements WebMvcConfigurer {
     @Resource
     private OperationLogInterceptor operationLogInterceptor;
 
+    @Value("${upload.base-dir:/data/cfc-uploads}")
+    private String uploadBaseDir;
+
     @Override
     public void addCorsMappings(CorsRegistry registry) {
         registry.addMapping("/**")
@@ -113,6 +117,6 @@ public class WebConfig implements WebMvcConfigurer {
     @Override
     public void addResourceHandlers(ResourceHandlerRegistry registry) {
         registry.addResourceHandler("/uploads/**")
-                .addResourceLocations("file:" + System.getProperty("user.dir") + "/uploads/");
+                .addResourceLocations("file:" + uploadBaseDir + "/");
     }
 }

+ 54 - 21
cfc-backend/src/main/java/com/etotem/cfc/service/FileStorageService.java

@@ -12,6 +12,8 @@ import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.List;
 import java.util.UUID;
 
 @Service
@@ -19,45 +21,76 @@ public class FileStorageService {
 
     private static final Logger log = LoggerFactory.getLogger(FileStorageService.class);
 
-    @Value("${file.upload-dir:./uploads}")
-    private String uploadDir;
+    @Value("${upload.base-dir:/data/cfc-uploads}")
+    private String baseDir;
 
+    @Value("${upload.allowed-types:image/jpeg,image/png,image/gif,image/webp}")
+    private String allowedTypesStr;
+
+    private List<String> allowedTypes;
     private Path uploadPath;
 
     @PostConstruct
     public void init() {
-        uploadPath = Paths.get(uploadDir).toAbsolutePath().normalize();
+        allowedTypes = Arrays.asList(allowedTypesStr.split(","));
+        uploadPath = Paths.get(baseDir).toAbsolutePath().normalize();
         try {
             Files.createDirectories(uploadPath);
         } catch (IOException e) {
             log.error("初始化上传目录失败: {}", uploadPath, e);
         }
+        log.info("FileStorageService init: baseDir={}, allowedTypes={}", uploadPath, allowedTypes);
     }
 
-    public String upload(MultipartFile file, String subDir) {
+    public String store(MultipartFile file, String subDir) {
         if (file == null || file.isEmpty()) {
-            return null;
+            throw new IllegalArgumentException("文件为空");
         }
-        try {
-            String originalName = file.getOriginalFilename();
-            String ext = "";
-            if (originalName != null && originalName.contains(".")) {
-                ext = originalName.substring(originalName.lastIndexOf("."));
-            }
-            String filename = UUID.randomUUID().toString().replace("-", "") + ext;
+        String contentType = file.getContentType();
+        if (contentType == null || !allowedTypes.contains(contentType)) {
+            throw new IllegalArgumentException("不支持的文件类型,仅支持: " + allowedTypes);
+        }
+        String originalName = file.getOriginalFilename();
+        String ext = "";
+        if (originalName != null && originalName.contains(".")) {
+            ext = originalName.substring(originalName.lastIndexOf("."));
+        }
+        String filename = UUID.randomUUID().toString().replace("-", "") + ext;
 
-            Path targetDir = uploadPath.resolve(subDir != null ? subDir : "general");
+        Path targetDir = uploadPath.resolve(subDir != null ? subDir : "general");
+        try {
             Files.createDirectories(targetDir);
+        } catch (IOException e) {
+            throw new RuntimeException("创建上传目录失败: " + targetDir, e);
+        }
 
-            Path targetPath = targetDir.resolve(filename);
+        Path targetPath = targetDir.resolve(filename);
+        try {
             file.transferTo(targetPath.toFile());
-
-            String relativePath = (subDir != null ? "/" + subDir : "") + "/" + filename;
-            log.info("文件已保存: {}", relativePath);
-            return relativePath;
         } catch (IOException e) {
-            log.error("文件上传失败", e);
-            return null;
+            throw new RuntimeException("文件保存失败", e);
+        }
+
+        String url = "/uploads/" + (subDir != null ? subDir + "/" : "") + filename;
+        log.info("文件已保存: original={}, url={}", originalName, url);
+        return url;
+    }
+
+    public boolean delete(String url) {
+        if (url == null || url.isEmpty() || !url.startsWith("/uploads/")) {
+            return false;
+        }
+        String relativePath = url.replace("/uploads/", "");
+        Path filePath = uploadPath.resolve(relativePath);
+        if (Files.exists(filePath)) {
+            try {
+                Files.delete(filePath);
+                log.info("文件已删除: {}", url);
+                return true;
+            } catch (IOException e) {
+                log.error("文件删除失败: {}", url, e);
+            }
         }
+        return false;
     }
-}
+}

+ 4 - 0
cfc-backend/src/main/resources/application.yml

@@ -93,6 +93,10 @@ math:
   verify-mode: dify  # dify | eval; dify=走Dify工作流, eval=服务端计算(兜底)
   workflow-id: ""    # Dify 数学判题工作流 ID(留空则使用 eval 兜底)
 
+upload:
+  base-dir: /data/cfc-uploads
+  allowed-types: image/jpeg,image/png,image/gif,image/webp
+
 shop:
   order:
     payment-timeout-minutes: 30  # 订单超时时间(分钟)