yalasuo

批量下载测试代码

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;

JTBC