package com.simuwang.daq.service; import cn.hutool.core.bean.BeanUtil; 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.map.MapUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.http.HttpUtil; import cn.hutool.json.JSONUtil; import com.simuwang.base.common.conts.DateConst; import com.simuwang.base.common.conts.EmailParseStatusConst; import com.simuwang.base.common.conts.EmailTypeConst; import com.simuwang.base.common.conts.NavParseStatusConst; import com.simuwang.base.common.util.EmailUtil; import com.simuwang.base.common.util.FileUtil; import com.simuwang.base.config.DaqProperties; import com.simuwang.base.config.EmailRuleConfig; import com.simuwang.base.mapper.*; import com.simuwang.base.pojo.dos.*; import com.simuwang.base.pojo.dto.EmailContentInfoDTO; import com.simuwang.base.pojo.dto.EmailFundNavDTO; import com.simuwang.base.pojo.dto.MailboxInfoDTO; import com.simuwang.base.pojo.dto.report.PythonResult; import com.simuwang.base.pojo.dto.report.ReportData; import com.simuwang.daq.components.PythonReportConverter; import com.simuwang.daq.components.writer.ReportWriterFactory; import jakarta.mail.*; import jakarta.mail.internet.MimeMultipart; import jakarta.mail.search.ComparisonTerm; import jakarta.mail.search.ReceivedDateTerm; import jakarta.mail.search.SearchTerm; 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 java.io.File; import java.math.BigDecimal; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; /** * @author mozuwen * @date 2024-09-04 * @description 邮件解析服务 */ @Service public class EmailParseService { private static final Logger log = LoggerFactory.getLogger(EmailParseService.class); private final String pyBaseUrl; private final EmailTypeRuleMapper emailTypeRuleMapper; private final EmailRuleConfig emailRuleConfig; private final EmailFieldMappingMapper emailFieldMapper; private final EmailParserFactory emailParserFactory; private final EmailParseInfoMapper emailParseInfoMapper; private final EmailFileInfoMapper emailFileInfoMapper; private final EmailFundNavMapper emailFundNavMapper; private final EmailFundAssetMapper emailFundAssetMapper; private final AssetMapper assetMapper; private final NavMapper navMapper; private final FundService fundService; private final FundAliasMapper fundAliasMapper; @Value("${email.file.path}") private String path; @Autowired private FundInfoMapper fundInfoMapper; @Autowired private ReportWriterFactory reportWriterFactory; public EmailParseService(EmailTypeRuleMapper emailTypeRuleMapper, EmailRuleConfig emailRuleConfig, EmailFieldMappingMapper emailFieldMapper, EmailParserFactory emailParserFactory, EmailParseInfoMapper emailParseInfoMapper, EmailFileInfoMapper emailFileInfoMapper, EmailFundNavMapper emailFundNavMapper, EmailFundAssetMapper emailFundAssetMapper, AssetMapper assetMapper, NavMapper navMapper, FundService fundService, FundAliasMapper fundAliasMapper, DaqProperties properties) { this.emailTypeRuleMapper = emailTypeRuleMapper; this.emailRuleConfig = emailRuleConfig; this.emailFieldMapper = emailFieldMapper; this.emailParserFactory = emailParserFactory; this.emailParseInfoMapper = emailParseInfoMapper; this.emailFileInfoMapper = emailFileInfoMapper; this.emailFundNavMapper = emailFundNavMapper; this.emailFundAssetMapper = emailFundAssetMapper; this.assetMapper = assetMapper; this.navMapper = navMapper; this.fundService = fundService; this.fundAliasMapper = fundAliasMapper; this.pyBaseUrl = properties.getPyBaseUrl(); } /** * 解析指定邮箱指定时间范围内的邮件 * * @param mailboxInfoDTO 邮箱配置信息 * @param startDate 邮件起始日期(yyyy-MM-dd HH:mm:ss) * @param endDate 邮件截止日期(yyyy-MM-dd HH:mm:ss, 为null,将解析邮件日期小于等于startDate的当天邮件) */ public void parseEmail(MailboxInfoDTO mailboxInfoDTO, Date startDate, Date endDate) { log.info("开始邮件解析 -> 邮箱信息:{},开始时间:{},结束时间:{}", mailboxInfoDTO, DateUtil.format(startDate, DateConst.YYYY_MM_DD_HH_MM_SS), DateUtil.format(endDate, DateConst.YYYY_MM_DD_HH_MM_SS)); // 邮件类型配置 Map> emailTypeMap = getEmailType(); // 邮件字段识别映射表 Map> emailFieldMap = getEmailFieldMapping(); Map> emailContentMap; try { emailContentMap = realEmail(mailboxInfoDTO, emailTypeMap, startDate, endDate); } catch (Exception e) { log.info("采集邮件失败 -> 邮箱配置信息:{},堆栈信息:{}", mailboxInfoDTO, ExceptionUtil.stacktraceToString(e)); return; } for (Map.Entry> emailEntry : emailContentMap.entrySet()) { List emailContentInfoDTOList = emailEntry.getValue(); if (CollUtil.isEmpty(emailContentInfoDTOList)) { log.warn("未采集到正文或附件"); continue; } log.info("开始解析邮件数据 -> 邮件主题:{},邮件日期:{}", emailContentInfoDTOList.get(0).getEmailTitle(), emailContentInfoDTOList.get(0).getEmailDate()); List emailFundNavDTOList = CollUtil.newArrayList(); Map> fileNameNavMap = MapUtil.newHashMap(); for (EmailContentInfoDTO emailContentInfoDTO : emailContentInfoDTOList) { try { List fundNavDTOList = parseEmail(emailContentInfoDTO, emailFieldMap); fileNameNavMap.put(emailContentInfoDTO, fundNavDTOList); emailFundNavDTOList.addAll(fundNavDTOList); } catch (Exception e) { log.error("堆栈信息:{}", ExceptionUtil.stacktraceToString(e)); } } // 保存相关信息 -> 邮件信息表,邮件文件表,邮件净值表,邮件规模表,基金净值表 saveRelatedTable(mailboxInfoDTO.getAccount(), emailContentInfoDTOList, fileNameNavMap); log.info("结束邮件解析 -> 邮箱信息:{},开始时间:{},结束时间:{}", mailboxInfoDTO, DateUtil.format(startDate, DateConst.YYYY_MM_DD_HH_MM_SS), DateUtil.format(endDate, DateConst.YYYY_MM_DD_HH_MM_SS)); } } public List parseEmail(EmailContentInfoDTO emailContentInfoDTO, Map> emailFieldMap) { Integer emailType = emailContentInfoDTO.getEmailType(); AbstractEmailParser emailParser = emailParserFactory.getInstance(emailType); return emailParser.parse(emailContentInfoDTO, emailFieldMap); } public void saveRelatedTable(String emailAddress, List emailContentInfoDTOList, Map> fileNameNavMap) { String emailTitle = CollUtil.isNotEmpty(emailContentInfoDTOList) ? emailContentInfoDTOList.get(0).getEmailTitle() : null; String emailDate = CollUtil.isNotEmpty(emailContentInfoDTOList) ? emailContentInfoDTOList.get(0).getEmailDate() : null; Integer emailType = CollUtil.isNotEmpty(emailContentInfoDTOList) ? emailContentInfoDTOList.get(0).getEmailType() : null; Integer emailId = CollUtil.isNotEmpty(emailContentInfoDTOList) ? emailContentInfoDTOList.get(0).getEmailId() : null; String senderEmail = CollUtil.isNotEmpty(emailContentInfoDTOList) ? emailContentInfoDTOList.get(0).getSenderEmail() : null; Date parseDate = new Date(); int emailParseStatus = EmailParseStatusConst.SUCCESS; EmailParseInfoDO emailParseInfoDO = buildEmailParseInfo(emailId, emailAddress, senderEmail, emailDate, emailTitle, emailType, emailParseStatus, parseDate); emailId = saveEmailParseInfo(emailParseInfoDO); // python 报告解析接口结果 List dataList = ListUtil.list(false); for (Map.Entry> fileNameNavEntry : fileNameNavMap.entrySet()) { // 保存邮件文件表 EmailContentInfoDTO emailContentInfoDTO = fileNameNavEntry.getKey(); String fileName = emailContentInfoDTO.getFileName(); Integer fileId = saveEmailFileInfo(emailId, emailContentInfoDTO.getFileId(), fileName, emailContentInfoDTO.getFilePath(), parseDate); List fundNavDTOList = fileNameNavEntry.getValue(); if (CollUtil.isEmpty(fundNavDTOList) && !Objects.equals(EmailTypeConst.REPORT_EMAIL_TYPE, emailType)) { continue; } // python接口解析结果 ReportData data = this.requestPyAndResult(fileId, emailContentInfoDTO); if (data != null) { // 保存报告解析数据 this.reportWriterFactory.getInstance(data.getReportType()).write(data); dataList.add(data); } for (EmailFundNavDTO fundNavDTO : fundNavDTOList) { // 设置净值数据的解析状态 setNavParseStatus(fundNavDTO, emailTitle); } // 保存净值表和规模表 saveNavAndAssetNet(fileId, fundNavDTOList, parseDate); } // 更新邮件解析结果 -> 当【净值日期】和【备案编码/基金名称】能正常解读,即识别为【成功】 long successNavCount = fileNameNavMap.values().stream().flatMap(List::stream).filter(Objects::nonNull).count(); emailParseStatus = successNavCount >= 1 ? EmailParseStatusConst.SUCCESS : EmailParseStatusConst.FAIL; // 报告邮件有一条成功就表示整体成功 if (Objects.equals(EmailTypeConst.REPORT_EMAIL_TYPE, emailType) && CollUtil.isNotEmpty(dataList)) { long count = dataList.size(); emailParseStatus = count >= 1 ? EmailParseStatusConst.SUCCESS : EmailParseStatusConst.FAIL; } emailParseInfoMapper.updateParseStatus(emailId, emailParseStatus); } private ReportData requestPyAndResult(int fileId, EmailContentInfoDTO emailContentInfoDTO) { String fileName = emailContentInfoDTO.getFileName(); Integer emailType = emailContentInfoDTO.getEmailType(); ReportData reportData = null; if (Objects.equals(EmailTypeConst.REPORT_EMAIL_TYPE, emailType)) { if (StrUtil.isBlank(fileName)) { return null; } Pattern pattern = Pattern.compile("S(?:[A-Z]{0}[0-9]{5}|[A-Z][0-9]{4}|[A-Z]{2}[0-9]{3}|[A-Z]{3}[0-9]{2})"); Matcher matcher = pattern.matcher(fileName); String registerNumber = null; if (matcher.find()) { registerNumber = matcher.group(); } int type = 0; if (fileName.contains("季报")) { type = 1; } else if (fileName.contains("年报")) { type = 2; } String api = "/api/v1/parse/amac_report"; Map params = MapUtil.newHashMap(16); params.put("file_id", fileId); params.put("file_path", emailContentInfoDTO.getFilePath()); params.put("register_number", registerNumber); params.put("file_type", type); params.put("file_name", fileName); if (StrUtil.isNotBlank(registerNumber)) { FundAndCompanyInfoDO info = this.fundInfoMapper.queryFundAndTrustByRegisterNumber(registerNumber); if (info != null) { params.put("fund_name", info.getFundName()); params.put("trust_name", info.getCompanyName()); } } long millis = System.currentTimeMillis(); try { String body = HttpUtil.post(this.pyBaseUrl + api, JSONUtil.toJsonStr(params)); PythonResult result = PythonReportConverter.convert(JSONUtil.parseObj(body), type); reportData = result.getData(); } catch (Exception e) { log.error("请求python的报告解析接口报错\n{}", ExceptionUtil.stacktraceToString(e)); } finally { if (log.isInfoEnabled()) { log.info("当前报告{}解析完成,总计耗时{}ms", params, (System.currentTimeMillis() - millis)); } } } return reportData; } private void saveNavAndAssetNet(Integer fileId, List fundNavDTOList, Date parseDate) { if (CollUtil.isEmpty(fundNavDTOList)) { return; } // 净值数据 List emailFundNavDOList = fundNavDTOList.stream() .map(e -> buildEmailFundNavDo(fileId, e, parseDate)).filter(CollUtil::isNotEmpty).flatMap(List::stream).collect(Collectors.toList()); if (CollUtil.isNotEmpty(emailFundNavDOList)) { // 先删除文件id下的净值数据(考虑到重新解析的需求,如果是首次解析,那么file_id下不存在净值数据) emailFundNavMapper.deleteByFileId(fileId); emailFundNavMapper.batchInsert(emailFundNavDOList); List navDOList = emailFundNavDOList.stream().filter(e -> StrUtil.isNotBlank(e.getFundId())) .map(e -> BeanUtil.copyProperties(e, NavDO.class)).collect(Collectors.toList()); saveNavDo(navDOList); } // 保存规模数据 List emailFundAssetDOList = fundNavDTOList.stream() .map(e -> buildEmailFundAssetDo(fileId, e, parseDate)).filter(CollUtil::isNotEmpty).flatMap(List::stream).collect(Collectors.toList()); if (CollUtil.isNotEmpty(emailFundAssetDOList)) { // 先删除file_id下的规模数据(考虑到重新解析的需求,如果是首次解析,那么file_id下不存在规模数据) emailFundAssetMapper.deleteByFileId(fileId); emailFundAssetMapper.batchInsert(emailFundAssetDOList); List assetDOList = emailFundAssetDOList.stream().filter(e -> StrUtil.isNotBlank(e.getFundId())) .map(e -> BeanUtil.copyProperties(e, AssetDO.class)).collect(Collectors.toList()); saveAssetDo(assetDOList); } } public void saveNavDo(List navDOList) { if (CollUtil.isEmpty(navDOList)) { return; } Map> fundIdNavMap = navDOList.stream().collect(Collectors.groupingBy(NavDO::getFundId)); for (Map.Entry> entry : fundIdNavMap.entrySet()) { List navDOS = entry.getValue(); List priceDateList = navDOS.stream().map(NavDO::getPriceDate).map(e -> DateUtil.format(e, DateConst.YYYY_MM_DD)).collect(Collectors.toList()); List dateList = navMapper.queryFundNavByDate(entry.getKey(), priceDateList); List updateNavDoList = navDOS.stream().filter(e -> dateList.contains(DateUtil.format(e.getPriceDate(), DateConst.YYYY_MM_DD))).collect(Collectors.toList()); List insertNavDoList = navDOS.stream().filter(e -> !dateList.contains(DateUtil.format(e.getPriceDate(), DateConst.YYYY_MM_DD))).collect(Collectors.toList()); if (CollUtil.isNotEmpty(insertNavDoList)) { Map> priceDateNavDoListMap = insertNavDoList.stream().collect(Collectors.groupingBy(NavDO::getPriceDate)); boolean hasDuplicationDateData = priceDateNavDoListMap.values().stream().map(List::size).anyMatch(e -> e > 1); if (!hasDuplicationDateData) { navMapper.batchInsert(insertNavDoList); } // 要插入的数据中存在相同日期的数据 -> 只能一条一条的插入数据了 insertNavDoList.forEach(e -> saveNavDo(ListUtil.toList(e))); } if (CollUtil.isNotEmpty(updateNavDoList)) { navMapper.batchUpdate(updateNavDoList); } } } public void saveAssetDo(List assetDOList) { if (CollUtil.isEmpty(assetDOList)) { return; } Map> fundIdNavMap = assetDOList.stream().collect(Collectors.groupingBy(AssetDO::getFundId)); for (Map.Entry> entry : fundIdNavMap.entrySet()) { List assetDOS = entry.getValue(); List priceDateList = assetDOS.stream().map(AssetDO::getPriceDate).map(e -> DateUtil.format(e, DateConst.YYYY_MM_DD)).collect(Collectors.toList()); List dateList = assetMapper.queryFundNavByDate(entry.getKey(), priceDateList); List updateAssetDoList = assetDOS.stream().filter(e -> dateList.contains(DateUtil.format(e.getPriceDate(), DateConst.YYYY_MM_DD))).collect(Collectors.toList()); List insertAssetDoList = assetDOS.stream().filter(e -> !dateList.contains(DateUtil.format(e.getPriceDate(), DateConst.YYYY_MM_DD))).collect(Collectors.toList()); if (CollUtil.isNotEmpty(insertAssetDoList)) { Map> priceDateAssetDoListMap = insertAssetDoList.stream().collect(Collectors.groupingBy(AssetDO::getPriceDate)); boolean hasDuplicationDateData = priceDateAssetDoListMap.values().stream().map(List::size).anyMatch(e -> e > 1); if (!hasDuplicationDateData) { assetMapper.batchInsert(insertAssetDoList); } // 要插入的数据中存在相同日期的数据 -> 只能一条一条的插入数据了 insertAssetDoList.forEach(e -> saveAssetDo(ListUtil.toList(e))); } if (CollUtil.isNotEmpty(updateAssetDoList)) { assetMapper.batchUpdate(updateAssetDoList); } } } private List buildEmailFundAssetDo(Integer fileId, EmailFundNavDTO fundNavDTO, Date parseDate) { List fundAssetDOList = CollUtil.newArrayList(); BigDecimal assetNet = StrUtil.isNotBlank(fundNavDTO.getAssetNet()) ? new BigDecimal(fundNavDTO.getAssetNet()) : null; BigDecimal assetShare = StrUtil.isNotBlank(fundNavDTO.getAssetShare()) ? new BigDecimal(fundNavDTO.getAssetShare()) : null; if (assetNet == null) { return fundAssetDOList; } Integer isStored = fundNavDTO.getParseStatus() != null && (fundNavDTO.getParseStatus().equals(NavParseStatusConst.ASSET_NET_NEGATIVE) || fundNavDTO.getParseStatus().equals(NavParseStatusConst.SUCCESS)) ? 1 : 0; Date priceDate = DateUtil.parse(fundNavDTO.getPriceDate(), DateConst.YYYY_MM_DD); if (CollUtil.isNotEmpty(fundNavDTO.getFundIdList())) { for (String fundId : fundNavDTO.getFundIdList()) { EmailFundAssetDO emailFundAssetDO = new EmailFundAssetDO(); emailFundAssetDO.setFileId(fileId); emailFundAssetDO.setPriceDate(priceDate); emailFundAssetDO.setFundId(fundId); emailFundAssetDO.setFundName(fundNavDTO.getFundName()); emailFundAssetDO.setRegisterNumber(fundNavDTO.getRegisterNumber()); emailFundAssetDO.setAssetNet(assetNet); emailFundAssetDO.setAssetShare(assetShare); emailFundAssetDO.setIsStored(isStored); emailFundAssetDO.setExceptionStatus(fundNavDTO.getParseStatus()); emailFundAssetDO.setIsvalid(1); emailFundAssetDO.setCreatorId(0); emailFundAssetDO.setCreateTime(parseDate); emailFundAssetDO.setUpdaterId(0); emailFundAssetDO.setUpdateTime(parseDate); fundAssetDOList.add(emailFundAssetDO); } } else { EmailFundAssetDO emailFundAssetDO = new EmailFundAssetDO(); emailFundAssetDO.setFileId(fileId); emailFundAssetDO.setPriceDate(priceDate); emailFundAssetDO.setFundName(fundNavDTO.getFundName()); emailFundAssetDO.setRegisterNumber(fundNavDTO.getRegisterNumber()); emailFundAssetDO.setAssetNet(assetNet); emailFundAssetDO.setAssetShare(assetShare); emailFundAssetDO.setIsStored(isStored); emailFundAssetDO.setExceptionStatus(fundNavDTO.getParseStatus()); emailFundAssetDO.setIsvalid(1); emailFundAssetDO.setCreatorId(0); emailFundAssetDO.setCreateTime(parseDate); emailFundAssetDO.setUpdaterId(0); emailFundAssetDO.setUpdateTime(parseDate); fundAssetDOList.add(emailFundAssetDO); } return fundAssetDOList; } private List buildEmailFundNavDo(Integer fileId, EmailFundNavDTO fundNavDTO, Date parseDate) { List fundNavDOList = CollUtil.newArrayList(); Date priceDate = DateUtil.parse(fundNavDTO.getPriceDate(), DateConst.YYYY_MM_DD); BigDecimal nav = StrUtil.isNotBlank(fundNavDTO.getNav()) ? new BigDecimal(fundNavDTO.getNav()) : null; BigDecimal cumulativeNavWithdrawal = StrUtil.isNotBlank(fundNavDTO.getCumulativeNavWithdrawal()) ? new BigDecimal(fundNavDTO.getCumulativeNavWithdrawal()) : null; if (nav == null && cumulativeNavWithdrawal == null) { return CollUtil.newArrayList(); } Integer isStored = fundNavDTO.getParseStatus() != null && !fundNavDTO.getParseStatus().equals(NavParseStatusConst.NAV_DEFICIENCY) && !fundNavDTO.getParseStatus().equals(NavParseStatusConst.NOT_MATCH) ? 1 : 0; if (CollUtil.isNotEmpty(fundNavDTO.getFundIdList())) { for (String fundId : fundNavDTO.getFundIdList()) { EmailFundNavDO emailFundNavDO = new EmailFundNavDO(); emailFundNavDO.setFileId(fileId); emailFundNavDO.setIsStored(isStored); emailFundNavDO.setPriceDate(priceDate); emailFundNavDO.setNav(nav); emailFundNavDO.setFundId(fundId); emailFundNavDO.setCumulativeNavWithdrawal(cumulativeNavWithdrawal); emailFundNavDO.setFundName(fundNavDTO.getFundName()); emailFundNavDO.setRegisterNumber(fundNavDTO.getRegisterNumber()); emailFundNavDO.setExceptionStatus(fundNavDTO.getParseStatus()); emailFundNavDO.setTemplateId(fundNavDTO.getTemplateId()); emailFundNavDO.setIsvalid(1); emailFundNavDO.setCreatorId(0); emailFundNavDO.setCreateTime(parseDate); emailFundNavDO.setUpdaterId(0); emailFundNavDO.setUpdateTime(parseDate); fundNavDOList.add(emailFundNavDO); } } else { EmailFundNavDO emailFundNavDO = new EmailFundNavDO(); emailFundNavDO.setFileId(fileId); emailFundNavDO.setPriceDate(priceDate); emailFundNavDO.setNav(nav); emailFundNavDO.setCumulativeNavWithdrawal(cumulativeNavWithdrawal); emailFundNavDO.setFundName(fundNavDTO.getFundName()); emailFundNavDO.setRegisterNumber(fundNavDTO.getRegisterNumber()); emailFundNavDO.setExceptionStatus(fundNavDTO.getParseStatus()); emailFundNavDO.setTemplateId(fundNavDTO.getTemplateId()); emailFundNavDO.setIsStored(isStored); emailFundNavDO.setIsvalid(1); emailFundNavDO.setCreatorId(0); emailFundNavDO.setCreateTime(parseDate); emailFundNavDO.setUpdaterId(0); emailFundNavDO.setUpdateTime(parseDate); fundNavDOList.add(emailFundNavDO); } return fundNavDOList; } private Integer saveEmailFileInfo(Integer emailId, Integer fileId, String fileName, String filePath, Date parseDate) { EmailFileInfoDO emailFileInfoDO = buildEmailFileInfoDO(emailId, fileId, fileName, filePath, parseDate); if (emailFileInfoDO.getId() != null) { emailFileInfoMapper.updateTimeById(fileId, parseDate); return emailFileInfoDO.getId(); } emailFileInfoMapper.insert(emailFileInfoDO); return emailFileInfoDO.getId(); } private EmailFileInfoDO buildEmailFileInfoDO(Integer emailId, Integer fileId, String fileName, String filePath, Date parseDate) { EmailFileInfoDO emailFileInfoDO = new EmailFileInfoDO(); emailFileInfoDO.setId(fileId); emailFileInfoDO.setEmailId(emailId); emailFileInfoDO.setFileName(fileName); emailFileInfoDO.setFilePath(filePath); emailFileInfoDO.setIsvalid(1); emailFileInfoDO.setCreatorId(0); emailFileInfoDO.setCreateTime(parseDate); emailFileInfoDO.setUpdaterId(0); emailFileInfoDO.setUpdateTime(parseDate); return emailFileInfoDO; } private void setNavParseStatus(EmailFundNavDTO fundNavDTO, String emailTitle) { // 1.单位净值或累计净值缺失 if (StrUtil.isBlank(fundNavDTO.getNav()) || StrUtil.isBlank(fundNavDTO.getCumulativeNavWithdrawal())) { fundNavDTO.setParseStatus(NavParseStatusConst.NAV_DEFICIENCY); return; } // 2.匹配基金(考虑到解析估值表时已经匹配上基金的情况) List fundIdList = fundNavDTO.getFundIdList(); if (CollUtil.isEmpty(fundIdList)) { fundIdList = fundService.getFundIdByNamesAndCode(fundNavDTO.getFundName(), fundNavDTO.getRegisterNumber()); if (CollUtil.isEmpty(fundIdList)) { fundNavDTO.setParseStatus(NavParseStatusConst.NOT_MATCH); } } fundNavDTO.setFundIdList(fundIdList); // 写入别名管理表fund_alias saveFundAlias(fundNavDTO.getFundName(), fundNavDTO.getRegisterNumber(), fundIdList); if (CollUtil.isEmpty(fundIdList)) { return; } // 考虑单独规模文件时 -> 无单位净值和累计净值 // 3.单位净值或累计净值不大于0 if (!emailTitle.contains("规模")) { if (StrUtil.isBlank(fundNavDTO.getNav()) || StrUtil.isBlank(fundNavDTO.getCumulativeNavWithdrawal()) || (fundNavDTO.getNav().compareTo("0") <= 0 || fundNavDTO.getCumulativeNavWithdrawal().compareTo("0") <= 0)) { fundNavDTO.setParseStatus(NavParseStatusConst.NAV_NEGATIVE); return; } } // 4.资产净值不大于0 if (StrUtil.isNotBlank(fundNavDTO.getAssetNet()) && fundNavDTO.getAssetNet().compareTo("0") <= 0) { fundNavDTO.setParseStatus(NavParseStatusConst.ASSET_NET_NEGATIVE); return; } fundNavDTO.setParseStatus(NavParseStatusConst.SUCCESS); } private void saveFundAlias(String fundName, String registerNumber, List fundIdList) { // 未识别到基金名称和备案编码的数据不写入别名管理 if (StrUtil.isBlank(fundName) && StrUtil.isBlank(registerNumber)) { return; } List fundAliasDOList = CollUtil.newArrayList(); if (StrUtil.isNotBlank(fundName) && StrUtil.isNotBlank(registerNumber)) { fundAliasDOList = fundAliasMapper.queryFundIdByNameAndRegisterNumber(fundName, registerNumber); } if (StrUtil.isBlank(fundName) && StrUtil.isNotBlank(registerNumber)) { fundAliasDOList = fundAliasMapper.queryFundIdByRegisterNumber(registerNumber); } if (StrUtil.isNotBlank(fundName) && StrUtil.isBlank(registerNumber)) { fundAliasDOList = fundAliasMapper.queryFundIdByName(fundName); } // 未匹配基金且已写入别名表 if (CollUtil.isEmpty(fundIdList) && CollUtil.isNotEmpty(fundAliasDOList)) { return; } // 未匹配基金且未写入别名表 if (CollUtil.isEmpty(fundIdList) && CollUtil.isEmpty(fundAliasDOList)) { fundAliasMapper.batchInsert(ListUtil.toList(buildFundAliasDO(fundName, registerNumber, null))); log.info("写入别名表 -> 基金名称:{},备案编码:{},基金id:{}", fundName, registerNumber, fundIdList); return; } // 匹配上基金 -> 需要判断此时基金id是否已经在别名管理表 if (CollUtil.isNotEmpty(fundAliasDOList)) { List collect = fundAliasDOList.stream().filter(Objects::nonNull).map(FundAliasDO::getTargetFundId).filter(Objects::nonNull).distinct().toList(); fundIdList = fundIdList.stream().filter(e -> !collect.contains(e)).toList(); } List fundAliasDOS = CollUtil.isNotEmpty(fundIdList) ? fundIdList.stream().map(e -> buildFundAliasDO(fundName, registerNumber, e)).toList() : null; if (CollUtil.isNotEmpty(fundAliasDOS)) { fundAliasMapper.batchInsert(fundAliasDOS); log.info("写入别名表 -> 基金名称:{},备案编码:{},基金id:{}", fundName, registerNumber, fundIdList); } } public FundAliasDO buildFundAliasDO(String fundName, String registerNumber, String fundId) { FundAliasDO fundAliasDO = new FundAliasDO(); fundAliasDO.setTargetFundId(fundId); fundAliasDO.setSourceFundName(fundName); fundAliasDO.setSourceRegisterNumber(registerNumber); fundAliasDO.setIsvalid(1); fundAliasDO.setCreatorId(0); fundAliasDO.setCreateTime(new Date()); fundAliasDO.setUpdateTime(new Date()); fundAliasDO.setUpdaterId(0); return fundAliasDO; } private Integer saveEmailParseInfo(EmailParseInfoDO emailParseInfoDO) { if (emailParseInfoDO == null) { return null; } // 重新邮件功能 -> 修改解析时间和更新时间 if (emailParseInfoDO.getId() != null) { emailParseInfoMapper.updateParseTime(emailParseInfoDO.getId(), emailParseInfoDO.getParseDate()); return emailParseInfoDO.getId(); } emailParseInfoMapper.insert(emailParseInfoDO); return emailParseInfoDO.getId(); } private EmailParseInfoDO buildEmailParseInfo(Integer emailId, String emailAddress, String senderEmail, String emailDate, String emailTitle, Integer emailType, Integer parseStatus, Date parseDate) { EmailParseInfoDO emailParseInfoDO = new EmailParseInfoDO(); emailParseInfoDO.setId(emailId); emailParseInfoDO.setSenderEmail(senderEmail); emailParseInfoDO.setEmail(emailAddress); emailParseInfoDO.setEmailDate(DateUtil.parse(emailDate, DateConst.YYYY_MM_DD_HH_MM_SS)); emailParseInfoDO.setParseDate(parseDate); emailParseInfoDO.setEmailTitle(emailTitle); emailParseInfoDO.setEmailType(emailType); emailParseInfoDO.setParseStatus(parseStatus); emailParseInfoDO.setIsvalid(1); emailParseInfoDO.setCreatorId(0); emailParseInfoDO.setCreateTime(parseDate); emailParseInfoDO.setUpdaterId(0); emailParseInfoDO.setUpdateTime(parseDate); return emailParseInfoDO; } public Map> getEmailFieldMapping() { List emailFieldMappingDOList = emailFieldMapper.getEmailFieldMapping(); return emailFieldMappingDOList.stream() .collect(Collectors.toMap(EmailFieldMappingDO::getCode, v -> Arrays.stream(v.getName().split(",")).toList())); } public Map> getEmailType() { Map> emailTypeMap = MapUtil.newHashMap(3, true); EmailTypeRuleDO emailTypeRuleDO = emailTypeRuleMapper.getEmailTypeRule(); String nav = emailTypeRuleDO != null && StrUtil.isNotBlank(emailTypeRuleDO.getNav()) ? emailTypeRuleDO.getNav() : emailRuleConfig.getNav(); String valuation = emailTypeRuleDO != null && StrUtil.isNotBlank(emailTypeRuleDO.getValuation()) ? emailTypeRuleDO.getValuation() : emailRuleConfig.getValuation(); String report = emailTypeRuleDO != null && StrUtil.isNotBlank(emailTypeRuleDO.getReport()) ? emailTypeRuleDO.getReport() : emailRuleConfig.getReport(); emailTypeMap.put(EmailTypeConst.VALUATION_EMAIL_TYPE, Arrays.stream(valuation.split(",")).toList()); emailTypeMap.put(EmailTypeConst.REPORT_EMAIL_TYPE, Arrays.stream(report.split(",")).toList()); emailTypeMap.put(EmailTypeConst.NAV_EMAIL_TYPE, Arrays.stream(nav.split(",")).toList()); return emailTypeMap; } /** * 读取邮件 * * @param mailboxInfoDTO 邮箱配置信息 * @param emailTypeMap 邮件类型识别规则映射表 * @param startDate 邮件起始日期 * @param endDate 邮件截止日期(为null,将解析邮件日期小于等于startDate的当天邮件) * @return 读取到的邮件信息 * @throws Exception 异常信息 */ private Map> realEmail(MailboxInfoDTO mailboxInfoDTO, Map> emailTypeMap, Date startDate, Date endDate) throws Exception { Store store = EmailUtil.getStoreNew(mailboxInfoDTO); if (store == null) { return MapUtil.newHashMap(); } // 默认读取收件箱的邮件 Folder folder = store.getFolder("INBOX"); folder.open(Folder.READ_ONLY); Message[] messages = getEmailMessage(folder, mailboxInfoDTO.getProtocol(), startDate); if (messages == null || messages.length == 0) { log.info("获取不到邮件 -> 邮箱信息:{},开始时间:{},结束时间:{}", mailboxInfoDTO, startDate, endDate); return MapUtil.newHashMap(); } Map> emailMessageMap = MapUtil.newHashMap(); for (Message message : messages) { List emailContentInfoDTOList = CollUtil.newArrayList(); String uuidKey = UUID.randomUUID().toString().replaceAll("-", ""); Integer emailType; String senderEmail; try { Date emailDate = message.getSentDate(); boolean isNotParseConditionSatisfied = emailDate == null || (endDate != null && emailDate.compareTo(endDate) > 0) || (startDate != null && emailDate.compareTo(startDate) < 0); if (isNotParseConditionSatisfied) { continue; } senderEmail = getSenderEmail(message.getFrom()); emailType = EmailUtil.getEmailTypeBySubject(message.getSubject(), emailTypeMap); String emailDateStr = DateUtil.format(emailDate, DateConst.YYYY_MM_DD_HH_MM_SS); if (emailType == null) { log.info("邮件不满足解析条件 -> 邮件主题:{},邮件日期:{}", message.getSubject(), emailDateStr); continue; } log.info("邮件采集成功 -> 邮件主题:{},邮件日期:{}", message.getSubject(), emailDateStr); Object content = message.getContent(); // 1.邮件为MIME多部分消息体:可能既有邮件又有正文 if (content instanceof MimeMultipart) { emailContentInfoDTOList = EmailUtil.collectMimeMultipart(message, mailboxInfoDTO.getAccount(), path); } // 2.邮件只有正文 if (content instanceof String) { EmailContentInfoDTO emailContentInfoDTO = new EmailContentInfoDTO(); emailContentInfoDTO.setEmailContent(content.toString()); emailContentInfoDTO.setEmailDate(emailDateStr); emailContentInfoDTO.setEmailTitle(message.getSubject()); String fileName = message.getSubject() + DateUtil.format(emailDate, DateConst.YYYYMMDDHHMMSS24); String filePath = path + mailboxInfoDTO.getAccount() + "/" + DateUtil.format(emailDate, DateConst.YYYY_MM_DD) + "/" + fileName + ".html"; File saveFile = new File(filePath); saveFile.setReadable(true); if (!saveFile.exists()) { if (!saveFile.getParentFile().exists()) { saveFile.getParentFile().mkdirs(); saveFile.getParentFile().setExecutable(true); } } FileUtil.writeFile(filePath, content.toString()); emailContentInfoDTO.setFilePath(filePath); emailContentInfoDTO.setFileName(fileName); emailContentInfoDTOList.add(emailContentInfoDTO); } if (CollUtil.isNotEmpty(emailContentInfoDTOList)) { emailContentInfoDTOList.forEach(e -> { e.setEmailType(emailType); e.setSenderEmail(senderEmail); }); emailMessageMap.put(uuidKey, emailContentInfoDTOList); } } catch (Exception e) { log.error("获取邮箱的邮件报错,堆栈信息:{}", ExceptionUtil.stacktraceToString(e)); } } folder.close(false); store.close(); return emailMessageMap; } private String getSenderEmail(Address[] senderAddress) { if (senderAddress == null || senderAddress.length == 0) { return null; } // 此时的address是含有编码(MIME编码方式)后的文本和实际的邮件地址 String address = senderAddress[0].toString(); // 正则表达式匹配邮件地址 Pattern pattern = Pattern.compile("<(\\S+)>"); Matcher matcher = pattern.matcher(address); if (matcher.find()) { return matcher.group(1); } return null; } private Message[] getEmailMessage(Folder folder, String protocol, Date startDate) { try { if (protocol.contains("imap")) { // 获取邮件日期大于等于startDate的邮件(搜索条件只支持按天) SearchTerm startDateTerm = new ReceivedDateTerm(ComparisonTerm.GE, startDate); return folder.search(startDateTerm); } else { return folder.getMessages(); } } catch (MessagingException e) { throw new RuntimeException(e); } } private static class PythonData { private Integer fileId; private Integer status; private String msg; private String register_number; public Integer getFileId() { return fileId; } public void setFileId(Integer fileId) { this.fileId = fileId; } public Integer getStatus() { return status; } public void setStatus(Integer status) { this.status = status; } public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } public String getRegister_number() { return register_number; } public void setRegister_number(String register_number) { this.register_number = register_number; } } }