This commit is contained in:
SunCheng
2026-02-10 17:49:19 +08:00
parent 3e18283e52
commit d052ae5197
104 changed files with 10369 additions and 3000 deletions

View File

@@ -1,11 +1,11 @@
using Service.AI;
using Service.AI;
namespace Service.EmailServices.EmailParse;
public class EmailParseForm95555(
ILogger<EmailParseForm95555> logger,
IOpenAiService openAiService
) : EmailParseServicesBase(logger, openAiService)
ISmartHandleService smartHandleService
) : EmailParseServicesBase(logger, smartHandleService)
{
public override bool CanParse(string from, string subject, string body)
{

View File

@@ -1,4 +1,4 @@
using HtmlAgilityPack;
using HtmlAgilityPack;
using Service.AI;
// ReSharper disable ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
// ReSharper disable ConditionalAccessQualifierIsNonNullableAccordingToAPIContract
@@ -8,8 +8,8 @@ namespace Service.EmailServices.EmailParse;
[UsedImplicitly]
public partial class EmailParseFormCcsvc(
ILogger<EmailParseFormCcsvc> logger,
IOpenAiService openAiService
) : EmailParseServicesBase(logger, openAiService)
ISmartHandleService smartHandleService
) : EmailParseServicesBase(logger, smartHandleService)
{
[GeneratedRegex("<.*?>")]
private static partial Regex HtmlRegex();

View File

@@ -1,4 +1,4 @@
using Service.AI;
using Service.AI;
namespace Service.EmailServices.EmailParse;
@@ -21,7 +21,7 @@ public interface IEmailParseServices
public abstract class EmailParseServicesBase(
ILogger<EmailParseServicesBase> logger,
IOpenAiService openAiService
ISmartHandleService smartHandleService
) : IEmailParseServices
{
public abstract bool CanParse(string from, string subject, string body);
@@ -44,8 +44,8 @@ public abstract class EmailParseServicesBase(
}
logger.LogInformation("规则解析邮件内容失败尝试使用AI进行解析");
// AI兜底
result = await ParseByAiAsync(emailContent) ?? [];
// AI兜底 - 使用 SmartHandleService 统一封装
result = await smartHandleService.ParseEmailByAiAsync(emailContent) ?? [];
if (result.Length == 0)
{
@@ -64,128 +64,8 @@ public abstract class EmailParseServicesBase(
DateTime? occurredAt
)[]> ParseEmailContentAsync(string emailContent);
private async Task<(
string card,
string reason,
decimal amount,
decimal balance,
TransactionType type,
DateTime? occurredAt
)[]?> ParseByAiAsync(string body)
{
var systemPrompt = $"""
你是一个信息抽取助手。
仅输出严格的JSON数组不要包含任何多余文本。
每个交易记录包含字段: card(字符串), reason(字符串), amount(数字), balance(数字), type(字符串,值为'收入'或'支出'), occurredAt(字符串yyyy-MM-dd HH:mm:ss格式日期时间)。
如果缺失,请推断或置空。
[重要] 当前时间为{DateTime.Now:yyyy-MM-dd HH:mm:ss},请根据当前时间推断交易发生的时间。
""";
var userPrompt = $"""
从下面这封银行账单相关邮件正文中提取所有交易记录返回JSON数组格式
每个元素包含: card, reason, amount, balance, type(收入或支出), occurredAt(非必要)。
正文如下:\n\n{body}
""";
var contentText = await openAiService.ChatAsync(systemPrompt, userPrompt);
if (string.IsNullOrWhiteSpace(contentText))
{
logger.LogWarning("AI未返回任何内容无法解析邮件");
return null;
}
logger.LogDebug("AI返回的内容: {Content}", contentText);
// 清理可能的 markdown 代码块标记
contentText = contentText.Trim();
if (contentText.StartsWith("```"))
{
// 移除开头的 ```json 或 ```
var firstNewLine = contentText.IndexOf('\n');
if (firstNewLine > 0)
{
contentText = contentText.Substring(firstNewLine + 1);
}
// 移除结尾的 ```
if (contentText.EndsWith("```"))
{
contentText = contentText.Substring(0, contentText.Length - 3);
}
contentText = contentText.Trim();
}
// contentText 期望是 JSON 数组
using var jsonDoc = JsonDocument.Parse(contentText);
var arrayElement = jsonDoc.RootElement;
// 如果返回的是单个对象而不是数组,尝试兼容处理
if (arrayElement.ValueKind == JsonValueKind.Object)
{
logger.LogWarning("AI返回的内容是单个对象而非数组尝试兼容处理");
var result = ParseSingleRecord(arrayElement);
return result != null ? [result.Value] : null;
}
if (arrayElement.ValueKind != JsonValueKind.Array)
{
logger.LogWarning("AI返回的内容不是JSON数组无法解析邮件");
return null;
}
var results = new List<(string card, string reason, decimal amount, decimal balance, TransactionType type, DateTime? occurredAt)>();
foreach (var obj in arrayElement.EnumerateArray())
{
var record = ParseSingleRecord(obj);
if (record != null)
{
logger.LogInformation("解析到一条交易记录: {@Record}", record.Value);
results.Add(record.Value);
}
}
logger.LogInformation("使用AI成功解析邮件内容提取到 {Count} 条交易记录", results.Count);
return results.Count > 0 ? results.ToArray() : null;
}
private (string card, string reason, decimal amount, decimal balance, TransactionType type, DateTime? occurredAt)? ParseSingleRecord(JsonElement obj)
{
var card = obj.TryGetProperty("card", out var pCard) ? pCard.GetString() ?? string.Empty : string.Empty;
var reason = obj.TryGetProperty("reason", out var pReason) ? pReason.GetString() ?? string.Empty : string.Empty;
var typeStr = obj.TryGetProperty("type", out var pType) ? pType.GetString() ?? string.Empty : string.Empty;
var occurredAtStr = obj.TryGetProperty("occurredAt", out var pOccurredAt) ? pOccurredAt.GetString() ?? string.Empty : string.Empty;
var amount = 0m;
if (obj.TryGetProperty("amount", out var pAmount))
{
if (pAmount.ValueKind == JsonValueKind.Number && pAmount.TryGetDecimal(out var d)) amount = d;
else if (pAmount.ValueKind == JsonValueKind.String && decimal.TryParse(pAmount.GetString(), NumberStyles.Any, CultureInfo.InvariantCulture, out var ds)) amount = ds;
}
var balance = 0m;
if (obj.TryGetProperty("balance", out var pBalance))
{
if (pBalance.ValueKind == JsonValueKind.Number && pBalance.TryGetDecimal(out var d2)) balance = d2;
else if (pBalance.ValueKind == JsonValueKind.String && decimal.TryParse(pBalance.GetString(), NumberStyles.Any, CultureInfo.InvariantCulture, out var ds2)) balance = ds2;
}
if (string.IsNullOrWhiteSpace(card) || string.IsNullOrWhiteSpace(reason))
{
return null;
}
var occurredAt = (DateTime?)null;
if (DateTime.TryParse(occurredAtStr, out var occurredAtValue))
{
occurredAt = occurredAtValue;
}
var type = DetermineTransactionType(typeStr, reason, amount);
return (card, reason, amount, balance, type, occurredAt);
}
/// <summary>
/// 判断交易类型
/// 判断交易类型(供子类使用的通用方法)
/// </summary>
protected TransactionType DetermineTransactionType(string typeStr, string reason, decimal amount)
{