“mitao”
2026-03-24 e7f9345c528ab4d6f18d8e450d8178845989d5e1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
package com.tcm.training.admin.upload;
 
import com.tcm.training.admin.common.ApiResponse;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
 
import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.io.IOException;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
 
@RestController
@RequestMapping({"/api/v1", "/api"})
public class UploadController {
 
    private static final long MAX_SIZE = 5 * 1024 * 1024L; // 5MB
    private static final Set<String> ALLOWED_TYPES = new HashSet<>(
            Arrays.asList("image/jpeg", "image/png", "image/gif", "image/webp")
    );
 
    @Value("${app.upload.dir:uploads}")
    private String uploadDir;
 
    @Value("${app.upload.path-prefix:/uploads}")
    private String pathPrefix;
 
    /**
     * 图片上传
     * POST /api/v1/upload/image
     * @return { url: "访问地址" }
     */
    @PostMapping("/upload/image")
    public ApiResponse<UploadResult> uploadImage(
            @RequestParam("file") MultipartFile file,
            HttpServletRequest request) throws IOException {
 
        // 校验文件非空
        if (file == null || file.isEmpty()) {
            throw new IllegalArgumentException("请选择要上传的文件");
        }
 
        // 校验文件大小
        if (file.getSize() > MAX_SIZE) {
            throw new IllegalArgumentException("文件大小不能超过 5MB");
        }
 
        // 校验文件类型
        String contentType = file.getContentType();
        if (contentType == null || !ALLOWED_TYPES.contains(contentType)) {
            throw new IllegalArgumentException("仅支持 JPG、PNG、GIF、WebP 格式");
        }
 
        // 生成子目录(按日期)和唯一文件名
        String dateDir = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
        String originalName = file.getOriginalFilename();
        String ext = "";
        if (originalName != null && originalName.contains(".")) {
            ext = originalName.substring(originalName.lastIndexOf("."));
        }
        String fileName = UUID.randomUUID().toString().replace("-", "") + ext;
 
        // 创建目录并保存文件(强制绝对路径,规避 transferTo 临时文件问题)
        File dir = new File(uploadDir, dateDir).getAbsoluteFile();
        if (!dir.exists() && !dir.mkdirs()) {
            throw new IOException("创建上传目录失败: " + dir.getAbsolutePath());
        }
        File dest = new File(dir, fileName);
        // 用 InputStream 手动拷贝,避免 Spring transferTo 依赖临时文件的问题
        try (java.io.InputStream in = file.getInputStream();
             java.io.OutputStream out = new java.io.FileOutputStream(dest)) {
            byte[] buf = new byte[8192];
            int n;
            while ((n = in.read(buf)) != -1) {
                out.write(buf, 0, n);
            }
        }
 
        // 构造访问 URL
        String baseUrl = request.getScheme() + "://" + request.getServerName()
                + ":" + request.getServerPort();
        String url = baseUrl + pathPrefix + "/" + dateDir + "/" + fileName;
 
        return ApiResponse.ok(new UploadResult(url));
    }
 
    public static class UploadResult {
        private final String url;
 
        public UploadResult(String url) {
            this.url = url;
        }
 
        public String getUrl() {
            return url;
        }
    }
}