waynelone revidoval tento gist 5 months ago. Přejít na revizi
1 file changed, 0 insertions, 0 deletions
DemoController.java přejmenováno na FileController.java
Soubor přejmenován beze změn
waynelone revidoval tento gist 5 months ago. Přejít na revizi
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 revidoval tento gist 5 months ago. Přejít na revizi
4 files changed, 194 insertions
DemoController.java(vytvořil soubor)
| @@ -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(vytvořil soubor)
| @@ -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(vytvořil soubor)
| @@ -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(vytvořil soubor)
| @@ -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> | |