Browse Source

档案-四性检测
1-8 信息包一致性检测
3-5 信息包中包含的内容数据合规性检测

LHB 5 days ago
parent
commit
7234ceccff

+ 31 - 9
blade-service/blade-archive/src/main/java/org/springblade/archive/service/impl/ArchiveExaminingReportImpl.java

@@ -20,10 +20,7 @@ import org.springblade.archive.mapper.ArchiveExaminingReportMapper;
 import org.springblade.archive.service.IArchiveExaminingReportDetailService;
 import org.springblade.archive.service.IArchiveExaminingReportService;
 import org.springblade.archive.socket.WebSocketServer;
-import org.springblade.archive.utils.ClamAVClientScanner;
-import org.springblade.archive.utils.FileUtils;
-import org.springblade.archive.utils.RemoteFileExtension;
-import org.springblade.archive.utils.RemoteFileMD5Calculator;
+import org.springblade.archive.utils.*;
 import org.springblade.archive.vo.ArchiveExaminingSocketVo;
 import org.springblade.archive.vo.ArchiveExaminingVo;
 import org.springblade.archive.vo.ArchivesAutoVO;
@@ -167,6 +164,8 @@ public class ArchiveExaminingReportImpl extends BaseServiceImpl<ArchiveExamining
         int metadataCompliance = 0;
         //1-6 对元数据项数据重复性检测
         int metadataRepeat = 0;
+        //1-8 信息包一致性检测
+        int infoConsistency = 0;
         //2-3 对元数据项完整性检测
         int metadataComplete = 0;
         //2-4 对元数据项必填项
@@ -176,6 +175,8 @@ public class ArchiveExaminingReportImpl extends BaseServiceImpl<ArchiveExamining
         int fileType = 0;
         //3-2 文件是否能够访问
         int fileIsAccess = 0;
+        //3-5 信息包加密
+        int infoEncryption = 0;
         //4-1 病毒检测
         int virusDetection = 0;
         //4-3 病毒安装
@@ -211,7 +212,10 @@ public class ArchiveExaminingReportImpl extends BaseServiceImpl<ArchiveExamining
                 //获取文件元数据项
                 List<HashMap<String, Object>> list = metadataClassificationClient.getMetadaFileByFileId(file.getId());
                 //优先检查文件是否可读
-                if(StringUtils.isEmpty(file.getPdfFileUrl()) || !validateWithHead(file.getPdfFileUrl())){
+                if(StringUtils.isEmpty(file.getPdfFileUrl())
+                        || !validateWithHead(file.getPdfFileUrl())
+                        || StringUtils.isEmpty(file.getFileUrl())
+                        || !validateWithHead(file.getFileUrl())){
                     Map<String, String> map = new HashMap<>();
                     map.put("examiningItem", ArchiveConstant.ARCHIVE_EXAMINING_STANDARD + "对电子档案内容数据的可读性检测");
                     map.put("unqualifiedObject", file.getFileName());
@@ -289,7 +293,13 @@ public class ArchiveExaminingReportImpl extends BaseServiceImpl<ArchiveExamining
                     }
                     if(vo.getAuthenticityList().contains("7")){}
                     if(vo.getAuthenticityList().contains("8")){
-                        //TODO
+                        if(!file.getFileMd5().equals(fileMd5) || !file.getPdfMd5().equals(pdfMd5)){
+                            Map<String, String> map = new HashMap<>();
+                            map.put("examiningItem", ArchiveConstant.ARCHIVE_EXAMINING_STANDARD + "对固化信息有效性检测");
+                            map.put("unqualifiedObject", file.getFileName());
+                            mapList.add(map);
+                            infoConsistency++;
+                        }
                     }
                     if(vo.getAuthenticityList().contains("9")){}
                     if(vo.getAuthenticityList().contains("10")){}
@@ -383,7 +393,19 @@ public class ArchiveExaminingReportImpl extends BaseServiceImpl<ArchiveExamining
 
                     }
                     if(vo.getUsabilityList().contains("5")){
-                        //TODO
+                        //文件加密压缩认证
+                        FileAnalysisResult fileAnalysisResult = null;
+                        try {
+                            fileAnalysisResult = RemoteFileSecurityChecker.analyzeRemoteFile(file.getFileUrl());
+                        } catch (IOException e) {}
+                        //是否使用公共压缩算法 是否加密
+                        if(fileAnalysisResult == null){
+                            Map<String, String> map = new HashMap<>();
+                            map.put("examiningItem", ArchiveConstant.ARCHIVE_EXAMINING_STANDARD + "对元数据项数据重复性检测");
+                            map.put("unqualifiedObject", file.getFileName());
+                            mapList.add(map);
+                            infoEncryption++;
+                        }
                     }
                     if(vo.getUsabilityList().contains("6")){}
                     report.setReportDetailStatus(3);
@@ -436,7 +458,7 @@ public class ArchiveExaminingReportImpl extends BaseServiceImpl<ArchiveExamining
             }
             if(vo.getUsabilityList().contains("5")){
                 detailService.save(new ArchiveExaminingReportDetail(vo.getProjectId(), id, ArchiveConstant.ARCHIVE_EXAMINING_USABILITY,
-                        ArchiveConstant.ARCHIVE_EXAMINING_STANDARD + "对信息包中包含的内容数据合规性检测", 0, "无", 0));
+                        ArchiveConstant.ARCHIVE_EXAMINING_STANDARD + "对信息包中包含的内容数据合规性检测", infoEncryption, infoEncryption == 0 ? "无" : "详见附件", infoEncryption == 0 ? 0 : 1));
             }
         }
         //真实性
@@ -459,7 +481,7 @@ public class ArchiveExaminingReportImpl extends BaseServiceImpl<ArchiveExamining
             }
             if(vo.getAuthenticityList().contains("8")){
                 detailService.save(new ArchiveExaminingReportDetail(vo.getProjectId(), id, ArchiveConstant.ARCHIVE_EXAMINING_AUTHENTICITY,
-                        ArchiveConstant.ARCHIVE_EXAMINING_STANDARD + "对信息包一致性检测", 0, "无", 0));
+                        ArchiveConstant.ARCHIVE_EXAMINING_STANDARD + "对信息包一致性检测", infoConsistency, infoConsistency == 0 ? "无" : "详见附件", infoConsistency == 0 ? 0 : 1));
             }
         }
         //完整信

+ 97 - 0
blade-service/blade-archive/src/main/java/org/springblade/archive/utils/FileAnalysisResult.java

@@ -0,0 +1,97 @@
+package org.springblade.archive.utils;
+
+
+import lombok.Data;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+@Data
+public class FileAnalysisResult {
+    private String filePath;
+    private String remoteUrl;
+    private String format;
+    private String compressionAlgorithm;
+    private Boolean standardCompression;
+    private Set<String> nonStandardAlgorithms = new HashSet<>();
+    private Boolean encrypted;
+    private String encryptionType;
+    private List<String> issues = new ArrayList<>();
+    private long fileSize;
+    private String downloadStatus;
+
+    public FileAnalysisResult(String filePath) {
+        this.filePath = filePath;
+    }
+
+    // Getter和Setter方法
+    public void setRemoteUrl(String remoteUrl) { this.remoteUrl = remoteUrl; }
+    public void setFormat(String format) { this.format = format; }
+    public void setCompressionAlgorithm(String algorithm) { this.compressionAlgorithm = algorithm; }
+    public void setStandardCompression(boolean standard) { this.standardCompression = standard; }
+    public void setNonStandardAlgorithms(Set<String> algorithms) { this.nonStandardAlgorithms = algorithms; }
+    public void setEncrypted(boolean encrypted) { this.encrypted = encrypted; }
+    public void setEncryptionType(String encryptionType) { this.encryptionType = encryptionType; }
+    public void setFileSize(long fileSize) { this.fileSize = fileSize; }
+    public void setDownloadStatus(String status) { this.downloadStatus = status; }
+    public void addIssue(String issue) { this.issues.add(issue); }
+
+    public void printReport() {
+        System.out.println("=== 远程文件安全分析报告 ===");
+        if (remoteUrl != null) {
+            System.out.println("远程URL: " + remoteUrl);
+        }
+        System.out.println("本地临时文件: " + filePath);
+
+        if (downloadStatus != null) {
+            System.out.println("下载状态: " + downloadStatus);
+        }
+
+        if (fileSize > 0) {
+            System.out.println("文件大小: " + (fileSize / 1024) + " KB");
+        }
+
+        if (format != null) {
+            System.out.println("格式: " + format);
+            System.out.println("压缩算法: " + compressionAlgorithm);
+            System.out.println("使用标准压缩: " + (standardCompression ? "是" : "否"));
+
+            if (!standardCompression && !nonStandardAlgorithms.isEmpty()) {
+                System.out.println("非标准压缩算法:");
+                for (String algo : nonStandardAlgorithms) {
+                    System.out.println("  - " + algo);
+                }
+            }
+
+            System.out.println("是否加密: " + (encrypted ? "是" : "否"));
+            if (encrypted && encryptionType != null) {
+                System.out.println("加密类型: " + encryptionType);
+            }
+        }
+
+        if (!issues.isEmpty()) {
+            System.out.println("问题详情:");
+            for (String issue : issues) {
+                System.out.println("  ⚠️ " + issue);
+            }
+        }
+
+        System.out.println("\n安全评估: " + getSecurityAssessment());
+        System.out.println("=================================\n");
+    }
+
+    public String getSecurityAssessment() {
+        if (!standardCompression) {
+            return "❌ 高风险 - 发现非标准压缩算法";
+        }
+        if (encrypted) {
+            return "⚠️ 中风险 - 文件已加密";
+        }
+        if (!issues.isEmpty()) {
+            return "⚠️ 注意 - 存在潜在安全问题";
+        }
+        return "✅ 安全 - 文件使用标准压缩且未加密";
+    }
+}

+ 352 - 0
blade-service/blade-archive/src/main/java/org/springblade/archive/utils/FileFormatSecurityChecker.java

@@ -0,0 +1,352 @@
+package org.springblade.archive.utils;
+import java.io.*;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.*;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+public class FileFormatSecurityChecker {
+
+    // PDF相关常量
+    private static final byte[] PDF_HEADER = "%PDF".getBytes();
+    private static final String[] PDF_STANDARD_FILTERS = {
+            "FlateDecode", "LZWDecode", "ASCII85Decode", "ASCIIHexDecode",
+            "RunLengthDecode", "CCITTFaxDecode", "DCTDecode", "JBIG2Decode"
+    };
+
+    public static FileAnalysisResult analyzeFile(String filePath) throws IOException {
+        File file = new File(filePath);
+        if (!file.exists()) {
+            throw new FileNotFoundException("文件不存在: " + filePath);
+        }
+
+        String filename = file.getName().toLowerCase();
+        FileAnalysisResult result = new FileAnalysisResult(filePath);
+
+        if (filename.endsWith(".pdf")) {
+            return analyzePdf(file, result);
+        } else if (filename.endsWith(".xlsx")) {
+            return analyzeXlsx(file, result);
+        } else if (filename.endsWith(".png")) {
+            return analyzePng(file, result);
+        } else if (filename.endsWith(".jpg") || filename.endsWith(".jpeg")) {
+            return analyzeJpeg(file, result);
+        } else {
+            result.addIssue("不支持的文件格式");
+            return result;
+        }
+    }
+
+    // PDF文件分析
+    private static FileAnalysisResult analyzePdf(File file, FileAnalysisResult result) throws IOException {
+        result.setFormat("PDF");
+
+        byte[] fileContent = Files.readAllBytes(file.toPath());
+        String content = new String(fileContent, "ISO-8859-1");
+
+        // 1. 检查加密
+        boolean isEncrypted = checkPdfEncryption(content);
+        result.setEncrypted(isEncrypted);
+
+        // 2. 检查压缩算法
+        CompressionAnalysis compressionAnalysis = checkPdfCompression(content);
+        result.setCompressionAlgorithm(compressionAnalysis.getAlgorithm());
+        result.setStandardCompression(compressionAnalysis.isStandard());
+        result.setNonStandardAlgorithms(compressionAnalysis.getNonStandardAlgorithms());
+
+        // 3. 检查其他安全问题
+        checkPdfSecurity(content, result);
+
+        return result;
+    }
+
+    private static boolean checkPdfEncryption(String pdfContent) {
+        // 检查加密字典
+        return pdfContent.contains("/Encrypt") ||
+                pdfContent.contains("/Filter/Standard") ||
+                pdfContent.contains("/CFM/") ||
+                pdfContent.contains("/StmF/") ||
+                pdfContent.contains("/StrF/");
+    }
+
+    private static CompressionAnalysis checkPdfCompression(String pdfContent) {
+        CompressionAnalysis analysis = new CompressionAnalysis();
+        Set<String> foundAlgorithms = new HashSet<>();
+        Set<String> nonStandardAlgorithms = new HashSet<>();
+
+        // 查找所有使用的压缩过滤器
+        String[] lines = pdfContent.split("\n");
+        for (String line : lines) {
+            if (line.contains("/Filter")) {
+                // 提取过滤器名称
+                for (String filter : PDF_STANDARD_FILTERS) {
+                    if (line.contains(filter)) {
+                        foundAlgorithms.add(filter);
+                    }
+                }
+
+                // 检查非标准过滤器
+                if (line.contains("/Filter") && !containsStandardFilter(line)) {
+                    // 提取可能的非标准过滤器名称
+                    String[] parts = line.split("/");
+                    for (String part : parts) {
+                        if (part.trim().length() > 3 && !part.trim().equals("Filter")) {
+                            boolean isStandard = false;
+                            for (String stdFilter : PDF_STANDARD_FILTERS) {
+                                if (part.contains(stdFilter.replace("Decode", ""))) {
+                                    isStandard = true;
+                                    break;
+                                }
+                            }
+                            if (!isStandard) {
+                                nonStandardAlgorithms.add(part.trim());
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        // 设置结果
+        if (!foundAlgorithms.isEmpty()) {
+            analysis.setAlgorithm(String.join(", ", foundAlgorithms));
+        } else {
+            analysis.setAlgorithm("未压缩或未知压缩");
+        }
+
+        analysis.setStandard(nonStandardAlgorithms.isEmpty());
+        analysis.setNonStandardAlgorithms(nonStandardAlgorithms);
+
+        return analysis;
+    }
+
+    private static boolean containsStandardFilter(String line) {
+        for (String filter : PDF_STANDARD_FILTERS) {
+            if (line.contains(filter)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static void checkPdfSecurity(String pdfContent, FileAnalysisResult result) {
+        // 检查JavaScript(可能的安全风险)
+        if (pdfContent.contains("/JavaScript") || pdfContent.contains("/JS")) {
+            result.addIssue("包含JavaScript代码");
+        }
+
+        // 检查外部引用
+        if (pdfContent.contains("/Launch") || pdfContent.contains("/URI")) {
+            result.addIssue("包含外部引用或启动动作");
+        }
+    }
+
+    // XLSX文件分析
+    private static FileAnalysisResult analyzeXlsx(File file, FileAnalysisResult result) throws IOException {
+        result.setFormat("XLSX");
+
+        try (ZipFile zipFile = new ZipFile(file)) {
+            Enumeration<? extends ZipEntry> entries = zipFile.entries();
+
+            // 1. 检查加密
+            boolean isEncrypted = checkXlsxEncryption(zipFile);
+            result.setEncrypted(isEncrypted);
+
+            // 2. 检查压缩算法
+            CompressionAnalysis compressionAnalysis = checkXlsxCompression(zipFile);
+            result.setCompressionAlgorithm(compressionAnalysis.getAlgorithm());
+            result.setStandardCompression(compressionAnalysis.isStandard());
+            result.setNonStandardAlgorithms(compressionAnalysis.getNonStandardAlgorithms());
+
+            // 3. 检查其他安全问题
+            checkXlsxSecurity(zipFile, result);
+        }
+
+        return result;
+    }
+
+    private static boolean checkXlsxEncryption(ZipFile zipFile) {
+        return zipFile.getEntry("EncryptedPackage") != null ||
+                zipFile.getEntry("EncryptionInfo") != null ||
+                zipFile.getEntry("encryption") != null;
+    }
+
+    private static CompressionAnalysis checkXlsxCompression(ZipFile zipFile) {
+        CompressionAnalysis analysis = new CompressionAnalysis();
+        Set<String> nonStandardAlgorithms = new HashSet<>();
+
+        Enumeration<? extends ZipEntry> entries = zipFile.entries();
+        while (entries.hasMoreElements()) {
+            ZipEntry entry = entries.nextElement();
+            int method = entry.getMethod();
+
+            // ZIP标准压缩方法
+            if (method != ZipEntry.STORED && method != ZipEntry.DEFLATED) {
+                nonStandardAlgorithms.add("ZIP压缩方法: " + method);
+            }
+        }
+
+        analysis.setAlgorithm("DEFLATE (标准ZIP压缩)");
+        analysis.setStandard(nonStandardAlgorithms.isEmpty());
+        analysis.setNonStandardAlgorithms(nonStandardAlgorithms);
+
+        return analysis;
+    }
+
+    private static void checkXlsxSecurity(ZipFile zipFile, FileAnalysisResult result) {
+        // 检查宏
+        if (zipFile.getEntry("xl/vbaProject.bin") != null) {
+            result.addIssue("包含VBA宏");
+        }
+
+        // 检查外部链接
+        try {
+            ZipEntry externalLinks = zipFile.getEntry("xl/externalLinks/");
+            if (externalLinks != null) {
+                result.addIssue("包含外部链接");
+            }
+        } catch (Exception e) {
+            // 忽略
+        }
+    }
+
+    // PNG文件分析
+    private static FileAnalysisResult analyzePng(File file, FileAnalysisResult result) throws IOException {
+        result.setFormat("PNG");
+        result.setEncrypted(false); // PNG不支持加密
+
+        byte[] data = Files.readAllBytes(file.toPath());
+
+        // 1. 检查文件头
+        if (!isValidPngHeader(data)) {
+            result.addIssue("无效的PNG文件头");
+            return result;
+        }
+
+        // 2. 检查压缩算法
+        CompressionAnalysis compressionAnalysis = checkPngCompression(data);
+        result.setCompressionAlgorithm(compressionAnalysis.getAlgorithm());
+        result.setStandardCompression(compressionAnalysis.isStandard());
+        result.setNonStandardAlgorithms(compressionAnalysis.getNonStandardAlgorithms());
+
+        return result;
+    }
+
+    private static boolean isValidPngHeader(byte[] data) {
+        // PNG文件头: 89 50 4E 47 0D 0A 1A 0A
+        return data.length >= 8 &&
+                data[0] == (byte)0x89 &&
+                data[1] == 0x50 &&
+                data[2] == 0x4E &&
+                data[3] == 0x47 &&
+                data[4] == 0x0D &&
+                data[5] == 0x0A &&
+                data[6] == 0x1A &&
+                data[7] == 0x0A;
+    }
+
+    private static CompressionAnalysis checkPngCompression(byte[] data) {
+        CompressionAnalysis analysis = new CompressionAnalysis();
+
+        // 在IHDR块后查找压缩方法
+        // PNG压缩方法位于第12个字节(从0开始),但需要找到IDAT块
+        for (int i = 8; i < data.length - 4; i++) {
+            // 查找IDAT块
+            if (data[i] == 'I' && data[i+1] == 'D' && data[i+2] == 'A' && data[i+3] == 'T') {
+                // IDAT块中的压缩方法通常是DEFLATE
+                // PNG标准只允许使用DEFLATE压缩
+                analysis.setAlgorithm("DEFLATE");
+                analysis.setStandard(true);
+                return analysis;
+            }
+        }
+
+        // 如果找不到IDAT块,检查是否有非标准块
+        analysis.setAlgorithm("未知");
+        analysis.setStandard(false);
+        analysis.addNonStandardAlgorithm("无法识别标准压缩块");
+
+        return analysis;
+    }
+
+    // JPEG文件分析
+    private static FileAnalysisResult analyzeJpeg(File file, FileAnalysisResult result) throws IOException {
+        result.setFormat("JPEG");
+        result.setEncrypted(false); // JPEG不支持加密
+
+        byte[] data = Files.readAllBytes(file.toPath());
+
+        // 1. 检查文件头
+        if (!isValidJpegHeader(data)) {
+            result.addIssue("无效的JPEG文件头");
+            return result;
+        }
+
+        // 2. 检查压缩算法
+        CompressionAnalysis compressionAnalysis = checkJpegCompression(data);
+        result.setCompressionAlgorithm(compressionAnalysis.getAlgorithm());
+        result.setStandardCompression(compressionAnalysis.isStandard());
+        result.setNonStandardAlgorithms(compressionAnalysis.getNonStandardAlgorithms());
+
+        return result;
+    }
+
+    private static boolean isValidJpegHeader(byte[] data) {
+        // JPEG文件头: FF D8
+        return data.length >= 2 &&
+                data[0] == (byte)0xFF &&
+                data[1] == (byte)0xD8;
+    }
+
+    private static CompressionAnalysis checkJpegCompression(byte[] data) {
+        CompressionAnalysis analysis = new CompressionAnalysis();
+
+        // JPEG使用标准的DCT和Huffman编码
+        // 检查是否有非标准标记
+        Set<String> nonStandardAlgorithms = new HashSet<>();
+
+        for (int i = 0; i < data.length - 1; i++) {
+            if (data[i] == (byte)0xFF) {
+                int marker = data[i + 1] & 0xFF;
+
+                // 检查非标准或保留的标记
+                if (marker >= 0x02 && marker <= 0xBF && marker != 0x00) {
+                    // 这些是标准标记范围,但某些特定值可能表示非标准扩展
+                    if (marker == 0xFF || marker == 0x00) {
+                        // 无效标记
+                        nonStandardAlgorithms.add("无效JPEG标记: 0x" + Integer.toHexString(marker));
+                    }
+                }
+            }
+        }
+
+        analysis.setAlgorithm("DCT + Huffman (标准JPEG压缩)");
+        analysis.setStandard(nonStandardAlgorithms.isEmpty());
+        analysis.setNonStandardAlgorithms(nonStandardAlgorithms);
+
+        return analysis;
+    }
+}
+
+// 压缩分析结果类
+class CompressionAnalysis {
+    private String algorithm;
+    private boolean isStandard;
+    private Set<String> nonStandardAlgorithms = new HashSet<>();
+
+    // getter和setter方法
+    public String getAlgorithm() { return algorithm; }
+    public void setAlgorithm(String algorithm) { this.algorithm = algorithm; }
+
+    public boolean isStandard() { return isStandard; }
+    public void setStandard(boolean standard) { isStandard = standard; }
+
+    public Set<String> getNonStandardAlgorithms() { return nonStandardAlgorithms; }
+    public void setNonStandardAlgorithms(Set<String> nonStandardAlgorithms) {
+        this.nonStandardAlgorithms = nonStandardAlgorithms;
+    }
+    public void addNonStandardAlgorithm(String algorithm) {
+        this.nonStandardAlgorithms.add(algorithm);
+    }
+}

+ 114 - 0
blade-service/blade-archive/src/main/java/org/springblade/archive/utils/RemoteFileSecurityChecker.java

@@ -0,0 +1,114 @@
+package org.springblade.archive.utils;
+
+import java.io.*;
+import java.net.*;
+import java.nio.file.*;
+import java.util.*;
+import java.util.zip.ZipFile;
+
+public class RemoteFileSecurityChecker {
+
+    // 临时文件目录
+    private static final String TEMP_DIR = System.getProperty("java.io.tmpdir") + "/file-security-check/";
+
+    static {
+        // 创建临时目录
+        new File(TEMP_DIR).mkdirs();
+    }
+
+    /**
+     * 分析远程文件
+     */
+    public static FileAnalysisResult analyzeRemoteFile(String fileUrl) throws IOException {
+        System.out.println("开始分析远程文件: " + fileUrl);
+        File tempFile = null;
+        // 下载文件到临时位置
+        if(fileUrl.contains("http")){
+            tempFile = downloadRemoteFile(fileUrl);
+        }else{
+            tempFile = new File(fileUrl);
+        }
+
+        try {
+            // 使用原有的分析逻辑
+            FileAnalysisResult result = FileFormatSecurityChecker.analyzeFile(tempFile.getAbsolutePath());
+            result.setRemoteUrl(fileUrl);
+            return result;
+        } finally {
+            // 清理临时文件
+            if (tempFile.exists()) {
+                tempFile.delete();
+            }
+        }
+    }
+
+    /**
+     * 下载远程文件到临时位置
+     */
+    private static File downloadRemoteFile(String fileUrl) throws IOException {
+        URL url = new URL(fileUrl);
+        String filename = extractFilenameFromUrl(fileUrl);
+        File tempFile = new File(TEMP_DIR + System.currentTimeMillis() + "_" + filename);
+
+        System.out.println("下载文件到: " + tempFile.getAbsolutePath());
+
+        try (InputStream in = url.openStream();
+             FileOutputStream out = new FileOutputStream(tempFile)) {
+
+            byte[] buffer = new byte[8192];
+            int bytesRead;
+            long totalBytes = 0;
+
+            while ((bytesRead = in.read(buffer)) != -1) {
+                out.write(buffer, 0, bytesRead);
+                totalBytes += bytesRead;
+
+                // 文件大小限制检查(可选)
+                if (totalBytes > 1 * 1024 * 1024 * 1024) { // 1024MB限制
+                    throw new IOException("文件大小超过限制(1024MB)");
+                }
+            }
+        }
+
+        return tempFile;
+    }
+
+    /**
+     * 从URL中提取文件名
+     */
+    private static String extractFilenameFromUrl(String fileUrl) {
+        try {
+            URL url = new URL(fileUrl);
+            String path = url.getPath();
+            if (path.contains("/")) {
+                return path.substring(path.lastIndexOf("/") + 1);
+            }
+            return path;
+        } catch (MalformedURLException e) {
+            // 如果URL解析失败,尝试直接从字符串提取
+            String[] parts = fileUrl.split("/");
+            return parts[parts.length - 1];
+        }
+    }
+
+    /**
+     * 批量分析远程文件
+     */
+    public static List<FileAnalysisResult> analyzeMultipleRemoteFiles(List<String> fileUrls) {
+        List<FileAnalysisResult> results = new ArrayList<>();
+
+        for (String fileUrl : fileUrls) {
+            try {
+                FileAnalysisResult result = analyzeRemoteFile(fileUrl);
+                results.add(result);
+            } catch (Exception e) {
+                FileAnalysisResult errorResult = new FileAnalysisResult(fileUrl);
+                errorResult.addIssue("下载或分析失败: " + e.getMessage());
+                errorResult.setRemoteUrl(fileUrl);
+                results.add(errorResult);
+            }
+        }
+
+        return results;
+    }
+}