20251211-记录一个导出Excel的故事
事件经过
今天修改了一个关于导出Excel的Bug。内容如下:
{
"id": "4648",
"url": "GET //web/export_excel?createTimeStart=2025-12-08%2000%3A00%3A00&createTimeEnd=2025-12-10%2000%3A00%3A00",
"body": "",
"createName": null,
"createTime": "2025-12-10 14:33:56",
"msg": "BindException\norg.springframework.validation.BeanPropertyBindingResult: 2 errors\nField error in object 'zyWebOrderQueryParams' on field 'createTimeEnd': rejected value [2025-12-10 00:00:00]; codes [typeMismatch.zyWebOrderQueryParams.createTimeEnd,typeMismatch.createTimeEnd,typeMismatch.java.time.LocalDateTime,typeMismatch]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [zyWebOrderQueryParams.createTimeEnd,createTimeEnd]; arguments []; default message [createTimeEnd]]; default message [Failed to convert property value of type 'java.lang.String' to required type 'java.time.LocalDateTime' for property 'createTimeEnd'; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [@com.fasterxml.jackson.annotation.JsonFormat java.time.LocalDateTime] for value '2025-12-10 00:00:00'; nested exception is java.lang.IllegalArgumentException: Parse attempt failed for value [2025-12-10 00:00:00]]\nField error in object 'zyWebOrderQueryParams' on field 'createTimeStart': rejected value [2025-12-08 00:00:00]; codes [typeMismatch.zyWebOrderQueryParams.createTimeStart,typeMismatch.createTimeStart,typeMismatch.java.time.LocalDateTime,typeMismatch]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [zyWebOrderQueryParams.createTimeStart,createTimeStart]; arguments []; default message [createTimeStart]]; default message [Failed to convert property value of type 'java.lang.String' to required type 'java.time.LocalDateTime' for property 'createTimeStart'; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [@com.fasterxml.jackson.annotation.JsonFormat java.time.LocalDateTime] for value '2025-12-08 00:00:00'; nested exception is java.lang.IllegalArgumentException: Parse attempt failed for value [2025-12-08 00:00:00]]",
}
归纳一下:
- 这是报错是一个关于导出Excel文件的问题;
- 采用URL,问号传参;参数createTimeStart被转义了;
- 后端绑定时,错误解析了被转义的参数;
解决办法:
- 正确解析转义即可。
这个接口,为什么要设计成 GET 请求,通过问号传参的形式来下载文件? 问号传参本来就不适合复杂请求啊。拼接字符有限制,而且还会转义特殊字符。所以,最简单的做法,就是把 GET 请求改成 POST 请求,采用Body传参。可当时为什么要这么设计呢?这不禁勾起来了我的思绪。
原因
- GET 请求可以通过浏览器直接打开下载;
- 之前是导出所有数据,偶有需要筛选条件的,也只是一个两个简单的筛选条件;
- 当时的前端开发人员,是个新手,不会做啊。
- 而当时还没有AI工具,他查的资料,都是来自博客园,csdn,百度等等。
- 最终,还是没做出来。
- 同时,项目进度也比较赶。于是就把这个事情搁置了;
- 于是乎,后来导出excel的做法,就一直使用 GET 请求,问号传参的形式了;
正确做法
前端通过js该如何处理 POST 请求,Body传参来下载文件呢?我找了一下元宝AI。
深度思考
我们需要使用JavaScript(可能是浏览器环境或Node.js环境)来调用这个API并下载文件。
由于是POST请求,并且返回的是文件(Excel),我们需要将响应作为文件下载。
在浏览器环境中,我们可以使用XMLHttpRequest或fetch API。但是,考虑到需要保存文件,使用fetch可能更现代。
步骤:
发送POST请求到指定的URL,携带JSON数据。
获取响应,响应应该是二进制的Excel文件。
将响应转换为Blob对象,然后创建一个下载链接,触发下载。
注意:由于跨域问题,如果API不在同一个域下,可能需要服务器支持CORS。但这里我们假设可以访问(或者是在同域下)。
另外,请求头中包含了Authorization,需要带上。
我们将使用fetch API来实现。
代码结构:
准备请求参数(url, headers, body)
使用fetch发送请求
检查响应状态,如果成功则处理二进制数据
创建Blob对象,并生成下载链接
模拟点击链接进行下载
由于是demo,我们直接在浏览器中运行,可能会遇到跨域问题,所以最好在支持CORS的环境下测试,或者确保API允许跨域。
如果是在Node.js环境下,可以使用node-fetch和fs模块,但这里要求弹窗保存文件,所以应该是浏览器环境。
示例
<!DOCTYPE html>
<html>
<body>
<button onclick="downloadExcel()">导出Excel</button>
<script>
async function downloadExcel() {
const url = 'xxx/export_excel';
const data = {
createTimeStart: "2025-12-08 00:00:00",
createTimeEnd: "2025-12-10 00:00:00"
};
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Accept': '*/*',
'Accept-Encoding': 'gzip, deflate, br',
'Authorization': 'CfkE9uncli_1-WEB',
'Connection': 'keep-alive',
'Content-Type': 'application/json',
'User-Agent': 'PostmanRuntime-ApipostRuntime/1.1.0'
},
body: JSON.stringify(data)
});
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
// 获取文件名(从响应头提取)
const contentDisposition = response.headers.get('Content-Disposition');
let filename = 'export.xlsx';
if (contentDisposition) {
const filenameMatch = contentDisposition.match(/filename="?(.+)"?/);
if (filenameMatch.length > 1) filename = filenameMatch[1];
}
// 转换为Blob并下载
const blob = await response.blob();
const downloadUrl = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = downloadUrl;
a.download = filename;
document.body.appendChild(a);
a.click();
// 清理资源
setTimeout(() => {
document.body.removeChild(a);
URL.revokeObjectURL(downloadUrl);
}, 100);
} catch (error) {
console.error('下载失败:', error);
alert('文件下载失败: ' + error.message);
}
}
</script>
</body>
</html>