收尾完成。结论很明确:这次的问题并不在后端——文件确实生成并返回了,只是前端没把它保存下来。

一、后端确实无误:日志可以证明

tabycore-app/nohup.log 里你三次点击的记录都在:

Code
复制成功
16:14:51.976 getMissExportDataForCar 车险需补录保单导出sql = select distinct ...
16:14:54.814 Obj2ExcelUtils excelExportWeb - 导出结束时间
16:15:11.392 / 16:15:11.487 (第二次,1.4s 出文件)
16:15:49.946 / 16:15:50.034 (第三次,0.7s 出文件)

这行“导出结束时间”只有在走完 POI 写盘时才会打印;如果返回 000003(无数据),代码在生成文件之前就已经返回,根本不会打印这行。你在 Network 中看到的乱码,首字节是 D0 CF 11 E0 A1 B1 1A E1,这正是 OLE2/CFB 魔数,说明它是一个合法的 .xls 文件体,不是报错文本。

二、真正原因:XHR 不会因为 Content-Disposition: attachment 自动下载

attachment 这个响应头只对“浏览器导航”生效(地址栏、location.href、window.open、表单提交)。用 XMLHttpRequest/axios 发起请求时,浏览器会把文件字节交给 JS,不会自己弹出下载。如果 JS 没有用 responseType:'blob' 并把字节流转换成 <a download>,结果就是:200、响应体有内容、但既没有文件也没有提示。

这也是项目里的老问题:tabyperf 的 axios transformResponse 是空函数(没有 return),导致 res.data 为 undefined,回落到 responseText 后,前端只拿到一串二进制字符串。

三、后端已做的加固(本次新增,PropertyInsuranceController.java)

  1. 先查数据、再设置响应头:/policyData/miss/export、/policyData/export、/endorse/export 三个接口统一。失败(000002 参数、000003 无数据、000001 生成失败)时,调用 writeExportError() → response.reset() + application/json + {"code","msg","flag":false},前端能正常解析并弹出中文提示。以前是“先挂 attachment 头再抛异常”,前端拿到的是一段带 excel 响应头的 JSON,导致既无法下载文件,也看不到提示。
  2. writeExcelStream() 改为先读成 byte[] 再写,并设置 Content-Length,用来替代 chunked 传输(chunked 下代理和 XHR 更容易拿到半截或空 blob);文件名同时给出 filename="export.xls" 和 RFC 5987 filename*=UTF-8''...。
  3. 添加字节日志:成功时打印 导出已返回文件, file=保单缺失项导出.xls, bytes=N;客户端中途取消下载只记录 warn,不抛出异常。以后出现同类问题时,看一眼日志就能分清是“后端没给文件”还是“前端没存文件”。
  4. 产险批单导出 /endorse/export 存在同一个缺陷,现在也一并改成这套 helper。

编译验证:verify9 跑通,1311 个源文件 0 错误 + 诱饵被捕获(类型检查确实生效)。

四、前端需要改的一处(请你确认,我没有动前端)

static/dist/icrm/js/9.52a36f9f0fd47c7d1270.js 里的 downloadMissExport 已经使用了 XHR + responseType="blob" + <a download>,整体思路是对的,但结尾是:

JavaScript
复制成功
o.click();
document.body.removeChild(o);
window.URL.revokeObjectURL(o.href); // ← 紧跟 click 同步 revoke

Chromium 的 blob 下载是异步启动的,点击后立刻 revokeObjectURL 有可能把还没开始读取的 blob URL 释放掉,表现为“没有文件、没有报错、控制台也没有异常”。源码里改成:

JavaScript
复制成功
document.body.appendChild(a);
a.click();
setTimeout(function () { // 给下载留出启动时间
document.body.removeChild(a);
window.URL.revokeObjectURL(a.href);
}, 4000);

如果这个页面实际走的是 axios 那条路径,那么 responseType:'blob'、用 Content-Type 是否包含 json 判断错误并弹出 msg,以及 new Blob([res.data]) 后通过 a.download 保存,这三样都必须具备。

五、重试验证步骤

  1. 重启后端(只改了 Java,无 SQL 脚本)。
  2. 页面上再次点击“导出需补录保单”。
  3. 查看日志:出现 导出已返回文件 ... bytes=NNNN 说明后端这一步完全正常,问题在前端保存环节;如果没有这行,而是出现 writeExportError 那条路径,就说明是查询条件导致没有数据,页面此时应该会弹出提示。

要不要我直接把那份 dist 里的 revokeObjectURL 改动一下?只改一行并加延迟,不碰其他逻辑,这样你就可以立刻重新验证,不必等 tabyperf 重新打包。

本页笔记

加载中…