Files
EmailBill/Service/EmailHandleService.cs
孙诚 cb11d80d1f
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 20s
Docker Build & Deploy / Deploy to Production (push) Successful in 5s
功能添加
2025-12-26 15:21:31 +08:00

278 lines
8.3 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Service.EmailParseServices;
namespace Service;
public interface IEmailHandleService
{
Task<bool> HandleEmailAsync(
string from,
string subject,
DateTime date,
string body
);
Task<bool> RefreshTransactionRecordsAsync(long emailMessageId);
}
public class EmailHandleService(
IOptions<EmailSettings> emailSettings,
ILogger<EmailHandleService> logger,
IEmailMessageRepository emailRepo,
ITransactionRecordRepository trxRepo,
IEnumerable<IEmailParseServices> emailParsers
) : IEmailHandleService
{
public async Task<bool> HandleEmailAsync(
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(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)
{
logger.LogWarning("未能成功解析邮件内容,跳过账单处理");
return true;
}
logger.LogInformation("成功解析邮件,共 {Count} 条交易记录", parsed.Length);
bool allSuccess = true;
foreach (var (card, reason, amount, balance, type, occurredAt) in parsed)
{
logger.LogInformation("处理交易记录: 卡号 {Card}, 交易原因 {Reason}, 金额 {Amount}, 余额 {Balance}, 类型 {Type}", card, reason, amount, balance, type);
var success = await SaveTransactionRecordAsync(
card,
reason,
amount,
balance,
type,
occurredAt ?? date,
emailMessage.Id
);
if (!success)
{
allSuccess = false;
}
}
return allSuccess;
}
public async Task<bool> 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);
bool allSuccess = true;
foreach (var (card, reason, amount, balance, type, occurredAt) in parsed)
{
logger.LogInformation("刷新交易记录: 卡号 {Card}, 交易原因 {Reason}, 金额 {Amount}, 余额 {Balance}, 类型 {Type}", card, reason, amount, balance, type);
var success = await SaveTransactionRecordAsync(
card,
reason,
amount,
balance,
type,
occurredAt ?? emailMessage.ReceivedDate,
emailMessage.Id
);
if (!success)
{
allSuccess = false;
}
}
return allSuccess;
}
private async Task<EmailMessage?> SaveEmailAsync(
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 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<bool> SaveTransactionRecordAsync(
string card,
string reason,
decimal amount,
decimal balance,
TransactionType type,
DateTime occurredAt,
long emailMessageId
)
{
// 根据 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 updated;
}
var trx = new TransactionRecord
{
Card = card,
Reason = reason,
Amount = amount,
Balance = balance,
Type = type,
OccurredAt = occurredAt,
EmailMessageId = emailMessageId,
ImportFrom = $"邮件"
};
var inserted = await trxRepo.AddAsync(trx);
if (inserted)
{
logger.LogInformation("交易记录已落库,卡号 {Card}, 金额 {Amount}", card, amount);
}
else
{
logger.LogWarning("交易记录落库失败,卡号 {Card}, 金额 {Amount}", card, amount);
}
return inserted;
}
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);
}
}