using Service.AI; using Service.EmailServices.EmailParse; using Service.Message; namespace Service.EmailServices; public interface IEmailHandleService { Task HandleEmailAsync( string to, string from, string subject, DateTime date, string body ); Task RefreshTransactionRecordsAsync(long emailMessageId); } public class EmailHandleService( IOptions emailSettings, ILogger logger, IEmailMessageRepository emailRepo, ITransactionRecordRepository trxRepo, IEnumerable emailParsers, IMessageService messageService, ISmartHandleService smartHandleService ) : IEmailHandleService { public async Task HandleEmailAsync( string to, string from, string subject, DateTime date, string body ) { var filterForm = emailSettings.Value.FilterFromAddresses; if (filterForm.Length == 0) { logger.LogWarning("未配置邮件过滤条件,跳过账单处理"); return false; } if (!filterForm.Any(f => from.Contains(f))) { logger.LogInformation("邮件不符合发件人过滤条件,跳过账单处理"); return false; } var emailMessage = await SaveEmailAsync(to, from, subject, date, body); if (emailMessage == null) { throw new InvalidOperationException("邮件保存失败,无法继续处理"); } var parsed = await ParseEmailBodyAsync( from, subject, string.IsNullOrEmpty(emailMessage.Body) ? emailMessage.HtmlBody : emailMessage.Body ); if (parsed == null || parsed.Length == 0) { await messageService.AddAsync( "邮件解析失败", $"来自 {from} 发送给 {to} 的邮件(主题:{subject})未能成功解析内容,可能格式已变更或不受支持。", url: "/balance?tab=email" ); logger.LogWarning("未能成功解析邮件内容,跳过账单处理"); return true; } logger.LogInformation("成功解析邮件,共 {Count} 条交易记录", parsed.Length); var allSuccess = true; var records = new List(); foreach (var (card, reason, amount, balance, type, occurredAt) in parsed) { logger.LogInformation("处理交易记录: 卡号 {Card}, 交易原因 {Reason}, 金额 {Amount}, 余额 {Balance}, 类型 {Type}", card, reason, amount, balance, type); var record = await SaveTransactionRecordAsync( card, reason, amount, balance, type, occurredAt ?? date, emailMessage.Id, $"邮件 By {GetEmailByName(to)}" ); if (record == null) { allSuccess = false; continue; } records.Add(record); } _ = AutoClassifyAsync(records.ToArray()); return allSuccess; } public async Task RefreshTransactionRecordsAsync(long emailMessageId) { var emailMessage = await emailRepo.GetByIdAsync(emailMessageId); if (emailMessage == null) { logger.LogWarning("未找到指定ID的邮件记录,无法刷新交易记录,ID: {Id}", emailMessageId); return false; } var filterForm = emailSettings.Value.FilterFromAddresses; if (filterForm.Length == 0) { logger.LogWarning("未配置邮件过滤条件,跳过账单处理"); return false; } if (!filterForm.Any(f => emailMessage.From.Contains(f))) { logger.LogInformation("邮件不符合发件人过滤条件,跳过账单处理"); return true; } var parsed = await ParseEmailBodyAsync( emailMessage.From, emailMessage.Subject, string.IsNullOrEmpty(emailMessage.Body) ? emailMessage.HtmlBody : emailMessage.Body ); if (parsed == null || parsed.Length == 0) { logger.LogWarning("未能成功解析邮件内容,跳过账单处理"); return false; } logger.LogInformation("成功解析邮件,共 {Count} 条交易记录", parsed.Length); var allSuccess = true; var records = new List(); foreach (var (card, reason, amount, balance, type, occurredAt) in parsed) { logger.LogInformation("刷新交易记录: 卡号 {Card}, 交易原因 {Reason}, 金额 {Amount}, 余额 {Balance}, 类型 {Type}", card, reason, amount, balance, type); var record = await SaveTransactionRecordAsync( card, reason, amount, balance, type, occurredAt ?? emailMessage.ReceivedDate, emailMessage.Id, $"邮件 By {GetEmailByName(emailMessage.To)}" ); if (record == null) { allSuccess = false; continue; } records.Add(record); } _ = AutoClassifyAsync(records.ToArray()); return allSuccess; } private async Task AutoClassifyAsync(TransactionRecord[] records) { var clone = records.ToArray().DeepClone(); if (clone?.Any() != true) { return; } var analyzedList = await AnalyzeClassifyAsync(clone); foreach (var analyzed in analyzedList) { var record = records.FirstOrDefault(r => r.Id == analyzed.Id); if (record == null) { continue; } record.UnconfirmedClassify = analyzed.Classify; record.UnconfirmedType = analyzed.Type; record.Classify = string.Empty; } await trxRepo.UpdateRangeAsync(records); // 消息 await messageService.AddAsync( "交易记录待确认分类", $"共有 {records.Length} 条交易记录待确认分类,请点击前往确认。", MessageType.Url, "/unconfirmed-classification" ); } private string GetEmailByName(string to) { return emailSettings.Value.SmtpList.FirstOrDefault(s => s.Email == to)?.Name ?? to; } private async Task SaveEmailAsync( string to, string from, string subject, DateTime date, string body ) { var emailEntity = new EmailMessage { From = from, Subject = subject, ReceivedDate = date, }; // 正则判断是否为HTML内容 if (Regex.IsMatch(body, @"<[^>]+>")) { emailEntity.HtmlBody = body; } else { emailEntity.Body = body; } try { var emailMd5 = emailEntity.ComputeBodyHash(); var existsEmail = await emailRepo.ExistsAsync(emailMd5); if (existsEmail != null) { logger.LogInformation("检测到重复邮件,跳过入库:{From} | {Subject} | {Date}", from, subject, date); return existsEmail; } emailEntity.Md5 = emailMd5; var toName = emailSettings.Value.SmtpList .FirstOrDefault(s => s.Email == to)?.Name ?? ""; emailEntity.To = string.IsNullOrEmpty(toName) ? to : $"{toName} <{to}>"; var ok = await emailRepo.AddAsync(emailEntity); if (ok) { logger.LogInformation("邮件已落库,ID: {Id}", emailEntity.Id); return emailEntity; } logger.LogError("邮件落库失败"); return null; } catch (Exception ex) { // 原始邮件落库失败不阻塞交易记录,但记录告警 logger.LogWarning(ex, "原始邮件落库失败"); return null; } } private async Task SaveTransactionRecordAsync( string card, string reason, decimal amount, decimal balance, TransactionType type, DateTime occurredAt, long emailMessageId, string importFrom ) { // 根据 emailMessageId 检查是否已存在记录:存在则更新,否则新增 var existing = await trxRepo.ExistsByEmailMessageIdAsync(emailMessageId, occurredAt); if (existing != null) { existing.Card = card; existing.Reason = reason; existing.Amount = amount; existing.Balance = balance; existing.Type = type; existing.OccurredAt = occurredAt; var updated = await trxRepo.UpdateAsync(existing); if (updated) { logger.LogInformation("交易记录已更新,卡号 {Card}, 金额 {Amount}", card, amount); } else { logger.LogWarning("交易记录更新失败,卡号 {Card}, 金额 {Amount}", card, amount); return null; } return existing; } var trx = new TransactionRecord { Card = card, Reason = reason, Amount = amount, Balance = balance, Type = type, OccurredAt = occurredAt, EmailMessageId = emailMessageId, ImportFrom = importFrom }; var inserted = await trxRepo.AddAsync(trx); if (inserted) { logger.LogInformation("交易记录已落库,卡号 {Card}, 金额 {Amount}", card, amount); } else { logger.LogWarning("交易记录落库失败,卡号 {Card}, 金额 {Amount}", card, amount); return null; } return trx; } private async Task<(string card, string reason, decimal amount, decimal balance, TransactionType type, DateTime? occurredAt)[]?> ParseEmailBodyAsync(string from, string subject, string body) { var service = emailParsers.FirstOrDefault(s => s.CanParse(from, subject, body)); if (service == null) { logger.LogWarning("未找到合适的邮件解析服务,跳过解析"); return null; } return await service.ParseAsync(body); } private async Task AnalyzeClassifyAsync(TransactionRecord[] records) { var result = new List(); await smartHandleService.SmartClassifyAsync(records.Select(r => r.Id).ToArray(), chunk => { // 处理分类结果 var (type, data) = chunk; if (type != "data") { logger.LogWarning("未知的分类结果类型: {Type}, {Data}. 跳过分类", type, data); return; } try { var item = JsonSerializer.Deserialize(data); var recordId = item?["id"]?.GetValue(); var classify = item?["Classify"]?.GetValue(); var recordType = item?["Type"]?.GetValue(); if (recordId == null || string.IsNullOrEmpty(classify) || recordType == null) { logger.LogWarning("AI分类结果数据不完整,跳过分类: {Data}", data); return; } if (recordType < (int)TransactionType.Expense || recordType > (int)TransactionType.None) { logger.LogWarning("AI分类结果交易类型无效,跳过分类: {Data}", data); return; } var record = records.FirstOrDefault(r => r.Id == recordId); if (record == null) { logger.LogWarning("未找到对应的交易记录(AI返回内容有误),跳过分类,ID: {Id}", recordId); return; } record.Classify = classify; record.Type = (TransactionType)recordType; result.Add(record); } catch (Exception ex) { logger.LogWarning(ex, "解析AI分类结果失败,跳过分类: {Data}", data); } }); return result.ToArray(); } }