20251211-记录一个导出Excel的故事

Table of Contents

事件经过

  今天修改了一个关于导出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]]",
}

归纳一下:

  1. 这是报错是一个关于导出Excel文件的问题;
  2. 采用URL,问号传参;参数createTimeStart被转义了;
  3. 后端绑定时,错误解析了被转义的参数;

解决办法:

  • 正确解析转义即可。

  这个接口,为什么要设计成 GET 请求,通过问号传参的形式来下载文件? 问号传参本来就不适合复杂请求啊。拼接字符有限制,而且还会转义特殊字符。所以,最简单的做法,就是把 GET 请求改成 POST 请求,采用Body传参。可当时为什么要这么设计呢?这不禁勾起来了我的思绪。

原因

  1. GET 请求可以通过浏览器直接打开下载;
  2. 之前是导出所有数据,偶有需要筛选条件的,也只是一个两个简单的筛选条件;
  3. 当时的前端开发人员,是个新手,不会做啊。
    • 而当时还没有AI工具,他查的资料,都是来自博客园,csdn,百度等等。
    • 最终,还是没做出来。
  4. 同时,项目进度也比较赶。于是就把这个事情搁置了;
  5. 于是乎,后来导出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>

Date: 2025-12-11 四 16:19