Java端:
java
@Autowired
private RestTemplate restTemplate;
@Override
public void download(HttpServletResponse response) {
String baseUrl = "/contract/download";
String fullUrl = SignApiUtil.BASE_DOMAIN + baseUrl;
String timestamp = SignApiUtil.getTimestamp();
String nonce = SignApiUtil.getNonce();
String signature = SignApiUtil.getSignature(timestamp, nonce);
// 请求头
HttpHeaders headers = new HttpHeaders();
headers.set("x-qys-accesstoken", SignApiUtil.APP_TOKEN);
headers.set("x-qys-timestamp", timestamp);
headers.set("x-qys-nonce", nonce);
headers.set("x-qys-signature", signature);
// 请求参数
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("contractId", "3499343568015819654");
UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(fullUrl).queryParams(params);
HttpEntity<?> requestEntity = new HttpEntity<>(headers);
restTemplate.setErrorHandler(new DefaultResponseErrorHandler() {
@Override
protected boolean hasError(HttpStatus statusCode) {
return false;
}
});
try {
ResponseEntity<byte[]> responseEntity = restTemplate.exchange(
builder.toUriString(),
HttpMethod.GET,
requestEntity,
byte[].class
);
byte[] fileBytes = responseEntity.getBody();
if (fileBytes == null) {
throw new RuntimeException("文件内容为空");
}
String realFileName = "download";
String contentDisposition = responseEntity.getHeaders().getFirst(HttpHeaders.CONTENT_DISPOSITION);
if (contentDisposition != null && contentDisposition.trim().length() > 0) {
// 解析文件名
String fileNameEncoded = contentDisposition
.replaceFirst("(?i)^.*filename=\"?", "")
.replaceFirst("\"?$", "");
// URL解码
realFileName = java.net.URLDecoder.decode(fileNameEncoded, "UTF-8");
}
// 设置响应
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
// 正确设置下载文件名
String encodedFileName = URLEncoder.encode(realFileName, StandardCharsets.UTF_8.name());
response.setHeader(HttpHeaders.CONTENT_DISPOSITION,
"attachment;filename=" + encodedFileName +
";filename*=UTF-8''" + encodedFileName);
// 暴露给前端,让JS能读取到文件名
response.setHeader("Access-Control-Expose-Headers", HttpHeaders.CONTENT_DISPOSITION);
// 输出文件
try (OutputStream os = response.getOutputStream()) {
os.write(fileBytes);
os.flush();
}
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("文件下载失败:" + e.getMessage(), e);
}
}
JavaScript端:
javascript
fetch("/main/download", {
method: "GET"
}).then(async (resp) => {
if (!resp.ok) {
throw new Error(`请求失败,状态码:${resp.status}`);
}
// 从响应头拿文件名
let disposition = resp.headers.get("Content-Disposition");
let fileName = "download.pdf";
if (disposition) {
const match = disposition.match(/filename\*?=(?:UTF-8'')?([^;]+)/);
if (match && match[1]) {
fileName = decodeURIComponent(match[1].replace(/"/g, ""));
}
}
const blob = await resp.blob();
const downloadUrl = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = downloadUrl;
a.download = fileName;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(downloadUrl);
}).catch(error => {
console.error("模板下载失败:", error);
});