Files
EmailBill/WebApi/Controllers/TransactionRecordController.cs

1205 lines
45 KiB
C#
Raw Normal View History

2025-12-25 11:20:56 +08:00
namespace WebApi.Controllers;
2025-12-26 17:13:57 +08:00
using Repository;
2025-12-25 11:20:56 +08:00
[ApiController]
[Route("api/[controller]/[action]")]
public class TransactionRecordController(
ITransactionRecordRepository transactionRepository,
ITransactionCategoryRepository categoryRepository,
IOpenAiService openAiService,
2025-12-29 20:30:15 +08:00
ITextSegmentService textSegmentService,
2025-12-25 11:20:56 +08:00
ILogger<TransactionRecordController> logger
) : ControllerBase
{
/// <summary>
/// 获取交易记录列表(分页)
/// </summary>
[HttpGet]
2025-12-27 11:50:12 +08:00
public async Task<PagedResponse<TransactionRecord>> GetListAsync(
2025-12-27 22:05:50 +08:00
[FromQuery] int pageIndex = 1,
[FromQuery] int pageSize = 20,
2025-12-26 17:56:08 +08:00
[FromQuery] string? searchKeyword = null,
[FromQuery] string? classify = null,
[FromQuery] int? type = null,
[FromQuery] int? year = null,
2025-12-27 22:05:50 +08:00
[FromQuery] int? month = null,
[FromQuery] bool sortByAmount = false
2025-12-25 11:20:56 +08:00
)
{
try
{
2025-12-26 17:56:08 +08:00
TransactionType? transactionType = type.HasValue ? (TransactionType)type.Value : null;
2025-12-27 22:05:50 +08:00
var list = await transactionRepository.GetPagedListAsync(
pageIndex,
2025-12-29 20:30:15 +08:00
pageSize,
searchKeyword,
classify,
transactionType,
year,
2025-12-27 22:05:50 +08:00
month,
sortByAmount);
var total = await transactionRepository.GetTotalCountAsync(
2025-12-29 20:30:15 +08:00
searchKeyword,
classify,
transactionType,
year,
2025-12-26 17:56:08 +08:00
month);
2025-12-25 11:20:56 +08:00
2025-12-27 11:50:12 +08:00
return new PagedResponse<TransactionRecord>
2025-12-25 11:20:56 +08:00
{
Success = true,
Data = list.ToArray(),
2025-12-27 22:05:50 +08:00
Total = (int)total
2025-12-25 11:20:56 +08:00
};
}
catch (Exception ex)
{
2025-12-27 22:05:50 +08:00
logger.LogError(ex, "获取交易记录列表失败,页码: {PageIndex}, 页大小: {PageSize}", pageIndex, pageSize);
2025-12-27 11:50:12 +08:00
return PagedResponse<TransactionRecord>.Fail($"获取交易记录列表失败: {ex.Message}");
2025-12-25 11:20:56 +08:00
}
}
/// <summary>
/// 根据ID获取交易记录详情
/// </summary>
[HttpGet("{id}")]
2025-12-27 11:50:12 +08:00
public async Task<BaseResponse<TransactionRecord>> GetByIdAsync(long id)
2025-12-25 11:20:56 +08:00
{
try
{
var transaction = await transactionRepository.GetByIdAsync(id);
if (transaction == null)
{
2025-12-27 11:50:12 +08:00
return BaseResponse<TransactionRecord>.Fail("交易记录不存在");
2025-12-25 11:20:56 +08:00
}
2025-12-27 11:50:12 +08:00
return new BaseResponse<TransactionRecord>
2025-12-25 11:20:56 +08:00
{
Success = true,
Data = transaction
};
}
catch (Exception ex)
{
logger.LogError(ex, "获取交易记录详情失败交易ID: {TransactionId}", id);
2025-12-27 11:50:12 +08:00
return BaseResponse<TransactionRecord>.Fail($"获取交易记录详情失败: {ex.Message}");
2025-12-25 11:20:56 +08:00
}
}
/// <summary>
/// 根据邮件ID获取交易记录列表
/// </summary>
[HttpGet("{emailId}")]
2025-12-27 11:50:12 +08:00
public async Task<BaseResponse<List<TransactionRecord>>> GetByEmailIdAsync(long emailId)
2025-12-25 11:20:56 +08:00
{
try
{
var transactions = await transactionRepository.GetByEmailIdAsync(emailId);
2025-12-27 11:50:12 +08:00
return new BaseResponse<List<TransactionRecord>>
2025-12-25 11:20:56 +08:00
{
Success = true,
Data = transactions
};
}
catch (Exception ex)
{
logger.LogError(ex, "获取邮件交易记录失败邮件ID: {EmailId}", emailId);
2025-12-27 11:50:12 +08:00
return BaseResponse<List<TransactionRecord>>.Fail($"获取邮件交易记录失败: {ex.Message}");
2025-12-25 11:20:56 +08:00
}
}
/// <summary>
/// 创建交易记录
/// </summary>
[HttpPost]
public async Task<BaseResponse> CreateAsync([FromBody] CreateTransactionDto dto)
{
try
{
// 解析日期字符串
if (!DateTime.TryParse(dto.OccurredAt, out var occurredAt))
{
return BaseResponse.Fail("交易时间格式不正确");
}
2025-12-27 11:50:12 +08:00
var transaction = new TransactionRecord
2025-12-25 11:20:56 +08:00
{
OccurredAt = occurredAt,
Reason = dto.Reason ?? string.Empty,
Amount = dto.Amount,
Type = dto.Type,
Classify = dto.Classify ?? string.Empty,
ImportFrom = "手动录入",
EmailMessageId = 0 // 手动录入的记录EmailMessageId 设为 0
};
var result = await transactionRepository.AddAsync(transaction);
if (result)
{
return new BaseResponse
{
Success = true
};
}
else
{
return BaseResponse.Fail("创建交易记录失败");
}
}
catch (Exception ex)
{
logger.LogError(ex, "创建交易记录失败,交易信息: {@TransactionDto}", dto);
return BaseResponse.Fail($"创建交易记录失败: {ex.Message}");
}
}
/// <summary>
/// 更新交易记录
/// </summary>
[HttpPost]
public async Task<BaseResponse> UpdateAsync([FromBody] UpdateTransactionDto dto)
{
try
{
var transaction = await transactionRepository.GetByIdAsync(dto.Id);
if (transaction == null)
{
return BaseResponse.Fail("交易记录不存在");
}
// 更新可编辑字段
transaction.Reason = dto.Reason ?? string.Empty;
transaction.Amount = dto.Amount;
transaction.Balance = dto.Balance;
transaction.Type = dto.Type;
transaction.Classify = dto.Classify ?? string.Empty;
var success = await transactionRepository.UpdateAsync(transaction);
if (success)
{
return new BaseResponse
{
Success = true
};
}
else
{
return BaseResponse.Fail("更新交易记录失败");
}
}
catch (Exception ex)
{
logger.LogError(ex, "更新交易记录失败交易ID: {TransactionId}, 交易信息: {@TransactionDto}", dto.Id, dto);
return BaseResponse.Fail($"更新交易记录失败: {ex.Message}");
}
}
/// <summary>
/// 删除交易记录
/// </summary>
[HttpPost]
public async Task<BaseResponse> DeleteByIdAsync([FromQuery] long id)
{
try
{
var success = await transactionRepository.DeleteAsync(id);
if (success)
{
return new BaseResponse
{
Success = true
};
}
else
{
return BaseResponse.Fail("删除交易记录失败,记录不存在");
}
}
catch (Exception ex)
{
logger.LogError(ex, "删除交易记录失败交易ID: {TransactionId}", id);
return BaseResponse.Fail($"删除交易记录失败: {ex.Message}");
}
}
/// <summary>
/// 获取指定月份每天的消费统计
/// </summary>
[HttpGet]
public async Task<BaseResponse<List<DailyStatisticsDto>>> GetDailyStatisticsAsync(
[FromQuery] int year,
[FromQuery] int month
)
{
try
{
var statistics = await transactionRepository.GetDailyStatisticsAsync(year, month);
var result = statistics.Select(s => new DailyStatisticsDto(s.Key, s.Value.count, s.Value.amount)).ToList();
return new BaseResponse<List<DailyStatisticsDto>>
{
Success = true,
Data = result
};
}
catch (Exception ex)
{
logger.LogError(ex, "获取日历统计数据失败,年份: {Year}, 月份: {Month}", year, month);
return BaseResponse<List<DailyStatisticsDto>>.Fail($"获取日历统计数据失败: {ex.Message}");
}
}
/// <summary>
2025-12-26 17:13:57 +08:00
/// 获取月度统计数据
/// </summary>
[HttpGet]
public async Task<BaseResponse<MonthlyStatistics>> GetMonthlyStatisticsAsync(
[FromQuery] int year,
[FromQuery] int month
)
{
try
{
var statistics = await transactionRepository.GetMonthlyStatisticsAsync(year, month);
return new BaseResponse<MonthlyStatistics>
{
Success = true,
Data = statistics
};
}
catch (Exception ex)
{
logger.LogError(ex, "获取月度统计数据失败,年份: {Year}, 月份: {Month}", year, month);
return BaseResponse<MonthlyStatistics>.Fail($"获取月度统计数据失败: {ex.Message}");
}
}
/// <summary>
/// 获取分类统计数据
/// </summary>
[HttpGet]
public async Task<BaseResponse<List<CategoryStatistics>>> GetCategoryStatisticsAsync(
[FromQuery] int year,
[FromQuery] int month,
[FromQuery] TransactionType type
)
{
try
{
var statistics = await transactionRepository.GetCategoryStatisticsAsync(year, month, type);
return new BaseResponse<List<CategoryStatistics>>
{
Success = true,
Data = statistics
};
}
catch (Exception ex)
{
logger.LogError(ex, "获取分类统计数据失败,年份: {Year}, 月份: {Month}, 类型: {Type}", year, month, type);
return BaseResponse<List<CategoryStatistics>>.Fail($"获取分类统计数据失败: {ex.Message}");
}
}
/// <summary>
/// 获取趋势统计数据
2025-12-25 11:20:56 +08:00
/// </summary>
[HttpGet]
2025-12-26 17:13:57 +08:00
public async Task<BaseResponse<List<TrendStatistics>>> GetTrendStatisticsAsync(
[FromQuery] int startYear,
[FromQuery] int startMonth,
[FromQuery] int monthCount = 6
2025-12-25 11:20:56 +08:00
)
2025-12-26 17:13:57 +08:00
{
try
{
var statistics = await transactionRepository.GetTrendStatisticsAsync(startYear, startMonth, monthCount);
return new BaseResponse<List<TrendStatistics>>
{
Success = true,
Data = statistics
};
}
catch (Exception ex)
{
logger.LogError(ex, "获取趋势统计数据失败,开始年份: {Year}, 开始月份: {Month}, 月份数: {Count}", startYear, startMonth, monthCount);
return BaseResponse<List<TrendStatistics>>.Fail($"获取趋势统计数据失败: {ex.Message}");
}
}
/// <summary>
/// 智能分析账单(流式输出)
/// </summary>
public async Task AnalyzeBillAsync([FromBody] BillAnalysisRequest request)
{
Response.ContentType = "text/event-stream";
Response.Headers.Append("Cache-Control", "no-cache");
Response.Headers.Append("Connection", "keep-alive");
try
{
// 第一步使用AI生成聚合SQL查询
var now = DateTime.Now;
var sqlPrompt = $"""
{now:yyyy年M月d日}{now:yyyy-MM-dd}
{request.UserInput}
SQLite
TransactionRecord
- Id: bigint
- Card: nvarchar
- Reason: nvarchar /
- Amount: decimal
- OccurredAt: datetime TEXT类型'2025-12-26 10:30:00'
- Type: int 0=, 1=, 2=
- Classify: nvarchar
SQL
1.
2. 使 GROUP BY
3. 使SUM(ABS(Amount)) COUNT(*) AVG()MAX()MIN()
4. 使 OccurredAt "最近X个月/天"
5. Type = 0 Type = 1
6. TotalAmount, TransactionCount, AvgAmount
7. 使 ORDER BY
8. SQL语句
SQLite日期函数
- strftime('%Y', OccurredAt)
- strftime('%m', OccurredAt)
- strftime('%Y-%m-%d', OccurredAt)
- 使 YEAR()MONTH()DAY() SQLite不支持
1
SELECT Classify, COUNT(*) as TransactionCount, SUM(ABS(Amount)) as TotalAmount, AVG(ABS(Amount)) as AvgAmount FROM TransactionRecord WHERE Type = 0 AND OccurredAt >= '2025-10-01' AND OccurredAt < '2026-01-01' AND (Classify LIKE '%%' OR Reason LIKE '%%' OR Reason LIKE '%%' OR Reason LIKE '%%') GROUP BY Classify ORDER BY TotalAmount DESC
2
SELECT strftime('%Y', OccurredAt) as Year, strftime('%m', OccurredAt) as Month, COUNT(*) as TransactionCount, SUM(ABS(Amount)) as TotalAmount FROM TransactionRecord WHERE Type = 0 AND OccurredAt >= '2025-06-01' GROUP BY strftime('%Y', OccurredAt), strftime('%m', OccurredAt) ORDER BY Year, Month
3
SELECT COUNT(*) as TransactionCount, SUM(ABS(Amount)) as TotalAmount, AVG(ABS(Amount)) as AvgAmount, MAX(ABS(Amount)) as MaxAmount FROM TransactionRecord WHERE Type = 0 AND OccurredAt >= '2025-12-01' AND OccurredAt < '2026-01-01'
4 - 使
1000
SELECT OccurredAt, Classify, Reason, ABS(Amount) as Amount FROM TransactionRecord WHERE Type = 0 AND ABS(Amount) > 1000 ORDER BY Amount DESC LIMIT 50
SQL语句
""";
var sqlText = await openAiService.ChatAsync(sqlPrompt);
2025-12-29 20:30:15 +08:00
2025-12-26 17:13:57 +08:00
// 清理SQL文本
sqlText = sqlText?.Trim() ?? "";
sqlText = sqlText.TrimStart('`').TrimEnd('`');
if (sqlText.StartsWith("sql", StringComparison.OrdinalIgnoreCase))
{
sqlText = sqlText.Substring(3).Trim();
}
logger.LogInformation("AI生成的SQL: {Sql}", sqlText);
// 第二步执行动态SQL查询
List<dynamic> queryResults;
try
{
queryResults = await transactionRepository.ExecuteDynamicSqlAsync(sqlText);
}
catch (Exception ex)
{
logger.LogError(ex, "执行AI生成的SQL失败: {Sql}", sqlText);
// 如果SQL执行失败返回错误
var errorData = System.Text.Json.JsonSerializer.Serialize(new { content = "<div class='error-message'>SQL执行失败请重新描述您的问题</div>" });
await Response.WriteAsync($"data: {errorData}\n\n");
await Response.Body.FlushAsync();
return;
}
// 第三步将查询结果序列化为JSON直接传递给AI生成分析报告
2025-12-27 11:50:12 +08:00
var dataJson = System.Text.Json.JsonSerializer.Serialize(queryResults, new JsonSerializerOptions
2025-12-29 20:30:15 +08:00
{
2025-12-26 17:13:57 +08:00
WriteIndented = true,
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
});
2025-12-29 20:30:15 +08:00
2025-12-26 17:13:57 +08:00
var dataPrompt = $"""
{DateTime.Now:yyyy年M月d日}
{request.UserInput}
JSON格式
{dataJson}
1. 使HTML格式H5页面风格
2.
3. 使table > thead/tbody > tr > th/td
4. 使HTML标签h2h3ptableul/listrong
5. <span class='expense-value'></span>
6. <span class='income-value'></span>
7. <span class='highlight'></span>
8. htmlbodyhead
9. 使 style <style>
10. backgroundbackground-colorcolor
11. 使 div
12. JSON数据转换为易读的表格和文字说明
13.
14.
15.
HTML内容markdown代码块标记
""";
// 第四步流式输出AI分析结果
await foreach (var chunk in openAiService.ChatStreamAsync(dataPrompt))
{
var sseData = System.Text.Json.JsonSerializer.Serialize(new { content = chunk });
await Response.WriteAsync($"data: {sseData}\n\n");
await Response.Body.FlushAsync();
}
// 发送完成标记
await Response.WriteAsync("data: [DONE]\n\n");
await Response.Body.FlushAsync();
}
catch (Exception ex)
{
logger.LogError(ex, "智能分析账单失败");
var errorData = System.Text.Json.JsonSerializer.Serialize(new { content = $"<div class='error-message'>分析失败:{ex.Message}</div>" });
await Response.WriteAsync($"data: {errorData}\n\n");
await Response.Body.FlushAsync();
}
}
/// <summary>
/// 获取指定日期的交易记录
/// </summary>
[HttpGet]
2025-12-27 11:50:12 +08:00
public async Task<BaseResponse<List<TransactionRecord>>> GetByDateAsync([FromQuery] string date)
2025-12-25 11:20:56 +08:00
{
try
{
if (!DateTime.TryParse(date, out var targetDate))
{
2025-12-27 11:50:12 +08:00
return BaseResponse<List<TransactionRecord>>.Fail("日期格式不正确");
2025-12-25 11:20:56 +08:00
}
// 获取当天的开始和结束时间
var startDate = targetDate.Date;
var endDate = startDate.AddDays(1);
var records = await transactionRepository.GetByDateRangeAsync(startDate, endDate);
2025-12-27 11:50:12 +08:00
return new BaseResponse<List<TransactionRecord>>
2025-12-25 11:20:56 +08:00
{
Success = true,
Data = records
};
}
catch (Exception ex)
{
logger.LogError(ex, "获取指定日期的交易记录失败,日期: {Date}", date);
2025-12-27 11:50:12 +08:00
return BaseResponse<List<TransactionRecord>>.Fail($"获取指定日期的交易记录失败: {ex.Message}");
2025-12-25 11:20:56 +08:00
}
}
/// <summary>
/// 获取未分类的账单数量
/// </summary>
[HttpGet]
public async Task<BaseResponse<int>> GetUnclassifiedCountAsync()
{
try
{
var count = await transactionRepository.GetUnclassifiedCountAsync();
return new BaseResponse<int>
{
Success = true,
Data = count
};
}
catch (Exception ex)
{
logger.LogError(ex, "获取未分类账单数量失败");
return BaseResponse<int>.Fail($"获取未分类账单数量失败: {ex.Message}");
}
}
/// <summary>
/// 获取未分类的账单列表
/// </summary>
[HttpGet]
2025-12-27 11:50:12 +08:00
public async Task<BaseResponse<List<TransactionRecord>>> GetUnclassifiedAsync([FromQuery] int pageSize = 10)
{
try
{
var records = await transactionRepository.GetUnclassifiedAsync(pageSize);
2025-12-27 11:50:12 +08:00
return new BaseResponse<List<TransactionRecord>>
{
Success = true,
Data = records
};
}
catch (Exception ex)
{
logger.LogError(ex, "获取未分类账单列表失败");
2025-12-27 11:50:12 +08:00
return BaseResponse<List<TransactionRecord>>.Fail($"获取未分类账单列表失败: {ex.Message}");
}
}
/// <summary>
/// 智能分类 - 使用AI对账单进行分类流式响应
/// </summary>
[HttpPost]
public async Task SmartClassifyAsync([FromBody] SmartClassifyRequest request)
{
Response.ContentType = "text/event-stream";
Response.Headers.Append("Cache-Control", "no-cache");
Response.Headers.Append("Connection", "keep-alive");
try
{
2025-12-26 15:21:31 +08:00
// 验证账单ID列表
if (request.TransactionIds == null || request.TransactionIds.Count == 0)
{
await WriteEventAsync("error", "请提供要分类的账单ID");
return;
}
2025-12-29 20:30:15 +08:00
// 获取指定ID的账单作为样本
var sampleRecords = await transactionRepository.GetByIdsAsync(request.TransactionIds.ToArray());
2025-12-26 15:21:31 +08:00
2025-12-29 20:30:15 +08:00
if (sampleRecords.Length == 0)
{
2025-12-26 15:21:31 +08:00
await WriteEventAsync("error", "找不到指定的账单");
return;
}
2025-12-29 20:30:15 +08:00
// 重新按Reason分组所有待分类账单
var groupedRecords = sampleRecords
.GroupBy(r => r.Reason)
.Select(g => new
{
Reason = g.Key,
Ids = g.Select(r => r.Id).ToList(),
Count = g.Count(),
TotalAmount = g.Sum(r => r.Amount),
SampleType = g.First().Type
})
.OrderByDescending(g => Math.Abs(g.TotalAmount))
.ToList();
// 【增强功能】对每个分组的摘要进行分词,查询已分类的相似账单
var referenceRecords = new Dictionary<string, List<TransactionRecord>>();
foreach (var group in groupedRecords)
{
// 使用专业分词库提取关键词
var keywords = textSegmentService.ExtractKeywords(group.Reason);
if (keywords.Count > 0)
{
// 查询包含这些关键词且已分类的账单(带相关度评分)
// minMatchRate=0.4 表示至少匹配40%的关键词才被认为是相似的
var similarClassifiedWithScore = await transactionRepository.GetClassifiedByKeywordsWithScoreAsync(keywords, minMatchRate: 0.4, limit: 10);
if (similarClassifiedWithScore.Count > 0)
{
// 只取前5个最相关的
var topSimilar = similarClassifiedWithScore.Take(5).Select(x => x.record).ToList();
referenceRecords[group.Reason] = topSimilar;
// 记录调试信息
logger.LogDebug("摘要 '{Reason}' 提取关键词: {Keywords}, 找到 {Count} 个相似账单,相关度分数: {Scores}",
group.Reason,
string.Join(", ", keywords),
similarClassifiedWithScore.Count,
string.Join(", ", similarClassifiedWithScore.Select(x => $"{x.record.Reason}({x.relevanceScore:F2})")));
}
else
{
logger.LogDebug("摘要 '{Reason}' 提取关键词: {Keywords}, 未找到高相关度的相似账单",
group.Reason,
string.Join(", ", keywords));
}
}
}
// 获取所有分类
var categories = await categoryRepository.GetAllAsync();
2025-12-26 15:21:31 +08:00
// 构建分类信息
2025-12-26 15:21:31 +08:00
var categoryInfo = new StringBuilder();
foreach (var type in new[] { 0, 1, 2 })
{
var typeName = GetTypeName((TransactionType)type);
categoryInfo.AppendLine($"{typeName}: ");
var categoriesOfType = categories.Where(c => (int)c.Type == type).ToList();
foreach (var category in categoriesOfType)
{
2025-12-26 15:21:31 +08:00
categoryInfo.AppendLine($"- {category.Name}");
}
}
2025-12-29 20:30:15 +08:00
// 构建账单分组信息
var billsInfo = new StringBuilder();
foreach (var (group, index) in groupedRecords.Select((g, i) => (g, i)))
{
billsInfo.AppendLine($"{index + 1}. 摘要={group.Reason}, 当前类型={GetTypeName(group.SampleType)}, 当前分类={(string.IsNullOrEmpty(group.SampleType.ToString()) ? "" : group.SampleType.ToString())}, 涉及金额={group.TotalAmount}");
// 如果有相似的已分类账单,添加参考信息
if (referenceRecords.TryGetValue(group.Reason, out var references))
{
billsInfo.AppendLine(" 【参考】相似且已分类的账单:");
foreach (var refer in references.Take(3)) // 最多显示3个参考
{
billsInfo.AppendLine($" - 摘要={refer.Reason}, 分类={refer.Classify}, 类型={GetTypeName(refer.Type)}, 金额={refer.Amount}");
}
}
}
2025-12-26 15:21:31 +08:00
var systemPrompt = $$"""
2025-12-29 20:30:15 +08:00
2025-12-26 15:21:31 +08:00
{{categoryInfo}}
2025-12-26 15:21:31 +08:00
2025-12-29 20:30:15 +08:00
1.
2.
3. "其他"
4.
2025-12-29 20:30:15 +08:00
{"reason": "交易摘要", "type": 0:/1:/2:(Type为Number枚举值) ,"classify": "分类名称"}
2025-12-26 15:21:31 +08:00
JSON
""";
2025-12-26 17:13:57 +08:00
var userPrompt = $$"""
2025-12-29 20:30:15 +08:00
2025-12-26 17:13:57 +08:00
{{billsInfo}}
2025-12-26 17:13:57 +08:00
""";
// 流式调用AI
2025-12-29 20:30:15 +08:00
await WriteEventAsync("start", $"开始分类,共 {sampleRecords.Length} 条账单");
2025-12-29 20:30:15 +08:00
// 用于存储AI返回的分组分类结果
var classifyResults = new List<(string Reason, string Classify, TransactionType Type)>();
var buffer = new StringBuilder();
var sendedIds = new HashSet<long>();
await foreach (var chunk in openAiService.ChatStreamAsync(systemPrompt, userPrompt))
{
2025-12-29 20:30:15 +08:00
buffer.Append(chunk);
// 尝试解析完整的JSON对象
var bufferStr = buffer.ToString();
var startIdx = 0;
while (startIdx < bufferStr.Length)
{
var openBrace = bufferStr.IndexOf('{', startIdx);
if (openBrace == -1) break;
var closeBrace = FindMatchingBrace(bufferStr, openBrace);
if (closeBrace == -1) break;
var jsonStr = bufferStr.Substring(openBrace, closeBrace - openBrace + 1);
try
{
var result = JsonSerializer.Deserialize<GroupClassifyResult>(jsonStr);
if (result != null && !string.IsNullOrEmpty(result.Reason))
{
classifyResults.Add((result.Reason, result.Classify ?? "", result.Type));
// 每一条结果单独通知
var group = groupedRecords.FirstOrDefault(g => g.Reason == result.Reason);
if (group != null)
{
// 为该分组的所有账单ID返回分类结果
foreach (var id in group.Ids)
{
if (!sendedIds.Contains(id))
{
sendedIds.Add(id);
var resultJson = JsonSerializer.Serialize(new { id, result.Classify, result.Type });
await WriteEventAsync("data", resultJson);
}
}
}
}
}
catch (Exception ex)
{
logger.LogWarning(ex, "解析AI分类结果失败: {JsonStr}", jsonStr);
}
startIdx = closeBrace + 1;
}
}
await WriteEventAsync("end", "分类完成");
}
catch (Exception ex)
{
logger.LogError(ex, "智能分类失败");
await WriteEventAsync("error", $"智能分类失败: {ex.Message}");
}
}
/// <summary>
/// 批量更新账单分类
/// </summary>
[HttpPost]
public async Task<BaseResponse> BatchUpdateClassifyAsync([FromBody] List<BatchUpdateClassifyItem> items)
{
try
{
var successCount = 0;
var failCount = 0;
foreach (var item in items)
{
var record = await transactionRepository.GetByIdAsync(item.Id);
if (record != null)
{
record.Classify = item.Classify ?? string.Empty;
2025-12-26 15:21:31 +08:00
// 如果提供了Type也更新Type
if (item.Type.HasValue)
{
record.Type = item.Type.Value;
}
var success = await transactionRepository.UpdateAsync(record);
if (success)
successCount++;
else
failCount++;
}
else
{
failCount++;
}
}
return new BaseResponse
{
Success = true,
Message = $"批量更新完成,成功 {successCount} 条,失败 {failCount} 条"
};
}
catch (Exception ex)
{
logger.LogError(ex, "批量更新分类失败");
return BaseResponse.Fail($"批量更新分类失败: {ex.Message}");
}
}
2025-12-26 15:21:31 +08:00
/// <summary>
/// 获取按交易摘要分组的统计信息(支持分页)
/// </summary>
[HttpGet]
2025-12-27 11:50:12 +08:00
public async Task<PagedResponse<ReasonGroupDto>> GetReasonGroupsAsync(
2025-12-26 15:21:31 +08:00
[FromQuery] int pageIndex = 1,
[FromQuery] int pageSize = 20)
{
try
{
var (list, total) = await transactionRepository.GetReasonGroupsAsync(pageIndex, pageSize);
2025-12-27 11:50:12 +08:00
return new PagedResponse<ReasonGroupDto>
2025-12-26 15:21:31 +08:00
{
Success = true,
Data = list.ToArray(),
Total = total
};
}
catch (Exception ex)
{
logger.LogError(ex, "获取交易摘要分组失败");
2025-12-27 11:50:12 +08:00
return PagedResponse<ReasonGroupDto>.Fail($"获取交易摘要分组失败: {ex.Message}");
2025-12-26 15:21:31 +08:00
}
}
/// <summary>
/// 按摘要批量更新分类
/// </summary>
[HttpPost]
public async Task<BaseResponse<int>> BatchUpdateByReasonAsync([FromBody] BatchUpdateByReasonDto dto)
{
try
{
var count = await transactionRepository.BatchUpdateByReasonAsync(dto.Reason, dto.Type, dto.Classify);
return new BaseResponse<int>
{
Success = true,
Data = count,
Message = $"成功更新 {count} 条记录"
};
}
catch (Exception ex)
{
logger.LogError(ex, "按摘要批量更新分类失败,摘要: {Reason}", dto.Reason);
return BaseResponse<int>.Fail($"按摘要批量更新分类失败: {ex.Message}");
}
}
/// <summary>
/// 自然语言分析 - 根据用户输入的自然语言查询交易记录并预设分类
/// </summary>
[HttpPost]
public async Task<BaseResponse<NlpAnalysisResult>> NlpAnalysisAsync([FromBody] NlpAnalysisRequest request)
{
try
{
if (string.IsNullOrWhiteSpace(request.UserInput))
{
return BaseResponse<NlpAnalysisResult>.Fail("请输入查询条件");
}
// 获取所有分类
var categories = await categoryRepository.GetAllAsync();
var categoryInfo = new StringBuilder();
foreach (var type in new[] { 0, 1, 2 })
{
var typeName = GetTypeName((TransactionType)type);
categoryInfo.AppendLine($"{typeName}: ");
var categoriesOfType = categories.Where(c => (int)c.Type == type).ToList();
foreach (var category in categoriesOfType)
{
categoryInfo.AppendLine($"- {category.Name}");
}
}
var systemPrompt = $$"""
{{categoryInfo}}
2025-12-27 11:50:12 +08:00
1. SQL, SQL语句SELECT * FROM TransactionRecord WHERE Reason LIKE '%%' OR Classify LIKE '%2%' LIMIT 500
[重要Table Schema:]
```
2025-12-26 15:21:31 +08:00
TransactionRecord (
Id LONG,
2025-12-27 11:50:12 +08:00
Reason STRING NOT NULL,
2025-12-26 15:21:31 +08:00
Amount DECIMAL,
RefundAmount DECIMAL,
Balance DECIMAL,
OccurredAt DATETIME,
EmailMessageId LONG,
Type INT,
2025-12-27 11:50:12 +08:00
Classify STRING NOT NULL,
ImportNo STRING NOT NULL,
ImportFrom STRING NOT NULL
2025-12-26 15:21:31 +08:00
)
2025-12-27 11:50:12 +08:00
```
[重要]
500SQL中加入时间过滤条件
[重要SQL限制]
SELECT * FROM TransactionRecord SQL语句
{{DateTime.Now:yyyy年M月d日}}
[重要SQLite日期函数]
- strftime('%Y', OccurredAt)
- strftime('%m', OccurredAt)
- strftime('%Y-%m-%d', OccurredAt)
- 使 YEAR()MONTH()DAY() SQLite不支持
2025-12-26 15:21:31 +08:00
2. 0:, 1:, 2:
3.
JSON格式输出
{
"sql": "查询SQL",
"targetType": ,
"targetClassify": "分类名称"
}
JSON
""";
var userPrompt = $"用户输入:{request.UserInput}";
// 调用AI分析
var aiResponse = await openAiService.ChatAsync(systemPrompt, userPrompt);
logger.LogInformation("NLP分析AI返回结果: {Response}", aiResponse);
if (string.IsNullOrWhiteSpace(aiResponse))
{
return BaseResponse<NlpAnalysisResult>.Fail("AI分析失败请检查AI配置");
}
// 解析AI返回的JSON
NlpAnalysisInfo? analysisInfo;
try
{
analysisInfo = JsonSerializer.Deserialize<NlpAnalysisInfo>(aiResponse);
if (analysisInfo == null)
{
return BaseResponse<NlpAnalysisResult>.Fail("AI返回格式错误");
}
}
catch (Exception ex)
{
logger.LogError(ex, "解析AI返回结果失败返回内容: {Response}", aiResponse);
return BaseResponse<NlpAnalysisResult>.Fail($"AI返回格式错误: {ex.Message}");
}
// 根据关键词查询交易记录
2025-12-27 11:50:12 +08:00
var allRecords = await transactionRepository.ExecuteRawSqlAsync(analysisInfo.Sql);
2025-12-26 15:21:31 +08:00
logger.LogInformation("NLP分析查询到 {Count} 条记录SQL: {Sql}", allRecords.Count, analysisInfo.Sql);
2025-12-29 20:30:15 +08:00
2025-12-26 15:21:31 +08:00
// 为每条记录预设分类
var recordsWithClassify = allRecords.Select(r => new TransactionRecordWithClassify
{
Id = r.Id,
Reason = r.Reason ?? string.Empty,
Amount = r.Amount,
Balance = r.Balance,
Card = r.Card ?? string.Empty,
OccurredAt = r.OccurredAt,
CreateTime = r.CreateTime,
ImportFrom = r.ImportFrom ?? string.Empty,
RefundAmount = r.RefundAmount,
UpsetedType = analysisInfo.TargetType,
UpsetedClassify = analysisInfo.TargetClassify,
TargetType = r.Type,
TargetClassify = r.Classify ?? string.Empty
}).ToList();
return new BaseResponse<NlpAnalysisResult>
{
Success = true,
Data = new NlpAnalysisResult
{
Records = recordsWithClassify,
TargetType = analysisInfo.TargetType,
TargetClassify = analysisInfo.TargetClassify,
SearchKeyword = analysisInfo.Sql
}
};
}
catch (Exception ex)
{
logger.LogError(ex, "NLP分析失败用户输入: {Input}", request.UserInput);
return BaseResponse<NlpAnalysisResult>.Fail($"NLP分析失败: {ex.Message}");
}
}
private async Task WriteEventAsync(string eventType, string data)
{
var message = $"event: {eventType}\ndata: {data}\n\n";
await Response.WriteAsync(message);
await Response.Body.FlushAsync();
}
2025-12-29 20:30:15 +08:00
/// <summary>
/// 查找匹配的右括号
/// </summary>
private static int FindMatchingBrace(string str, int startPos)
{
int braceCount = 0;
for (int i = startPos; i < str.Length; i++)
{
if (str[i] == '{') braceCount++;
else if (str[i] == '}')
{
braceCount--;
if (braceCount == 0) return i;
}
}
return -1;
}
private static string GetTypeName(TransactionType type)
{
return type switch
{
TransactionType.Expense => "支出",
TransactionType.Income => "收入",
TransactionType.None => "不计入收支",
_ => "未知"
};
}
2025-12-25 11:20:56 +08:00
}
/// <summary>
/// 创建交易记录DTO
/// </summary>
public record CreateTransactionDto(
string OccurredAt,
string? Reason,
decimal Amount,
TransactionType Type,
2025-12-26 15:21:31 +08:00
string? Classify
2025-12-25 11:20:56 +08:00
);
/// <summary>
/// 更新交易记录DTO
/// </summary>
public record UpdateTransactionDto(
long Id,
string? Reason,
decimal Amount,
decimal Balance,
TransactionType Type,
2025-12-26 15:21:31 +08:00
string? Classify
2025-12-25 11:20:56 +08:00
);
/// <summary>
/// 日历统计响应DTO
/// </summary>
public record DailyStatisticsDto(
string Date,
int Count,
decimal Amount
);
/// <summary>
/// 智能分类请求DTO
/// </summary>
public record SmartClassifyRequest(
2025-12-26 15:21:31 +08:00
List<long>? TransactionIds = null
);
2025-12-29 20:30:15 +08:00
/// <summary>
/// 分组分类结果DTO用于AI返回结果解析
/// </summary>
public record GroupClassifyResult
{
[JsonPropertyName("reason")]
public string Reason { get; set; } = string.Empty;
[JsonPropertyName("classify")]
public string? Classify { get; set; }
[JsonPropertyName("type")]
public TransactionType Type { get; set; }
}
/// <summary>
/// 批量更新分类项DTO
/// </summary>
public record BatchUpdateClassifyItem(
long Id,
string? Classify,
2025-12-26 15:21:31 +08:00
TransactionType? Type = null
);
/// <summary>
/// 按摘要批量更新DTO
/// </summary>
public record BatchUpdateByReasonDto(
string Reason,
TransactionType Type,
string Classify
);
/// <summary>
/// NLP分析请求DTO
/// </summary>
public record NlpAnalysisRequest(
string UserInput
);
/// <summary>
/// NLP分析结果DTO
/// </summary>
public record NlpAnalysisResult
{
public List<TransactionRecordWithClassify> Records { get; set; } = new();
public TransactionType TargetType { get; set; }
public string TargetClassify { get; set; } = string.Empty;
public string SearchKeyword { get; set; } = string.Empty;
}
/// <summary>
/// 带分类信息的交易记录DTO
/// </summary>
public record TransactionRecordWithClassify
{
public long Id { get; set; }
public string Reason { get; set; } = string.Empty;
public decimal Amount { get; set; }
public decimal Balance { get; set; }
public string Card { get; set; } = string.Empty;
public DateTime OccurredAt { get; set; }
public DateTime CreateTime { get; set; }
public string ImportFrom { get; set; } = string.Empty;
public decimal RefundAmount { get; set; }
public TransactionType UpsetedType { get; set; }
public string UpsetedClassify { get; set; } = string.Empty;
public TransactionType TargetType { get; set; }
public string TargetClassify { get; set; } = string.Empty;
}
/// <summary>
/// AI分析信息DTO内部使用
/// </summary>
public record NlpAnalysisInfo
{
[JsonPropertyName("sql")]
public string Sql { get; set; } = string.Empty;
[JsonPropertyName("targetType")]
public TransactionType TargetType { get; set; }
[JsonPropertyName("targetClassify")]
public string TargetClassify { get; set; } = string.Empty;
}
2025-12-26 17:13:57 +08:00
/// <summary>
/// 账单分析请求DTO
/// </summary>
public record BillAnalysisRequest(
string UserInput
);
/// <summary>
/// 账单查询信息DTO
/// </summary>
public class BillQueryInfo
{
[JsonPropertyName("timeRange")]
public TimeRangeInfo? TimeRange { get; set; }
[JsonPropertyName("categories")]
public List<string>? Categories { get; set; }
[JsonPropertyName("transactionType")]
public TransactionType? TransactionType { get; set; }
[JsonPropertyName("analysisType")]
public string? AnalysisType { get; set; }
}
/// <summary>
/// 时间范围信息DTO
/// </summary>
public class TimeRangeInfo
{
[JsonPropertyName("months")]
public int Months { get; set; }
[JsonPropertyName("startYear")]
public int StartYear { get; set; }
[JsonPropertyName("startMonth")]
public int? StartMonth { get; set; }
[JsonPropertyName("endYear")]
public int? EndYear { get; set; }
[JsonPropertyName("endMonth")]
public int? EndMonth { get; set; }
}