Kaynağa Gözat

feat:新增批量上传文件并解析内容的接口

wangzaijun 2 hafta önce
ebeveyn
işleme
f72a75862b

+ 1 - 1
.gitignore

@@ -32,5 +32,5 @@ build/
 ### VS Code ###
 .vscode/
 
-logs/
+./logs/
 !**/logs/

+ 0 - 0
logs/error.log


+ 14 - 0
logs/info.log

@@ -0,0 +1,14 @@
+2025-06-10 17:03:27.074 [main] - [] INFO  com.smppw.modaq.MoDaqApplication:53   - Starting MoDaqApplication using Java 17.0.5 with PID 12160 (D:\Documents\Wrokspace\codes\mo-daq-all\mo-daq\target\classes started by Administrator in D:\Documents\Wrokspace\codes\mo-daq-all)
+2025-06-10 17:03:27.076 [main] - [] INFO  com.smppw.modaq.MoDaqApplication:652  - No active profile set, falling back to 1 default profile: "default"
+2025-06-10 17:03:28.130 [main] - [] WARN  io.undertow.websockets.jsr:68   - UT026010: Buffer pool was not set on WebSocketDeploymentInfo, the default pool will be used
+2025-06-10 17:03:28.148 [main] - [] INFO  io.undertow.servlet:372  - Initializing Spring embedded WebApplicationContext
+2025-06-10 17:03:28.148 [main] - [] INFO  o.s.b.w.s.c.ServletWebServerApplicationContext:296  - Root WebApplicationContext: initialization completed in 1034 ms
+2025-06-10 17:03:29.387 [main] - [] INFO  io.undertow:120  - starting server: Undertow - 2.3.18.Final
+2025-06-10 17:03:29.395 [main] - [] INFO  org.xnio:95   - XNIO version 3.8.16.Final
+2025-06-10 17:03:29.405 [main] - [] INFO  org.xnio.nio:58   - XNIO NIO Implementation Version 3.8.16.Final
+2025-06-10 17:03:29.452 [main] - [] INFO  org.jboss.threads:55   - JBoss Threads version 3.5.0.Final
+2025-06-10 17:03:29.536 [main] - [] INFO  o.s.boot.web.embedded.undertow.UndertowWebServer:121  - Undertow started on port 9933 (http) with context path '/'
+2025-06-10 17:03:29.550 [main] - [] INFO  com.smppw.modaq.MoDaqApplication:59   - Started MoDaqApplication in 2.938 seconds (process running for 3.931)
+2025-06-10 17:03:39.000 [SpringApplicationShutdownHook] - [] INFO  o.s.boot.web.embedded.undertow.UndertowWebServer:325  - Commencing graceful shutdown. Waiting for active requests to complete
+2025-06-10 17:03:39.000 [SpringApplicationShutdownHook] - [] INFO  o.s.boot.web.embedded.undertow.UndertowWebServer:335  - Graceful shutdown complete
+2025-06-10 17:03:39.001 [SpringApplicationShutdownHook] - [] INFO  io.undertow:259  - stopping server: Undertow - 2.3.18.Final

+ 1 - 0
logs/warn.log

@@ -0,0 +1 @@
+2025-06-10 17:03:28.130 [main] - [] WARN  io.undertow.websockets.jsr:68   - UT026010: Buffer pool was not set on WebSocketDeploymentInfo, the default pool will be used

+ 18 - 6
mo-daq/src/main/java/com/smppw/modaq/application/api/ParseApi.java

@@ -7,6 +7,7 @@ import com.smppw.modaq.domain.dto.UploadReportParams;
 import com.smppw.modaq.domain.dto.UploadReportResult;
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
 
 import java.util.Date;
 import java.util.List;
@@ -33,14 +34,25 @@ public class ParseApi {
         return ResponseEntity.ok("success");
     }
 
-//    @GetMapping("reparse")
-//    public ResponseEntity<String> reparseReport(Integer emailId) {
-//        this.service.reparseEmail(emailId);
-//        return ResponseEntity.ok("success");
-//    }
-
+    /**
+     * 批量解析(文件路径)
+     *
+     * @param params 参数
+     * @return 解析结果
+     */
     @PostMapping("upload-parse")
     public ResponseEntity<List<UploadReportResult>> uploadReport(@RequestBody UploadReportParams params) {
         return ResponseEntity.ok(this.service.uploadReport(params));
     }
+
+    /**
+     * 批量上传文件流并解析
+     *
+     * @param files 文件流对象
+     * @return /
+     */
+    @PostMapping("upload-file-parse")
+    public ResponseEntity<List<UploadReportResult>> batchUploadReport(@RequestParam("files") MultipartFile[] files) {
+        return ResponseEntity.ok(this.service.batchUploadReport(files));
+    }
 }

+ 9 - 6
mo-daq/src/main/java/com/smppw/modaq/application/components/ReportParseUtils.java

@@ -570,12 +570,12 @@ public final class ReportParseUtils {
         emailType = EmailUtil.getEmailTypeBySubject(text);
         reportType = matchReportType(emailType, text);
         System.out.println(emailType + ",reportType=" + reportType + ",reportDate=" + matchReportDate(reportType, text));
-
-        text = "第一创业2025年合同变更公告.png";
-        emailType = EmailUtil.getEmailTypeBySubject(text);
-        reportType = matchReportType(emailType, text);
-        System.out.println(emailType + ",reportType=" + reportType + ",reportDate=" + matchReportDate(reportType, text));
-
+//
+//        text = "第一创业2025年合同变更公告.png";
+//        emailType = EmailUtil.getEmailTypeBySubject(text);
+//        reportType = matchReportType(emailType, text);
+//        System.out.println(emailType + ",reportType=" + reportType + ",reportDate=" + matchReportDate(reportType, text));
+//
         String date = "2025年6月6日";
         String input = ReportParseUtils.cleaningValue(date, false);
         Date date1 = DateUtils.toDate(input);
@@ -585,5 +585,8 @@ public final class ReportParseUtils {
         String s = "\"=?utf-8?b?5Y2D6LGh54G15rS76YWN572u57K+6YCJOeWPt+engeWLn+ivgeWIuOaKlei1hA==?=^M\n" +
                 " 基金-千象灵活配置精选9号私募证券投资基金2025年5月月度报告-20250609.pdf\"";
         System.out.println(MimeUtility.decodeText(s));
+
+        String ss = "量信量化 2 期私募证券投资基金";
+        System.out.println(StrUtil.removeAll(ss, " "));
     }
 }

+ 10 - 1
mo-daq/src/main/java/com/smppw/modaq/application/service/EmailParseApiService.java

@@ -3,6 +3,7 @@ package com.smppw.modaq.application.service;
 import com.smppw.modaq.domain.dto.MailboxInfoDTO;
 import com.smppw.modaq.domain.dto.UploadReportParams;
 import com.smppw.modaq.domain.dto.UploadReportResult;
+import org.springframework.web.multipart.MultipartFile;
 
 import java.util.Date;
 import java.util.List;
@@ -28,13 +29,21 @@ public interface EmailParseApiService {
     void parseEmail(MailboxInfoDTO mailboxInfoDTO, Date startDate, Date endDate, List<String> folderNames, List<Integer> emailTypes);
 
     /**
-     * 上传文件解析
+     * 批量文件解析(文件路径)
      *
      * @param params 上传参数
      * @return /
      */
     List<UploadReportResult> uploadReport(UploadReportParams params);
 
+    /**
+     * 批量上传文件解析
+     *
+     * @param files 文件流对象
+     * @return /
+     */
+    List<UploadReportResult> batchUploadReport(MultipartFile[] files);
+
 //    /**
 //     * 重新解析指定邮件
 //     *

+ 34 - 13
mo-daq/src/main/java/com/smppw/modaq/application/service/EmailParseApiServiceImpl.java

@@ -1,10 +1,13 @@
 package com.smppw.modaq.application.service;
 
 import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.collection.ListUtil;
 import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.exceptions.ExceptionUtil;
 import cn.hutool.core.util.StrUtil;
 import com.smppw.modaq.application.util.EmailUtil;
 import com.smppw.modaq.common.conts.DateConst;
+import com.smppw.modaq.common.enums.ReportParseStatus;
 import com.smppw.modaq.domain.dto.EmailContentInfoDTO;
 import com.smppw.modaq.domain.dto.MailboxInfoDTO;
 import com.smppw.modaq.domain.dto.UploadReportParams;
@@ -12,17 +15,15 @@ import com.smppw.modaq.domain.dto.UploadReportResult;
 import com.smppw.modaq.domain.entity.EmailFileInfoDO;
 import com.smppw.modaq.domain.entity.EmailParseInfoDO;
 import com.smppw.modaq.domain.entity.MailboxInfoDO;
-import com.smppw.modaq.domain.mapper.EmailFileInfoMapper;
-import com.smppw.modaq.domain.mapper.EmailParseInfoMapper;
 import com.smppw.modaq.domain.mapper.MailboxInfoMapper;
 import com.smppw.modaq.domain.service.EmailParseService;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
 
-import java.io.BufferedReader;
-import java.io.FileReader;
-import java.io.IOException;
+import java.io.*;
+import java.nio.file.Files;
 import java.util.Date;
 import java.util.List;
 
@@ -38,19 +39,17 @@ public class EmailParseApiServiceImpl implements EmailParseApiService {
 
     private final MailboxInfoMapper mailboxInfoMapper;
     private final EmailParseService emailParseService;
-    private final EmailParseInfoMapper emailParseInfoMapper;
-    private final EmailFileInfoMapper emailFileInfoMapper;
+//    private final EmailParseInfoMapper emailParseInfoMapper;
+//    private final EmailFileInfoMapper emailFileInfoMapper;
 //    private final ThreadPoolTaskExecutor asyncExecutor;
 //    private final EmailTaskInfoMapper emailTaskInfoMapper;
 
     public EmailParseApiServiceImpl(MailboxInfoMapper mailboxInfoMapper,
-                                    EmailParseService emailParseService,
-                                    EmailParseInfoMapper emailParseInfoMapper,
-                                    EmailFileInfoMapper emailFileInfoMapper) {
+                                    EmailParseService emailParseService) {
         this.mailboxInfoMapper = mailboxInfoMapper;
         this.emailParseService = emailParseService;
-        this.emailParseInfoMapper = emailParseInfoMapper;
-        this.emailFileInfoMapper = emailFileInfoMapper;
+//        this.emailParseInfoMapper = emailParseInfoMapper;
+//        this.emailFileInfoMapper = emailFileInfoMapper;
     }
 
     @Override
@@ -99,7 +98,29 @@ public class EmailParseApiServiceImpl implements EmailParseApiService {
     public List<UploadReportResult> uploadReport(UploadReportParams params) {
         return this.emailParseService.uploadReportResults(params);
     }
-//    private void endEmailTask(Integer id,Integer taskStatus) {
+
+    @Override
+    public List<UploadReportResult> batchUploadReport(MultipartFile[] files) {
+        List<UploadReportResult> dataList = ListUtil.list(false);
+        UploadReportParams params = new UploadReportParams();
+        for (MultipartFile file : files) {
+            String filename = file.getOriginalFilename();
+            File saveFile = this.emailParseService.generateSavePath("upload", new Date(), filename);
+            String filepath = saveFile.getPath();
+            try (InputStream is = file.getInputStream()) {
+                Files.copy(is, saveFile.toPath());
+            } catch (IOException e) {
+                log.warn("文件{} 上传失败:{}", filename, ExceptionUtil.stacktraceToString(e));
+                dataList.add(new UploadReportResult(filepath, ReportParseStatus.FILE_UPLOAD_FAIL));
+            }
+            params.getReportInfos().add(new UploadReportParams.ReportInfo(filepath));
+        }
+        List<UploadReportResult> tempList = this.emailParseService.uploadReportResults(params);
+        dataList.addAll(tempList);
+        return dataList;
+    }
+
+    //    private void endEmailTask(Integer id,Integer taskStatus) {
 //        try{
 //            EmailTaskInfoDO emailTaskInfoDO = new EmailTaskInfoDO();
 //            emailTaskInfoDO.setId(id);

+ 1 - 0
mo-daq/src/main/java/com/smppw/modaq/common/enums/ReportParseStatus.java

@@ -3,6 +3,7 @@ package com.smppw.modaq.common.enums;
 public enum ReportParseStatus implements StatusCode {
     SYSTEM_ERROR(20001, "系统异常"),
     ARCHIVE_FAIL(20002, "压缩包解压失败"),
+    FILE_UPLOAD_FAIL(20003, "文件上传失败"),
 
     AI_NOT_FOUND(20009, "AI资源找不到"),
     NO_SUPPORT_AI(20010, "报告[{}]不支持AI解析"),

+ 12 - 3
mo-daq/src/main/java/com/smppw/modaq/domain/dto/UploadReportParams.java

@@ -1,5 +1,6 @@
 package com.smppw.modaq.domain.dto;
 
+import cn.hutool.core.collection.ListUtil;
 import cn.hutool.core.io.FileUtil;
 import cn.hutool.core.util.StrUtil;
 import com.smppw.modaq.application.util.EmailUtil;
@@ -16,6 +17,10 @@ public class UploadReportParams {
     private String title;
     private List<ReportInfo> reportInfos;
 
+    public UploadReportParams() {
+        this.reportInfos = ListUtil.list(false);
+    }
+
     @Setter
     public static class ReportInfo {
         /**
@@ -32,8 +37,6 @@ public class UploadReportParams {
          * 报告路径,必传
          */
         private String reportPath;
-        @Getter
-        private transient String extName;
         /**
          * 报告路径转file对象
          */
@@ -55,8 +58,14 @@ public class UploadReportParams {
 
         public String getReportPath() {
             this.reportFile = FileUtil.file(reportPath);
-            this.extName = FileUtil.extName(this.reportFile);
             return reportPath;
         }
+
+        public ReportInfo() {
+        }
+
+        public ReportInfo(String reportPath) {
+            this.reportPath = reportPath;
+        }
     }
 }

+ 13 - 0
mo-daq/src/main/java/com/smppw/modaq/domain/dto/UploadReportResult.java

@@ -1,5 +1,6 @@
 package com.smppw.modaq.domain.dto;
 
+import com.smppw.modaq.common.enums.ReportParseStatus;
 import lombok.Getter;
 import lombok.Setter;
 
@@ -12,4 +13,16 @@ public class UploadReportResult {
 
     public UploadReportResult() {
     }
+
+    public UploadReportResult(String reportPath, int status, String msg) {
+        this.reportPath = reportPath;
+        this.status = status;
+        this.msg = msg;
+    }
+
+    public UploadReportResult(String reportPath, ReportParseStatus status) {
+        this.reportPath = reportPath;
+        this.status = status.getCode();
+        this.msg = status.getMsg();
+    }
 }

+ 23 - 0
mo-daq/src/main/java/com/smppw/modaq/domain/dto/report/ParseResult.java

@@ -1,5 +1,7 @@
 package com.smppw.modaq.domain.dto.report;
 
+import cn.hutool.core.util.StrUtil;
+import com.smppw.modaq.common.enums.ReportParseStatus;
 import lombok.Getter;
 import lombok.Setter;
 
@@ -17,6 +19,27 @@ public class ParseResult<T extends ReportData> {
 
     private T data;
 
+    public ParseResult() {
+    }
+
+    public ParseResult(Integer status, String msg, T data) {
+        this.status = status;
+        this.msg = msg;
+        this.data = data;
+    }
+
+    public ParseResult(ReportParseStatus parseStatus, T data) {
+        this.status = parseStatus.getCode();
+        this.msg = parseStatus.getMsg();
+        this.data = data;
+    }
+
+    public ParseResult(ReportParseStatus parseStatus, T data, String format) {
+        this.status = parseStatus.getCode();
+        this.msg = StrUtil.format(parseStatus.getMsg(), format);
+        this.data = data;
+    }
+
     @Override
     public String toString() {
         return "{" +

+ 0 - 1
mo-daq/src/main/java/com/smppw/modaq/domain/dto/report/ReportData.java

@@ -64,7 +64,6 @@ public abstract class ReportData implements Serializable {
     public String toString() {
         return "baseInfo=" + baseInfo +
                 ", fundInfo=" + fundInfo +
-                ", reportPath='" + reportPath + '\'' +
                 ", aiParse=" + aiParse;
     }
 

+ 1 - 1
mo-daq/src/main/java/com/smppw/modaq/domain/dto/report/ReportFundInfoDTO.java

@@ -122,7 +122,7 @@ public class ReportFundInfoDTO extends BaseReportDTO<ReportFundInfoDO> {
         if (StrUtil.isBlank(this.fundName)) {
             return null;
         }
-        return StrUtil.trim(this.fundName);
+        return StrUtil.removeAll(this.fundName, " ");
     }
 
     public ReportFundInfoDTO() {

+ 34 - 51
mo-daq/src/main/java/com/smppw/modaq/domain/service/EmailParseService.java

@@ -333,12 +333,9 @@ public class EmailParseService {
                     this.handleCompressedFiles(params.getTitle(), reportPath, e.getReportType(), dtos);
                 } catch (Exception ex) {
                     log.warn("报告{} 压缩包解压失败:{}", reportPath, ExceptionUtil.stacktraceToString(ex));
-                    ParseResult<ReportData> result = new ParseResult<>();
-                    result.setStatus(ReportParseStatus.ARCHIVE_FAIL.getCode());
-                    result.setMsg(ReportParseStatus.ARCHIVE_FAIL.getMsg());
                     ReportData reportData = new ReportData.DefaultReportData();
                     reportData.setReportPath(reportPath);
-                    dataList.add(result);
+                    dataList.add(new ParseResult<>(ReportParseStatus.ARCHIVE_FAIL, reportData));
                 }
             } else {
                 dtos.add(new EmailZipFileDTO(params.getTitle(), reportPath, e.getReportType()));
@@ -354,11 +351,7 @@ public class EmailParseService {
         List<UploadReportResult> resultList = ListUtil.list(false);
         for (ParseResult<ReportData> result : dataList) {
             ReportData data = result.getData();
-            UploadReportResult temp = new UploadReportResult();
-            temp.setReportPath(data.getReportPath());
-            temp.setMsg(result.getMsg());
-            temp.setStatus(result.getStatus());
-            resultList.add(temp);
+            resultList.add(new UploadReportResult(data.getReportPath(), result.getStatus(), result.getMsg()));
         }
         return resultList;
     }
@@ -460,7 +453,10 @@ public class EmailParseService {
         for (EmailZipFileDTO zipFile : dtos) {
             EmailFileInfoDO emailFile = this.saveEmailFileInfo(emailId, zipFile.getFilename(), zipFile.getFilepath());
             // 解析并保存报告
-            ParseResult<ReportData> parseResult = this.parseReportAndHandleResult(emailTitle, emailFile, zipFile);
+            ParseResult<ReportData> parseResult = this.parseReportAndHandleResult(emailTitle, emailFile.getId(), zipFile);
+            if (!Objects.equals(1, parseResult.getStatus())) {
+                log.error(parseResult.getMsg());
+            }
             if (parseResult.getData() == null) {
                 parseResult.setData(new ReportData.DefaultReportData());
             }
@@ -473,13 +469,13 @@ public class EmailParseService {
     /**
      * 解析报告并保存解析结果
      *
-     * @param emailTitle    邮件主题
-     * @param emailFileInfo 当前报告信息
-     * @param zipFile       当前报告的路径信息
+     * @param emailTitle 邮件主题
+     * @param fileId     当前文件数据库ID
+     * @param zipFile    当前报告的路径信息
      * @return /
      */
     private ParseResult<ReportData> parseReportAndHandleResult(String emailTitle,
-                                                               EmailFileInfoDO emailFileInfo,
+                                                               Integer fileId,
                                                                EmailZipFileDTO zipFile) {
         Integer emailType = zipFile.getEmailType();
         String fileName = zipFile.getFilename();
@@ -487,10 +483,7 @@ public class EmailParseService {
         ParseResult<ReportData> result = new ParseResult<>();
         boolean reportFlag = emailType == null || !EmailTypeConst.SUPPORT_EMAIL_TYPES.contains(emailType);
         if (reportFlag || StrUtil.isBlank(fileName) || fileName.endsWith(Constants.FILE_HTML)) {
-            result.setStatus(ReportParseStatus.NOT_A_REPORT.getCode());
-            result.setMsg(StrUtil.format(ReportParseStatus.NOT_A_REPORT.getMsg(), fileName));
-            log.error(result.getMsg());
-            return result;
+            return new ParseResult<>(ReportParseStatus.NOT_A_REPORT, null, fileName);
         }
         // 类型识别---先识别季度报告,没有季度再识别年度报告,最后识别月报
         ReportType reportType = ReportParseUtils.matchReportType(emailType, fileName);
@@ -504,19 +497,12 @@ public class EmailParseService {
         ReportParserFileType fileType = ReportParserFileType.getBySuffix(zipFile.getExtName());
         // 不支持的格式
         if (fileType == null) {
-            result.setStatus(ReportParseStatus.NO_SUPPORT_TEMPLATE.getCode());
-            result.setMsg(StrUtil.format(ReportParseStatus.NO_SUPPORT_TEMPLATE.getMsg(), fileName));
-            log.error(result.getMsg());
-            return result;
+            return new ParseResult<>(ReportParseStatus.NO_SUPPORT_TEMPLATE, null, fileName);
         }
         // 不是定期报告的判断逻辑放在不支持的格式下面
         if (reportType == null) {
-            result.setStatus(ReportParseStatus.NOT_A_REPORT.getCode());
-            result.setMsg(StrUtil.format(ReportParseStatus.NOT_A_REPORT.getMsg(), fileName));
-            log.error(result.getMsg());
-            return result;
+            return new ParseResult<>(ReportParseStatus.NOT_A_REPORT, null, fileName);
         }
-        Integer fileId = emailFileInfo.getId();
 
         // 首页和尾页转为png图片,首页用来识别基金名称和基金代码、尾页用来识别印章和联系人
         List<String> images = ListUtil.list(true);
@@ -551,25 +537,21 @@ public class EmailParseService {
             if (reportType != ReportType.OTHER && reportType != ReportType.WEEKLY) {
                 ReportParser<ReportData> instance = this.reportParserFactory.getInstance(reportType, fileType);
                 reportData = instance.parse(params);
-                result.setStatus(1);
-                result.setMsg("报告解析成功");
-                result.setData(reportData);
+                result = new ParseResult<>(1, "报告解析成功", reportData);
             } else {
                 if (log.isInfoEnabled()) {
                     log.info("报告{} 是周报或其他类型,直接用AI解析器解析", fileName);
                 }
             }
         } catch (ReportParseException e) {
-            log.error("解析失败:{}", StrUtil.format(e.getMsg(), fileName));
-            result.setStatus(e.getCode());
-            result.setMsg(StrUtil.format(e.getMsg(), fileName));
+            log.warn("解析失败:{}", StrUtil.format(e.getMsg(), fileName));
+            result = new ParseResult<>(e.getCode(), StrUtil.format(e.getMsg(), fileName), null);
             if (e instanceof NotSupportReportException) {
                 notSupportFile = true;
             }
         } catch (Exception e) {
-            log.error("解析错误:{}", ExceptionUtil.stacktraceToString(e));
-            result.setStatus(ReportParseStatus.PARSE_FAIL.getCode());
-            result.setMsg(StrUtil.format(ReportParseStatus.PARSE_FAIL.getMsg(), e.getMessage()));
+            log.warn("解析错误:{}", ExceptionUtil.stacktraceToString(e));
+            result = new ParseResult<>(ReportParseStatus.PARSE_FAIL, null, e.getMessage());
         } finally {
             // 如果解析结果是空的就用AI工具解析一次
             if (reportData == null && !notSupportFile) {
@@ -587,17 +569,13 @@ public class EmailParseService {
                     params = new ReportParserParams(fileId, fileName, filepath, reportType);
                     ReportParser<ReportData> instance = this.reportParserFactory.getInstance(reportType, ReportParserFileType.AI);
                     reportData = instance.parse(params);
-                    result.setStatus(1);
-                    result.setMsg("报告解析成功--AI");
-                    result.setData(reportData);
+                    result = new ParseResult<>(1, "报告解析成功--AI", reportData);
                 } catch (ReportParseException e) {
-                    log.error("AI解析失败:{}", StrUtil.format(e.getMsg(), fileName));
-                    result.setStatus(e.getCode());
-                    result.setMsg(StrUtil.format(e.getMsg(), fileName));
+                    log.warn("AI解析失败:{}", StrUtil.format(e.getMsg(), fileName));
+                    result = new ParseResult<>(e.getCode(), StrUtil.format(e.getMsg(), fileName), null);
                 } catch (Exception e) {
-                    log.error("AI解析错误:{}", ExceptionUtil.stacktraceToString(e));
-                    result.setStatus(ReportParseStatus.PARSE_FAIL.getCode());
-                    result.setMsg(StrUtil.format(ReportParseStatus.PARSE_FAIL.getMsg(), e.getMessage()));
+                    log.warn("AI解析错误:{}", ExceptionUtil.stacktraceToString(e));
+                    result = new ParseResult<>(ReportParseStatus.PARSE_FAIL, null, e.getMessage());
                 }
                 if (log.isInfoEnabled()) {
                     log.info("报告{} AI解析结束!结果是:{}", fileName, reportData);
@@ -979,12 +957,7 @@ public class EmailParseService {
             return;
         }
 
-        String emailDateStr = DateUtil.format(sendDate, DateConst.YYYYMMDD);
-        String filePath = path + File.separator + account + File.separator + emailDateStr + File.separator + "original" + File.separator;
-        // 压缩包重名时的后面的压缩包会覆盖前面压缩包的问题(不考虑普通文件)
-        String emailDate = DateUtil.format(sendDate, DateConst.YYYYMMDDHHMMSS24);
-        String realName = ArchiveUtil.isArchive(fileName) ? emailDate + fileName : fileName;
-        File saveFile = FileUtil.file(filePath + realName);
+        File saveFile = this.generateSavePath(account, sendDate, fileName);
         if (!saveFile.exists()) {
             if (!saveFile.getParentFile().exists()) {
                 boolean mkdirs = saveFile.getParentFile().mkdirs();
@@ -1011,6 +984,16 @@ public class EmailParseService {
         emailContentInfoDTOList.add(emailContentInfoDTO);
     }
 
+    public File generateSavePath(String account, Date sendDate, String fileName) {
+        String emailDateStr = DateUtil.format(sendDate, DateConst.YYYYMMDD);
+        String filePath = this.path + File.separator + account + File.separator +
+                emailDateStr + File.separator + "original" + File.separator;
+        // 压缩包重名时的后面的压缩包会覆盖前面压缩包的问题(不考虑普通文件)
+        String emailDate = DateUtil.format(sendDate, DateConst.YYYYMMDDHHMMSS24);
+        String realName = ArchiveUtil.isArchive(fileName) ? emailDate + fileName : fileName;
+        return FileUtil.file(filePath + realName);
+    }
+
     private void reMultipart(String account, String subject, Date emailDate, Multipart multipart,
                              List<EmailContentInfoDTO> emailContentInfoDTOList) throws Exception {
         for (int i = 0; i < multipart.getCount(); i++) {