Última atividade 5 months ago

前端根据 Content-Type 响应类型决定下载文件或提醒

waynelone revisou este gist 5 months ago. Ir para a revisão

1 file changed, 0 insertions, 0 deletions

DemoController.java renomeado para FileController.java

Arquivo renomeado sem alterações

waynelone revisou este gist 5 months ago. Ir para a revisão

1 file changed, 4 insertions, 4 deletions

DemoController.java

@@ -16,12 +16,12 @@ import java.net.URLEncoder;
16 16 import java.nio.charset.StandardCharsets;
17 17
18 18 /**
19 - * 学生信息
19 + * 文件
20 20 **/
21 21 @RestController
22 - @RequestMapping("api/student")
23 - @Tag(name = "学生信息")
24 - public class StudentController {
22 + @RequestMapping("api/file")
23 + @Tag(name = "文件")
24 + public class FileController {
25 25
26 26 @GetMapping("download-image")
27 27 @Operation(summary = "下载")

waynelone revisou este gist 5 months ago. Ir para a revisão

4 files changed, 194 insertions

DemoController.java(arquivo criado)

@@ -0,0 +1,41 @@
1 + import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
2 + import com.demo.common.reply.Result;
3 + import io.swagger.v3.oas.annotations.Operation;
4 + import io.swagger.v3.oas.annotations.Parameter;
5 + import io.swagger.v3.oas.annotations.enums.ParameterIn;
6 + import io.swagger.v3.oas.annotations.tags.Tag;
7 + import org.springframework.core.io.ClassPathResource;
8 + import org.springframework.core.io.Resource;
9 + import org.springframework.http.HttpHeaders;
10 + import org.springframework.http.ResponseEntity;
11 + import org.springframework.web.bind.annotation.GetMapping;
12 + import org.springframework.web.bind.annotation.RequestMapping;
13 + import org.springframework.web.bind.annotation.RestController;
14 +
15 + import java.net.URLEncoder;
16 + import java.nio.charset.StandardCharsets;
17 +
18 + /**
19 + * 学生信息
20 + **/
21 + @RestController
22 + @RequestMapping("api/student")
23 + @Tag(name = "学生信息")
24 + public class StudentController {
25 +
26 + @GetMapping("download-image")
27 + @Operation(summary = "下载")
28 + @Parameter(name = "filename", description = "文件名称", in = ParameterIn.QUERY)
29 + @ApiOperationSupport(order = 5)
30 + public ResponseEntity<Object> download(String filename) {
31 + Resource image = new ClassPathResource("static/" + filename);
32 + if (!image.exists()) {
33 + return ResponseEntity.ok(Result.error(404, "文件不存在"));
34 + }
35 + String fileName = URLEncoder.encode("网站图标.png", StandardCharsets.UTF_8);
36 + HttpHeaders headers = new HttpHeaders();
37 + headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + fileName);
38 + headers.add(HttpHeaders.CONTENT_TYPE, "image/png");
39 + return ResponseEntity.ok().headers(headers).body(image);
40 + }
41 + }

Result.java(arquivo criado)

@@ -0,0 +1,42 @@
1 + import io.swagger.v3.oas.annotations.media.Schema;
2 + import lombok.AllArgsConstructor;
3 + import lombok.Data;
4 + import lombok.NoArgsConstructor;
5 +
6 + /**
7 + * 响应结果
8 + **/
9 + @Data
10 + @AllArgsConstructor
11 + @NoArgsConstructor
12 + public class Result<T> {
13 + /**
14 + * 状态码
15 + */
16 + @Schema(description = "状态码 0-响应成功")
17 + private int code;
18 +
19 + /**
20 + * 消息
21 + */
22 + @Schema(description = "消息")
23 + private String msg;
24 +
25 + /**
26 + * 响应数据
27 + */
28 + @Schema(description = "响应数据")
29 + private T data;
30 +
31 + public static <T> Result<T> ok() {
32 + return new Result<>(0, "ok", null);
33 + }
34 +
35 + public static <T> Result<T> ok(T data) {
36 + return new Result<>(0, "ok", data);
37 + }
38 +
39 + public static <T> Result<T> error(Integer code, String msg) {
40 + return new Result<>(code, msg, null);
41 + }
42 + }

WebMvcConfig.java(arquivo criado)

@@ -0,0 +1,17 @@
1 + import org.springframework.context.annotation.Configuration;
2 + import org.springframework.web.servlet.config.annotation.CorsRegistry;
3 + import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
4 +
5 + @Configuration
6 + public class WebMvcConfig implements WebMvcConfigurer {
7 +
8 + @Override
9 + public void addCorsMappings(CorsRegistry registry) {
10 + registry.addMapping("/**")
11 + .allowedOriginPatterns("*")
12 + .allowCredentials(true)
13 + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
14 + .exposedHeaders("Content-Disposition") // 跨域时要开启此响应头 否则前端获取不到
15 + .maxAge(3600);
16 + }
17 + }

demo.html(arquivo criado)

@@ -0,0 +1,94 @@
1 + <!DOCTYPE html>
2 + <html lang="en">
3 +
4 + <head>
5 + <meta charset="UTF-8">
6 + <meta name="viewport" content="width=device-width, initial-scale=1.0">
7 + <title>测试文件下载</title>
8 + </head>
9 +
10 + <body>
11 + <button onclick="download('http://localhost:8080/api/file/download-image?filename=favicon.png')">下载文件</button>
12 + <script>
13 + /**
14 + * 使用 Promise 链式写法下载文件
15 + * 处理:1. URL编码的文件名 2. 200状态码下的JSON错误响应(文件不存在等)
16 + * @param {string} url - 后端文件下载接口地址
17 + * @returns {Promise<void>} - 无返回值,通过alert提示结果
18 + */
19 + function download(url) {
20 + // 返回Promise对象,开启链式调用
21 + return fetch(url, {
22 + method: 'GET',
23 + headers: {
24 + 'Accept': '*/*' // 允许接收文件(blob)和JSON两种响应
25 + }
26 + })
27 + // 第一步:解析响应(先判断Content-Type,区分文件/JSON)
28 + .then(response => {
29 + const contentType = response.headers.get('Content-Type');
30 + // 场景1:响应为JSON(即使状态码200,也可能是错误信息)
31 + if (contentType && contentType.includes('application/json')) {
32 + return response.json().then(jsonData => {
33 + // 抛出包含JSON错误信息的异常,交给catch处理
34 + throw new Error(JSON.stringify(jsonData));
35 + });
36 + }
37 + // 场景2:响应为文件(非JSON),直接返回response供后续处理
38 + return response;
39 + })
40 + // 第二步:处理文件下载(仅当响应为文件时执行)
41 + .then(fileResponse => {
42 + // 解析URL编码的文件名
43 + const disposition = fileResponse.headers.get('Content-Disposition');
44 + let fileName = 'downloaded-file'; // 默认文件名
45 + console.log('Content-Disposition:', disposition);
46 + if (disposition && disposition.includes('filename=')) {
47 + // 提取filename=后的内容,移除引号并解码URL编码
48 + fileName = decodeURIComponent(
49 + disposition.split('filename=')[1].replace(/["']/g, '')
50 + );
51 + }
52 +
53 + // 转换为Blob并触发下载
54 + return fileResponse.blob().then(blob => {
55 + if (window.navigator.msSaveOrOpenBlob) {
56 + navigator.msSaveBlob(blob, filename)
57 + } else {
58 + const blobUrl = URL.createObjectURL(blob);
59 + const a = document.createElement('a');
60 + a.href = blobUrl;
61 + a.download = fileName;
62 + document.body.appendChild(a);
63 + a.click();
64 +
65 + // 清理临时资源(避免内存泄漏)
66 + setTimeout(() => {
67 + document.body.removeChild(a);
68 + URL.revokeObjectURL(blobUrl);
69 + }, 100);
70 + }
71 + });
72 + })
73 + // 第三步:统一捕获所有错误(网络错误、JSON错误、业务异常等)
74 + .catch(error => {
75 + try {
76 + // 尝试解析JSON格式的错误信息(文件不存在等后端自定义错误)
77 + const errorData = JSON.parse(error.message);
78 + // 匹配"文件不存在"的错误码(code=404)
79 + if (errorData.code === 404) {
80 + alert(`下载失败:${errorData.msg || '文件不存在'}`);
81 + } else {
82 + alert(`下载失败:${errorData.msg || '未知错误'}`);
83 + }
84 + } catch (parseErr) {
85 + // 非JSON错误(网络异常、解析失败等)
86 + alert(`下载出错:${error.message || '请检查网络或接口地址'}`);
87 + }
88 + console.error('文件下载完整错误信息:', error);
89 + });
90 + }
91 + </script>
92 + </body>
93 +
94 + </html>
Próximo Anterior