1. xxl-job 设置虚拟用户 0 登录操作
2. Access-Control-Expose-Headers 允许暴露 content-disposition
This commit is contained in:
@@ -44,6 +44,10 @@
|
||||
<groupId>jakarta.validation</groupId>
|
||||
<artifactId>jakarta.validation-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.zt.plat</groupId>
|
||||
<artifactId>zt-spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
|
||||
@@ -35,6 +35,11 @@ public class XxlJobProperties {
|
||||
@NotNull(message = "执行器配置不能为空")
|
||||
private ExecutorProperties executor;
|
||||
|
||||
/**
|
||||
* 系统用户配置
|
||||
*/
|
||||
private SystemUserProperties systemUser = new SystemUserProperties();
|
||||
|
||||
/**
|
||||
* XXL-Job 调度器配置类
|
||||
*/
|
||||
@@ -96,4 +101,37 @@ public class XxlJobProperties {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* XXL-Job 系统用户配置类
|
||||
*/
|
||||
@Data
|
||||
public static class SystemUserProperties {
|
||||
|
||||
/**
|
||||
* 系统用户 ID
|
||||
*/
|
||||
private Long userId = 0L;
|
||||
|
||||
/**
|
||||
* 系统用户昵称
|
||||
*/
|
||||
private String nickname = "job";
|
||||
|
||||
/**
|
||||
* 系统租户 ID
|
||||
*/
|
||||
private Long tenantId = 1L;
|
||||
|
||||
/**
|
||||
* 系统公司 ID(可选)
|
||||
*/
|
||||
private Long companyId;
|
||||
|
||||
/**
|
||||
* 系统部门 ID(可选)
|
||||
*/
|
||||
private Long deptId;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.zt.plat.framework.quartz.config;
|
||||
|
||||
import com.zt.plat.framework.quartz.core.handler.XxlJobSystemAuthenticationAspect;
|
||||
import com.xxl.job.core.executor.XxlJobExecutor;
|
||||
import com.xxl.job.core.executor.impl.XxlJobSpringExecutor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -9,7 +10,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.EnableAspectJAutoProxy;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
/**
|
||||
@@ -22,6 +23,7 @@ import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
@ConditionalOnProperty(prefix = "xxl.job", name = "enabled", havingValue = "true", matchIfMissing = true)
|
||||
@EnableConfigurationProperties({XxlJobProperties.class})
|
||||
@EnableScheduling // 开启 Spring 自带的定时任务
|
||||
@EnableAspectJAutoProxy // 开启 AOP
|
||||
@Slf4j
|
||||
public class ZtXxlJobAutoConfiguration {
|
||||
|
||||
@@ -44,4 +46,17 @@ public class ZtXxlJobAutoConfiguration {
|
||||
return xxlJobExecutor;
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置 XXL-Job 系统认证切面
|
||||
*
|
||||
* 为 @XxlJob 注解的方法提供系统用户认证上下文
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public XxlJobSystemAuthenticationAspect xxlJobSystemAuthenticationAspect(XxlJobProperties properties) {
|
||||
log.info("[ZtXxlJobAutoConfiguration][注册 XXL-Job 系统认证切面] systemUserId=[{}], systemTenantId=[{}]",
|
||||
properties.getSystemUser().getUserId(), properties.getSystemUser().getTenantId());
|
||||
return new XxlJobSystemAuthenticationAspect(properties.getSystemUser());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
package com.zt.plat.framework.quartz.core.handler;
|
||||
|
||||
import com.zt.plat.framework.common.enums.UserTypeEnum;
|
||||
import com.zt.plat.framework.quartz.config.XxlJobProperties;
|
||||
import com.zt.plat.framework.security.core.LoginUser;
|
||||
import com.zt.plat.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import com.zt.plat.framework.web.core.util.WebFrameworkUtils;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* XXL-Job 系统认证切面
|
||||
*
|
||||
* 为 @XxlJob 注解的方法提供系统用户认证上下文,
|
||||
* 确保 Job 方法执行时能够获取到用户信息
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
@Aspect
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
@Order(-100) // 设置较高的优先级,确保在其他切面之前执行
|
||||
public class XxlJobSystemAuthenticationAspect {
|
||||
|
||||
private final XxlJobProperties.SystemUserProperties systemUserConfig;
|
||||
|
||||
@Around("@annotation(com.xxl.job.core.handler.annotation.XxlJob)")
|
||||
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||
// 获取当前登录用户
|
||||
LoginUser currentUser = SecurityFrameworkUtils.getLoginUser();
|
||||
try {
|
||||
// 如果当前没有登录用户,则设置系统用户上下文
|
||||
if (currentUser == null) {
|
||||
LoginUser systemUser = createSystemUser();
|
||||
setLoginUserForJob(systemUser);
|
||||
log.debug("[XxlJobSystemAuthenticationAspect][XXL-Job 方法执行,设置系统用户上下文] method=[{}], userId=[{}]",
|
||||
joinPoint.getSignature().toShortString(), systemUser.getId());
|
||||
} else {
|
||||
log.debug("[XxlJobSystemAuthenticationAspect][XXL-Job 方法执行,已存在用户上下文] method=[{}], userId=[{}]",
|
||||
joinPoint.getSignature().toShortString(), currentUser.getId());
|
||||
}
|
||||
|
||||
// 执行目标方法
|
||||
return joinPoint.proceed();
|
||||
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.error("[XxlJobSystemAuthenticationAspect][XXL-Job 方法执行异常] method=[{}], error=[{}]",
|
||||
joinPoint.getSignature().toShortString(), e.getMessage(), e);
|
||||
throw e;
|
||||
}
|
||||
finally {
|
||||
// 如果是我们设置的系统用户,执行完毕后清理上下文
|
||||
if (currentUser == null) {
|
||||
clearLoginUserForJob();
|
||||
log.debug("[XxlJobSystemAuthenticationAspect][XXL-Job 方法执行完毕,清理系统用户上下文] method=[{}]",
|
||||
joinPoint.getSignature().toShortString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 XXL-Job 系统用户
|
||||
*/
|
||||
private LoginUser createSystemUser() {
|
||||
LoginUser systemUser = new LoginUser();
|
||||
systemUser.setId(systemUserConfig.getUserId());
|
||||
systemUser.setUserType(UserTypeEnum.ADMIN.getValue());
|
||||
systemUser.setTenantId(systemUserConfig.getTenantId());
|
||||
systemUser.setVisitTenantId(systemUserConfig.getTenantId());
|
||||
systemUser.setExpiresTime(LocalDateTime.now().plusDays(1));
|
||||
|
||||
// 设置用户信息
|
||||
Map<String, String> info = new HashMap<>();
|
||||
info.put(LoginUser.INFO_KEY_NICKNAME, systemUserConfig.getNickname());
|
||||
info.put(LoginUser.INFO_KEY_TENANT_ID, String.valueOf(systemUserConfig.getTenantId()));
|
||||
|
||||
systemUser.setInfo(info);
|
||||
|
||||
return systemUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* 为 Job 设置登录用户到 Spring Security 上下文和 Web 上下文
|
||||
*/
|
||||
private void setLoginUserForJob(LoginUser loginUser) {
|
||||
// 1. 设置到 Spring Security 上下文
|
||||
Authentication authentication = new UsernamePasswordAuthenticationToken(
|
||||
loginUser, null, Collections.emptyList());
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
|
||||
// 2. 设置到 Web 请求上下文,供 DefaultDBFieldHandler 使用
|
||||
HttpServletRequest request = WebFrameworkUtils.getRequest();
|
||||
if (request != null) {
|
||||
WebFrameworkUtils.setLoginUserId(request, loginUser.getId());
|
||||
WebFrameworkUtils.setLoginUserType(request, loginUser.getUserType());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理 Job 的登录用户上下文
|
||||
*/
|
||||
private void clearLoginUserForJob() {
|
||||
SecurityContextHolder.getContext().setAuthentication(null);
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,7 @@ public class DefaultDBFieldHandler implements MetaObjectHandler {
|
||||
baseDO.setUpdateTime(current);
|
||||
}
|
||||
|
||||
Long userId = WebFrameworkUtils.getLoginUserId();
|
||||
Long userId = getUserId();
|
||||
String userNickname = SecurityFrameworkUtils.getLoginUserNickname();
|
||||
// 当前登录用户不为空,创建人为空,则当前登录用户为创建人
|
||||
if (Objects.nonNull(userId) && Objects.isNull(baseDO.getCreator())) {
|
||||
@@ -81,7 +81,7 @@ public class DefaultDBFieldHandler implements MetaObjectHandler {
|
||||
|
||||
// 当前登录用户不为空,更新人为空,则当前登录用户为更新人
|
||||
Object modifier = getFieldValByName("updater", metaObject);
|
||||
Long userId = WebFrameworkUtils.getLoginUserId();
|
||||
Long userId = getUserId();
|
||||
String userNickname = SecurityFrameworkUtils.getLoginUserNickname();
|
||||
if (Objects.nonNull(userId) && Objects.isNull(modifier)) {
|
||||
setFieldValByName("updater", userId.toString(), metaObject);
|
||||
@@ -96,6 +96,15 @@ public class DefaultDBFieldHandler implements MetaObjectHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private static Long getUserId() {
|
||||
Long userId = WebFrameworkUtils.getLoginUserId();
|
||||
if (userId == null) {
|
||||
// 如果不是 http 请求发起的操作,获取不到用户,从认证中获取
|
||||
userId = SecurityFrameworkUtils.getLoginUserId();
|
||||
}
|
||||
return userId;
|
||||
}
|
||||
|
||||
private void autoFillUserNames(BusinessBaseDO businessBaseDO) {
|
||||
String userNickname = SecurityFrameworkUtils.getLoginUserNickname();
|
||||
if (Objects.nonNull(userNickname)) {
|
||||
|
||||
@@ -88,6 +88,7 @@ public class ZtWebAutoConfiguration implements WebMvcConfigurer {
|
||||
config.addAllowedOriginPattern("*"); // 设置访问源地址
|
||||
config.addAllowedHeader("*"); // 设置访问源请求头
|
||||
config.addAllowedMethod("*"); // 设置访问源请求方法
|
||||
config.addExposedHeader("content-disposition"); // 暴露 content-disposition 头,用于文件下载
|
||||
// 创建 UrlBasedCorsConfigurationSource 对象
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/**", config); // 对接口配置跨域设置
|
||||
|
||||
@@ -37,6 +37,7 @@ public class CorsFilter implements WebFilter {
|
||||
headers.add("Access-Control-Allow-Origin", ALL);
|
||||
headers.add("Access-Control-Allow-Methods", ALL);
|
||||
headers.add("Access-Control-Allow-Headers", ALL);
|
||||
headers.add("Access-Control-Expose-Headers", "content-disposition"); // 暴露 content-disposition 头,用于文件下载
|
||||
headers.add("Access-Control-Max-Age", MAX_AGE);
|
||||
if (request.getMethod() == HttpMethod.OPTIONS) {
|
||||
response.setStatusCode(HttpStatus.OK);
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
package com.zt.plat.module.template.controller.admin.test;
|
||||
|
||||
import com.zt.plat.framework.common.pojo.CommonResult;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* CORS 测试控制器
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
@Tag(name = "管理后台 - CORS 测试")
|
||||
@RestController
|
||||
@RequestMapping("/template/test")
|
||||
@Slf4j
|
||||
public class CorsTestController {
|
||||
|
||||
@GetMapping("/download")
|
||||
@Operation(summary = "测试文件下载响应头", description = "测试 content-disposition 响应头是否能被前端获取")
|
||||
public ResponseEntity<String> testDownload() {
|
||||
log.info("[testDownload] 测试 content-disposition 响应头");
|
||||
|
||||
// 创建响应头
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
|
||||
// 设置 content-disposition 头,包含文件名
|
||||
String filename = "测试文件.txt";
|
||||
String encodedFilename = new String(filename.getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1);
|
||||
headers.add("Content-Disposition", "attachment; filename*=UTF-8''" +
|
||||
java.net.URLEncoder.encode(filename, StandardCharsets.UTF_8) +
|
||||
"; filename=\"" + encodedFilename + "\"");
|
||||
|
||||
// 设置其他常用的文件下载响应头
|
||||
headers.add("Content-Type", "application/octet-stream");
|
||||
headers.add("Cache-Control", "no-cache");
|
||||
|
||||
String responseBody = "这是一个测试文件的内容,用于验证 CORS 配置是否正确。\n" +
|
||||
"如果前端能够获取到 content-disposition 头信息,说明配置成功。\n" +
|
||||
"文件名应该是:" + filename;
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.headers(headers)
|
||||
.body(responseBody);
|
||||
}
|
||||
|
||||
@GetMapping("/cors-info")
|
||||
@Operation(summary = "获取 CORS 配置信息", description = "返回当前 CORS 配置的相关信息")
|
||||
public CommonResult<CorsInfo> getCorsInfo() {
|
||||
log.info("[getCorsInfo] 获取 CORS 配置信息");
|
||||
|
||||
CorsInfo corsInfo = new CorsInfo();
|
||||
corsInfo.setExposedHeaders("content-disposition");
|
||||
corsInfo.setAllowedOrigins("*");
|
||||
corsInfo.setAllowedMethods("GET, POST, PUT, DELETE, OPTIONS");
|
||||
corsInfo.setAllowedHeaders("*");
|
||||
corsInfo.setAllowCredentials(true);
|
||||
corsInfo.setMaxAge(3600L);
|
||||
corsInfo.setMessage("CORS 配置已启用,content-disposition 头已暴露给前端");
|
||||
|
||||
return CommonResult.success(corsInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* CORS 配置信息
|
||||
*/
|
||||
public static class CorsInfo {
|
||||
private String exposedHeaders;
|
||||
private String allowedOrigins;
|
||||
private String allowedMethods;
|
||||
private String allowedHeaders;
|
||||
private Boolean allowCredentials;
|
||||
private Long maxAge;
|
||||
private String message;
|
||||
|
||||
// Getters and Setters
|
||||
public String getExposedHeaders() {
|
||||
return exposedHeaders;
|
||||
}
|
||||
|
||||
public void setExposedHeaders(String exposedHeaders) {
|
||||
this.exposedHeaders = exposedHeaders;
|
||||
}
|
||||
|
||||
public String getAllowedOrigins() {
|
||||
return allowedOrigins;
|
||||
}
|
||||
|
||||
public void setAllowedOrigins(String allowedOrigins) {
|
||||
this.allowedOrigins = allowedOrigins;
|
||||
}
|
||||
|
||||
public String getAllowedMethods() {
|
||||
return allowedMethods;
|
||||
}
|
||||
|
||||
public void setAllowedMethods(String allowedMethods) {
|
||||
this.allowedMethods = allowedMethods;
|
||||
}
|
||||
|
||||
public String getAllowedHeaders() {
|
||||
return allowedHeaders;
|
||||
}
|
||||
|
||||
public void setAllowedHeaders(String allowedHeaders) {
|
||||
this.allowedHeaders = allowedHeaders;
|
||||
}
|
||||
|
||||
public Boolean getAllowCredentials() {
|
||||
return allowCredentials;
|
||||
}
|
||||
|
||||
public void setAllowCredentials(Boolean allowCredentials) {
|
||||
this.allowCredentials = allowCredentials;
|
||||
}
|
||||
|
||||
public Long getMaxAge() {
|
||||
return maxAge;
|
||||
}
|
||||
|
||||
public void setMaxAge(Long maxAge) {
|
||||
this.maxAge = maxAge;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>CORS content-disposition 测试</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Microsoft YaHei', Arial, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.container {
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
h1 {
|
||||
color: #333;
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.test-section {
|
||||
margin: 20px 0;
|
||||
padding: 20px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 5px;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
button {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
margin: 5px;
|
||||
font-size: 14px;
|
||||
}
|
||||
button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
.success {
|
||||
color: #28a745;
|
||||
font-weight: bold;
|
||||
}
|
||||
.error {
|
||||
color: #dc3545;
|
||||
font-weight: bold;
|
||||
}
|
||||
.result {
|
||||
margin-top: 15px;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
background-color: white;
|
||||
border: 1px solid #ddd;
|
||||
white-space: pre-wrap;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
.input-group {
|
||||
margin: 10px 0;
|
||||
}
|
||||
label {
|
||||
display: inline-block;
|
||||
width: 100px;
|
||||
font-weight: bold;
|
||||
}
|
||||
input {
|
||||
padding: 5px 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 3px;
|
||||
width: 400px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>CORS content-disposition 头测试</h1>
|
||||
|
||||
<div class="input-group">
|
||||
<label for="baseUrl">服务地址:</label>
|
||||
<input type="text" id="baseUrl" value="http://localhost:48080" placeholder="请输入服务器地址,如: http://localhost:48080">
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h3>测试1: 获取 CORS 配置信息</h3>
|
||||
<p>这个测试会调用接口获取当前的 CORS 配置信息</p>
|
||||
<button onclick="testCorsInfo()">测试 CORS 配置</button>
|
||||
<div id="corsInfoResult" class="result" style="display: none;"></div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h3>测试2: 文件下载响应头测试</h3>
|
||||
<p>这个测试会检查是否能获取到 content-disposition 响应头</p>
|
||||
<button onclick="testDownloadHeaders()">测试下载响应头</button>
|
||||
<button onclick="downloadFile()">直接下载文件</button>
|
||||
<div id="downloadResult" class="result" style="display: none;"></div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h3>测试结果说明</h3>
|
||||
<ul>
|
||||
<li><strong>成功</strong>: 能够获取到 content-disposition 头,说明 CORS 配置正确</li>
|
||||
<li><strong>失败</strong>: 无法获取响应头,可能是 CORS 配置问题</li>
|
||||
<li>请确保服务已启动,并且地址配置正确</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function getBaseUrl() {
|
||||
return document.getElementById('baseUrl').value.trim();
|
||||
}
|
||||
|
||||
async function testCorsInfo() {
|
||||
const resultDiv = document.getElementById('corsInfoResult');
|
||||
resultDiv.style.display = 'block';
|
||||
resultDiv.innerHTML = '正在测试...';
|
||||
|
||||
try {
|
||||
const response = await fetch(`${getBaseUrl()}/admin-api/template/test/cors-info`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
let result = `状态码: ${response.status}\n`;
|
||||
result += `响应头信息:\n`;
|
||||
for (let [key, value] of response.headers.entries()) {
|
||||
result += ` ${key}: ${value}\n`;
|
||||
}
|
||||
result += `\n响应数据:\n${JSON.stringify(data, null, 2)}`;
|
||||
|
||||
if (response.ok) {
|
||||
resultDiv.innerHTML = `<span class="success">✓ 接口调用成功</span>\n${result}`;
|
||||
} else {
|
||||
resultDiv.innerHTML = `<span class="error">✗ 接口调用失败</span>\n${result}`;
|
||||
}
|
||||
} catch (error) {
|
||||
resultDiv.innerHTML = `<span class="error">✗ 请求失败</span>\n错误信息: ${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
async function testDownloadHeaders() {
|
||||
const resultDiv = document.getElementById('downloadResult');
|
||||
resultDiv.style.display = 'block';
|
||||
resultDiv.innerHTML = '正在测试...';
|
||||
|
||||
try {
|
||||
const response = await fetch(`${getBaseUrl()}/admin-api/template/test/download`, {
|
||||
method: 'GET'
|
||||
});
|
||||
|
||||
let result = `状态码: ${response.status}\n`;
|
||||
result += `响应头信息:\n`;
|
||||
|
||||
let hasContentDisposition = false;
|
||||
let contentDisposition = null;
|
||||
|
||||
for (let [key, value] of response.headers.entries()) {
|
||||
result += ` ${key}: ${value}\n`;
|
||||
if (key.toLowerCase() === 'content-disposition') {
|
||||
hasContentDisposition = true;
|
||||
contentDisposition = value;
|
||||
}
|
||||
}
|
||||
|
||||
const responseText = await response.text();
|
||||
result += `\n响应内容:\n${responseText.substring(0, 200)}${responseText.length > 200 ? '...' : ''}`;
|
||||
|
||||
if (hasContentDisposition) {
|
||||
result = `<span class="success">✓ 成功获取 content-disposition 头</span>\n` +
|
||||
`Content-Disposition: ${contentDisposition}\n\n` + result;
|
||||
} else {
|
||||
result = `<span class="error">✗ 无法获取 content-disposition 头</span>\n` + result;
|
||||
}
|
||||
|
||||
resultDiv.innerHTML = result;
|
||||
} catch (error) {
|
||||
resultDiv.innerHTML = `<span class="error">✗ 请求失败</span>\n错误信息: ${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
function downloadFile() {
|
||||
const url = `${getBaseUrl()}/admin-api/template/test/download`;
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = '';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
const resultDiv = document.getElementById('downloadResult');
|
||||
resultDiv.style.display = 'block';
|
||||
resultDiv.innerHTML = `<span class="success">✓ 已触发文件下载</span>\n请检查浏览器下载文件夹`;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user