前后端联合实现文件下载,实现 SQL Server image 类型文件下载
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、应用效果
文件名称支持空格和加号