收尾完成。结论很明确:这次的问题并不在后端——文件确实生成并返回了,只是前端没把它保存下来。
一、后端确实无误:日志可以证明
tabycore-app/nohup.log 里你三次点击的记录都在:
这行“导出结束时间”只有在走完 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)
- 先查数据、再设置响应头:
/policyData/miss/export、/policyData/export、/endorse/export三个接口统一。失败(000002参数、000003无数据、000001生成失败)时,调用writeExportError()→response.reset()+application/json+{"code","msg","flag":false},前端能正常解析并弹出中文提示。以前是“先挂 attachment 头再抛异常”,前端拿到的是一段带 excel 响应头的 JSON,导致既无法下载文件,也看不到提示。 writeExcelStream()改为先读成byte[]再写,并设置Content-Length,用来替代 chunked 传输(chunked 下代理和 XHR 更容易拿到半截或空 blob);文件名同时给出filename="export.xls"和 RFC 5987filename*=UTF-8''...。- 添加字节日志:成功时打印
导出已返回文件, file=保单缺失项导出.xls, bytes=N;客户端中途取消下载只记录 warn,不抛出异常。以后出现同类问题时,看一眼日志就能分清是“后端没给文件”还是“前端没存文件”。 - 产险批单导出
/endorse/export存在同一个缺陷,现在也一并改成这套 helper。
编译验证:verify9 跑通,1311 个源文件 0 错误 + 诱饵被捕获(类型检查确实生效)。
四、前端需要改的一处(请你确认,我没有动前端)
static/dist/icrm/js/9.52a36f9f0fd47c7d1270.js 里的 downloadMissExport 已经使用了 XHR + responseType="blob" + <a download>,整体思路是对的,但结尾是:
Chromium 的 blob 下载是异步启动的,点击后立刻 revokeObjectURL 有可能把还没开始读取的 blob URL 释放掉,表现为“没有文件、没有报错、控制台也没有异常”。源码里改成:
如果这个页面实际走的是 axios 那条路径,那么 responseType:'blob'、用 Content-Type 是否包含 json 判断错误并弹出 msg,以及 new Blob([res.data]) 后通过 a.download 保存,这三样都必须具备。
五、重试验证步骤
- 重启后端(只改了 Java,无 SQL 脚本)。
- 页面上再次点击“导出需补录保单”。
- 查看日志:出现
导出已返回文件 ... bytes=NNNN说明后端这一步完全正常,问题在前端保存环节;如果没有这行,而是出现writeExportError那条路径,就说明是查询条件导致没有数据,页面此时应该会弹出提示。
要不要我直接把那份 dist 里的 revokeObjectURL 改动一下?只改一行并加延迟,不碰其他逻辑,这样你就可以立刻重新验证,不必等 tabyperf 重新打包。