
/*
 * @ProjectName: 智慧评标-主观分助手
 * @Package: com.zhpb.gdyd_zhpb_zgf.service.impl
 * @ClassName: FileServiceImpl
 * @Description: 文件服务实现类，处理文件上传、ZIP解压（支持递归解压嵌套ZIP）、文件路径管理等功能
 * @Author: 张志锋
 * @Date: 2025-10-31
 * @Version: 1.0
 * @Copyright: © 2025 智慧评标-主观分助手
 */
package com.zhpb.gdyd_zhpb_zgf.service.impl;
import com.zhpb.gdyd_zhpb_zgf.service.*;
import org.apache.commons.compress.utils.IOUtils;
import com.zhpb.gdyd_zhpb_zgf.context.ModelRequestContextHolder;
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import com.zhpb.gdyd_zhpb_zgf.entity.mysql.File;
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.utils.IOUtils;
import com.zhpb.gdyd_zhpb_zgf.entity.mysql.FileDirectory;
import com.zhpb.gdyd_zhpb_zgf.entity.mysql.FileType;
import com.zhpb.gdyd_zhpb_zgf.entity.mysql.Project;
import com.zhpb.gdyd_zhpb_zgf.entity.mysql.Supplier;
import com.zhpb.gdyd_zhpb_zgf.repository.mysql.FileRepository;
import com.zhpb.gdyd_zhpb_zgf.repository.mysql.FileTypeRepository;
import com.zhpb.gdyd_zhpb_zgf.repository.mysql.ProjectRepository;
import com.zhpb.gdyd_zhpb_zgf.repository.mysql.SupplierRepository;
import com.zhpb.gdyd_zhpb_zgf.repository.mysql.FileDirectoryRepository;
import com.zhpb.gdyd_zhpb_zgf.utils.HttpClientUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.scheduling.annotation.Async;

import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDDocumentOutline;
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlineItem;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import com.alibaba.fastjson2.JSON;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;
import java.util.Set;
import java.util.UUID;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
import java.util.Enumeration;
import java.nio.charset.Charset;

/**
 * 文件服务实现类
 */
@Service
public class FileServiceImpl implements FileService {

    private static final Logger logger = LoggerFactory.getLogger(FileServiceImpl.class);

    @Autowired
    private ProjectRepository projectRepository;

    @Autowired
    private FileRepository fileRepository;

    @Autowired
    private SupplierRepository supplierRepository;

    @Autowired
    private FileTypeRepository fileTypeRepository;

    @Autowired
    private FileTypeService fileTypeService;

    @Autowired
    private PdfReadService pdfReadService;

    @Autowired
    private com.zhpb.gdyd_zhpb_zgf.service.FilePreviewService filePreviewService;

    @Autowired
    private com.zhpb.gdyd_zhpb_zgf.service.PromptTemplateService promptTemplateService;

    @Autowired
    private FileDirectoryRepository fileDirectoryRepository;

    @Autowired
    private TransactionTemplate transactionTemplate;

    @Autowired
    private AIModelService aiModelService;

    @Autowired
    private ChapterContentResolver chapterContentResolver;

    @Autowired
    private FilePathResolver filePathResolver;

    @Autowired
    private OcrService ocrService;

    private ExecutorService directoryExecutor;

    @Value("${vectorize.base-url:http://localhost:8001}")
    private String vectorizeBaseUrl;

    @Value("${deepseek.api-key:}")
    private String deepseekApiKey;

    @Value("${deepseek.api-url:https://api.deepseek.com/v1/chat/completions}")
    private String deepseekApiUrl;

    @Value("${deepseek.model:deepseek-chat}")
    private String deepseekModel;

    @Value("${deepseek.temperature:0.7}")
    private double deepseekTemperature;

    @Value("${deepseek.max-tokens:4096}")
    private int deepseekMaxTokens;

    @Value("${file.download-replace-source:}")
    private String downloadReplaceSource;

    @Value("${file.download-replace-target:}")
    private String downloadReplaceTarget;

    @Value("${umi-ocr.enabled:false}")
    private boolean ocrEnabled;
    
    // 存储处理后的绝对路径
    private String absoluteUploadPath;
    private String absoluteExtractPath;
    
    // 时间格式
    private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss");

    // 7zip可用性标志
    private boolean sevenZipAvailable = false;
    private String sevenZipCommand = null;
    
    /**
     * 初始化路径和7zip检测
     */
    @PostConstruct
    public void init() {
        absoluteUploadPath = filePathResolver.getAbsoluteUploadPath().toString();
        absoluteExtractPath = filePathResolver.getAbsoluteExtractPath().toString();
        logger.info("文件上传路径: {}", absoluteUploadPath);
        logger.info("文件解压路径: {}", absoluteExtractPath);

        // 检测7zip可用性
        checkSevenZipAvailability();

        // 目录抽取线程池大小设为3，避免数据库连接池耗尽
        // 因为目录抽取涉及LLM调用且在事务中运行，持有连接时间较长
        int poolSize = 3;
        directoryExecutor = Executors.newFixedThreadPool(poolSize);
        logger.info("目录抽取线程池初始化完成, 核心线程数: {}", poolSize);

        // 记录OCR功能状态
        logger.info("OCR功能状态: {}", ocrEnabled ? "已启用" : "已禁用");
        if (ocrEnabled) {
            logger.info("PDF文件上传后将自动进行OCR识别处理");
        } else {
            logger.info("PDF文件上传后将跳过OCR识别处理（可在配置文件中设置 umi-ocr.enabled=true 启用）");
        }
    }

    @PreDestroy
    public void shutdown() {
        if (directoryExecutor != null) {
            directoryExecutor.shutdown();
        }
    }

    /**
     * 检测7zip工具的可用性
     */
    private void checkSevenZipAvailability() {
        logger.info("开始检测7-Zip工具可用性...");

        String[] sevenZipCommands = {
            "7z", "7za", "7zr",
            "C:\\Program Files\\7-Zip\\7z.exe",
            "C:\\Program Files (x86)\\7-Zip\\7z.exe",
            "/usr/bin/7z", "/usr/local/bin/7z",
            "/opt/7zip/7z", "/usr/lib/7zip/7z"
        };

        for (String cmd : sevenZipCommands) {
            try {
                Process process = new ProcessBuilder(cmd, "--help").start();
                int exitCode = process.waitFor();

                if (exitCode == 0) {
                    // 验证这是真的7zip
                    try (java.io.BufferedReader reader = new java.io.BufferedReader(
                            new java.io.InputStreamReader(process.getInputStream()))) {
                        String firstLine = reader.readLine();
                        if (firstLine != null && firstLine.toLowerCase().contains("7-zip")) {
                            sevenZipAvailable = true;
                            sevenZipCommand = cmd;
                            logger.info("✅ 7-Zip工具可用: {}", cmd);
                            logger.info("   版本信息: {}", firstLine.trim());
                            return;
                        }
                    }

                    // 如果读取失败但退出码为0，也认为是可用的
                    sevenZipAvailable = true;
                    sevenZipCommand = cmd;
                    logger.info("✅ 7-Zip工具可用: {}", cmd);
                    return;
                }
            } catch (Exception e) {
                // 继续尝试下一个命令
            }
        }

        sevenZipAvailable = false;
        sevenZipCommand = null;
        logger.warn("❌ 未检测到7-Zip工具");
        logger.warn("   这将影响分卷压缩文件的解压功能");
        logger.warn("   请安装7-Zip并确保其在系统PATH中，或将其添加到上述检测路径之一");
        logger.warn("   下载地址: https://www.7-zip.org/");
    }

    @Value("${file.allowed-types:pdf,doc,docx,txt}")
    private String allowedTypes;

    @Value("${file.filter.technical-only:true}")
    private boolean filterTechnicalOnly;

    // 支持的文件扩展名列表

    @Override
    public FileService.UploadResult uploadAndExtractZip(String projectId, MultipartFile zipFile) throws IOException {
        // 1. 创建或更新项目记录
        createOrUpdateProject(projectId, null);

        // 2. 生成文件夹名称：项目id_时间_uuid
        String timestamp = LocalDateTime.now().format(DATE_TIME_FORMATTER);
        String uuid = UUID.randomUUID().toString().replace("-", "").substring(0, 8);
        String folderName = projectId + "_" + timestamp + "_" + uuid;
        
        // 3. 创建上传目录（使用新的文件夹名称）
        Path projectUploadPath = Paths.get(absoluteUploadPath, folderName);
        Files.createDirectories(projectUploadPath);
        logger.info("上传目录: {}", projectUploadPath);

        // 4. 创建解压目录（使用新的文件夹名称）
        Path projectExtractPath = Paths.get(absoluteExtractPath, folderName);
        Files.createDirectories(projectExtractPath);
        logger.info("解压目录: {}", projectExtractPath);

        // 5. 保存ZIP文件
        String originalFilename = zipFile.getOriginalFilename();
        if (originalFilename == null || !originalFilename.toLowerCase().endsWith(".zip")) {
            throw new IllegalArgumentException("文件必须是ZIP格式");
        }

        Path zipFilePath = projectUploadPath.resolve(originalFilename);
        zipFile.transferTo(zipFilePath.toFile());
        String zipFilePathStr = zipFilePath.toAbsolutePath().toString();
        logger.info("ZIP文件已保存到: {}", zipFilePathStr);

        // 6. 解压ZIP文件到指定的解压目录
        logger.info("开始解压ZIP文件: {}", zipFilePathStr);
        logger.info("解压目标目录: {}", projectExtractPath.toAbsolutePath());
        List<String> extractedFilePaths;
        try {
            extractedFilePaths = extractZipFile(zipFilePath, projectExtractPath);
        } catch (Exception e) {
            logger.error("解压ZIP文件时发生异常: {}", e.getMessage(), e);
            // 不要抛出异常，而是返回空列表，让用户知道解压失败
            logger.error("ZIP文件解压失败，返回空文件列表。错误详情: {}", e.getMessage());
            extractedFilePaths = new ArrayList<>();
        }
        String extractPathStr = projectExtractPath.toAbsolutePath().toString();

        // 最终过滤：只过滤掉以点开头的临时文件（如 .DS_Store, .__MACOSX 等）
        List<String> finalExtractedFilePaths = new ArrayList<>();
        for (String filePath : extractedFilePaths) {
            Path path = Paths.get(filePath);
            String fileName = path.getFileName().toString();
            
            // 只过滤以点开头的文件（通常是隐藏文件或临时文件）
            if (!fileName.startsWith(".")) {
                finalExtractedFilePaths.add(filePath);
            } else {
                logger.warn("过滤掉临时/隐藏文件: {}", filePath);
            }
        }

        logger.info("ZIP文件解压完成，共解压 {} 个文件，过滤后返回 {} 个内容文件到目录: {}",
                extractedFilePaths.size(), finalExtractedFilePaths.size(), extractPathStr);
        logger.info("最终解压文件列表: {}", finalExtractedFilePaths);

        // 如果最终文件列表为空，记录更多调试信息
        if (finalExtractedFilePaths.isEmpty()) {
            logger.error("警告：没有解压出任何文件！");
            logger.error("原始解压文件数量: {}", extractedFilePaths.size());
            logger.error("ZIP文件路径: {}", zipFilePathStr);
            logger.error("解压目录: {}", extractPathStr);

            try {
                if (Files.exists(zipFilePath)) {
                    logger.error("ZIP文件存在，大小: {} 字节", Files.size(zipFilePath));
                } else {
                    logger.error("ZIP文件不存在！");
                }

                if (Files.exists(projectExtractPath)) {
                    logger.error("解压目录存在");
                    try (var paths = Files.walk(projectExtractPath)) {
                        List<Path> allFiles = paths.filter(Files::isRegularFile).collect(java.util.stream.Collectors.toList());
                        logger.error("解压目录中的所有文件 ({} 个):", allFiles.size());
                        for (Path file : allFiles) {
                            String ext = getFileExtension(file.getFileName().toString());
                            logger.error("  - {} (扩展名: {})", file.getFileName(), ext);
                        }
                    }
                } else {
                    logger.error("解压目录不存在");
                }
            } catch (Exception e) {
                logger.error("收集调试信息时出错: {}", e.getMessage());
            }
        }

        if (finalExtractedFilePaths.isEmpty()) {
            logger.warn("警告：ZIP文件中没有解压出任何文件，可能是ZIP文件为空");
            // 检查ZIP文件内容
            try {
                long zipSize = Files.size(zipFilePath);
                logger.warn("ZIP文件大小: {} 字节", zipSize);
                if (zipSize == 0) {
                    logger.error("ZIP文件为空文件！");
                } else {
                    logger.warn("ZIP文件不为空，但没有解压出任何文件，尝试列出ZIP内容...");
                    listZipContents(zipFilePath);
                }
            } catch (Exception e) {
                logger.error("检查ZIP文件大小时出错: {}", e.getMessage());
            }
        } else {
            // 按文件扩展名统计
            java.util.Map<String, Integer> extensionCount = new java.util.HashMap<>();
            int zeroByteFileCount = 0;
            for (String path : finalExtractedFilePaths) {
                String ext = getFileExtension(path);
                extensionCount.put(ext.isEmpty() ? "无扩展名" : ext, extensionCount.getOrDefault(ext, 0) + 1);
                
                // 统计0KB文件
                try {
                    Path filePath = Paths.get(path);
                    if (Files.exists(filePath) && Files.size(filePath) == 0) {
                        zeroByteFileCount++;
                    }
                } catch (Exception e) {
                    // 忽略统计错误
                }
            }
            logger.info("解压文件统计（按扩展名）: {}", extensionCount);
            if (zeroByteFileCount > 0) {
                logger.info("0KB文件统计: {} 个文件", zeroByteFileCount);
            }
            
            // 统计包含"商务文件"、"技术文件"、"报价文件"的路径
            long businessFileCount = extractedFilePaths.stream()
                    .filter(p -> p.contains("商务文件")).count();
            long technicalFileCount = extractedFilePaths.stream()
                    .filter(p -> p.contains("技术文件")).count();
            long quotationFileCount = extractedFilePaths.stream()
                    .filter(p -> p.contains("报价文件")).count();
            logger.info("文件路径统计 - 商务文件: {}, 技术文件: {}, 报价文件: {}", 
                    businessFileCount, technicalFileCount, quotationFileCount);
            
            // 统计"广东迅力供应链管理有限公司"的文件
            long xunliFileCount = extractedFilePaths.stream()
                    .filter(p -> p.contains("广东迅力供应链管理有限公司")).count();
            logger.info("广东迅力供应链管理有限公司的文件数: {}", xunliFileCount);
            if (xunliFileCount > 0) {
                List<String> xunliFiles = extractedFilePaths.stream()
                        .filter(p -> p.contains("广东迅力供应链管理有限公司"))
                        .collect(java.util.stream.Collectors.toList());
                logger.info("广东迅力供应链管理有限公司的文件列表（前10个）: {}", 
                        xunliFiles.stream().limit(10)
                            .map(p -> java.nio.file.Paths.get(p).getFileName().toString())
                            .collect(java.util.stream.Collectors.joining(", ")));
            }
        }
        
        // 7. 异步处理所有后续操作（数据库保存、向量化、目录抽取）
        // 同步执行写库部分，确保接口返回前即可查询到数据
        final List<String> extractedPaths = new ArrayList<>(extractedFilePaths);
        final List<String> finalPaths = new ArrayList<>(finalExtractedFilePaths);

        updateProjectFilePaths(projectId, zipFilePathStr, extractedPaths);
        saveFilesToDatabase(projectId, extractedPaths, null, null);

        CompletableFuture.runAsync(() -> {
            try {
                asyncProcessUploadedFiles(projectId, zipFilePathStr,
                        new ArrayList<>(extractedPaths), new ArrayList<>(finalPaths), null, null);
            } catch (Exception e) {
                logger.error("异步处理上传文件异常", e);
            }
        });

        return new FileService.UploadResult(zipFilePathStr, extractPathStr, finalExtractedFilePaths);
    }

    /**
     * 处理上传后的所有操作（数据库保存、向量化、目录抽取）
     */
    public void asyncProcessUploadedFiles(String projectId, String zipFilePathStr,
                                          List<String> extractedFilePaths,
                                          List<String> finalExtractedFilePaths,
                                          String supplierId,
                                          Long explicitFileTypeId) {
        try {
            logger.info("开始异步处理上传文件，项目ID: {}, 供应商ID: {}, 文件类型ID: {}, 文件数量: {}", 
                    projectId, supplierId, explicitFileTypeId, extractedFilePaths.size());
        
            // 1. 保存文件信息到数据库
            saveFilesToDatabase(projectId, extractedFilePaths, supplierId, explicitFileTypeId);

            // 2. 更新项目表中的文件路径
            updateProjectFilePaths(projectId, zipFilePathStr, extractedFilePaths);

            // 3. 筛选需要向量化的文件并异步读取内容
        List<String> vectorizableFilePaths = filterVectorizableFiles(extractedFilePaths);
        if (!vectorizableFilePaths.isEmpty()) {
            logger.info("发现 {} 个需要向量化的文件，将异步读取内容并进行向量化", vectorizableFilePaths.size());

            // 分别处理PDF和其他类型文件
            List<String> pdfFilePaths = filterPdfFiles(vectorizableFilePaths);
            if (!pdfFilePaths.isEmpty()) {
                logger.info("发现 {} 个PDF文件，将异步读取内容并进行向量化", pdfFilePaths.size());
                pdfReadService.readPdfFilesAsync(pdfFilePaths, projectId);

                // 同时进行OCR识别处理
                if (ocrEnabled) {
                    logger.info("开始异步处理PDF文件的OCR识别");
                } else {
                    logger.info("OCR功能已关闭，跳过PDF文件的OCR识别");
                }
                processPdfOcrForFiles(pdfFilePaths, projectId, supplierId);
            }

            // 这里可以扩展为调用相应的服务来处理其他文件类型
        } else {
            logger.info("未发现需要向量化的文件");
        }
        
            // 4. 异步处理文件目录抽取
            if (!finalExtractedFilePaths.isEmpty()) {
                logger.info("开始异步处理文件目录抽取，共 {} 个文件", finalExtractedFilePaths.size());
                processFileDirectoriesAsync(finalExtractedFilePaths, projectId);
            } else {
                logger.info("没有文件需要进行目录抽取");
            }

            logger.info("异步处理上传文件完成，项目ID: {}", projectId);

        } catch (Exception e) {
            logger.error("异步处理上传文件失败，项目ID: {}", projectId, e);
        }
    }

    /**
     * 保存文件信息到数据库并更新项目文件路径（保留兼容性）
     */
    public void asyncSaveFilesAndUpdateProject(String projectId, String zipFilePathStr, List<String> extractedFilePaths) {
        asyncProcessUploadedFiles(projectId, zipFilePathStr, extractedFilePaths, extractedFilePaths, null, null);
    }

    /**
     * 从文件路径列表中筛选出PDF文件
     *
     * @param filePaths 文件路径列表
     * @return PDF文件路径列表
     */
    private List<String> filterPdfFiles(List<String> filePaths) {
        List<String> pdfFiles = new ArrayList<>();
        for (String filePath : filePaths) {
            if (filePath != null && filePath.toLowerCase().endsWith(".pdf")) {
                pdfFiles.add(filePath);
            }
        }
        return pdfFiles;
    }

    /**
     * 处理PDF文件的OCR识别
     *
     * @param pdfFilePaths PDF文件路径列表
     * @param projectId 项目ID
     * @param supplierId 供应商ID
     */
    private void processPdfOcrForFiles(List<String> pdfFilePaths, String projectId, String supplierId) {
        // 检查OCR功能是否启用
        if (!ocrEnabled) {
            logger.info("OCR功能已关闭，跳过PDF文件OCR处理: projectId={}, supplierId={}", projectId, supplierId);
            return;
        }

        logger.info("准备处理 {} 个PDF文件的OCR: projectId={}, supplierId={}", pdfFilePaths.size(), projectId, supplierId);

        for (String filePath : pdfFilePaths) {
            try {
                logger.debug("查找PDF文件的数据库记录: {}", filePath);

                // 首先尝试通过存储的路径查找（normalized路径）
                Path absolutePath = Paths.get(filePath).toAbsolutePath().normalize();
                String storedPath = filePathResolver.normalizeForStorage(absolutePath);

                java.util.Optional<File> fileOpt = fileRepository.findByFilePath(storedPath);
                if (fileOpt.isPresent()) {
                    File file = fileOpt.get();
                    logger.info("✅ 通过存储路径找到PDF文件记录，开始OCR处理: {} (ID: {}, 扩展名: {})",
                               storedPath, file.getId(), file.getFileExtension());
                    ocrService.processPdfOcrAsync(file.getId(), filePath, projectId, supplierId);
                } else {
                    logger.warn("❌ 通过存储路径未找到PDF文件记录: {}", storedPath);

                    // 尝试通过原始绝对路径查找
                    fileOpt = fileRepository.findByFilePath(absolutePath.toString());
                    if (fileOpt.isPresent()) {
                        File file = fileOpt.get();
                        logger.info("✅ 通过绝对路径找到PDF文件记录，开始OCR处理: {} (ID: {})", absolutePath, file.getId());
                        ocrService.processPdfOcrAsync(file.getId(), filePath, projectId, supplierId);
                    } else {
                        logger.warn("❌ 通过绝对路径也未找到PDF文件记录: {}", absolutePath);

                        // 最后尝试通过文件名查找（作为备用方案）
                        String fileName = java.nio.file.Paths.get(filePath).getFileName().toString();
                        logger.debug("尝试通过文件名查找: {}", fileName);
                        java.util.List<File> filesByName = fileRepository.findByProjectIdAndFileName(projectId, fileName);
                        if (!filesByName.isEmpty()) {
                            File file = filesByName.get(0);
                            logger.info("✅ 通过文件名找到PDF文件记录，开始OCR处理: {} (ID: {})", filePath, file.getId());
                            ocrService.processPdfOcrAsync(file.getId(), filePath, projectId, supplierId);
                        } else {
                            logger.error("❌ 通过所有方式都未找到PDF文件记录: {}", fileName);
                        }
                    }
                }
            } catch (Exception e) {
                logger.error("❌ 处理PDF文件OCR时出错: {}", filePath, e);
            }
        }
    }

    /**
     * 过滤需要向量化的文件类型（PDF、Word、PPT、TXT、JSON）
     * 只返回真正的内容文件，不包含压缩文件
     *
     * @param filePaths 文件路径列表
     * @return 需要向量化的文件路径列表
     */
    private List<String> filterVectorizableFiles(List<String> filePaths) {
        List<String> vectorizableFiles = new ArrayList<>();

        // 只支持指定的文件类型：pdf, word, ppt, txt, json
        Set<String> supportedExtensions = new HashSet<>(Arrays.asList(
            ".pdf",    // PDF文件
            ".doc", ".docx",  // Word文件
            ".ppt", ".pptx",  // PowerPoint文件
            ".txt",    // 文本文件
            ".json"    // JSON文件
        ));

        for (String filePath : filePaths) {
            if (filePath != null && !filePath.trim().isEmpty()) {
                String lowerCasePath = filePath.toLowerCase();
                for (String ext : supportedExtensions) {
                    if (lowerCasePath.endsWith(ext)) {
                        vectorizableFiles.add(filePath);
                        break; // 找到匹配的扩展名就跳出内循环
                    }
                }
            }
        }

        logger.info("过滤得到 {} 个需要向量化的文件 (pdf, word, ppt, txt, json)", vectorizableFiles.size());
        return vectorizableFiles;
    }
    
    /**
     * 保存文件信息到文件表
     */
    @Transactional
    private void saveFilesToDatabase(String projectId, List<String> filePaths, String explicitSupplierId, Long explicitFileTypeId) {
        try {
            for (String filePath : filePaths) {
                if (filePath == null || filePath.trim().isEmpty()) {
                    continue;
                }

                try {
                    Path path = Paths.get(filePath);
                    if (!Files.exists(path)) {
                        logger.warn("文件不存在，跳过保存: {}", filePath);
                        continue;
                    }

                    Path absolutePath = path.toAbsolutePath().normalize();
                    path = absolutePath;
                    String storedPath = filePathResolver.normalizeForStorage(absolutePath);

                    java.util.Optional<File> existingFile = fileRepository.findByFilePath(storedPath);
                    if (existingFile.isPresent()) {
                        logger.debug("文件已存在于数据库，跳过: {}", storedPath);
                        continue;
                    }

                    java.util.Optional<File> legacyFile = fileRepository.findByFilePath(absolutePath.toString());
                    if (legacyFile.isPresent()) {
                        File legacy = legacyFile.get();
                        legacy.setFilePath(storedPath);
                        fileRepository.save(legacy);
                        logger.debug("更新已有文件记录路径: {}", storedPath);
                        continue;
                    }

                    // 获取文件信息
                    String fileName = path.getFileName().toString();
                    String fileExtension = getFileExtension(fileName);
                    long fileSize = Files.size(path);
                    
                    // 0KB文件也应该被保存，记录日志以便调试
                    if (fileSize == 0) {
                        logger.debug("检测到0KB文件: {} (路径: {})", fileName, filePath);
                    }

                    // 从路径识别文件类型（技术文件、商务文件、报价文件等）
                    Long fileTypeId = explicitFileTypeId;
                    if (fileTypeId == null) {
                        fileTypeId = extractFileTypeIdFromPath(filePath);
                    }
                    if (fileTypeId == null) {
                        // 如果无法从路径识别，尝试通过文件名识别
                        FileType fileType = fileTypeService.identifyFileType(fileName, filePath);
                        if (fileType != null) {
                            fileTypeId = fileType.getId();
                            logger.debug("通过文件名识别文件类型: {} -> {}", fileName, fileType.getTypeName());
                        } else {
                            // 如果无法识别，创建一个"未知类型"
                            FileType unknownType = fileTypeService.createOrGetFileType("未知类型", "无法自动识别的文件类型");
                            fileTypeId = unknownType.getId();
                            logger.debug("使用未知类型: {}", fileName);
                        }
                    }

                    // 从路径提取供应商信息
                    String supplierId = explicitSupplierId;
                    if (supplierId == null) {
                        supplierId = extractSupplierIdFromPath(filePath);
                    } else {
                        // 如果明确指定了supplierId，确保它存在于数据库中
                        try {
                            if (supplierRepository.findBySupplierId(supplierId) == null) {
                                Supplier newSupplier = new Supplier();
                                newSupplier.setSupplierId(supplierId);
                                newSupplier.setSupplierName(supplierId); // 暂无名称
                                newSupplier.setStatus("active");
                                supplierRepository.save(newSupplier);
                            }
                        } catch (Exception e) {
                            logger.error("处理供应商信息时出错: {}", e.getMessage(), e);
                        }
                    }

                    // 创建并保存File实体
                    File file = new File();
                    file.setFilePath(storedPath);
                    file.setFileName(fileName);
                    file.setFileExtension(fileExtension);
                    file.setFileSize(fileSize);
                    file.setProjectId(projectId);
                    file.setFileTypeId(fileTypeId);
                    file.setSupplierId(supplierId);
                    file.setStatus("uploaded");
                    
                    // 计算MD5
                    try {
                        String md5 = calculateMD5(path);
                        file.setFileMd5(md5);
                    } catch (Exception e) {
                        logger.warn("计算文件MD5失败: {}", e.getMessage());
                    }
                    
                    // 设置目录抽取初始状态为processing，以便异步处理
                    file.setCatalogExtractionStatus("processing");
                    file.setCatalogExtractionRetryCount(0);
                    
                    fileRepository.save(file);
                    logger.info("文件已保存到数据库: fileId={}, fileName={}, catalogStatus=processing", 
                            file.getFileId(), fileName);
                } catch (Exception e) {
                    logger.error("保存文件信息到数据库时出错: {}", filePath, e);
                }
            }
        } catch (Exception e) {
            logger.error("批量保存文件信息到数据库失败: {}", e.getMessage(), e);
        }
    }
                      
    /**
     * 计算文件MD5值
     */
    private String calculateMD5(Path filePath) throws IOException {
        try {
            long fileSize = Files.size(filePath);
            
            // 对于0KB文件，返回空字符串的MD5值
            if (fileSize == 0) {
                java.security.MessageDigest md = java.security.MessageDigest.getInstance("MD5");
                byte[] digest = md.digest();
                StringBuilder sb = new StringBuilder();
                for (byte b : digest) {
                    sb.append(String.format("%02x", b));
                }
                return sb.toString();
            }
            
            // 对于非空文件，正常计算MD5
            try (java.io.InputStream is = Files.newInputStream(filePath)) {
                java.security.MessageDigest md = java.security.MessageDigest.getInstance("MD5");
                byte[] buffer = new byte[8192];
                int read;
                while ((read = is.read(buffer)) > 0) {
                    md.update(buffer, 0, read);
                }
                byte[] digest = md.digest();
                StringBuilder sb = new StringBuilder();
                for (byte b : digest) {
                    sb.append(String.format("%02x", b));
                }
                return sb.toString();
            }
        } catch (java.security.NoSuchAlgorithmException e) {
            throw new IOException("MD5算法不可用", e);
        }
    }

    /**
     * 更新项目表中的文件路径
     */
    @Transactional
    private void updateProjectFilePaths(String projectId, String zipFilePath, List<String> extractedFilePaths) {
        try {
            Project project = projectRepository.findByProjectId(projectId);
            if (project != null) {
                // 构建文件路径列表
                // 1. 上传目录中的原始ZIP文件路径（不删除，保留）
                List<String> allFilePaths = new ArrayList<>();
                allFilePaths.add(zipFilePath);
                
                // 2. 添加所有解压后的文件路径（不包括解压目录中已删除的ZIP文件）
                // 注意：extractedFilePaths 中已经包含了所有成功解压的文件（不包括已删除的ZIP）
                allFilePaths.addAll(extractedFilePaths);
                
                // 去重（避免重复路径）
                List<String> uniqueFilePaths = new ArrayList<>(new java.util.LinkedHashSet<>(allFilePaths));
                
                // 保存为JSON格式
                String filePathsJson = JSON.toJSONString(uniqueFilePaths);
                project.setFilePaths(filePathsJson);
                project.setUpdateTime(LocalDateTime.now());
                projectRepository.save(project);
                logger.info("项目 {} 的文件路径已更新，共 {} 个文件（原始ZIP: 1个，解压文件: {}个）", 
                        projectId, uniqueFilePaths.size(), extractedFilePaths.size());
                logger.debug("文件路径JSON: {}", filePathsJson);
            } else {
                logger.warn("项目不存在: {}", projectId);
            }
        } catch (Exception e) {
            logger.error("更新项目文件路径失败: {}", e.getMessage(), e);
            // 不抛出异常，避免影响主流程
        }
    }
    
    @Override
    @Transactional
    public Project createOrUpdateProject(String projectId, String projectName) {
        Project project = projectRepository.findByProjectId(projectId);
        
        if (project == null) {
            // 创建新项目
            project = new Project();
            project.setProjectId(projectId);
            project.setProjectName(projectName != null ? projectName : projectId);
            project.setStatus("ACTIVE");
            project.setCreateTime(LocalDateTime.now());
            project.setUpdateTime(LocalDateTime.now());
            project = projectRepository.save(project);
            logger.info("创建新项目: {}", projectId);
        } else {
            // 更新项目
            if (projectName != null && !projectName.isEmpty()) {
                project.setProjectName(projectName);
            }
            project.setUpdateTime(LocalDateTime.now());
            project = projectRepository.save(project);
            logger.info("更新项目: {}", projectId);
        }
        
        return project;
    }

    /**
     * 解压ZIP文件（带递归深度限制）
     *
     * @param zipFilePath ZIP文件路径
     * @param extractPath 解压目标路径
     * @return 解压后的文件路径列表
     */
    private List<String> extractZipFile(Path zipFilePath, Path extractPath) throws IOException {
        return extractZipFile(zipFilePath, extractPath, 0);
    }
    
    /**
     * 解压ZIP文件（递归版本，支持多层嵌套解压）
     *
     * @param zipFilePath ZIP文件路径
     * @param extractPath 解压目标路径
     * @param depth 当前递归深度（用于防止无限递归，最大深度限制为10）
     * @return 解压后的文件路径列表
     */
    private List<String> extractZipFile(Path zipFilePath, Path extractPath, int depth) throws IOException {
        logger.info("=== 开始调用extractZipFile方法 ===");
        logger.info("ZIP文件路径: {}", zipFilePath);
        logger.info("解压目标路径: {}", extractPath);
        logger.info("递归深度: {}", depth);

        // 1. 检查文件是否真的是ZIP文件（通过Magic Bytes），防止HTML文件冒充
        try (InputStream is = Files.newInputStream(zipFilePath)) {
            byte[] buffer = new byte[4];
            int read = is.read(buffer);
            if (read >= 1 && buffer[0] == '<') {
                // 很可能是HTML文件
                 String preview = new String(buffer, 0, read);
                // 再读一点内容以便确认
                byte[] moreBuffer = new byte[100];
                int moreRead = is.read(moreBuffer);
                if (moreRead > 0) {
                    preview += new String(moreBuffer, 0, moreRead);
                }
                String errorMsg = "下载的文件是HTML页面而不是ZIP文件 (预览: " + preview.replace("\n", " ").replace("\r", " ") + "...)";
                logger.error(errorMsg);
                throw new IOException(errorMsg);
            }
        } catch (IOException e) {
             if (e.getMessage() != null && e.getMessage().contains("HTML页面")) {
                throw e;
            }
            logger.warn("检查文件头失败: {}", e.getMessage());
        }

        // 防止无限递归，最大深度限制为10层
        if (depth >= 10) {
            logger.warn("ZIP文件嵌套深度超过限制（10层），停止递归解压: {}", zipFilePath);
            return new ArrayList<>();
        }
        
        List<String> extractedFiles = new ArrayList<>();
        int totalEntries = 0;
        int skippedEntries = 0;
        // 记录被跳过的文件信息（用于调试）
        List<String> skippedFiles = new ArrayList<>();
        List<String> skippedReasons = new ArrayList<>();

        logger.info("开始读取ZIP文件 [深度: {}]: {}", depth, zipFilePath);
        
        // 检查ZIP文件是否存在
        if (!Files.exists(zipFilePath)) {
            throw new IOException("ZIP文件不存在: " + zipFilePath);
        }
        
        // 检查ZIP文件大小
        long zipFileSize = Files.size(zipFilePath);
        logger.info("ZIP文件大小: {} 字节", zipFileSize);
        
        if (zipFileSize == 0) {
            logger.warn("ZIP文件为空");
            return extractedFiles;
        }
        
        // 快速检测明显的非ZIP文件（例如上游传了JSON/TXT）
        String lowerName = zipFilePath.getFileName().toString().toLowerCase(Locale.ROOT);
        try (InputStream detectIs = Files.newInputStream(zipFilePath)) {
            byte[] head = new byte[4];
            int read = detectIs.read(head);
            boolean looksJson = read > 0 && (head[0] == '{' || head[0] == '[');
            if (looksJson || lowerName.endsWith(".json") || lowerName.endsWith(".txt")) {
                String magicBytes = read > 0
                        ? String.format("%02X %02X %02X %02X", head[0], head.length > 1 ? head[1] : 0,
                                head.length > 2 ? head[2] : 0, head.length > 3 ? head[3] : 0)
                        : "unknown";
                logger.info("检测到非ZIP文件，跳过解压: {} (Magic Bytes: {})", zipFilePath, magicBytes);
                return extractedFiles;
            }
        } catch (Exception e) {
            logger.warn("检测文件类型失败，继续尝试作为ZIP处理: {}", e.getMessage());
        }
        
        // 先尝试使用ZipFile（更高效），如果失败则回退到ZipInputStream（更宽容）
        boolean useZipInputStream = false;
        
        // 尝试1: 使用ZipFile（支持UTF-8编码）
        ZipFile zipFile = null;
        try {
            // 尝试使用UTF-8编码打开ZIP文件
            try {
                zipFile = new ZipFile(zipFilePath.toFile(), Charset.forName("UTF-8"));
            } catch (Exception e) {
                // 如果UTF-8失败，尝试默认编码
                logger.debug("使用UTF-8编码打开ZIP文件失败，尝试默认编码: {}", e.getMessage());
                zipFile = new ZipFile(zipFilePath.toFile());
            }
            
            logger.info("使用ZipFile方式读取ZIP文件条目...");
            Enumeration<? extends ZipEntry> entries = zipFile.entries();
            
            while (entries.hasMoreElements()) {
                ZipEntry entry = entries.nextElement();
                totalEntries++;
                
                String entryName = entry.getName();
                long entrySize = entry.getSize();
                boolean isDirectory = entry.isDirectory() || entryName.endsWith("/");
                
                // 记录条目信息（包括预期大小，用于后续验证）
                logger.info("处理ZIP条目 [{}]: {}, 类型: {}, ZIP中记录的大小: {} 字节", 
                        totalEntries, entryName, isDirectory ? "目录" : "文件", 
                        entrySize != -1 ? entrySize : "未知");

                // 跳过目录（目录不算文件，所以不记录到skippedFiles，只计数）
                if (isDirectory) {
                    logger.debug("跳过目录: {}", entryName);
                    skippedEntries++;
                    continue;
                }

                // 规范化路径（统一使用正斜杠）
                String normalizedEntryName = entryName.replace('\\', '/');

                // 移除路径过滤逻辑，让所有文件都能被解压用于向量化
                
                // 检查文件扩展名
                String extension = getFileExtension(entryName);
                
                // 不再检查文件类型，允许所有文件类型解压
                // 文件类型过滤将在返回结果时进行
                
                // 处理嵌套的ZIP文件（分卷文件在ZIP解压完成后统一处理）
                if ("zip".equalsIgnoreCase(extension)) {
                    // 先解压嵌套的ZIP文件到临时位置
                    Path nestedZipPath = extractPath.resolve(normalizedEntryName).normalize();
                    Path normalizedExtractPath = extractPath.normalize();
                    
                    if (!nestedZipPath.startsWith(normalizedExtractPath)) {
                        logger.warn("检测到路径遍历攻击，跳过嵌套ZIP文件: {} (目标路径: {})", entryName, nestedZipPath);
                        skippedEntries++;
                        skippedFiles.add(entryName);
                        skippedReasons.add("路径遍历攻击：嵌套ZIP文件");
                        continue;
                    }
                    
                    // 创建父目录
                    Path parentDir = nestedZipPath.getParent();
                    if (parentDir != null) {
                        Files.createDirectories(parentDir);
                    }
                    
                    // 保存嵌套的ZIP文件
                    try (InputStream entryInputStream = zipFile.getInputStream(entry);
                         BufferedInputStream bis = new BufferedInputStream(entryInputStream);
                         BufferedOutputStream bos = new BufferedOutputStream(
                                 Files.newOutputStream(nestedZipPath))) {
                        
                        byte[] buffer = new byte[8192];
                        int len;
                        long totalBytes = 0;
                        while ((len = bis.read(buffer)) > 0) {
                            bos.write(buffer, 0, len);
                            totalBytes += len;
                        }
                        // 确保文件被正确创建（即使是0KB文件）
                        bos.flush();
                        logger.info("嵌套ZIP文件已保存: {} (大小: {} 字节)", nestedZipPath, totalBytes);
                    } catch (IOException e) {
                        logger.error("保存嵌套ZIP文件失败: {}, 错误: {}", entryName, e.getMessage(), e);
                        skippedEntries++;
                        continue;
                    }
                    
                    // 递归解压嵌套的ZIP文件
                    try {
                        // 创建嵌套ZIP的解压目录（使用文件名去掉.zip后缀）
                        String nestedZipName = nestedZipPath.getFileName().toString();
                        String nestedDirName = nestedZipName.substring(0, nestedZipName.lastIndexOf('.'));
                        Path nestedExtractPath = nestedZipPath.getParent().resolve(nestedDirName);
                        
                        // 递归解压嵌套的ZIP文件
                        List<String> nestedFiles = extractZipFile(nestedZipPath, nestedExtractPath, depth + 1);
                        extractedFiles.addAll(nestedFiles);
                        
                        logger.info("嵌套ZIP文件已解压 [深度: {}]: {} -> {} (解压 {} 个文件)", 
                                depth + 1, nestedZipPath, nestedExtractPath, nestedFiles.size());
                        
                        // 解压完成后，删除解压目录中的嵌套ZIP文件（保留上传目录中的原始ZIP）
                        try {
                            Files.deleteIfExists(nestedZipPath);
                            logger.info("已删除解压目录中的嵌套ZIP文件: {}", nestedZipPath);
                        } catch (IOException deleteEx) {
                            logger.warn("删除嵌套ZIP文件失败: {}, 错误: {}", nestedZipPath, deleteEx.getMessage());
                        }
                        
                    } catch (Exception e) {
                        logger.error("递归解压嵌套ZIP文件失败 [深度: {}]: {}, 错误: {}", 
                                depth + 1, entryName, e.getMessage(), e);
                        skippedEntries++;
                        skippedFiles.add(entryName);
                        skippedReasons.add("解压失败: " + (e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName()));
                    }
                    continue;
                }

                // 分卷文件（.z01, .z02等）现在在ZIP解压完成后统一处理，这里只解压保存
                
                // 过滤以点开头的文件（如 .DS_Store, .__MACOSX, ._xxx 等临时/隐藏文件）
                String fileName = Paths.get(normalizedEntryName).getFileName().toString();
                if (fileName.startsWith(".")) {
                    String reason = "跳过临时/隐藏文件: " + fileName;
                    logger.debug("跳过临时/隐藏文件: {} (原始条目: {})", fileName, entryName);
                    skippedEntries++;
                    skippedFiles.add(entryName);
                    skippedReasons.add(reason);
                    continue;
                }
                
                // 防止路径遍历攻击
                Path targetPath = extractPath.resolve(normalizedEntryName).normalize();
                Path normalizedExtractPath = extractPath.normalize();
                
                if (!targetPath.startsWith(normalizedExtractPath)) {
                    String reason = "路径遍历攻击防护";
                    logger.warn("检测到路径遍历攻击，跳过文件: {} (目标路径: {})", entryName, targetPath);
                    skippedEntries++;
                    skippedFiles.add(entryName);
                    skippedReasons.add(reason);
                    continue;
                }

                // 创建父目录
                Path parentDir = targetPath.getParent();
                if (parentDir != null) {
                    Files.createDirectories(parentDir);
                    logger.debug("创建父目录: {}", parentDir);
                }

                // 写入文件
                long totalBytes = 0;
                BufferedOutputStream bos = null;
                try (InputStream entryInputStream = zipFile.getInputStream(entry);
                     BufferedInputStream bis = new BufferedInputStream(entryInputStream)) {
                    
                    // 确保文件目录存在
                    Files.createDirectories(targetPath.getParent());
                    
                    bos = new BufferedOutputStream(Files.newOutputStream(targetPath), 8192);
                    byte[] buffer = new byte[8192];
                    int len;
                    while ((len = bis.read(buffer)) != -1) {  // 使用 != -1 而不是 > 0，确保0KB文件也能正确处理
                        if (len > 0) {
                            bos.write(buffer, 0, len);
                            totalBytes += len;
                        }
                    }
                    // 确保所有数据都被写入磁盘
                    bos.flush();
                } catch (IOException e) {
                    logger.error("解压文件时发生IO错误: {}, 错误: {}", targetPath, e.getMessage(), e);
                    throw e;
                } finally {
                    // 确保输出流被正确关闭
                    if (bos != null) {
                        try {
                            bos.close();
                        } catch (IOException e) {
                            logger.warn("关闭输出流失败: {}", e.getMessage());
                        }
                    }
                }
                
                // 验证文件已创建并验证实际大小（在流关闭后验证）
                if (!Files.exists(targetPath)) {
                    logger.warn("文件解压后不存在，可能创建失败: {}", targetPath);
                    skippedEntries++;
                    skippedFiles.add(entryName);
                    skippedReasons.add("文件创建失败");
                    continue;
                }
                
                // 验证文件实际大小（在流关闭后验证）
                long actualFileSize = Files.size(targetPath);
                
                // 检查ZIP条目中记录的大小（如果可用）与实际解压大小是否匹配
                if (entrySize != -1 && entrySize > 0 && totalBytes == 0) {
                    logger.error("严重错误：ZIP条目记录大小为 {} 字节，但实际读取为 0 字节！文件: {}", 
                            entrySize, targetPath);
                    // 这种情况下，文件可能没有正确解压，跳过
                    skippedEntries++;
                    skippedFiles.add(entryName);
                    skippedReasons.add("读取失败：ZIP记录大小不为0但读取为0");
                    try {
                        Files.deleteIfExists(targetPath); // 删除可能损坏的空文件
                    } catch (IOException e) {
                        logger.warn("删除损坏文件失败: {}", e.getMessage());
                    }
                    continue;
                }
                
                if (totalBytes != actualFileSize) {
                    logger.warn("文件大小不匹配！读取: {} 字节，磁盘: {} 字节，文件: {}", 
                            totalBytes, actualFileSize, targetPath);
                }
                
                // 记录解压信息（包括ZIP中的预期大小）
                if (entrySize != -1 && entrySize != totalBytes && entrySize > 0) {
                    logger.warn("ZIP条目大小与解压大小不一致：ZIP记录 {} 字节，实际解压 {} 字节，文件: {}", 
                            entrySize, totalBytes, targetPath);
                }
                
                logger.info("文件已解压: {} (ZIP记录: {} 字节, 实际解压: {} 字节, 磁盘: {} 字节)", 
                        targetPath, entrySize != -1 ? entrySize : "未知", totalBytes, actualFileSize);
                
                // 记录解压的文件路径（使用绝对路径）- 包括0KB文件
                String absolutePath = targetPath.toAbsolutePath().toString();
                
                // 如果解压出来的文件还是ZIP文件，继续递归解压
                // 注意：这里处理的ZIP文件是普通文件解压后发现的，不是ZIP条目中的ZIP
                // ZIP条目中的ZIP已经在上面处理过了
                if ("zip".equalsIgnoreCase(extension)) {
                    try {
                        // 创建嵌套ZIP的解压目录（使用文件名去掉.zip后缀）
                        String zipFileName = targetPath.getFileName().toString();
                        String dirName = zipFileName.substring(0, zipFileName.lastIndexOf('.'));
                        Path nestedExtractPath = targetPath.getParent().resolve(dirName);
                        
                        // 递归解压，depth+1 表示进入下一层
                        List<String> nestedFiles = extractZipFile(targetPath, nestedExtractPath, depth + 1);
                        extractedFiles.addAll(nestedFiles);
                        
                        logger.info("检测到解压后的ZIP文件，继续递归解压 [深度: {}]: {} -> {} (解压 {} 个文件)", 
                                depth + 1, targetPath, nestedExtractPath, nestedFiles.size());
                        
                        // 解压完成后，删除解压目录中的ZIP文件（保留上传目录中的原始ZIP）
                        try {
                            Files.deleteIfExists(targetPath);
                            logger.info("已删除解压目录中的ZIP文件: {}", targetPath);
                        } catch (IOException deleteEx) {
                            logger.warn("删除ZIP文件失败: {}, 错误: {}", targetPath, deleteEx.getMessage());
                        }
                        
                    } catch (Exception e) {
                        logger.error("递归解压解压后的ZIP文件失败 [深度: {}]: {}, 错误: {}", 
                                depth + 1, targetPath, e.getMessage(), e);
                        // 递归解压失败时不添加ZIP文件到返回列表
                        skippedEntries++;
                        skippedFiles.add(entryName);
                        skippedReasons.add("递归解压失败: " + e.getMessage());
                    }
                } else {
                    // 只添加内容文件（PDF, Word, PPT, TXT, JSON）到返回列表
                    if (isContentFileType(extension)) {
                    extractedFiles.add(absolutePath);
                    } else {
                        logger.debug("跳过非内容文件: {} (扩展名: {})", entryName, extension);
                    }
                }
            }
            
        } catch (Exception e) {
            logger.error("使用ZipFile方式解压失败: {}", e.getMessage(), e);
            useZipInputStream = true;
        } finally {
            if (zipFile != null) {
                try {
                    zipFile.close();
                } catch (IOException closeEx) {
                    logger.warn("关闭ZipFile失败: {}", closeEx.getMessage());
                }
            }
        }
        
        // 如果上面的方法失败，尝试使用ZipInputStream
        if (useZipInputStream || extractedFiles.isEmpty()) {
            logger.info("尝试使用ZipInputStream方式解压...");
            // 这里可以添加ZipInputStream的备用逻辑
        }
        
        logger.info("ZIP文件解压完成 [深度: {}]: {} (共处理 {} 个条目，跳过 {} 个，成功解压 {} 个文件)", 
                depth, zipFilePath, totalEntries, skippedEntries, extractedFiles.size());
        
        return extractedFiles;
    }

    /**
     * 从PDF中提取可能的标题
     */
    private java.util.List<String> extractPotentialTitlesFromPDF(String filePath) {
        java.util.List<String> titles = new java.util.ArrayList<>();

        try {
            // 使用PDFBox正确读取PDF文件的文本内容
            java.nio.file.Path path = java.nio.file.Paths.get(filePath);
            if (!java.nio.file.Files.exists(path)) {
                logger.warn("PDF文件不存在: {}", filePath);
                return titles;
            }

            // 使用PDFBox提取PDF文本内容
            org.apache.pdfbox.pdmodel.PDDocument document = org.apache.pdfbox.Loader.loadPDF(new java.io.File(filePath));
            org.apache.pdfbox.text.PDFTextStripper stripper = new org.apache.pdfbox.text.PDFTextStripper();

            // 只读取前几页以提高性能，并设置文本提取选项
            stripper.setStartPage(1);
            stripper.setEndPage(Math.min(document.getNumberOfPages(), 5)); // 最多读取前5页
            stripper.setSortByPosition(true); // 按位置排序
            stripper.setAddMoreFormatting(false); // 简化格式

            String content = stripper.getText(document);
            document.close();

            // 过滤和清理文本内容
            content = content.replaceAll("\\n\\s*\\n\\s*\\n", "\n\n"); // 合并多余的空行
            content = content.replaceAll("[\\r\\n]+", "\n"); // 统一换行符

            // 查找可能的标题模式
            // 只匹配真正的章节标题，避免匹配表格数据、百分比等
            java.util.regex.Pattern titlePattern = java.util.regex.Pattern.compile(
                "(?m)^\\s*(?:第[一二三四五六七八九十\\d]+章|[\\d]+\\.[\\d]+(?:\\.[\\d]+)*)[^\\n]*(?:概述|要求|规范|说明|内容|规定|标准|方法|程序|测试|报告|证书|证明|文件|资料|参数|指标|要求|条件|条款|偏离|承诺|情况|信息|表|图|附录|附件)[^\\n]{0,50}"
            );

            java.util.regex.Matcher matcher = titlePattern.matcher(content);
            java.util.Set<String> uniqueTitles = new java.util.HashSet<>();

            while (matcher.find() && uniqueTitles.size() < 20) { // 限制最多20个标题
                String title = matcher.group().trim();
                // 清理标题
                title = title.replaceAll("^[\\s\\*]+", "").replaceAll("[\\s]+$", "");

                // 过滤掉纯数字、百分比或不合理的标题
                if (title.length() > 3 && title.length() < 100 &&
                    !uniqueTitles.contains(title) &&
                    !title.matches("^\\d+(\\.\\d+)*%?$") && // 排除纯数字和百分比
                    !title.matches("^\\d{1,2}\\.\\d{1,2}%$") && // 排除如"2.60%"这样的百分比
                    title.matches(".*[一二三四五六七八九十百千万章节条款要求规范].*")) { // 必须包含章节相关关键词

                    uniqueTitles.add(title);
                    titles.add(title);
                    logger.debug("提取到有效章节标题: {}", title);
                    logger.debug("提取到标题: {}", title);
                }
            }

            // 如果没有找到标题模式，尝试查找其他可能的标题
            if (titles.isEmpty()) {
                // 查找包含关键词的行
                String[] keywords = {"概述", "介绍", "背景", "目的", "范围", "定义", "要求", "规范", "标准", "技术"};
                java.util.regex.Pattern keywordPattern = java.util.regex.Pattern.compile(
                    "(?m)^\\s*([^\\n]{10,60}(?:" + String.join("|", keywords) + ")[^\\n]{0,40})"
                );

                matcher = keywordPattern.matcher(content);
                uniqueTitles.clear();

                while (matcher.find() && uniqueTitles.size() < 10) {
                    String title = matcher.group(1).trim();
                    if (title.length() > 5 && title.length() < 80 && !uniqueTitles.contains(title)) {
                        uniqueTitles.add(title);
                        titles.add(title);
                        logger.debug("通过关键词提取到标题: {}", title);
                    }
                }
            }

            logger.info("从PDF中提取到 {} 个潜在标题", titles.size());

        } catch (Exception e) {
            logger.warn("提取PDF标题失败: {}", e.getMessage());
        }

        return titles;
    }

    /**
     * 递归提取PDF目录项
     */
    private void extractOutlineItems(PDOutlineItem item, String indent, java.util.List<com.alibaba.fastjson2.JSONObject> chapters, int level) {
        while (item != null) {
            try {
                String title = item.getTitle();
                if (title != null && !title.trim().isEmpty()) {
                    com.alibaba.fastjson2.JSONObject chapter = new com.alibaba.fastjson2.JSONObject();
                    chapter.put("chapterId", "chap-" + chapters.size());
                    chapter.put("title", title.trim());
                    chapter.put("level", level);

                    // 尝试获取页码信息
                    int startPage = -1;
                    try {
                        if (item.getDestination() != null) {
                            // 如果有直接的目的地，尝试获取页码
                            Object dest = item.getDestination();
                            if (dest instanceof org.apache.pdfbox.pdmodel.interactive.documentnavigation.destination.PDPageDestination) {
                                org.apache.pdfbox.pdmodel.interactive.documentnavigation.destination.PDPageDestination pageDest =
                                    (org.apache.pdfbox.pdmodel.interactive.documentnavigation.destination.PDPageDestination) dest;
                                startPage = pageDest.retrievePageNumber() + 1; // PDFBox页码从0开始
                            }
                        } else if (item.getAction() != null) {
                            // 如果有动作，尝试从中提取页码
                            Object action = item.getAction();
                            if (action instanceof org.apache.pdfbox.pdmodel.interactive.action.PDActionGoTo) {
                                org.apache.pdfbox.pdmodel.interactive.action.PDActionGoTo goToAction =
                                    (org.apache.pdfbox.pdmodel.interactive.action.PDActionGoTo) action;
                                if (goToAction.getDestination() instanceof org.apache.pdfbox.pdmodel.interactive.documentnavigation.destination.PDPageDestination) {
                                    org.apache.pdfbox.pdmodel.interactive.documentnavigation.destination.PDPageDestination pageDest =
                                        (org.apache.pdfbox.pdmodel.interactive.documentnavigation.destination.PDPageDestination) goToAction.getDestination();
                                    startPage = pageDest.retrievePageNumber() + 1;
                                }
                            }
                        }
                    } catch (Exception e) {
                        logger.debug("无法获取章节页码信息: {}", title, e);
                    }

                    // 如果获取到了起始页码
                    if (startPage != -1) {
                        chapter.put("page", String.valueOf(startPage));
                        chapter.put("startPage", startPage);
                        
                        // 尝试计算结束页码
                        // 逻辑：结束页码 = 下一个章节的起始页码 - 1
                        // 如果没有下一个章节，则暂时设为起始页码，后续统一处理
                        int endPage = startPage;
                        
                        // 尝试获取下一个兄弟节点的页码
                        PDOutlineItem nextSibling = item.getNextSibling();
                        if (nextSibling != null) {
                            int nextPage = getPageNumberFromOutlineItem(nextSibling);
                            if (nextPage != -1 && nextPage > startPage) {
                                endPage = nextPage - 1;
                            }
                        } 
                        // 如果没有兄弟节点，但有父节点，尝试获取父节点的下一个兄弟节点的页码
                        // 这部分逻辑比较复杂，且PDFBox的结构可能不支持直接向上查找
                        // 因此，我们采取一种简化的策略：
                        // 在所有章节提取完成后，统一遍历一次列表，修正endPage
                        
                        chapter.put("endPage", endPage);
                    } else {
                        // 如果没有页码信息，设置默认值
                        chapter.put("page", "1");
                        chapter.put("startPage", 1);
                        chapter.put("endPage", 1);
                    }

                    chapter.put("children", new com.alibaba.fastjson2.JSONArray());

                    chapters.add(chapter);
                    logger.debug("{}• {}", indent, title);
                }

                // 递归处理子项
                if (item.hasChildren()) {
                    extractOutlineItems(item.getFirstChild(), indent + "  ", chapters, level + 1);
                }

            } catch (Exception e) {
                logger.warn("处理目录项失败: {}", e.getMessage());
            }

            item = item.getNextSibling();
        }
    }

    /**
     * 辅助方法：从目录项获取页码
     */
    private int getPageNumberFromOutlineItem(PDOutlineItem item) {
        try {
            if (item.getDestination() != null) {
                Object dest = item.getDestination();
                if (dest instanceof org.apache.pdfbox.pdmodel.interactive.documentnavigation.destination.PDPageDestination) {
                    return ((org.apache.pdfbox.pdmodel.interactive.documentnavigation.destination.PDPageDestination) dest).retrievePageNumber() + 1;
                }
            } else if (item.getAction() != null) {
                Object action = item.getAction();
                if (action instanceof org.apache.pdfbox.pdmodel.interactive.action.PDActionGoTo) {
                    org.apache.pdfbox.pdmodel.interactive.action.PDActionGoTo goToAction =
                        (org.apache.pdfbox.pdmodel.interactive.action.PDActionGoTo) action;
                    if (goToAction.getDestination() instanceof org.apache.pdfbox.pdmodel.interactive.documentnavigation.destination.PDPageDestination) {
                        return ((org.apache.pdfbox.pdmodel.interactive.documentnavigation.destination.PDPageDestination) goToAction.getDestination()).retrievePageNumber() + 1;
                    }
                }
            }
        } catch (Exception e) {
            // ignore
        }
        return -1;
    }

    /**
     * 构建层级目录结构
     */
    private com.alibaba.fastjson2.JSONArray buildHierarchicalStructure(java.util.List<com.alibaba.fastjson2.JSONObject> chapters) {
        com.alibaba.fastjson2.JSONArray rootArray = new com.alibaba.fastjson2.JSONArray();
        if (chapters.isEmpty()) {
            return rootArray;
        }

        java.util.List<com.alibaba.fastjson2.JSONObject> levelParents = new java.util.ArrayList<>();
        for (com.alibaba.fastjson2.JSONObject chapter : chapters) {
            int level = Math.max(0, chapter.getIntValue("level"));
            if (chapter.getJSONArray("children") == null) {
                chapter.put("children", new com.alibaba.fastjson2.JSONArray());
            }

            if (level == 0 || levelParents.isEmpty() || levelParents.get(0) == null) {
                rootArray.add(chapter);
                levelParents.clear();
                levelParents.add(chapter);
                continue;
        }

            int parentIndex = Math.min(level - 1, levelParents.size() - 1);
            com.alibaba.fastjson2.JSONObject parent = parentIndex >= 0 ? levelParents.get(parentIndex) : null;
            if (parent == null) {
                // 找不到父节点时降级为根节点
                rootArray.add(chapter);
                levelParents.clear();
                levelParents.add(chapter);
                continue;
            }

            com.alibaba.fastjson2.JSONArray parentChildren = parent.getJSONArray("children");
            if (parentChildren == null) {
                parentChildren = new com.alibaba.fastjson2.JSONArray();
                parent.put("children", parentChildren);
            }
            parentChildren.add(chapter);

            if (level >= levelParents.size()) {
                while (levelParents.size() < level) {
                    levelParents.add(null);
                }
                levelParents.add(chapter);
            } else {
                levelParents.set(level, chapter);
            }
            for (int idx = level + 1; idx < levelParents.size(); idx++) {
                levelParents.set(idx, null);
            }
        }

        limitDirectoryDepth(rootArray, 2); // 只保留最多两层目录
        return rootArray;
    }

    /**
     * 控制目录层级深度
     */
    private void limitDirectoryDepth(com.alibaba.fastjson2.JSONArray nodes, int depthRemaining) {
        if (nodes == null || depthRemaining <= 0) {
            return;
        }

        for (int i = 0; i < nodes.size(); i++) {
            com.alibaba.fastjson2.JSONObject node = nodes.getJSONObject(i);
            if (depthRemaining == 1) {
                node.put("children", new com.alibaba.fastjson2.JSONArray());
            } else {
                limitDirectoryDepth(node.getJSONArray("children"), depthRemaining - 1);
            }
        }
    }

    /**
     * 生成基本的目录结构（当无法从PDF中准确抽取时使用）
     */
    private com.alibaba.fastjson2.JSONArray generateBasicDirectoryStructure(String filePath, String fileId, String fileName) {
        try {
            com.alibaba.fastjson2.JSONArray structured = buildStructuredDirectoryFallback(filePath);
            if (structured != null && !structured.isEmpty()) {
                logger.info("使用文本解析生成基础目录结构，文件ID: {}", fileId);
                return structured;
            }

            com.alibaba.fastjson2.JSONArray directoryArray = new com.alibaba.fastjson2.JSONArray();
            com.alibaba.fastjson2.JSONObject fallbackChapter = createChapterNode("chap-auto-0",
                    fileName != null ? fileName.replaceAll("\\.pdf$", "") : "文档内容");
            directoryArray.add(fallbackChapter);
            logger.info("生成默认基础目录结构，文件ID: {}", fileId);
            return directoryArray;

        } catch (Exception e) {
            logger.error("生成基本目录结构失败，文件ID: {}", fileId, e);
            return null;
        }
    }

    private com.alibaba.fastjson2.JSONArray buildStructuredDirectoryFallback(String filePath) {
        String text = readPdfTextForOutline(filePath);
        if (text == null || text.isBlank()) {
            return new com.alibaba.fastjson2.JSONArray();
        }
        java.util.List<TitleNode> nodes = parseOutlineTitles(text);
        if (nodes.isEmpty()) {
            return new com.alibaba.fastjson2.JSONArray();
        }
        com.alibaba.fastjson2.JSONArray result = new com.alibaba.fastjson2.JSONArray();
        com.alibaba.fastjson2.JSONObject currentParent = null;
        int parentIndex = 0;
        int childIndex = 0;
        for (TitleNode node : nodes) {
            if (node.depth <= 0 || currentParent == null) {
                com.alibaba.fastjson2.JSONObject parent = createChapterNode("chap-auto-" + parentIndex++, node.title);
                result.add(parent);
                currentParent = parent;
                childIndex = 0;
            } else {
                com.alibaba.fastjson2.JSONArray children = currentParent.getJSONArray("children");
                if (children == null) {
                    children = new com.alibaba.fastjson2.JSONArray();
                    currentParent.put("children", children);
                }
                if (children.size() >= 20) {
                    continue;
                }
                com.alibaba.fastjson2.JSONObject child = createChapterNode(
                        currentParent.getString("chapterId") + "-" + childIndex++, node.title);
                children.add(child);
            }
        }
        return result;
    }

    private com.alibaba.fastjson2.JSONObject createChapterNode(String chapterId, String title) {
        com.alibaba.fastjson2.JSONObject node = new com.alibaba.fastjson2.JSONObject();
        node.put("chapterId", chapterId);
        node.put("title", title);
        node.put("page", "1");
        node.put("startPage", 1);
        node.put("endPage", 1);
        node.put("children", new com.alibaba.fastjson2.JSONArray());
        return node;
    }

    private String readPdfTextForOutline(String filePath) {
        try (org.apache.pdfbox.pdmodel.PDDocument document = org.apache.pdfbox.Loader.loadPDF(new java.io.File(filePath))) {
            org.apache.pdfbox.text.PDFTextStripper stripper = new org.apache.pdfbox.text.PDFTextStripper();
            stripper.setStartPage(1);
            stripper.setEndPage(Math.min(document.getNumberOfPages(), 15));
            stripper.setSortByPosition(true);
            return stripper.getText(document);
        } catch (Exception e) {
            logger.warn("读取PDF文本以生成目录失败: {}", e.getMessage());
            return "";
        }
    }

    private java.util.List<TitleNode> parseOutlineTitles(String text) {
        java.util.List<TitleNode> nodes = new java.util.ArrayList<>();
        java.util.Set<String> uniqueTitles = new java.util.LinkedHashSet<>();
        String[] lines = text.split("\\r?\\n");
        for (String rawLine : lines) {
            String line = rawLine == null ? "" : rawLine.trim();
            if (line.length() < 2) {
                continue;
            }
            TitleNode node = toTitleNode(line);
            if (node == null || node.title.length() < 2) {
                continue;
            }
            if (uniqueTitles.add(node.title)) {
                nodes.add(node);
            }
            if (nodes.size() >= 50) {
                break;
            }
        }
        return nodes;
    }

    private TitleNode toTitleNode(String line) {
        for (PatternMapping mapping : PATTERN_MAPPINGS) {
            Matcher matcher = mapping.pattern.matcher(line);
            if (matcher.find()) {
                String cleaned = line.substring(matcher.end()).trim();
                if (cleaned.isEmpty()) {
                    cleaned = line;
                }
                return new TitleNode(cleaned, mapping.depth);
            }
        }
            return null;
        }

    private static class TitleNode {
        final String title;
        final int depth;

        TitleNode(String title, int depth) {
            this.title = title;
            this.depth = depth;
        }
    }

    private static class PatternMapping {
        final Pattern pattern;
        final int depth;

        PatternMapping(String regex, int depth) {
            this.pattern = Pattern.compile(regex);
            this.depth = depth;
        }
    }

    private static final java.util.List<PatternMapping> PATTERN_MAPPINGS = java.util.List.of(
        new PatternMapping("^第[一二三四五六七八九十百千0-9]+章[\\s、\\.．-]*", 0),
        new PatternMapping("^[一二三四五六七八九十]+、", 0),
        new PatternMapping("^\\d{1,2}[\\.．、\\s]+", 0),
        new PatternMapping("^\\d{1,2}\\.\\d{1,2}[\\.．、\\s]+", 1),
        new PatternMapping("^\\d{1,2}\\.\\d{1,2}\\.\\d{1,2}[\\.．、\\s]+", 2),
        new PatternMapping("^[（(][一二三四五六七八九十]+[）)]", 1),
        new PatternMapping("^[（(]\\d+[）)]", 1)
    );

    /**
     * 从文件路径中提取文件ID
     */
    private String extractFileIdFromPath(String filePath) {
        try {
            // 从文件路径中查找UUID格式的文件ID
            // 例如: .../extracts/project-001_20251103_112040_39ad4981/eb949f7d-ba5f-4112-9daa-37d4ab7282e1.pdf
            Path path = Paths.get(filePath);
            String fileName = path.getFileName().toString();

            // 移除扩展名
            int lastDotIndex = fileName.lastIndexOf('.');
            if (lastDotIndex > 0) {
                String nameWithoutExt = fileName.substring(0, lastDotIndex);

                // 检查是否是UUID格式
                if (nameWithoutExt.matches("^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$")) {
                    return nameWithoutExt;
                }
            }
        } catch (Exception e) {
            logger.warn("提取文件ID失败: {}", filePath, e);
        }
        return null;
    }

    /**
     * 根据文件路径和项目ID从数据库中查找文件ID
     */
    private String findFileIdByPath(String filePath, String projectId) {
        try {
            Path path = Paths.get(filePath);
            String fileName = path.getFileName().toString();

            logger.debug("从数据库中查找文件ID: projectId={}, fileName={}", projectId, fileName);

            // 从数据库中查找匹配的文件记录
            List<File> files = fileRepository.findByProjectIdAndFileName(projectId, fileName);
            if (!files.isEmpty()) {
                String fileId = files.get(0).getFileId();
                logger.debug("找到文件ID: {} for fileName: {}", fileId, fileName);
                return fileId;
            } else {
                logger.warn("数据库中未找到匹配的文件记录: projectId={}, fileName={}", projectId, fileName);
            }
        } catch (Exception e) {
            logger.error("从数据库查找文件ID失败: filePath={}, projectId={}", filePath, projectId, e);
        }
        return null;
    }

    /**
     * 处理所有章节的总结（并行处理优化，修复乐观锁冲突）
     */
    @Transactional
    private void processChapterSummaries(String fileId) {
        try {
            logger.info("开始并行处理章节总结，文件ID: {}", fileId);

            // 获取所有章节
            List<FileDirectory> directories = fileDirectoryRepository.findByFileIdOrderBySortOrderAsc(fileId);
            
            if (directories.isEmpty()) {
                logger.info("没有需要处理的章节，文件ID: {}", fileId);
                return;
            }
            
            logger.info("共有 {} 个章节需要处理，文件ID: {}", directories.size(), fileId);
            
            // 用于收集每个章节的总结结果 (chapterId -> summary)
            java.util.concurrent.ConcurrentHashMap<String, String> summaryResults = 
                new java.util.concurrent.ConcurrentHashMap<>();
            
            // 使用并行处理，控制并发数为3避免数据库连接池耗尽
            int batchSize = 3;
            List<List<FileDirectory>> batches = new ArrayList<>();
            for (int i = 0; i < directories.size(); i += batchSize) {
                batches.add(directories.subList(i, Math.min(i + batchSize, directories.size())));
            }
            
            for (List<FileDirectory> batch : batches) {
                // 每批章节并行生成总结（只生成，不保存）
                List<CompletableFuture<Void>> futures = batch.stream()
                    .map(directory -> CompletableFuture.runAsync(() -> {
                        try {
                            String summary = generateChapterSummary(directory);
                            if (summary != null && !summary.isEmpty()) {
                                // 将结果存入并发Map，而不是直接更新实体
                                summaryResults.put(directory.getChapterId(), summary);
                                logger.debug("章节总结生成完成: fileId={}, chapterId={}", 
                                    fileId, directory.getChapterId());
                            }
                        } catch (Exception e) {
                            logger.error("生成章节总结失败: fileId={}, chapterId={}", 
                                fileId, directory.getChapterId(), e);
                        }
                    }))
                    .collect(Collectors.toList());
                
                // 等待当前批次完成
                CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
            }
            
            // 统一保存所有结果（在主线程中按顺序保存，避免乐观锁冲突）
            if (!summaryResults.isEmpty()) {
                logger.info("开始保存 {} 个章节的总结结果", summaryResults.size());
                for (FileDirectory directory : directories) {
                    String summary = summaryResults.get(directory.getChapterId());
                    if (summary != null) {
                        // 重新从数据库加载最新的实体，避免过期实体问题
                        FileDirectory freshDirectory = fileDirectoryRepository.findByFileIdAndChapterId(
                            fileId, directory.getChapterId());
                        if (freshDirectory != null) {
                            freshDirectory.setSummary(summary);
                            fileDirectoryRepository.save(freshDirectory);
                        }
                    }
                }
            }

            logger.info("章节总结并行处理完成，文件ID: {}, 章节数: {}, 成功总结: {}", 
                fileId, directories.size(), summaryResults.size());

        } catch (Exception e) {
            logger.error("处理章节总结出错，文件ID: {}", fileId, e);
        }
    }

    /**
     * 使用AI模型生成章节总结
     */
    private String generateChapterSummary(FileDirectory directory) {
        try {
            // 获取文档摘要提示词模板
            com.zhpb.gdyd_zhpb_zgf.entity.mysql.PromptTemplate summaryTemplate = promptTemplateService.getActiveTemplate("summary");
            if (summaryTemplate == null) {
                throw new RuntimeException("未找到激活的文档摘要提示词模板，请在提示词配置页面启用或创建summary类型的提示词模板");
            }

            String promptTemplate = summaryTemplate.getPromptContent();
            logger.info("使用数据库提示词模板: {} (ID: {}), 内容长度: {}", summaryTemplate.getTemplateName(), summaryTemplate.getTemplateId(), promptTemplate.length());
            logger.debug("提示词内容: {}", promptTemplate);

            // 使用模板替换变量
            String prompt = promptTemplate.replace("{title}", directory.getTitle());
            logger.debug("最终使用的提示词: {}", prompt);

            logger.info("调用AI模型生成章节总结: chapterId={}", directory.getChapterId());

            String userMessage = chapterContentResolver.buildUserMessage(directory);

            ModelRequestContextHolder.setRequestSource("DOC_SUMMARY_AUTO");
            String summary;
            try {
                summary = aiModelService.generateResponse(prompt, userMessage);
            } finally {
                ModelRequestContextHolder.clear();
            }

            if (summary != null && !summary.trim().isEmpty()) {
                logger.info("AI模型返回章节总结，长度: {}", summary.length());
                return summary.trim();
            }

            return null;

        } catch (Exception e) {
            logger.error("调用AI模型生成章节总结失败: chapterId={}", directory.getChapterId(), e);
            return null;
        }
    }

    @Override
    public void processProcurementFilesAsync(String projectId, String packageId, List<String> fileUrls, String traceId) {
        // 异步处理采购文件下载
        CompletableFuture.runAsync(() -> {
            try {
                logger.info("开始异步处理采购文件下载: traceId={}, projectId={}, fileCount={}",
                           traceId, projectId, fileUrls.size());

                // 确保项目存在
                createOrUpdateProject(projectId, null);

                // 替换下载地址
                List<String> processedFileUrls = new ArrayList<>();
                if (fileUrls != null) {
                    for (String url : fileUrls) {
                        processedFileUrls.add(replaceDownloadUrl(url));
                    }
                }

                // 下载文件并处理
                List<String> downloadedFiles = downloadFilesFromUrls(processedFileUrls, projectId, null, traceId);

                if (!downloadedFiles.isEmpty()) {
                    // 处理下载的文件（解压、保存到数据库、向量化等）
                    for (String zipFilePath : downloadedFiles) {
                        try {
                            // 处理ZIP文件
                            processDownloadedZipFile(projectId, zipFilePath, null, traceId, null);
                        } catch (Exception e) {
                            logger.error("处理下载的ZIP文件失败: file={}, traceId={}", zipFilePath, traceId, e);
                        }
                    }
                }

                logger.info("采购文件异步处理完成: traceId={}", traceId);

            } catch (Exception e) {
                logger.error("异步处理采购文件下载失败: traceId={}", traceId, e);
            }
        });
    }

    @Override
    public void processResponseFilesAsync(String projectId, String packageId, String supplierId,
                                        List<String> fileUrls, String fileType, String jsonFileId,
                                        String unitId, String traceId) {
        // 异步处理应答文件下载
        CompletableFuture.runAsync(() -> {
            try {
                logger.info("开始异步处理应答文件下载: traceId={}, projectId={}, supplierId={}, fileCount={}",
                           traceId, projectId, supplierId, fileUrls.size());

                // 确保项目存在
                createOrUpdateProject(projectId, null);
                // 确保供应商存在，名称暂用supplierId
                if (supplierId != null && !supplierId.isEmpty()) {
                    ensureSupplierExists(supplierId, supplierId);
                }

                // 替换下载地址
                List<String> processedFileUrls = new ArrayList<>();
                if (fileUrls != null) {
                    for (String url : fileUrls) {
                        processedFileUrls.add(replaceDownloadUrl(url));
                    }
                }
                
                // 映射fileType到数据库ID
                Long dbFileTypeId = null;
                if ("1".equals(fileType)) {
                    dbFileTypeId = 6L; // 商务相关的文件
                } else if ("2".equals(fileType)) {
                    dbFileTypeId = 5L; // 技术方案、技术规范等相关文件
                }

                // 下载文件并处理
                List<String> downloadedFiles = downloadFilesFromUrls(processedFileUrls, projectId, supplierId, traceId);

                if (!downloadedFiles.isEmpty()) {
                    // 处理下载的文件（解压、保存到数据库、向量化等）
                    for (String zipFilePath : downloadedFiles) {
                        try {
                            // 处理ZIP文件
                            processDownloadedZipFile(projectId, zipFilePath, supplierId, traceId, dbFileTypeId);
                        } catch (Exception e) {
                            logger.error("处理下载的ZIP文件失败: file={}, traceId={}", zipFilePath, traceId, e);
                        }
                    }
                }

                logger.info("应答文件异步处理完成: traceId={}", traceId);

            } catch (Exception e) {
                logger.error("异步处理应答文件下载失败: traceId={}", traceId, e);
            }
        });
    }

    /**
     * 从URL列表下载文件
     */
    private List<String> downloadFilesFromUrls(List<String> fileUrls, String projectId, String supplierId, String traceId) {
        List<String> downloadedFiles = new ArrayList<>();

        for (String url : fileUrls) {
            try {
                logger.info("开始下载文件: url={}, traceId={}", url, traceId);

                // 生成下载文件名
                String fileName = generateFileNameFromUrl(url);
                Path baseUploadPath = getUploadPath(projectId);
                if (supplierId != null && !supplierId.isEmpty()) {
                    baseUploadPath = baseUploadPath.resolve("supplier").resolve(supplierId);
                }
                Files.createDirectories(baseUploadPath);
                Path downloadPath = baseUploadPath.resolve(fileName);

                // 下载文件
                downloadFileFromUrl(url, downloadPath.toString());

                downloadedFiles.add(downloadPath.toString());
                logger.info("文件下载完成: url={}, localPath={}, traceId={}", url, downloadPath, traceId);

            } catch (Exception e) {
                logger.error("下载文件失败: url={}, traceId={}", url, traceId, e);
            }
        }

        return downloadedFiles;
    }

    /**
     * 确保供应商存在；若不存在则以supplierId作为名称创建
     */
    private void ensureSupplierExists(String supplierId, String supplierName) {
        if (supplierId == null || supplierId.isEmpty()) {
            return;
        }
        try {
            Supplier existing = supplierRepository.findBySupplierId(supplierId);
            if (existing == null) {
                Supplier newSupplier = new Supplier();
                newSupplier.setSupplierId(supplierId);
                newSupplier.setSupplierName(
                        supplierName != null && !supplierName.isEmpty() ? supplierName : supplierId);
                newSupplier.setStatus("active");
                supplierRepository.save(newSupplier);
                logger.info("自动创建供应商: {}", supplierId);
            }
        } catch (Exception e) {
            logger.warn("自动创建供应商失败: supplierId={}, error={}", supplierId, e.getMessage());
        }
    }

    /**
     * 处理下载的ZIP文件
     */
    private void processDownloadedZipFile(String projectId, String zipFilePath, String supplierId, String traceId, Long explicitFileTypeId) throws IOException {
        logger.info("开始处理下载的ZIP文件: projectId={}, supplierId={}, zipFile={}, traceId={}, fileTypeId={}", 
                projectId, supplierId, zipFilePath, traceId, explicitFileTypeId);

        Path zipPath = Paths.get(zipFilePath);
        if (!Files.exists(zipPath)) {
            throw new FileNotFoundException("ZIP文件不存在: " + zipFilePath);
        }

        // 获取解压路径
        Path extractPath = getExtractPath(projectId);
        if (supplierId != null && !supplierId.isEmpty()) {
            extractPath = extractPath.resolve("supplier").resolve(supplierId);
        }
        Files.createDirectories(extractPath);

        // 解压ZIP文件
        List<String> extractedFilePaths = extractZipFile(zipPath, extractPath);

        if (extractedFilePaths.isEmpty()) {
            logger.warn("ZIP文件解压后没有找到有效文件: {}", zipFilePath);
            return;
        }

        // 保存ZIP文件路径信息（可选）
        String savedZipPath = zipPath.toString();

        // 异步处理解压后的文件
        asyncProcessUploadedFiles(projectId, savedZipPath, extractedFilePaths, extractedFilePaths, supplierId, explicitFileTypeId);

        logger.info("ZIP文件处理完成: projectId={}, zipFile={}, extractedFiles={}, traceId={}",
                   projectId, zipFilePath, extractedFilePaths.size(), traceId);
    }

    /**
     * 从URL生成文件名
     */
    private String generateFileNameFromUrl(String url) {
        try {
            String fileName = url.substring(url.lastIndexOf('/') + 1);
            if (fileName.contains("?")) {
                fileName = fileName.substring(0, fileName.indexOf("?"));
            }
            if (fileName.isEmpty()) {
                fileName = "downloaded_file_" + System.currentTimeMillis() + ".zip";
            }
            return fileName;
        } catch (Exception e) {
            return "downloaded_file_" + System.currentTimeMillis() + ".zip";
        }
    }

    /**
     * 替换下载URL中的Host
     */
    private String replaceDownloadUrl(String url) {
        if (url == null || downloadReplaceSource == null || downloadReplaceSource.isEmpty() ||
            downloadReplaceTarget == null || downloadReplaceTarget.isEmpty()) {
            return url;
        }
        
        if (url.contains(downloadReplaceSource)) {
            String newUrl = url.replace(downloadReplaceSource, downloadReplaceTarget);
            logger.debug("替换下载地址: {} -> {}", url, newUrl);
            return newUrl;
        }
        
        return url;
    }

    /**
     * 从URL下载文件
     */
    private void downloadFileFromUrl(String url, String localPath) throws IOException {
        logger.info("正在下载文件: {} -> {}", url, localPath);

        try {
            // 使用HttpClientUtils下载文件
            byte[] fileData = HttpClientUtils.downloadFile(url);

            // 写入本地文件
            java.io.File file = new java.io.File(localPath);
            file.getParentFile().mkdirs();

            try (FileOutputStream fos = new FileOutputStream(file)) {
                fos.write(fileData);
            }

            logger.info("文件下载成功: url={}, localPath={}, size={}", url, localPath, fileData.length);

        } catch (Exception e) {
            logger.error("文件下载失败: url={}", url, e);
            throw new IOException("文件下载失败: " + url, e);
        }
    }

    /**
     * 检查文件扩展名是否为内容文件类型（PDF、Word、PPT、TXT、JSON）
     */
    private boolean isContentFileType(String extension) {
        if (extension == null || extension.isEmpty()) {
            return false;
        }
        String ext = extension.toLowerCase();
        return ext.equals("pdf") || ext.equals("doc") || ext.equals("docx") 
                || ext.equals("ppt") || ext.equals("pptx") 
                || ext.equals("txt") || ext.equals("json");
    }

    /**
     * 获取项目的上传路径
     */
    @Override
    public Path getUploadPath(String projectId) {
        return Paths.get(absoluteUploadPath, projectId);
    }

    /**
     * 获取项目的解压路径
     */
    @Override
    public Path getExtractPath(String projectId) {
        return Paths.get(absoluteExtractPath, projectId);
    }

    /**
     * 获取文件扩展名
     */
    private String getFileExtension(String fileName) {
        if (fileName == null || fileName.isEmpty()) {
            return "";
        }
        int lastDot = fileName.lastIndexOf('.');
        return lastDot > 0 ? fileName.substring(lastDot + 1).toLowerCase() : "";
    }

    /**
     * 列出ZIP文件内容（用于调试）
     */
    private void listZipContents(Path zipFilePath) {
        try (java.util.zip.ZipFile zipFile = new java.util.zip.ZipFile(zipFilePath.toFile())) {
            java.util.Enumeration<? extends java.util.zip.ZipEntry> entries = zipFile.entries();
            logger.info("ZIP文件内容列表:");
            while (entries.hasMoreElements()) {
                java.util.zip.ZipEntry entry = entries.nextElement();
                logger.info("  - {} (大小: {} 字节)", entry.getName(), entry.getSize());
            }
        } catch (Exception e) {
            logger.warn("列出ZIP内容失败: {}", e.getMessage());
        }
    }

    /**
     * 异步处理文件目录抽取
     */
    @Async
    public void processFileDirectoriesAsync(List<String> filePaths, String projectId) {
        long startTime = System.currentTimeMillis();
        logger.info("[目录摘取] ========== 开始异步处理文件目录抽取 ==========");
        logger.info("[目录摘取] 项目ID: {}, 文件数量: {}", projectId, filePaths != null ? filePaths.size() : 0);
        
        if (filePaths == null || filePaths.isEmpty()) {
            logger.warn("[目录摘取] 文件路径列表为空，跳过处理");
            return;
        }
        
        List<CompletableFuture<Void>> futures = new ArrayList<>();
        for (String filePath : filePaths) {
            futures.add(CompletableFuture.runAsync(() -> processDirectoryTask(filePath, projectId), directoryExecutor));
        }

        for (CompletableFuture<Void> future : futures) {
            try {
                future.join();
            } catch (Exception e) {
                logger.error("[目录摘取] 并发处理文件目录失败: {}", e.getMessage());
            }
        }

        long duration = System.currentTimeMillis() - startTime;
        logger.info("[目录摘取] ========== 完成! 耗时: {}ms，项目ID: {} ==========", duration, projectId);
    }

    /**
     * 处理单个文件的目录抽取任务
     */
    private void processDirectoryTask(String filePath, String projectId) {
        logger.info("[目录摘取] 开始处理文件: {}", filePath);
        try {
            Path path = Paths.get(filePath);
            String fileName = path.getFileName().toString();
            if (fileName.startsWith(".")) {
                logger.info("[目录摘取] 跳过临时/隐藏文件: {}", fileName);
                return;
            }
            
            transactionTemplate.execute(status -> {
                try {
                    Path absolutePath = path.toAbsolutePath().normalize();
                    String storedPath = filePathResolver.normalizeForStorage(absolutePath);
                    java.util.Optional<File> fileOpt = fileRepository.findByFilePath(storedPath);
                    
                    if (fileOpt.isPresent()) {
                        File file = fileOpt.get();
                        file.setCatalogExtractionStatus("processing");
                        file.setCatalogExtractionRetryCount(file.getCatalogExtractionRetryCount() != null ? file.getCatalogExtractionRetryCount() + 1 : 1);
                        fileRepository.save(file);

                        // 执行实际的目录抽取
                        com.alibaba.fastjson2.JSONArray directoryStructure = null;
                        String fileExtension = file.getFileExtension();
                        if ("pdf".equalsIgnoreCase(fileExtension)) {
                            directoryStructure = extractDirectoryFromPDF(filePath, file.getFileId(), file.getFileName());
                        }

                        if (directoryStructure != null && !directoryStructure.isEmpty()) {
                            // 先删除旧的目录数据
                            fileDirectoryRepository.deleteByFileId(file.getFileId());
                            // 保存新的目录数据
                            saveDirectoryItems(file.getFileId(), directoryStructure, null, 1);
                            file.setCatalogExtractionStatus("completed");
                            file.setCatalogExtractionTime(LocalDateTime.now());
                            fileRepository.save(file);
                            logger.info("[目录摘取] 文件目录抽取成功: fileId={}, fileName={}, 目录项数={}", 
                                    file.getFileId(), file.getFileName(), directoryStructure.size());
                        } else {
                            // 尝试生成基本目录结构
                            com.alibaba.fastjson2.JSONArray basicDirectory = generateBasicDirectoryStructure(filePath, file.getFileId(), file.getFileName());
                            if (basicDirectory != null && !basicDirectory.isEmpty()) {
                                fileDirectoryRepository.deleteByFileId(file.getFileId());
                                saveDirectoryItems(file.getFileId(), basicDirectory, null, 1);
                                file.setCatalogExtractionStatus("completed");
                                file.setCatalogExtractionTime(LocalDateTime.now());
                                fileRepository.save(file);
                                logger.info("[目录摘取] 使用基本目录结构: fileId={}, fileName={}", file.getFileId(), file.getFileName());
                            } else {
                                file.setCatalogExtractionStatus("failed");
                                file.setDescription("目录抽取失败：未提取到有效目录");
                                fileRepository.save(file);
                                logger.warn("[目录摘取] 文件目录抽取失败: fileId={}, fileName={}", file.getFileId(), file.getFileName());
                            }
                        }
                    } else {
                        logger.warn("[目录摘取] 未找到文件记录，跳过目录抽取: {}", filePath);
                    }
                    return null;
                } catch (Exception e) {
                    logger.error("[目录摘取] 事务内处理失败: {}", filePath, e);
                    status.setRollbackOnly();
                    throw e;
                }
            });
        } catch (Exception e) {
            logger.error("[目录摘取] 处理文件目录失败: {}", filePath, e);
        }
    }

    /**
     * 从文件路径提取文件类型ID
     */
    private Long extractFileTypeIdFromPath(String filePath) {
        if (filePath == null) {
            return null;
        }
        String lowerPath = filePath.toLowerCase();
        FileType fileType = null;
        if (lowerPath.contains("技术文件") || lowerPath.contains("技术标") || lowerPath.contains("技术方案")) {
            fileType = fileTypeService.createOrGetFileType("技术文件", "技术类投标文件");
        } else if (lowerPath.contains("商务文件") || lowerPath.contains("商务标")) {
            fileType = fileTypeService.createOrGetFileType("商务文件", "商务类投标文件");
        } else if (lowerPath.contains("报价文件") || lowerPath.contains("报价标") || lowerPath.contains("价格")) {
            fileType = fileTypeService.createOrGetFileType("报价文件", "报价类投标文件");
        }
        return fileType != null ? fileType.getId() : null;
    }

    /**
     * 从文件路径提取供应商ID
     */
    private String extractSupplierIdFromPath(String filePath) {
        if (filePath == null) {
            return null;
        }
        // 尝试从路径中提取供应商名称
        // 典型路径格式: /uploads/projectId/供应商名称/技术文件/xxx.pdf
        try {
            Path path = Paths.get(filePath);
            int count = path.getNameCount();
            // 查找可能的供应商目录
            for (int i = 0; i < count - 1; i++) {
                String segment = path.getName(i).toString();
                // 排除常见的非供应商目录名
                if (!segment.equals("uploads") && !segment.equals("extracts") && 
                    !segment.contains("技术文件") && !segment.contains("商务文件") && 
                    !segment.contains("报价文件") && !segment.endsWith(".pdf") &&
                    !segment.endsWith(".doc") && !segment.endsWith(".docx") &&
                    segment.length() > 4 && !segment.matches("^\\d+_.*")) {
                    // 可能是供应商名称
                    Supplier existing = supplierRepository.findBySupplierId(segment);
                    if (existing != null) {
                        return segment;
                    }
                    // 尝试按名称查找
                    existing = supplierRepository.findBySupplierName(segment);
                    if (existing != null) {
                        return existing.getSupplierId();
                    }
                }
            }
        } catch (Exception e) {
            logger.debug("从路径提取供应商ID失败: {}", e.getMessage());
        }
        return null;
    }

    /**
     * 根据项目ID获取项目文件信息
     */
    @Override
    public com.zhpb.gdyd_zhpb_zgf.dto.ProjectFilesResponse.ProjectFilesData getProjectFiles(String projectId) {
        com.zhpb.gdyd_zhpb_zgf.dto.ProjectFilesResponse.ProjectFilesData data = 
            new com.zhpb.gdyd_zhpb_zgf.dto.ProjectFilesResponse.ProjectFilesData();
        data.setProjectId(projectId);
        
        List<File> files = fileRepository.findByProjectId(projectId);
        data.setTotalFiles(files.size());
        
        // 转换为DTO
        List<com.zhpb.gdyd_zhpb_zgf.dto.ProjectFilesResponse.FileInfo> fileInfoList = new ArrayList<>();
        for (File file : files) {
            com.zhpb.gdyd_zhpb_zgf.dto.ProjectFilesResponse.FileInfo info = 
                new com.zhpb.gdyd_zhpb_zgf.dto.ProjectFilesResponse.FileInfo();
            info.setFileId(file.getFileId());
            info.setFileName(file.getFileName());
            info.setFileSize(file.getFileSize());
            info.setFileType(file.getFileExtension());
            info.setCompanyName(file.getSupplierId());
            fileInfoList.add(info);
        }
        data.setFiles(fileInfoList);
        
        return data;
    }

    /**
     * 从PDF中提取目录结构
     */
    private com.alibaba.fastjson2.JSONArray extractDirectoryFromPDF(String filePath, String fileId, String fileName) {
        try {
            logger.info("[目录摘取] 开始处理PDF文件: {}", fileName);
            
            int totalPages = 1;
            java.util.List<com.alibaba.fastjson2.JSONObject> chapters = new java.util.ArrayList<>();
            
            try (org.apache.pdfbox.pdmodel.PDDocument document = org.apache.pdfbox.Loader.loadPDF(new java.io.File(filePath))) {
                totalPages = document.getNumberOfPages();
                logger.info("[目录摘取] PDF总页数: {}", totalPages);
                
                // 尝试从PDF大纲中提取目录
                org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDDocumentOutline outline = 
                    document.getDocumentCatalog().getDocumentOutline();
                if (outline != null && outline.getFirstChild() != null) {
                    logger.info("[目录摘取] 发现PDF大纲，开始提取...");
                    extractOutlineItems(outline.getFirstChild(), "", chapters, 1);
                    logger.info("[目录摘取] 从大纲提取到 {} 个章节", chapters.size());
                }
            }
            
            if (!chapters.isEmpty()) {
                // 构建层级结构
                com.alibaba.fastjson2.JSONArray hierarchicalArray = buildHierarchicalStructure(chapters);
                logger.info("[目录摘取] 成功构建层级目录结构，根节点数: {}", hierarchicalArray.size());
                return hierarchicalArray;
            }
            
            // 回退方案：使用文件名作为单个章节
            String title = fileName;
            if (title != null && title.toLowerCase().endsWith(".pdf")) {
                title = title.substring(0, title.length() - 4);
            }
            if (title == null || title.isBlank()) {
                title = "文档内容";
            }
            
            com.alibaba.fastjson2.JSONArray directoryArray = new com.alibaba.fastjson2.JSONArray();
            com.alibaba.fastjson2.JSONObject chapter = new com.alibaba.fastjson2.JSONObject();
            chapter.put("chapterId", "chap-0");
            chapter.put("title", title);
            chapter.put("level", 1);
            chapter.put("page", "1");
            chapter.put("startPage", 1);
            chapter.put("endPage", totalPages);
            chapter.put("children", new com.alibaba.fastjson2.JSONArray());
            directoryArray.add(chapter);
            
            logger.info("[目录摘取] 使用文件名作为默认章节: '{}', 页数范围: 1-{}", title, totalPages);
            return directoryArray;
            
        } catch (Exception e) {
            logger.error("[目录摘取] 处理PDF失败: {}", e.getMessage(), e);
            return null;
        }
    }

    /**
     * 保存目录项到数据库
     */
    private void saveDirectoryItems(String fileId, com.alibaba.fastjson2.JSONArray items, String parentChapterId, int sortStart) {
        if (items == null || items.isEmpty()) {
            return;
        }
        
        int sortOrder = sortStart;
        for (int i = 0; i < items.size(); i++) {
            com.alibaba.fastjson2.JSONObject item = items.getJSONObject(i);
            try {
                FileDirectory directory = new FileDirectory();
                directory.setFileId(fileId);
                directory.setChapterId(item.getString("chapterId"));
                directory.setTitle(item.getString("title"));
                directory.setPage(item.getString("page"));
                directory.setStartPage(item.getInteger("startPage"));
                directory.setEndPage(item.getInteger("endPage"));
                directory.setParentChapterId(parentChapterId);
                directory.setLevel(item.getIntValue("level", 1));
                directory.setSortOrder(sortOrder++);
                directory.setSummary(item.getString("summary"));
                
                fileDirectoryRepository.save(directory);
                
                // 递归保存子章节
                com.alibaba.fastjson2.JSONArray children = item.getJSONArray("children");
                if (children != null && !children.isEmpty()) {
                    saveDirectoryItems(fileId, children, item.getString("chapterId"), 1);
                }
                
            } catch (Exception e) {
                logger.error("[目录摘取] 保存目录项失败: {}", e.getMessage(), e);
            }
        }
    }
}
