1、前端 Vue3
QualityFile.vue
<script setup lang="ts" name="QualityFile">
......
// 下載,實現 SQL Server image 類型文件下載
const onDownloadClick = async (fileNo: string) => {// const result = await qualityFileDownloadFileWithPutService(fileNo);// const result = await qualityFileDownloadFileService(fileNo);const result = await qualityFileDownloadFileWithPostService(fileNo);downloadFile(result);
};
......
</script><template>
......<el-table-column label="操作" width="150" header-align="center" align="center" fixed="right"><template #default="scope"><BasePreventReClickButtonclass="table-btn"type="primary"size="default"text@click="onDownloadClick(scope.row.fileNo)">下載</BasePreventReClickButton></template></el-table-column>
......
</template>
qualityFile.ts
import request from "@/utils/request";
import type { IQualityFile, IQualityFileQueryObj } from "@/views/resources/QualityFile/types";/*** 下載質量體系文件,實現 SQL Server image 類型文件下載,使用 get 請求* @param fileNo 文件編號(可能包含特殊字符如 /)* @returns 文件流 {@link Blob}*/
export const qualityFileDownloadFileService = (fileNo: string) => {// 對特殊字符進行編碼處理const encodedFileNo = encodeURIComponent(fileNo);return request.get("/resources/qualityFile/downloadFile", {params: {fileNo: encodedFileNo},// 響應類型為 blob,用于接收二進制數據流responseType: "blob"});
};/*** 下載質量體系文件,實現 SQL Server image 類型文件下載,使用 post 請求* @param fileNo 文件編號(可能包含特殊字符如 /)* @returns 文件流 {@link Blob}*/
export const qualityFileDownloadFileWithPostService = (fileNo: string) => {// 使用 post 請求避免 URL 解析問題return request.post("/resources/qualityFile/downloadFile",{// 使用 post,直接傳遞參數,無需編碼fileNo: fileNo},{// 響應類型為 blob,用于接收二進制數據流responseType: "blob"});
};/*** 下載質量體系文件,實現 SQL Server image 類型文件下載,使用 put 請求* @param fileNo 文件編號(可能包含特殊字符如 /)* @returns 文件流 {@link Blob}*/
export const qualityFileDownloadFileWithPutService = (fileNo: string) => {return request.put("/resources/qualityFile/downloadFile", null, {params: {fileNo: fileNo},// 響應類型為 blob,用于接收二進制數據流responseType: "blob"});
};
download.ts
import { type AxiosResponse } from "axios";/*** 下載文件* @param response Axios 響應對象(需包含 Blob 數據)* @param fileName 可選文件名(未提供時從 Content-Disposition 中提取)*/
export const downloadFile = (response: AxiosResponse<Blob>, fileName?: string) => {try {// 從響應標頭中獲取文件名(后端需設置 Content-Disposition)// 從響應標頭中獲取 content-disposition 屬性的信息const contentDisposition = response.headers["content-disposition"];// let fileName = "download-file";// if (contentDisposition) {// // 通過正則表達式解析出文件名稱,數據示例:['filename=%E6%96%87%E4%BB%B6.txt', '%E6%96%87%E4%BB%B6.txt', '', index: 11, input: 'attachment;filename=%E6%96%87%E4%BB%B6.txt', groups: undefined]// const fileNameMatch = contentDisposition.match(/filename=(.*?)(;|$)/);// if (fileNameMatch && fileNameMatch.length > 2) {// // 獲取原始文件名稱(索引為 1 的元素內容)// // decodeURIComponent 是 JavaScript 的內置函數,用于解碼通過 URL 傳輸的編碼字符// // matchArray[1] 通常是通過正則表達式匹配得到的文件名字符串,可能包含 URL 編碼(如 %E6%96%87%E4%BB%B6.txt)// // 經過解碼后,fileName 得到的是可讀的原始文件名(如 文件.txt)// fileName = decodeURIComponent(fileNameMatch[1]);// }// }const fileNameMatch = contentDisposition.match(/filename="?(.+)"?/);// 后端使用 URLEncoder 編碼,前端使用 decodeURIComponent 解碼// let downloadFileName = fileName || fileNameMatch ? decodeURIComponent(fileNameMatch[1]) : "download-file";// 處理編碼問題// 原值:CZCDC∕QM-2018-B2 4.2 人員.doc// 后端編碼傳過來的值:CZCDC%E2%88%95QM-2018-B2+4.2+%E4%BA%BA%E5%91%98.doc// 前端使用 decodeURIComponent 解碼后的值:CZCDC∕QM-2018-B2+4.2+人員.doc// 將 + 替換為 空格// downloadFileName = downloadFileName.replace("+", " "); // 只替換前面第一個 +// downloadFileName = downloadFileName.replace(/\+/g, " "); // 使用正則表達式替換所有 +// 統一編碼解碼規則:后端使用 UriUtils 編碼,前端使用 decodeURIComponent 解碼,此方案支持空格和+等特殊字符let downloadFileName = "download-file";try {downloadFileName = fileName || fileNameMatch ? decodeURIComponent(fileNameMatch[1]) : "download-file";} catch (error) {console.error("解碼失敗:", error);}// 創建 Blob 對象// 將接收到的響應消息體的內容(二進制數據流)response.data,創建為 Blob 對象,用于對文件的操作const blob = new Blob([response.data]);// 下載文件// 創建鏈接標簽 aconst link = document.createElement("a");link.style.display = "none";// 設置鏈接路徑,將響應消息體的內容(二進制數據流)轉換為 url 地址對象link.href = URL.createObjectURL(blob);// 設置下載的文件名稱link.download = downloadFileName;// 增加鏈接標簽document.body.appendChild(link);// 觸發下載,模擬點擊鏈接標簽,下載文件link.click();// 清理資源// 移除 url 地址對象,釋放資源URL.revokeObjectURL(link.href);// 移除鏈接標簽document.body.removeChild(link);} catch (error) {console.error("下載文件失敗!", error);throw new Error(`下載文件失敗: ${error instanceof Error ? error.message : String(error)}`);}
};/*** 下載靜態文件* @param fileUrl 靜態文件地址*/
export const downloadStaticFile = (fileUrl: string, fileName?: string) => {// 文件路徑,開頭的 / 表示 public 目錄// const fileUrl = "/template/試劑導入模板.xlsx";// todo 檢查路徑if (!fileUrl) return;// 下載文件// 創建鏈接標簽 aconst link = document.createElement("a");// 設置鏈接路徑link.href = fileUrl;// 設置下載的文件名(可選)if (fileName) link.download = fileName;// 增加鏈接標簽document.body.appendChild(link);// 觸發下載,模擬點擊鏈接標簽,下載文件link.click();// 移除鏈接標簽document.body.removeChild(link);
};
2、后端 Spring boot + Mybatis
控制層:FileDownloadController.java
package com.weiyu.controller;import com.weiyu.anno.Debounce;
import com.weiyu.pojo.FileData;
import com.weiyu.service.FileDownloadService;
import com.weiyu.utils.FileDownloadUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.Map;/*** 文件下載 Controller*/
@RestController
@Slf4j
public class FileDownloadController {@Autowiredprivate FileDownloadService fileDownloadService;/*** 質量體系文件下載,實現 SQL Server image 類型文件下載,使用 @GetMapping 接收請求* @param fileNo 文件編號(可能包含特殊字符如 /)* @return 文件數據流 {@link ResponseEntity}<{@link Resource}>* @apiNote 本接口使用防抖機制,5s 內重復請求會被忽略*/@GetMapping("/resources/qualityFile/downloadFile")@Debounce(key = "/resources/qualityFile/downloadFile", value = 5000)public ResponseEntity<Resource> downloadFileForQualityFile(@RequestParam String fileNo) {log.info("【質量體系文件下載】,實現 SQL Server image 類型文件下載,使用 @GetMapping 接收請求," +"/resources/qualityFile/downloadFile,fileNo = {}", fileNo);// 解碼參數(Spring 默認會自動解碼,但顯式處理更安全)String decodedFileNo = URLDecoder.decode(fileNo, StandardCharsets.UTF_8);// 獲取文件數據FileData fileData = fileDownloadService.queryFileDataForQualityFile(decodedFileNo);return FileDownloadUtil.downloadFile(fileData);}/*** 質量體系文件下載,實現 SQL Server image 類型文件下載,使用 @PostMapping 接收請求* @param argsMap 參數Map,包含 fileNo* @return 文件數據流 {@link ResponseEntity}<{@link Resource}>* @apiNote 本接口使用防抖機制,5s 內重復請求會被忽略*/@PostMapping("/resources/qualityFile/downloadFile")@Debounce(key = "/resources/qualityFile/downloadFile", value = 5000)public ResponseEntity<Resource> downloadFileForQualityFileWithPost(@RequestBody Map<String, String> argsMap) {log.info("【質量體系文件下載】,實現 SQL Server image 類型文件下載,使用 @PostMapping 接收請求," +"/resources/qualityFile/downloadFile,argsMap = {}", argsMap);// 從參數Map中獲取文件編號String fileNo = argsMap.get("fileNo");// 獲取文件數據FileData fileData = fileDownloadService.queryFileDataForQualityFile(fileNo);return fileDownloadService.downloadFile(fileData);}
}
服務層接口實現:FileDownloadServiceImpl.java
package com.weiyu.service.impl;import com.weiyu.mapper.FileDownloadMapper;
import com.weiyu.pojo.FileData;
import com.weiyu.service.FileDownloadService;
import com.weiyu.utils.FileDownloadUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;/*** 文件下載 Service 接口實現*/
@Service
public class FileDownloadServiceImpl implements FileDownloadService {@Autowiredprivate FileDownloadMapper fileDownloadMapper;/*** 查詢質量體系文件數據* @param fileNo 文件編號*/@Overridepublic FileData queryFileDataForQualityFile(String fileNo) {return fileDownloadMapper.selectFileDataForQualityFile(fileNo);}/*** 下載文件* @param fileData 文件數據對象*/@Overridepublic ResponseEntity<Resource> downloadFile(FileData fileData) {return FileDownloadUtil.downloadFile(fileData);}
}
數據表結構數據傳輸對象 DTO:FileData.java
package com.weiyu.pojo;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;/*** 文件數據*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class FileData {private String fileName;private byte[] fileContent;
}
持久層:FileDownloadMapper.java
package com.weiyu.mapper;import com.weiyu.pojo.FileData;
import org.apache.ibatis.annotations.Mapper;/*** 文件下載 Mapper*/
@Mapper
public interface FileDownloadMapper {/*** 查詢質量體系文件數據* @param fileNo 文件編號*/FileData selectFileDataForQualityFile(String fileNo);
}
持久層數據庫sql查詢:FileDownloadMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.weiyu.mapper.FileDownloadMapper"><!--mssql--><!-- 查詢質量體系文件數據 --><select id="selectFileDataForQualityFile" resultType="com.weiyu.pojo.FileData">selectcfm_ContentFileName as fileName, cfm_Content as fileContentfrom ControledFileMainwhere Cfm_BigType = '3' and Cfm_ID = #{fileNo}</select>
</mapper>
文件下載工具類:FileDownloadUtil.java
package com.weiyu.utils;import com.weiyu.pojo.FileData;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.util.UriUtils;import java.nio.charset.StandardCharsets;/*** 文件下載工具*/
public class FileDownloadUtil {/*** 下載文件* @param fileData 文件數據對象 {@link FileData}* @return 文件數據流 {@link ResponseEntity}<{@link Resource}>*/public static ResponseEntity<Resource> downloadFile(FileData fileData) {// 創建資源對象ByteArrayResource resource = new ByteArrayResource(fileData.getFileContent());// 資源為nullif (resource.contentLength() == 0) {return ResponseEntity.noContent().build();}// 編碼示例:空格 編碼為 +,前端解碼后還是 +// URLEncoder.encode("CZCDC∕QM-2018-B2 4.2 人員.doc", StandardCharsets.UTF_8));// 編碼為:CZCDC%E2%88%95QM-2018-B2+4.2+%E4%BA%BA%E5%91%98.doc// 前端解碼為:CZCDC∕QM-2018-B2+4.2+人員.doc// 這個問題的根本原因在于 URLEncoder.encode 使用 application/x-www-form-urlencoded 編碼標準,它使用 + 表示空格。但在某些上下文中,這個 + 沒有被正確解碼回空格。// String encodedFileName = URLEncoder.encode(fileData.getFileName(), StandardCharsets.UTF_8);// 后端處理空格編碼,將 + 替換為 空格// encodedFileName = encodedFileName.replace("+", " ");// 統一編碼解碼規則:后端使用 UriUtils 編碼,前端使用 decodeURIComponent 解碼,此方案支持空格和+等特殊字符String encodedFileName = UriUtils.encode(fileData.getFileName(), StandardCharsets.UTF_8);// 返回響應實體return ResponseEntity// 設置狀態.ok()// 設置內容類型為 MediaType.APPLICATION_OCTET_STREAM,八位字節的二進制數據流.contentType(MediaType.APPLICATION_OCTET_STREAM).contentLength(resource.contentLength())// 設置響應標頭,添加屬性 Content-Disposition,Content-Disposition就是當用戶想把請求所得的內容存為一個文件的時候提供一個默認的文件名。// 其屬性值必須要加上attachment,如: attachment;filename="name.xlsx",就是文件名稱的信息,并且文件名稱需要用雙引號包裹(不支持中文編碼,需要編碼轉換)// 設置內容處置為附件,并指定文件名,到時前端就可以解析這個響應頭拿到這個文件名稱進行下載// .header("Content-Disposition", "attachment;filename=\"" + URLEncoder.encode(fileName, StandardCharsets.UTF_8) +"\"")// 實際測試發現文件名稱不用雙引號包裹,也是可以達到需求目標,并且前端通過正則表達式解析出文件名稱時還簡單一些// 文件名通常放在雙引號內,如果文件名包含空格或特殊字符,使用雙引號是必要的.header("Content-Disposition", "attachment;filename=" + encodedFileName)// 設置響應消息體為 resource.body(resource);}
}
3、應用效果
文件名稱支持空格和加號