2026-02-10 17:49:19 +08:00
|
|
|
|
using Service.AI;
|
2026-01-28 11:19:23 +08:00
|
|
|
|
|
|
|
|
|
|
namespace Service.EmailServices.EmailParse;
|
2025-12-25 11:20:56 +08:00
|
|
|
|
|
|
|
|
|
|
public interface IEmailParseServices
|
|
|
|
|
|
{
|
2025-12-25 13:40:26 +08:00
|
|
|
|
bool CanParse(string from, string subject, string body);
|
2025-12-25 11:20:56 +08:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 解析邮件内容,提取交易信息
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
Task<(
|
|
|
|
|
|
string card,
|
|
|
|
|
|
string reason,
|
|
|
|
|
|
decimal amount,
|
|
|
|
|
|
decimal balance,
|
|
|
|
|
|
TransactionType type,
|
|
|
|
|
|
DateTime? occurredAt
|
|
|
|
|
|
)[]> ParseAsync(string emailContent);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public abstract class EmailParseServicesBase(
|
|
|
|
|
|
ILogger<EmailParseServicesBase> logger,
|
2026-02-10 17:49:19 +08:00
|
|
|
|
ISmartHandleService smartHandleService
|
2025-12-25 11:20:56 +08:00
|
|
|
|
) : IEmailParseServices
|
|
|
|
|
|
{
|
2025-12-25 13:40:26 +08:00
|
|
|
|
public abstract bool CanParse(string from, string subject, string body);
|
2025-12-25 11:20:56 +08:00
|
|
|
|
|
|
|
|
|
|
public async Task<(
|
|
|
|
|
|
string card,
|
|
|
|
|
|
string reason,
|
|
|
|
|
|
decimal amount,
|
|
|
|
|
|
decimal balance,
|
|
|
|
|
|
TransactionType type,
|
|
|
|
|
|
DateTime? occurredAt
|
|
|
|
|
|
)[]> ParseAsync(string emailContent)
|
|
|
|
|
|
{
|
|
|
|
|
|
var result = await ParseEmailContentAsync(emailContent);
|
|
|
|
|
|
|
|
|
|
|
|
if (result.Length > 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
logger.LogInformation("使用规则成功解析邮件内容,提取到 {Count} 条交易记录", result.Length);
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
logger.LogInformation("规则解析邮件内容失败,尝试使用AI进行解析");
|
2026-02-10 17:49:19 +08:00
|
|
|
|
// AI兜底 - 使用 SmartHandleService 统一封装
|
|
|
|
|
|
result = await smartHandleService.ParseEmailByAiAsync(emailContent) ?? [];
|
2025-12-25 11:20:56 +08:00
|
|
|
|
|
2026-01-30 10:41:19 +08:00
|
|
|
|
if (result.Length == 0)
|
2025-12-25 11:20:56 +08:00
|
|
|
|
{
|
|
|
|
|
|
logger.LogWarning("AI解析邮件内容也未能提取到任何交易记录");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public abstract Task<(
|
|
|
|
|
|
string card,
|
|
|
|
|
|
string reason,
|
|
|
|
|
|
decimal amount,
|
|
|
|
|
|
decimal balance,
|
|
|
|
|
|
TransactionType type,
|
|
|
|
|
|
DateTime? occurredAt
|
|
|
|
|
|
)[]> ParseEmailContentAsync(string emailContent);
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2026-02-10 17:49:19 +08:00
|
|
|
|
/// 判断交易类型(供子类使用的通用方法)
|
2025-12-25 11:20:56 +08:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
protected TransactionType DetermineTransactionType(string typeStr, string reason, decimal amount)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 优先使用明确的类型字符串
|
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(typeStr))
|
|
|
|
|
|
{
|
|
|
|
|
|
if (typeStr.Contains("收入") || typeStr.Contains("income") || typeStr.Equals("收", StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
|
return TransactionType.Income;
|
|
|
|
|
|
if (typeStr.Contains("支出") || typeStr.Contains("expense") || typeStr.Equals("支", StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
|
return TransactionType.Expense;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 根据交易原因中的关键词判断
|
|
|
|
|
|
var lowerReason = reason.ToLower();
|
|
|
|
|
|
|
|
|
|
|
|
// 收入关键词
|
2025-12-31 11:49:25 +08:00
|
|
|
|
string[] incomeKeywords =
|
2026-01-18 22:04:56 +08:00
|
|
|
|
[
|
2025-12-31 11:49:25 +08:00
|
|
|
|
"工资", "奖金", "退款",
|
|
|
|
|
|
"返现", "收入", "转入",
|
|
|
|
|
|
"存入", "利息", "分红",
|
|
|
|
|
|
"入账", "收款",
|
|
|
|
|
|
|
|
|
|
|
|
// 常见扩展
|
|
|
|
|
|
"实发工资", "薪资", "薪水", "薪酬",
|
|
|
|
|
|
"提成", "佣金", "劳务费",
|
|
|
|
|
|
"报销入账", "报销款", "补贴", "补助",
|
|
|
|
|
|
|
|
|
|
|
|
"退款成功", "退回", "退货退款",
|
|
|
|
|
|
"返现入账", "返利", "返佣",
|
|
|
|
|
|
|
|
|
|
|
|
"到账", "已到账", "入账成功",
|
|
|
|
|
|
"收款成功", "收到款项", "到账金额",
|
|
|
|
|
|
"资金转入", "资金收入",
|
|
|
|
|
|
|
|
|
|
|
|
"转账收入", "转账入账", "他行来账",
|
|
|
|
|
|
"工资代发", "代发工资", "单位打款",
|
|
|
|
|
|
|
|
|
|
|
|
"利息收入", "收益", "收益发放", "理财收益",
|
|
|
|
|
|
"分红收入", "股息", "红利",
|
|
|
|
|
|
|
|
|
|
|
|
// 平台常用词
|
|
|
|
|
|
"红包", "红包收入", "红包入账",
|
|
|
|
|
|
"奖励金", "活动奖励", "补贴金",
|
|
|
|
|
|
"现金奖励", "推广奖励", "返现奖励",
|
|
|
|
|
|
|
|
|
|
|
|
// 存取类
|
|
|
|
|
|
"现金存入", "柜台存入", "ATM存入",
|
|
|
|
|
|
"他人转入", "他人汇入"
|
2026-01-18 22:04:56 +08:00
|
|
|
|
];
|
2025-12-25 11:20:56 +08:00
|
|
|
|
if (incomeKeywords.Any(k => lowerReason.Contains(k)))
|
|
|
|
|
|
return TransactionType.Income;
|
|
|
|
|
|
|
|
|
|
|
|
// 支出关键词
|
2025-12-31 11:49:25 +08:00
|
|
|
|
string[] expenseKeywords =
|
2026-01-18 22:04:56 +08:00
|
|
|
|
[
|
2025-12-31 11:49:25 +08:00
|
|
|
|
"消费", "支付", "购买",
|
|
|
|
|
|
"转出", "取款", "支出",
|
|
|
|
|
|
"扣款", "缴费", "付款",
|
|
|
|
|
|
"刷卡",
|
|
|
|
|
|
|
|
|
|
|
|
// 常见扩展
|
|
|
|
|
|
"支出金额", "支出人民币", "已支出",
|
|
|
|
|
|
"已消费", "消费支出", "消费人民币",
|
|
|
|
|
|
"已支付", "成功支付", "支付成功", "交易支付",
|
|
|
|
|
|
"已扣款", "扣款成功", "扣费", "扣费成功",
|
|
|
|
|
|
"转账", "转账支出", "向外转账", "已转出",
|
|
|
|
|
|
"提现", "现金支出", "现金取款",
|
|
|
|
|
|
"扣除", "扣除金额", "记账支出",
|
|
|
|
|
|
|
|
|
|
|
|
// 账单/通知类用语
|
|
|
|
|
|
"本期应还", "本期应还金额", "本期账单金额",
|
|
|
|
|
|
"本期应还人民币", "最低还款额",
|
|
|
|
|
|
"本期欠款", "欠款金额",
|
|
|
|
|
|
|
|
|
|
|
|
// 线上平台常见用语
|
|
|
|
|
|
"订单支付", "订单扣款", "订单消费",
|
|
|
|
|
|
"交易支出", "交易扣款", "交易成功支出",
|
|
|
|
|
|
"话费充值", "流量充值", "水费", "电费", "燃气费",
|
|
|
|
|
|
"物业费", "服务费", "手续费", "年费", "会费",
|
|
|
|
|
|
"利息支出", "还款支出", "代扣", "代缴",
|
|
|
|
|
|
|
|
|
|
|
|
// 信用卡/花呗等场景
|
|
|
|
|
|
"信用卡还款", "花呗还款", "白条还款",
|
|
|
|
|
|
"分期还款", "账单还款", "自动还款"
|
2026-01-18 22:04:56 +08:00
|
|
|
|
];
|
2025-12-25 11:20:56 +08:00
|
|
|
|
if (expenseKeywords.Any(k => lowerReason.Contains(k)))
|
|
|
|
|
|
return TransactionType.Expense;
|
|
|
|
|
|
|
|
|
|
|
|
// 根据金额正负判断(如果金额为负数,可能是支出)
|
|
|
|
|
|
if (amount < 0)
|
|
|
|
|
|
return TransactionType.Expense;
|
|
|
|
|
|
if (amount > 0)
|
|
|
|
|
|
return TransactionType.Income;
|
|
|
|
|
|
|
|
|
|
|
|
// 默认为支出
|
|
|
|
|
|
return TransactionType.Expense;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|