1
批量下载技术报告deepseek_html_20260122_399bbe.html
前端fielService.js
import axios from 'axios';
// ==================== 全局配置 ====================
const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://www.fox360.cn:3000';
// ==================== Axios 配置 ====================
const api = axios.create({
baseURL: API_BASE_URL,
timeout: 30000,
headers: {
'Content-Type': 'application/json',
},
});
// 请求拦截器 - 添加认证token
api.interceptors.request.use(
(config) => {
const token = localStorage.getItem('access_token') || localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
console.log('📌 添加认证token:', token.substring(0, 20) + '...');
} else {
console.warn('⚠️ 未找到认证token');
}
return config;
},
(error) => {
console.error('请求拦截器错误:', error);
return Promise.reject(error);
}
);
// 响应拦截器 - 处理错误
api.interceptors.response.use(
(response) => {
console.log(`✅ ${response.config.method?.toUpperCase()} ${response.config.url} 成功`);
return response;
},
(error) => {
console.error('❌ API 错误:', {
url: error.config?.url,
method: error.config?.method,
status: error.response?.status,
statusText: error.response?.statusText,
data: error.response?.data,
headers: error.response?.headers
});
if (error.response?.status === 401) {
console.log('认证失败,清除本地存储');
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
localStorage.removeItem('user_info');
// 触发重新登录事件
window.dispatchEvent(new CustomEvent('auth-expired'));
}
return Promise.reject(error);
}
);
class FileService {
constructor() {
this.downloads = new Set();
this.debugMode = true; // 启用调试模式
}
// ==================== 调试方法 ====================
/**
* 测试下载端点
*/
async testDownloadEndpoint(fileId) {
try {
console.log('🧪 测试下载端点...');
// 1. 检查认证token
const token = localStorage.getItem('access_token') || localStorage.getItem('token');
console.log('认证token:', token ? '存在' : '不存在');
if (token) {
console.log('Token长度:', token.length, 'Token前20位:', token.substring(0, 20) + '...');
}
// 2. 测试端点
const url = `${API_BASE_URL}/api/files/${fileId}/download`;
console.log('测试URL:', url);
// 使用fetch测试,以便查看原始响应
const response = await fetch(url, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`,
'Accept': 'application/json'
}
});
console.log('响应状态:', response.status, response.statusText);
console.log('响应头:', Object.fromEntries(response.headers.entries()));
// 尝试解析响应体
const contentType = response.headers.get('content-type');
let data;
if (contentType && contentType.includes('application/json')) {
data = await response.json();
console.log('JSON响应数据:', data);
} else if (contentType && contentType.includes('text/')) {
data = await response.text();
console.log('文本响应数据:', data.substring(0, 200));
} else {
console.log('无法识别的content-type:', contentType);
}
return {
success: response.ok,
status: response.status,
contentType,
data,
headers: Object.fromEntries(response.headers.entries())
};
} catch (error) {
console.error('测试下载端点失败:', error);
return {
success: false,
error: error.message
};
}
}
// ==================== 下载方法 ====================
/**
* 获取文件下载URL - 兼容多种格式
*/
async getDownloadUrl(fileId) {
try {
console.log(`🔗 获取文件 ${fileId} 的下载URL...`);
const response = await api.get(`/api/files/${fileId}/download`);
console.log('原始响应数据:', response.data);
// 处理各种可能的响应格式
let downloadUrl = null;
let urlSource = 'unknown';
// 检查常见的URL字段
if (response.data && typeof response.data === 'object') {
// 格式1: { url: "..." } - 标准格式
if (response.data.url) {
downloadUrl = response.data.url;
urlSource = 'url';
}
// 格式2: { downloadUrl: "..." } - 后端实际格式
else if (response.data.downloadUrl) {
downloadUrl = response.data.downloadUrl;
urlSource = 'downloadUrl';
}
// 格式3: 嵌套格式 { data: { url: "..." } }
else if (response.data.data && response.data.data.url) {
downloadUrl = response.data.data.url;
urlSource = 'data.url';
}
// 格式4: { file: { downloadUrl: "..." } }
else if (response.data.file && response.data.file.downloadUrl) {
downloadUrl = response.data.file.downloadUrl;
urlSource = 'file.downloadUrl';
}
// 格式5: 其他可能的字段名
else {
// 搜索所有可能的URL字段
const possibleFields = ['download_url', 'downloadURL', 'download-url', 'download', 'link', 'href', 'src'];
for (const field of possibleFields) {
if (response.data[field]) {
downloadUrl = response.data[field];
urlSource = field;
break;
}
}
}
}
// 格式6: 直接返回URL字符串
else if (typeof response.data === 'string' && response.data.startsWith('http')) {
downloadUrl = response.data;
urlSource = 'string';
}
if (!downloadUrl) {
console.error('无法从响应中提取下载URL:', response.data);
throw new Error('未获取到有效的下载URL');
}
console.log(`✅ 获取到下载URL (来源: ${urlSource}):`, downloadUrl);
return {
success: true,
url: downloadUrl,
source: urlSource,
rawData: response.data,
cached: response.data.cached || false
};
} catch (error) {
console.error('❌ 获取下载URL失败:', error);
// 提供详细的错误信息
let errorMessage = '获取下载链接失败';
if (error.response) {
console.error('服务器响应详情:', {
status: error.response.status,
statusText: error.response.statusText,
data: error.response.data,
headers: error.response.headers
});
if (error.response.status === 401) {
errorMessage = '身份认证失败,请重新登录';
} else if (error.response.status === 404) {
errorMessage = '文件不存在或已被删除';
} else if (error.response.status === 403) {
errorMessage = '没有下载权限';
} else if (error.response.status === 500) {
errorMessage = '服务器内部错误,文件可能已损坏';
}
}
throw new Error(`${errorMessage}: ${error.message}`);
}
}
/**
* 下载单个文件 - 增强版
*/
async downloadFile(fileId, options = {}) {
// 防止重复下载
if (this.downloads.has(fileId)) {
console.log('⏸️ 文件已在下载中,跳过');
return {
success: false,
message: '文件正在下载中',
fileId,
skipped: true
};
}
try {
this.downloads.add(fileId);
console.log('🚀 开始下载文件:', fileId);
// 1. 先测试端点,如果是500错误直接返回
if (this.debugMode) {
const testResult = await this.testDownloadEndpoint(fileId);
console.log('端点测试结果:', testResult);
if (testResult.status === 500) {
throw new Error('服务器内部错误(500),此文件可能存在问题');
}
}
// 2. 先获取文件信息,验证文件存在
console.log(`📋 获取文件信息: ${fileId}`);
let fileName = options.fileName;
let fileSize = 0;
try {
const fileInfo = await this.getFileInfo(fileId);
fileName = fileInfo.filename || fileInfo.name || fileInfo.fileName || `文件_${fileId}`;
fileSize = fileInfo.size || 0;
console.log('获取到文件名:', fileName, '大小:', this.formatFileSize(fileSize));
} catch (error) {
console.warn('无法获取文件信息:', error.message);
fileName = `文件_${fileId}`;
}
// 3. 获取下载URL
const urlResult = await this.getDownloadUrl(fileId);
console.log('URL获取结果:', urlResult);
if (!urlResult.success || !urlResult.url) {
throw new Error('获取下载链接失败');
}
let downloadUrl = urlResult.url;
// 4. 检查URL是否需要处理
let finalUrl = downloadUrl;
// 如果URL是相对路径,添加基础URL
if (finalUrl && !finalUrl.startsWith('http')) {
if (finalUrl.startsWith('/')) {
finalUrl = `${API_BASE_URL}${finalUrl}`;
} else {
finalUrl = `${API_BASE_URL}/${finalUrl}`;
}
console.log('URL已补全:', finalUrl);
}
// 5. 多种下载方式尝试
console.log('尝试下载方式 1: a标签');
try {
const link = document.createElement('a');
link.href = finalUrl;
link.download = fileName;
link.target = '_blank'; // 在新窗口打开
link.style.display = 'none';
document.body.appendChild(link);
link.click();
console.log('✅ a标签点击成功');
// 清理
setTimeout(() => {
if (link.parentNode) {
link.parentNode.removeChild(link);
}
}, 3000);
return {
success: true,
message: '下载已开始',
fileId,
fileName,
fileSize,
url: finalUrl,
method: 'a-tag'
};
} catch (aTagError) {
console.warn('a标签方式失败,尝试方式 2:', aTagError.message);
}
// 方式2: window.open
console.log('尝试下载方式 2: window.open');
try {
window.open(finalUrl, '_blank');
return {
success: true,
message: '在新窗口打开下载',
fileId,
fileName,
fileSize,
url: finalUrl,
method: 'window-open'
};
} catch (windowError) {
console.warn('window.open方式失败,尝试方式 3:', windowError.message);
}
// 方式3: iframe
console.log('尝试下载方式 3: iframe');
try {
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = finalUrl;
document.body.appendChild(iframe);
setTimeout(() => {
if (iframe.parentNode) {
iframe.parentNode.removeChild(iframe);
}
}, 5000);
return {
success: true,
message: '通过iframe下载',
fileId,
fileName,
fileSize,
url: finalUrl,
method: 'iframe'
};
} catch (iframeError) {
console.warn('iframe方式失败:', iframeError.message);
}
// 所有方式都失败
throw new Error('所有下载方式都失败了');
} catch (error) {
console.error('❌ 下载失败:', error);
return {
success: false,
message: `下载失败: ${error.message}`,
fileId,
error: error.message
};
} finally {
this.downloads.delete(fileId);
}
}
/**
* 批量下载文件
*/
async batchDownload(fileIds, options = {}) {
console.log('📦 批量下载:', fileIds);
try {
if (!fileIds || !Array.isArray(fileIds) || fileIds.length === 0) {
throw new Error('请选择要下载的文件');
}
// 检查是否有失败的已知文件ID
const failedFiles = fileIds.filter(id =>
['f2c10d75-cea7-4277-b3e0-7a471295de5b', '9da5bbaf-8608-4835-816f-3c37775b4e14'].includes(id)
);
if (failedFiles.length > 0 && options.ignoreFailed !== true) {
console.warn('发现已知失败文件:', failedFiles);
return {
success: false,
message: `发现 ${failedFiles.length} 个已知有问题的文件,无法下载`,
failedFiles,
totalFiles: fileIds.length
};
}
// 如果是单个文件,直接下载
if (fileIds.length === 1) {
return await this.downloadFile(fileIds[0], options);
}
// 先测试批量下载端点
console.log('测试批量下载端点...');
try {
const testResponse = await api.post('/api/files/batch/download', {
fileIds,
format: options.format || 'zip',
compressionLevel: options.compressionLevel || 6
});
console.log('批量下载测试响应:', testResponse.data);
// 处理批量下载响应
if (testResponse.data && testResponse.data.downloadUrl) {
// 有直接的下载URL
const downloadUrl = testResponse.data.downloadUrl;
const fullUrl = downloadUrl.startsWith('http') ? downloadUrl : `${API_BASE_URL}${downloadUrl.startsWith('/') ? '' : '/'}${downloadUrl}`;
// 使用a标签下载
const link = document.createElement('a');
link.href = fullUrl;
link.download = options.zipFileName || `批量下载_${Date.now()}.zip`;
link.target = '_blank';
link.style.display = 'none';
document.body.appendChild(link);
link.click();
setTimeout(() => {
if (link.parentNode) {
link.parentNode.removeChild(link);
}
}, 3000);
return {
success: true,
message: '开始下载压缩包',
totalFiles: testResponse.data.totalFiles || fileIds.length,
zipSize: testResponse.data.zipSize,
downloadUrl: fullUrl,
method: 'zip-download'
};
} else if (testResponse.data && Array.isArray(testResponse.data.files)) {
// 返回文件列表,逐个下载
console.log('开始顺序下载...');
return await this.downloadFilesSequentially(testResponse.data.files, options);
} else {
console.warn('批量下载响应格式未知,使用顺序下载');
return await this.downloadFilesSequentiallyByIds(fileIds, options);
}
} catch (batchError) {
console.warn('批量下载端点失败,使用顺序下载:', batchError.message);
return await this.downloadFilesSequentiallyByIds(fileIds, options);
}
} catch (error) {
console.error('批量下载失败:', error);
return {
success: false,
message: `批量下载失败: ${error.message}`,
error: error.message
};
}
}
// ==================== 辅助方法 ====================
async downloadFilesSequentially(files, options = {}) {
const results = [];
let successCount = 0;
for (let i = 0; i < files.length; i++) {
const file = files[i];
try {
console.log(`下载 ${i + 1}/${files.length}:`, file.filename || file.id);
// 延迟避免浏览器阻止
if (i > 0) {
await new Promise(resolve => setTimeout(resolve, 1000));
}
const result = await this.downloadFile(file.id, {
fileName: file.filename || `文件_${i + 1}`
});
if (result.success) {
successCount++;
}
results.push(result);
} catch (error) {
console.error(`文件下载失败:`, error);
results.push({ fileId: file.id, success: false, error: error.message });
}
}
return {
success: successCount > 0,
message: `下载完成: ${successCount} 成功, ${files.length - successCount} 失败`,
total: files.length,
successCount,
results
};
}
async downloadFilesSequentiallyByIds(fileIds, options = {}) {
const results = [];
let successCount = 0;
for (let i = 0; i < fileIds.length; i++) {
const fileId = fileIds[i];
try {
console.log(`下载 ${i + 1}/${fileIds.length}:`, fileId);
if (i > 0) {
await new Promise(resolve => setTimeout(resolve, 1000));
}
const result = await this.downloadFile(fileId, {
fileName: options.fileName || `文件_${i + 1}`
});
if (result.success) {
successCount++;
}
results.push(result);
} catch (error) {
console.error(`文件下载失败:`, error);
results.push({ fileId, success: false, error: error.message });
}
}
return {
success: successCount > 0,
message: `下载完成: ${successCount} 成功, ${fileIds.length - successCount} 失败`,
total: fileIds.length,
successCount,
results
};
}
// ==================== 文件信息方法 ====================
async getFileInfo(fileId) {
try {
console.log('获取文件信息:', fileId);
const response = await api.get(`/api/files/${fileId}`);
return response.data;
} catch (error) {
console.error('获取文件信息失败:', error);
// 如果获取失败,返回默认信息
return {
id: fileId,
filename: `文件_${fileId.substring(0, 8)}`,
size: 0,
mimetype: 'unknown'
};
}
}
async getFiles(params = {}) {
try {
const response = await api.get('/api/files', { params });
// 处理各种响应格式
let files = [];
if (response.data) {
if (response.data.data && Array.isArray(response.data.data)) {
files = response.data.data;
} else if (Array.isArray(response.data)) {
files = response.data;
} else if (response.data.files && Array.isArray(response.data.files)) {
files = response.data.files;
}
}
return {
files,
pagination: response.data.pagination || {
page: params.page || 1,
limit: params.limit || 20,
total: files.length,
totalPages: Math.ceil(files.length / (params.limit || 20))
}
};
} catch (error) {
console.error('获取文件列表失败:', error);
return { files: [], pagination: null };
}
}
// ==================== 工具方法 ====================
formatFileSize(bytes) {
if (!bytes || bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
enableDebug() {
this.debugMode = true;
console.log('🔧 调试模式已启用');
}
disableDebug() {
this.debugMode = false;
console.log('🔧 调试模式已禁用');
}
/**
* 获取已知有问题的文件ID列表
*/
getKnownProblemFiles() {
return ['f2c10d75-cea7-4277-b3e0-7a471295de5b', '9da5bbaf-8608-4835-816f-3c37775b4e14'];
}
}
// 创建并导出单例实例
export const fileService = new FileService();
export default fileService;