ソースを参照

fix:确认单AI解析失败时用OCR识别关键信息

wangzaijun 1 週間 前
コミット
42c0cdd66c
18 ファイル変更305 行追加141 行削除
  1. 91 2
      mo-daq/src/main/java/com/smppw/modaq/application/components/OCRReportParser.java
  2. 2 2
      mo-daq/src/main/java/com/smppw/modaq/application/components/ReportParseUtils.java
  3. 2 2
      mo-daq/src/main/java/com/smppw/modaq/application/components/report/parser/AbstractReportParser.java
  4. 2 2
      mo-daq/src/main/java/com/smppw/modaq/application/components/report/parser/ai/AbstractAIReportParser.java
  5. 0 38
      mo-daq/src/main/java/com/smppw/modaq/domain/dto/report/BaseReportDTO.java
  6. 0 11
      mo-daq/src/main/java/com/smppw/modaq/domain/dto/report/OCRLetterParseData.java
  7. 2 1
      mo-daq/src/main/java/com/smppw/modaq/domain/dto/report/ReportAssetAllocationDTO.java
  8. 6 5
      mo-daq/src/main/java/com/smppw/modaq/domain/dto/report/ReportFinancialIndicatorsDTO.java
  9. 4 4
      mo-daq/src/main/java/com/smppw/modaq/domain/dto/report/ReportFundInfoDTO.java
  10. 31 31
      mo-daq/src/main/java/com/smppw/modaq/domain/dto/report/ReportFundTransactionDTO.java
  11. 3 2
      mo-daq/src/main/java/com/smppw/modaq/domain/dto/report/ReportInvestmentIndustryDTO.java
  12. 6 6
      mo-daq/src/main/java/com/smppw/modaq/domain/dto/report/ReportNetReportDTO.java
  13. 6 5
      mo-daq/src/main/java/com/smppw/modaq/domain/dto/report/ReportShareChangeDTO.java
  14. 52 0
      mo-daq/src/main/java/com/smppw/modaq/domain/dto/report/ocr/OCRLetterParseData.java
  15. 1 1
      mo-daq/src/main/java/com/smppw/modaq/domain/dto/report/OCRParseData.java
  16. 54 23
      mo-daq/src/main/java/com/smppw/modaq/domain/service/EmailParseService.java
  17. 38 1
      mo-daq/src/main/java/com/smppw/modaq/infrastructure/util/DateUtils.java
  18. 5 5
      mo-daq/src/test/java/com/smppw/modaq/MoDaqApplicationTests.java

+ 91 - 2
mo-daq/src/main/java/com/smppw/modaq/application/components/OCRReportParser.java

@@ -10,7 +10,8 @@ import cn.hutool.json.JSONUtil;
 import com.smppw.modaq.common.enums.ReportMonthlyType;
 import com.smppw.modaq.common.enums.ReportParseStatus;
 import com.smppw.modaq.common.exception.ReportParseException;
-import com.smppw.modaq.domain.dto.report.OCRParseData;
+import com.smppw.modaq.domain.dto.report.ocr.OCRLetterParseData;
+import com.smppw.modaq.domain.dto.report.ocr.OCRParseData;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -20,6 +21,94 @@ import java.util.Objects;
 public class OCRReportParser {
     private final Logger logger = LoggerFactory.getLogger(this.getClass());
 
+    public OCRLetterParseData parseLetterData(String filename, String ocrApi, String ocrImgUrl) throws ReportParseException {
+        Map<String, Object> paramsMap = MapUtil.newHashMap(4);
+        paramsMap.put("image_url", ocrImgUrl);
+        paramsMap.put("user_msg", """
+                请提取文件中的基金名称、产品代码、投资人姓名、证件类型、证件号码、基金账户、交易账号、业务类型、申请日期、申请金额、申请份额、确认日期、确认金额、确认份额、单位净值。
+                要求准确无误的提取上述关键信息、不要遗漏和捏造虚假信息。
+                返回数据格式以json方式输出,格式为:{"基金名称":"","产品代码":"","投资人姓名":"","证件类型":"","证件号码":"","基金账户":"","交易账号":"","业务类型":"","申请日期":"","申请金额":"","申请份额":"","确认日期":"","确认金额":"","确认份额":"","单位净值":""}
+                """);
+        OCRLetterParseData res = new OCRLetterParseData();
+        String objectStr = null;
+        try {
+            objectStr = this.parseOcrResult(ocrApi, paramsMap);
+            JSONObject jsonObject = JSONUtil.parseObj(objectStr);
+            String fundName = this.cleanData(jsonObject.getStr("基金名称"));
+            String fundCode = this.cleanData(jsonObject.getStr("产品代码"));
+            String investorName = this.cleanData(jsonObject.getStr("投资人姓名"));
+            String certificateType = this.cleanData(jsonObject.getStr("证件类型"));
+            String certificateNumber = this.cleanData(jsonObject.getStr("证件号码"));
+            String fundAccount = this.cleanData(jsonObject.getStr("基金账户"));
+            String tradingAccount = this.cleanData(jsonObject.getStr("交易账号"));
+            String transactionType = this.cleanData(jsonObject.getStr("业务类型"));
+            String applyDate = this.cleanData(jsonObject.getStr("申请日期"));
+            String applyAmount = this.cleanData(jsonObject.getStr("申请金额"));
+            String applyShare = this.cleanData(jsonObject.getStr("申请份额"));
+            String holdingDate = this.cleanData(jsonObject.getStr("确认日期"));
+            String amount = this.cleanData(jsonObject.getStr("确认金额"));
+            String share = this.cleanData(jsonObject.getStr("确认份额"));
+            String nav = this.cleanData(jsonObject.getStr("单位净值"));
+            if (StrUtil.isNotBlank(fundName) && (fundName.contains("基金") || fundName.contains("资产管理")) && !fundName.contains("公司")) {
+                res.setFundName(fundName);
+            }
+            if (StrUtil.isNotBlank(fundCode)) {
+                res.setFundCode(ReportParseUtils.matchFundCode(fundCode));
+            }
+            if (StrUtil.isNotBlank(investorName)) {
+                res.setInvestorName(investorName);
+            }
+            if (StrUtil.isNotBlank(certificateType)) {
+                res.setCertificateType(certificateType);
+            }
+            if (StrUtil.isNotBlank(certificateNumber)) {
+                res.setCertificateNumber(certificateNumber);
+            }
+            if (StrUtil.isNotBlank(fundAccount)) {
+                res.setFundAccount(fundAccount);
+            }
+            if (StrUtil.isNotBlank(tradingAccount)) {
+                res.setTradingAccount(tradingAccount);
+            }
+            if (StrUtil.isNotBlank(transactionType)) {
+                res.setTransactionType(transactionType);
+            }
+            if (StrUtil.isNotBlank(applyDate)) {
+                res.setApplyDate(applyDate);
+            }
+            if (StrUtil.isNotBlank(applyAmount)) {
+                res.setApplyAmount(applyAmount);
+            }
+            if (StrUtil.isNotBlank(applyShare)) {
+                res.setApplyShare(applyShare);
+            }
+            if (StrUtil.isNotBlank(holdingDate)) {
+                res.setHoldingDate(holdingDate);
+            }
+            if (StrUtil.isNotBlank(amount)) {
+                res.setAmount(amount);
+            }
+            if (StrUtil.isNotBlank(share)) {
+                res.setShare(share);
+            }
+            if (StrUtil.isNotBlank(nav)) {
+                res.setNav(nav);
+            }
+            return res;
+        } catch (IORuntimeException e) {
+            this.logger.warn("确认单{} OCR解析错误:{}", filename, ReportParseStatus.AI_NOT_FOUND.getMsg());
+            throw new ReportParseException(ReportParseStatus.AI_NOT_FOUND);
+        } catch (Exception e) {
+            this.logger.warn("确认单{} OCR识别错误:{}", filename, ExceptionUtil.stacktraceToString(e));
+            throw new ReportParseException(ReportParseStatus.SYSTEM_ERROR);
+        } finally {
+            if (logger.isInfoEnabled()) {
+                this.logger.info("确认单{} OCR识别参数{},OCR识别结果:{},处理后的结果是:{}",
+                        filename, paramsMap, objectStr, res);
+            }
+        }
+    }
+
     public ReportMonthlyType parseMonthlyType(String filename, String ocrApi, String ocrImgUrl) throws ReportParseException {
         Map<String, Object> paramsMap = MapUtil.newHashMap(4);
         paramsMap.put("image_url", ocrImgUrl);
@@ -59,7 +148,7 @@ public class OCRReportParser {
         paramsMap.put("user_msg", """
                 请提取文件中的基金名称、基金公司、产品代码,并判断是否有红色印章和是否有电话。
                 要求准确无误的提取上述关键信息、不要遗漏和捏造虚假信息。
-                返回数据格式以json方式输出,格式为:{"基金名称":"","基金公司":"产品代码":"","是否有红色印章":"","是否有电话":""}
+                返回数据格式以json方式输出,格式为:{"基金名称":"","基金公司":"","产品代码":"","是否有红色印章":"","是否有电话":""}
                 """);
         OCRParseData res = new OCRParseData();
         String objectStr = null;

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

@@ -9,7 +9,7 @@ import com.smppw.modaq.common.conts.EmailTypeConst;
 import com.smppw.modaq.common.conts.PatternConsts;
 import com.smppw.modaq.common.enums.ReportType;
 import com.smppw.modaq.domain.dto.report.ReportAssetAllocationDTO;
-import com.smppw.modaq.infrastructure.util.DateUtils;
+import com.smppw.modaq.infrastructure.util.ConvertUtil;
 import jakarta.mail.internet.MimeUtility;
 
 import java.time.YearMonth;
@@ -597,7 +597,7 @@ public final class ReportParseUtils {
 
         String date = "2025年6月6日";
         String input = ReportParseUtils.cleaningValue(date, false);
-        Date date1 = DateUtils.toDate(input);
+        Date date1 = ConvertUtil.toDate(input);
         System.out.println(date1);
 
 

+ 2 - 2
mo-daq/src/main/java/com/smppw/modaq/application/components/report/parser/AbstractReportParser.java

@@ -12,7 +12,7 @@ import com.smppw.modaq.common.exception.ReportParseException;
 import com.smppw.modaq.domain.dto.report.*;
 import com.smppw.modaq.domain.entity.EmailFieldMappingDO;
 import com.smppw.modaq.domain.mapper.EmailFieldMappingMapper;
-import com.smppw.modaq.infrastructure.util.DateUtils;
+import com.smppw.modaq.infrastructure.util.ConvertUtil;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -171,7 +171,7 @@ public abstract class AbstractReportParser<T extends ReportData> implements Repo
         reportInfo.setReportName(reportName);
         reportInfo.setReportType(params.getReportType().name());
         String reportDate = ReportParseUtils.matchReportDate(params.getReportType(), reportName);
-        reportInfo.setReportDate(DateUtils.toDate(reportDate));
+        reportInfo.setReportDate(ConvertUtil.toDate(reportDate));
         return reportInfo;
     }
 }

+ 2 - 2
mo-daq/src/main/java/com/smppw/modaq/application/components/report/parser/ai/AbstractAIReportParser.java

@@ -15,7 +15,7 @@ import com.smppw.modaq.domain.dto.report.ReportBaseInfoDTO;
 import com.smppw.modaq.domain.dto.report.ReportData;
 import com.smppw.modaq.domain.dto.report.ReportParserParams;
 import com.smppw.modaq.domain.mapper.EmailFieldMappingMapper;
-import com.smppw.modaq.infrastructure.util.DateUtils;
+import com.smppw.modaq.infrastructure.util.ConvertUtil;
 import org.springframework.beans.factory.annotation.Value;
 
 import java.util.Date;
@@ -88,7 +88,7 @@ public abstract class AbstractAIReportParser<T extends ReportData> extends Abstr
     protected ReportBaseInfoDTO buildReportInfo(ReportParserParams params) {
         ReportBaseInfoDTO reportInfo = super.buildReportInfo(params);
         if (reportInfo.getReportDate() == null) {
-            Date date = DateUtils.toDate(MapUtil.getStr(this.allInfoMap, "报告日期"));
+            Date date = ConvertUtil.toDate(MapUtil.getStr(this.allInfoMap, "报告日期"));
             reportInfo.setReportDate(date);
         }
         return reportInfo;

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

@@ -1,6 +1,5 @@
 package com.smppw.modaq.domain.dto.report;
 
-import cn.hutool.core.util.StrUtil;
 import com.smppw.modaq.common.conts.Constants;
 import com.smppw.modaq.domain.entity.report.BaseReportDO;
 import lombok.Getter;
@@ -8,7 +7,6 @@ import lombok.Setter;
 
 import java.io.Serial;
 import java.io.Serializable;
-import java.math.BigDecimal;
 import java.util.Date;
 
 /**
@@ -45,40 +43,4 @@ public abstract class BaseReportDTO<T extends BaseReportDO> implements Serializa
     public String toString() {
         return "fileId=" + fileId;
     }
-
-    protected BigDecimal toBigDecimal(String input) {
-        return this.toBigDecimal(input, null);
-    }
-
-    /**
-     * 字符串转数字,如果数据没有或者转换失败则用defaultValue字段填充
-     *
-     * @param input        待转换的字符串
-     * @param defaultValue 为null或者数字转换失败的默认值
-     * @return /
-     */
-    protected BigDecimal toBigDecimal(String input, BigDecimal defaultValue) {
-        if (StrUtil.isBlank(input)) {
-            return defaultValue;
-        }
-        try {
-            // 1. 清理输入,保留有效字符(数字、正负号、小数点、科学计数法符号、千分位逗号)
-            String cleanedInput = input.trim().replaceAll("[^\\d.,\\-+Ee]", "");
-            // 2. 移除所有空格(包括中间的空格)
-            cleanedInput = cleanedInput.replaceAll("\\s+", "");
-
-            // 3. 处理千分位逗号(替换为"",因为千分位逗号在最终数字中无效)
-            cleanedInput = cleanedInput.replaceAll(",", "");
-
-            // 4. 处理科学计数法中的符号(如 E 或 e 后的符号)
-            // 例如:"1.23E+4" → "1.23E4"(可选,但 Java 的 BigDecimal 可直接处理)
-
-            // 5. 验证最终格式是否合法(可选,但可能影响性能)
-            // 这里直接交给 BigDecimal 处理,捕获异常即可
-
-            return new BigDecimal(cleanedInput);
-        } catch (NumberFormatException e) {
-            return defaultValue;
-        }
-    }
 }

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

@@ -1,11 +0,0 @@
-package com.smppw.modaq.domain.dto.report;
-
-import lombok.Getter;
-import lombok.Setter;
-
-@Setter
-@Getter
-public class OCRLetterParseData {
-    private String fundName;
-    private String investorName;
-}

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

@@ -1,6 +1,7 @@
 package com.smppw.modaq.domain.dto.report;
 
 import com.smppw.modaq.domain.entity.report.ReportAssetAllocationDO;
+import com.smppw.modaq.infrastructure.util.ConvertUtil;
 import lombok.Getter;
 import lombok.Setter;
 
@@ -43,7 +44,7 @@ public class ReportAssetAllocationDTO extends BaseReportDTO<ReportAssetAllocatio
         entity.setFileId(this.getFileId());
         entity.setAssetType(this.assetType);
         entity.setColumnName(this.assetDetails);
-        entity.setMarketValue(this.toBigDecimal(this.marketValue));
+        entity.setMarketValue(ConvertUtil.toBigDecimal(this.marketValue));
         entity.setRemark(this.remark);
         return entity;
     }

+ 6 - 5
mo-daq/src/main/java/com/smppw/modaq/domain/dto/report/ReportFinancialIndicatorsDTO.java

@@ -2,6 +2,7 @@ package com.smppw.modaq.domain.dto.report;
 
 import cn.hutool.core.util.StrUtil;
 import com.smppw.modaq.domain.entity.report.ReportFinancialIndicatorsDO;
+import com.smppw.modaq.infrastructure.util.ConvertUtil;
 import lombok.Getter;
 import lombok.Setter;
 
@@ -61,11 +62,11 @@ public class ReportFinancialIndicatorsDTO extends BaseReportLevelDTO<ReportFinan
         ReportFinancialIndicatorsDO entity = new ReportFinancialIndicatorsDO();
         entity.setFileId(this.getFileId());
         entity.setLevel(this.getLevel());
-        entity.setFundAssetSize(this.toBigDecimal(this.assetNet));
-        entity.setNav(this.toBigDecimal(this.nav));
-        entity.setProfit(this.toBigDecimal(this.profit));
-        entity.setRealizedIncome(this.toBigDecimal(this.realizedIncome));
-        entity.setUndistributedProfit(this.toBigDecimal(this.undistributedProfit));
+        entity.setFundAssetSize(ConvertUtil.toBigDecimal(this.assetNet));
+        entity.setNav(ConvertUtil.toBigDecimal(this.nav));
+        entity.setProfit(ConvertUtil.toBigDecimal(this.profit));
+        entity.setRealizedIncome(ConvertUtil.toBigDecimal(this.realizedIncome));
+        entity.setUndistributedProfit(ConvertUtil.toBigDecimal(this.undistributedProfit));
         if (StrUtil.isNotBlank(this.yearly)) {
             Matcher matcher = Pattern.compile("\\d+").matcher(this.yearly);
             if (matcher.find()) {

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

@@ -3,7 +3,7 @@ package com.smppw.modaq.domain.dto.report;
 import cn.hutool.core.util.StrUtil;
 import com.smppw.modaq.application.components.ReportParseUtils;
 import com.smppw.modaq.domain.entity.report.ReportFundInfoDO;
-import com.smppw.modaq.infrastructure.util.DateUtils;
+import com.smppw.modaq.infrastructure.util.ConvertUtil;
 import lombok.Getter;
 import lombok.Setter;
 
@@ -146,16 +146,16 @@ public class ReportFundInfoDTO extends BaseReportDTO<ReportFundInfoDO> {
         entity.setCustodianName(this.custodianName);
         entity.setFundManager(this.fundManager);
         entity.setFundStrategyDescription(this.fundStrategyDescription);
-        entity.setInceptionDate(DateUtils.toDate(this.inceptionDate));
+        entity.setInceptionDate(ConvertUtil.toDate(this.inceptionDate));
         entity.setIndustryTrend(this.industryTrend);
         entity.setInvestmentObjective(this.investmentObjective);
-        entity.setLeverage(this.toBigDecimal(this.leverage));
+        entity.setLeverage(ConvertUtil.toBigDecimal(this.leverage));
         entity.setLeverageNote(this.leverageNote);
         entity.setOperationType(this.operationType);
         entity.setRegisterNumber(this.registerNumber);
         entity.setRiskReturnDesc(this.riskReturnDesc);
         entity.setSecondaryBenchmark(this.secondaryBenchmark);
-        entity.setDueDate(DateUtils.toDate(this.dueDate));
+        entity.setDueDate(ConvertUtil.toDate(this.dueDate));
         entity.setReviewed(Objects.equals("是", this.isReviewed) ? 1 : 0);
         this.initEntity(entity);
         return entity;

+ 31 - 31
mo-daq/src/main/java/com/smppw/modaq/domain/dto/report/ReportFundTransactionDTO.java

@@ -1,7 +1,7 @@
 package com.smppw.modaq.domain.dto.report;
 
 import com.smppw.modaq.domain.entity.report.ReportFundTransactionDO;
-import com.smppw.modaq.infrastructure.util.DateUtils;
+import com.smppw.modaq.infrastructure.util.ConvertUtil;
 import lombok.Getter;
 import lombok.Setter;
 
@@ -249,47 +249,47 @@ public class ReportFundTransactionDTO extends BaseReportDTO<ReportFundTransactio
         entity.setTransactionType(transactionType);
         entity.setBusinessReason(businessReason);
         entity.setStatus(status);
-        entity.setHoldingDate(DateUtils.toDate(holdingDate));
-        entity.setApplyDate(DateUtils.toDate(applyDate));
-        entity.setApplyAmount(this.toBigDecimal(applyAmount));
-        entity.setApplyShare(this.toBigDecimal(applyShare));
-        entity.setAmount(this.toBigDecimal(amount, BigDecimal.ZERO));
-        entity.setShare(this.toBigDecimal(share, BigDecimal.ZERO));
-        entity.setNetAmount(this.toBigDecimal(netAmount, BigDecimal.ZERO));
-        entity.setNav(this.toBigDecimal(nav));
-        entity.setConfirmationRatio(this.toBigDecimal(confirmationRatio));
+        entity.setHoldingDate(ConvertUtil.toDate(holdingDate));
+        entity.setApplyDate(ConvertUtil.toDate(applyDate));
+        entity.setApplyAmount(ConvertUtil.toBigDecimal(applyAmount));
+        entity.setApplyShare(ConvertUtil.toBigDecimal(applyShare));
+        entity.setAmount(ConvertUtil.toBigDecimal(amount, BigDecimal.ZERO));
+        entity.setShare(ConvertUtil.toBigDecimal(share, BigDecimal.ZERO));
+        entity.setNetAmount(ConvertUtil.toBigDecimal(netAmount, BigDecimal.ZERO));
+        entity.setNav(ConvertUtil.toBigDecimal(nav));
+        entity.setConfirmationRatio(ConvertUtil.toBigDecimal(confirmationRatio));
         entity.setTaConfirmationNumber(taConfirmationNumber);
         entity.setTaNumber(taNumber);
         entity.setApplyNo(applyNo);
-        entity.setShareBalance(this.toBigDecimal(shareBalance));
+        entity.setShareBalance(ConvertUtil.toBigDecimal(shareBalance));
         entity.setShareCategory(shareCategory);
         entity.setLargeRedemptionType(largeRedemptionType);
         entity.setRewardMark(rewardMark);
         entity.setHoldingDays(holdingDays);
-        entity.setShareRegistryDate(DateUtils.toDate(shareRegistryDate));
+        entity.setShareRegistryDate(ConvertUtil.toDate(shareRegistryDate));
         // --- 以下是费用
-        entity.setFee(this.toBigDecimal(fee));
-        entity.setInterest(this.toBigDecimal(interest));
-        entity.setInterestToFundAssets(this.toBigDecimal(interestToFundAssets));
-        entity.setTradeFee(this.toBigDecimal(tradeFee));
-        entity.setDefaultFee(this.toBigDecimal(defaultFee));
-        entity.setPerformanceFee(this.toBigDecimal(performanceFee));
-        entity.setFeeDiscounts(this.toBigDecimal(feeDiscounts));
-        entity.setPerformanceFeeDiscounts(this.toBigDecimal(performanceFeeDiscounts));
+        entity.setFee(ConvertUtil.toBigDecimal(fee));
+        entity.setInterest(ConvertUtil.toBigDecimal(interest));
+        entity.setInterestToFundAssets(ConvertUtil.toBigDecimal(interestToFundAssets));
+        entity.setTradeFee(ConvertUtil.toBigDecimal(tradeFee));
+        entity.setDefaultFee(ConvertUtil.toBigDecimal(defaultFee));
+        entity.setPerformanceFee(ConvertUtil.toBigDecimal(performanceFee));
+        entity.setFeeDiscounts(ConvertUtil.toBigDecimal(feeDiscounts));
+        entity.setPerformanceFeeDiscounts(ConvertUtil.toBigDecimal(performanceFeeDiscounts));
         // --- 以下是分红
         entity.setDividendType(dividendType);
-        entity.setDividendRegistryDate(DateUtils.toDate(dividendRegistryDate));
-        entity.setDividendPaymentDate(DateUtils.toDate(dividendPaymentDate));
-        entity.setBaseShareDividend(this.toBigDecimal(baseShareDividend));
+        entity.setDividendRegistryDate(ConvertUtil.toDate(dividendRegistryDate));
+        entity.setDividendPaymentDate(ConvertUtil.toDate(dividendPaymentDate));
+        entity.setBaseShareDividend(ConvertUtil.toBigDecimal(baseShareDividend));
         entity.setDividendMode(dividendMode);
-        entity.setUnitDividend(this.toBigDecimal(unitDividend));
-        entity.setDividendPerUnit(this.toBigDecimal(dividendPerUnit));
-        entity.setTotalDividendAmount(this.toBigDecimal(totalDividendAmount));
-        entity.setActualCashDividend(this.toBigDecimal(actualCashDividend));
-        entity.setFrozenShares(this.toBigDecimal(frozenShares));
-        entity.setFrozenAmount(this.toBigDecimal(frozenAmount));
-        entity.setActualPerformanceAmount(this.toBigDecimal(actualPerformanceAmount));
-        entity.setActualPerformanceShare(this.toBigDecimal(actualPerformanceShare));
+        entity.setUnitDividend(ConvertUtil.toBigDecimal(unitDividend));
+        entity.setDividendPerUnit(ConvertUtil.toBigDecimal(dividendPerUnit));
+        entity.setTotalDividendAmount(ConvertUtil.toBigDecimal(totalDividendAmount));
+        entity.setActualCashDividend(ConvertUtil.toBigDecimal(actualCashDividend));
+        entity.setFrozenShares(ConvertUtil.toBigDecimal(frozenShares));
+        entity.setFrozenAmount(ConvertUtil.toBigDecimal(frozenAmount));
+        entity.setActualPerformanceAmount(ConvertUtil.toBigDecimal(actualPerformanceAmount));
+        entity.setActualPerformanceShare(ConvertUtil.toBigDecimal(actualPerformanceShare));
         this.initEntity(entity);
         return entity;
     }

+ 3 - 2
mo-daq/src/main/java/com/smppw/modaq/domain/dto/report/ReportInvestmentIndustryDTO.java

@@ -1,6 +1,7 @@
 package com.smppw.modaq.domain.dto.report;
 
 import com.smppw.modaq.domain.entity.report.ReportInvestmentIndustryDO;
+import com.smppw.modaq.infrastructure.util.ConvertUtil;
 import lombok.Getter;
 import lombok.Setter;
 
@@ -43,8 +44,8 @@ public class ReportInvestmentIndustryDTO extends BaseReportDTO<ReportInvestmentI
         entity.setFileId(this.getFileId());
         entity.setIndustryName(this.industryName);
         entity.setInvestType(this.investType);
-        entity.setMarketValue(this.toBigDecimal(this.marketValue));
-        entity.setRatio(this.toBigDecimal(this.ratio));
+        entity.setMarketValue(ConvertUtil.toBigDecimal(this.marketValue));
+        entity.setRatio(ConvertUtil.toBigDecimal(this.ratio));
         return entity;
     }
 

+ 6 - 6
mo-daq/src/main/java/com/smppw/modaq/domain/dto/report/ReportNetReportDTO.java

@@ -1,7 +1,7 @@
 package com.smppw.modaq.domain.dto.report;
 
 import com.smppw.modaq.domain.entity.report.ReportNetReportDO;
-import com.smppw.modaq.infrastructure.util.DateUtils;
+import com.smppw.modaq.infrastructure.util.ConvertUtil;
 import lombok.Getter;
 import lombok.Setter;
 
@@ -51,11 +51,11 @@ public class ReportNetReportDTO extends BaseReportLevelDTO<ReportNetReportDO> {
         ReportNetReportDO entity = new ReportNetReportDO();
         entity.setFileId(this.getFileId());
         entity.setLevel(this.getLevel());
-        entity.setValuationDate(DateUtils.toDate(this.valuationDate));
-        entity.setCumulativeNav(this.toBigDecimal(this.cumulativeNavWithdrawal));
-        entity.setEndTotalShares(this.toBigDecimal(this.assetShare));
-        entity.setFundAssetSize(this.toBigDecimal(this.assetNet));
-        entity.setNav(this.toBigDecimal(this.nav));
+        entity.setValuationDate(ConvertUtil.toDate(this.valuationDate));
+        entity.setCumulativeNav(ConvertUtil.toBigDecimal(this.cumulativeNavWithdrawal));
+        entity.setEndTotalShares(ConvertUtil.toBigDecimal(this.assetShare));
+        entity.setFundAssetSize(ConvertUtil.toBigDecimal(this.assetNet));
+        entity.setNav(ConvertUtil.toBigDecimal(this.nav));
         return entity;
     }
 

+ 6 - 5
mo-daq/src/main/java/com/smppw/modaq/domain/dto/report/ReportShareChangeDTO.java

@@ -1,6 +1,7 @@
 package com.smppw.modaq.domain.dto.report;
 
 import com.smppw.modaq.domain.entity.report.ReportShareChangeDO;
+import com.smppw.modaq.infrastructure.util.ConvertUtil;
 import lombok.Getter;
 import lombok.Setter;
 
@@ -50,11 +51,11 @@ public class ReportShareChangeDTO extends BaseReportLevelDTO<ReportShareChangeDO
         ReportShareChangeDO entity = new ReportShareChangeDO();
         entity.setFileId(this.getFileId());
         entity.setLevel(this.getLevel());
-        entity.setRedemption(this.toBigDecimal(this.redemption));
-        entity.setInitTotalShares(this.toBigDecimal(this.initTotalShares));
-        entity.setSharePerAsset(this.toBigDecimal(this.sharePerAsset));
-        entity.setSplit(this.toBigDecimal(this.splitChangeShare));
-        entity.setSubscription(this.toBigDecimal(this.subscription));
+        entity.setRedemption(ConvertUtil.toBigDecimal(this.redemption));
+        entity.setInitTotalShares(ConvertUtil.toBigDecimal(this.initTotalShares));
+        entity.setSharePerAsset(ConvertUtil.toBigDecimal(this.sharePerAsset));
+        entity.setSplit(ConvertUtil.toBigDecimal(this.splitChangeShare));
+        entity.setSubscription(ConvertUtil.toBigDecimal(this.subscription));
         return entity;
     }
 

+ 52 - 0
mo-daq/src/main/java/com/smppw/modaq/domain/dto/report/ocr/OCRLetterParseData.java

@@ -0,0 +1,52 @@
+package com.smppw.modaq.domain.dto.report.ocr;
+
+import lombok.Getter;
+import lombok.Setter;
+
+@Setter
+@Getter
+public class OCRLetterParseData {
+    private String fundName;
+    private String fundCode;
+
+    /**
+     * 投资人的姓名
+     */
+    private String investorName;
+    /**
+     * 证件类型(例如:身份证、护照)
+     */
+    private String certificateType;
+    /**
+     * 投资人证件号码
+     */
+    private String certificateNumber;
+    /**
+     * 基金账户编号
+     */
+    private String fundAccount;
+    /**
+     * 投资者交易账号
+     */
+    private String tradingAccount;
+
+    /**
+     * 业务类型(例如:申购、赎回)
+     */
+    private String transactionType;
+
+    private String applyDate;
+    private String applyAmount;
+    private String applyShare;
+
+    private String holdingDate;
+    /**
+     * 确认的金额
+     */
+    private String amount;
+    /**
+     * 确认的基金份额数量
+     */
+    private String share;
+    private String nav;
+}

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

@@ -1,4 +1,4 @@
-package com.smppw.modaq.domain.dto.report;
+package com.smppw.modaq.domain.dto.report.ocr;
 
 import lombok.Getter;
 import lombok.Setter;

+ 54 - 23
mo-daq/src/main/java/com/smppw/modaq/domain/service/EmailParseService.java

@@ -26,10 +26,9 @@ import com.smppw.modaq.common.enums.ReportType;
 import com.smppw.modaq.common.exception.NotSupportReportException;
 import com.smppw.modaq.common.exception.ReportParseException;
 import com.smppw.modaq.domain.dto.*;
-import com.smppw.modaq.domain.dto.report.OCRParseData;
-import com.smppw.modaq.domain.dto.report.ParseResult;
-import com.smppw.modaq.domain.dto.report.ReportData;
-import com.smppw.modaq.domain.dto.report.ReportParserParams;
+import com.smppw.modaq.domain.dto.report.*;
+import com.smppw.modaq.domain.dto.report.ocr.OCRLetterParseData;
+import com.smppw.modaq.domain.dto.report.ocr.OCRParseData;
 import com.smppw.modaq.domain.entity.EmailFileInfoDO;
 import com.smppw.modaq.domain.entity.EmailParseInfoDO;
 import com.smppw.modaq.domain.mapper.EmailFileInfoMapper;
@@ -684,9 +683,9 @@ public class EmailParseService {
         if (reportData == null || CollUtil.isEmpty(images)) {
             return;
         }
-        OCRParseData parseRes = null;
         // 报告才识别尾页的印章和联系人,确认单不识别尾页
         if (ReportType.LETTER != reportType) {
+            OCRParseData parseRes = null;
             try {
                 // 首页和尾页相等时只读首页
                 String imageUrl = images.size() == 1 ? images.get(0) : images.get(1);
@@ -704,26 +703,58 @@ public class EmailParseService {
                     }
                 }
             }
-        }
-        // 首页和尾页不相等时解析首页的数据
-        if (images.size() != 1) {
-            try {
-                parseRes = new OCRReportParser().parse(fileName, this.ocrParserUrl, images.get(0));
-            } catch (Exception e) {
-                log.error("报告{} OCR识别首页基金名称和报告日期出错:{}", fileName, e.getMessage());
-            }
-        }
-        // 用首页识别基金名称、产品代码和基金管理人
-        if (reportData.getFundInfo() != null && parseRes != null) {
-            if (StrUtil.isBlank(reportData.getFundInfo().getFundName())) {
-                reportData.getFundInfo().setFundName(parseRes.getFundName());
+            // 首页和尾页不相等时解析首页的数据
+            if (images.size() != 1) {
+                try {
+                    parseRes = new OCRReportParser().parse(fileName, this.ocrParserUrl, images.get(0));
+                } catch (Exception e) {
+                    log.error("报告{} OCR识别首页基金名称和报告日期出错:{}", fileName, e.getMessage());
+                }
             }
-            if (StrUtil.isBlank(reportData.getFundInfo().getFundCode())) {
-                reportData.getFundInfo().setFundCode(parseRes.getFundCode());
+            // 用首页识别基金名称、产品代码和基金管理人
+            if (reportData.getFundInfo() != null && parseRes != null) {
+                if (StrUtil.isBlank(reportData.getFundInfo().getFundName())) {
+                    reportData.getFundInfo().setFundName(parseRes.getFundName());
+                }
+                if (StrUtil.isBlank(reportData.getFundInfo().getFundCode())) {
+                    reportData.getFundInfo().setFundCode(parseRes.getFundCode());
+                }
+                if (StrUtil.isBlank(reportData.getFundInfo().getCompanyName())
+                        || !reportData.getFundInfo().getCompanyName().contains("有限公司")) {
+                    reportData.getFundInfo().setCompanyName(parseRes.getCompanyName());
+                }
             }
-            if (StrUtil.isBlank(reportData.getFundInfo().getCompanyName())
-                    || !reportData.getFundInfo().getCompanyName().contains("有限公司")) {
-                reportData.getFundInfo().setCompanyName(parseRes.getCompanyName());
+        } else {
+            // 确认单AI解析失败时重新用OCR识别
+            LetterReportData letterReportData = (LetterReportData) reportData;
+            if (letterReportData.wasFailed()) {
+                OCRLetterParseData parseRes = new OCRReportParser().parseLetterData(fileName, this.ocrParserUrl, images.get(0));
+                if (parseRes == null) {
+                    return;
+                }
+                if (letterReportData.getFundInfo() != null) {
+                    letterReportData.getFundInfo().setFundName(parseRes.getFundName());
+                    letterReportData.getFundInfo().setFundCode(parseRes.getFundCode());
+                }
+                if (letterReportData.getInvestorInfo() == null) {
+                    letterReportData.setInvestorInfo(new ReportInvestorInfoDTO());
+                }
+                letterReportData.getInvestorInfo().setInvestorName(parseRes.getInvestorName());
+                letterReportData.getInvestorInfo().setCertificateNumber(parseRes.getCertificateNumber());
+                letterReportData.getInvestorInfo().setTradingAccount(parseRes.getTradingAccount());
+                letterReportData.getInvestorInfo().setFundAccount(parseRes.getFundAccount());
+                letterReportData.getInvestorInfo().setCertificateType(parseRes.getCertificateType());
+                if (letterReportData.getFundTransaction() == null) {
+                    letterReportData.setFundTransaction(new ReportFundTransactionDTO());
+                }
+                letterReportData.getFundTransaction().setTransactionType(parseRes.getTransactionType());
+                letterReportData.getFundTransaction().setApplyDate(parseRes.getApplyDate());
+                letterReportData.getFundTransaction().setApplyShare(parseRes.getApplyShare());
+                letterReportData.getFundTransaction().setApplyAmount(parseRes.getApplyAmount());
+                letterReportData.getFundTransaction().setHoldingDate(parseRes.getHoldingDate());
+                letterReportData.getFundTransaction().setAmount(parseRes.getAmount());
+                letterReportData.getFundTransaction().setShare(parseRes.getShare());
+                letterReportData.getFundTransaction().setNav(parseRes.getNav());
             }
         }
     }

+ 38 - 1
mo-daq/src/main/java/com/smppw/modaq/infrastructure/util/DateUtils.java

@@ -6,10 +6,11 @@ import cn.hutool.core.util.StrUtil;
 import com.smppw.modaq.common.conts.DateConst;
 import com.smppw.modaq.common.conts.PatternConsts;
 
+import java.math.BigDecimal;
 import java.util.Date;
 import java.util.regex.Matcher;
 
-public class DateUtils {
+public class ConvertUtil {
     /**
      * 字符串转日期类型
      *
@@ -43,4 +44,40 @@ public class DateUtils {
         }
         return null;
     }
+
+    public static BigDecimal toBigDecimal(String input) {
+        return toBigDecimal(input, null);
+    }
+
+    /**
+     * 字符串转数字,如果数据没有或者转换失败则用defaultValue字段填充
+     *
+     * @param input        待转换的字符串
+     * @param defaultValue 为null或者数字转换失败的默认值
+     * @return /
+     */
+    public static BigDecimal toBigDecimal(String input, BigDecimal defaultValue) {
+        if (StrUtil.isBlank(input)) {
+            return defaultValue;
+        }
+        try {
+            // 1. 清理输入,保留有效字符(数字、正负号、小数点、科学计数法符号、千分位逗号)
+            String cleanedInput = input.trim().replaceAll("[^\\d.,\\-+Ee]", "");
+            // 2. 移除所有空格(包括中间的空格)
+            cleanedInput = cleanedInput.replaceAll("\\s+", "");
+
+            // 3. 处理千分位逗号(替换为"",因为千分位逗号在最终数字中无效)
+            cleanedInput = cleanedInput.replaceAll(",", "");
+
+            // 4. 处理科学计数法中的符号(如 E 或 e 后的符号)
+            // 例如:"1.23E+4" → "1.23E4"(可选,但 Java 的 BigDecimal 可直接处理)
+
+            // 5. 验证最终格式是否合法(可选,但可能影响性能)
+            // 这里直接交给 BigDecimal 处理,捕获异常即可
+
+            return new BigDecimal(cleanedInput);
+        } catch (NumberFormatException e) {
+            return defaultValue;
+        }
+    }
 }

+ 5 - 5
mo-daq/src/test/java/com/smppw/modaq/MoDaqApplicationTests.java

@@ -25,9 +25,9 @@ public class MoDaqApplicationTests {
 
     @Test
     public void letterTest() {
-        MailboxInfoDTO emailInfoDTO = this.buildMailbox("*@simuwang.com", "*");
-        Date startDate = DateUtil.parse("2025-06-09 14:55:00", DateConst.YYYY_MM_DD_HH_MM_SS);
-        Date endDate = DateUtil.parse("2025-06-09 18:56:00", DateConst.YYYY_MM_DD_HH_MM_SS);
+        MailboxInfoDTO emailInfoDTO = this.buildMailbox("**@simuwang.com", "**");
+        Date startDate = DateUtil.parse("2025-06-18 08:47:00", DateConst.YYYY_MM_DD_HH_MM_SS);
+        Date endDate = DateUtil.parse("2025-06-18 18:56:00", DateConst.YYYY_MM_DD_HH_MM_SS);
         try {
             List<String> folderNames = ListUtil.list(false);
 //            folderNames.add("其他文件夹/报告公告");
@@ -42,8 +42,8 @@ public class MoDaqApplicationTests {
     @Test
     public void reportTest() {
         MailboxInfoDTO emailInfoDTO = this.buildMailbox("**@simuwang.com", "**");
-        Date startDate = DateUtil.parse("2025-06-12 13:54:00", DateConst.YYYY_MM_DD_HH_MM_SS);
-        Date endDate = DateUtil.parse("2025-06-12 13:57:00", DateConst.YYYY_MM_DD_HH_MM_SS);
+        Date startDate = DateUtil.parse("2025-06-18 08:47:00", DateConst.YYYY_MM_DD_HH_MM_SS);
+        Date endDate = DateUtil.parse("2025-06-18 13:57:00", DateConst.YYYY_MM_DD_HH_MM_SS);
         try {
             List<String> folderNames = ListUtil.list(false);
 //            folderNames.add("其他文件夹/报告公告");