namespace WebApi.Controllers; [ApiController] [Route("api/[controller]/[action]")] public class TransactionRecordController( ITransactionRecordRepository transactionRepository, ITransactionCategoryRepository categoryRepository, IOpenAiService openAiService, ILogger logger ) : ControllerBase { /// /// 获取交易记录列表(分页) /// [HttpGet] public async Task> GetListAsync( [FromQuery] DateTime? lastOccurredAt = null, [FromQuery] long? lastId = null, [FromQuery] string? searchKeyword = null ) { try { var (list, lastTime, lastIdResult) = await transactionRepository.GetPagedListAsync(lastOccurredAt, lastId, 20, searchKeyword); var total = await transactionRepository.GetTotalCountAsync(); return new PagedResponse { Success = true, Data = list.ToArray(), Total = (int)total, LastId = lastIdResult, LastTime = lastTime }; } catch (Exception ex) { logger.LogError(ex, "获取交易记录列表失败,时间: {LastTime}, ID: {LastId}", lastOccurredAt, lastId); return PagedResponse.Fail($"获取交易记录列表失败: {ex.Message}"); } } /// /// 根据ID获取交易记录详情 /// [HttpGet("{id}")] public async Task> GetByIdAsync(long id) { try { var transaction = await transactionRepository.GetByIdAsync(id); if (transaction == null) { return BaseResponse.Fail("交易记录不存在"); } return new BaseResponse { Success = true, Data = transaction }; } catch (Exception ex) { logger.LogError(ex, "获取交易记录详情失败,交易ID: {TransactionId}", id); return BaseResponse.Fail($"获取交易记录详情失败: {ex.Message}"); } } /// /// 根据邮件ID获取交易记录列表 /// [HttpGet("{emailId}")] public async Task>> GetByEmailIdAsync(long emailId) { try { var transactions = await transactionRepository.GetByEmailIdAsync(emailId); return new BaseResponse> { Success = true, Data = transactions }; } catch (Exception ex) { logger.LogError(ex, "获取邮件交易记录失败,邮件ID: {EmailId}", emailId); return BaseResponse>.Fail($"获取邮件交易记录失败: {ex.Message}"); } } /// /// 创建交易记录 /// [HttpPost] public async Task CreateAsync([FromBody] CreateTransactionDto dto) { try { // 解析日期字符串 if (!DateTime.TryParse(dto.OccurredAt, out var occurredAt)) { return BaseResponse.Fail("交易时间格式不正确"); } var transaction = new Entity.TransactionRecord { 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}"); } } /// /// 更新交易记录 /// [HttpPost] public async Task 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}"); } } /// /// 删除交易记录 /// [HttpPost] public async Task 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}"); } } /// /// 获取指定月份每天的消费统计 /// [HttpGet] public async Task>> 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> { Success = true, Data = result }; } catch (Exception ex) { logger.LogError(ex, "获取日历统计数据失败,年份: {Year}, 月份: {Month}", year, month); return BaseResponse>.Fail($"获取日历统计数据失败: {ex.Message}"); } } /// /// 获取指定日期的交易记录 /// [HttpGet] public async Task>> GetByDateAsync( [FromQuery] string date ) { try { if (!DateTime.TryParse(date, out var targetDate)) { return BaseResponse>.Fail("日期格式不正确"); } // 获取当天的开始和结束时间 var startDate = targetDate.Date; var endDate = startDate.AddDays(1); var records = await transactionRepository.GetByDateRangeAsync(startDate, endDate); return new BaseResponse> { Success = true, Data = records }; } catch (Exception ex) { logger.LogError(ex, "获取指定日期的交易记录失败,日期: {Date}", date); return BaseResponse>.Fail($"获取指定日期的交易记录失败: {ex.Message}"); } } /// /// 获取未分类的账单数量 /// [HttpGet] public async Task> GetUnclassifiedCountAsync() { try { var count = await transactionRepository.GetUnclassifiedCountAsync(); return new BaseResponse { Success = true, Data = count }; } catch (Exception ex) { logger.LogError(ex, "获取未分类账单数量失败"); return BaseResponse.Fail($"获取未分类账单数量失败: {ex.Message}"); } } /// /// 获取未分类的账单列表 /// [HttpGet] public async Task>> GetUnclassifiedAsync([FromQuery] int pageSize = 10) { try { var records = await transactionRepository.GetUnclassifiedAsync(pageSize); return new BaseResponse> { Success = true, Data = records }; } catch (Exception ex) { logger.LogError(ex, "获取未分类账单列表失败"); return BaseResponse>.Fail($"获取未分类账单列表失败: {ex.Message}"); } } /// /// 智能分类 - 使用AI对账单进行分类(流式响应) /// [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 { // 验证账单ID列表 if (request.TransactionIds == null || request.TransactionIds.Count == 0) { await WriteEventAsync("error", "请提供要分类的账单ID"); return; } // 获取指定ID的账单 var records = new List(); foreach (var id in request.TransactionIds) { var record = await transactionRepository.GetByIdAsync(id); if (record != null && record.Classify == string.Empty) { records.Add(record); } } if (records.Count == 0) { await WriteEventAsync("error", "找不到指定的账单"); return; } // 获取所有分类 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 billsInfo = string.Join("\n", records.Select((r, i) => $"{i + 1}. ID={r.Id}, 摘要={r.Reason}, 金额={r.Amount}, 类型={GetTypeName(r.Type)}")); var systemPrompt = $$""" 你是一个专业的账单分类助手。请根据提供的账单信息和分类列表,为每个账单选择最合适的分类。 可用的分类列表: {{categoryInfo}} 分类规则: 1. 根据账单的摘要和金额,选择最匹配的分类 2. 如果无法确定分类,可以选择""其他"" 请对每个账单进行分类,每次输出一个账单的分类结果,格式如下: {"id": 账单ID, "type": 0:支出/1:收入/2:不计入收支(Type为Number枚举值) ,"classify": 分类名称} 只输出JSON,不要有其他文字说明。 """; var userPrompt = $@"请为以下账单进行分类: {billsInfo} 请逐个输出分类结果。"; // 流式调用AI await WriteEventAsync("start", $"开始分类 {records.Count} 条账单"); await foreach (var chunk in openAiService.ChatStreamAsync(systemPrompt, userPrompt)) { await WriteEventAsync("data", chunk); } await WriteEventAsync("end", "分类完成"); } catch (Exception ex) { logger.LogError(ex, "智能分类失败"); await WriteEventAsync("error", $"智能分类失败: {ex.Message}"); } } /// /// 批量更新账单分类 /// [HttpPost] public async Task BatchUpdateClassifyAsync([FromBody] List 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; // 如果提供了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}"); } } /// /// 获取按交易摘要分组的统计信息(支持分页) /// [HttpGet] public async Task> GetReasonGroupsAsync( [FromQuery] int pageIndex = 1, [FromQuery] int pageSize = 20) { try { var (list, total) = await transactionRepository.GetReasonGroupsAsync(pageIndex, pageSize); return new PagedResponse { Success = true, Data = list.ToArray(), Total = total }; } catch (Exception ex) { logger.LogError(ex, "获取交易摘要分组失败"); return PagedResponse.Fail($"获取交易摘要分组失败: {ex.Message}"); } } /// /// 按摘要批量更新分类 /// [HttpPost] public async Task> BatchUpdateByReasonAsync([FromBody] BatchUpdateByReasonDto dto) { try { var count = await transactionRepository.BatchUpdateByReasonAsync(dto.Reason, dto.Type, dto.Classify); return new BaseResponse { Success = true, Data = count, Message = $"成功更新 {count} 条记录" }; } catch (Exception ex) { logger.LogError(ex, "按摘要批量更新分类失败,摘要: {Reason}", dto.Reason); return BaseResponse.Fail($"按摘要批量更新分类失败: {ex.Message}"); } } /// /// 自然语言分析 - 根据用户输入的自然语言查询交易记录并预设分类 /// [HttpPost] public async Task> NlpAnalysisAsync([FromBody] NlpAnalysisRequest request) { try { if (string.IsNullOrWhiteSpace(request.UserInput)) { return BaseResponse.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}} 你需要分析用户的需求,提取以下信息: 1. 查询SQL, 根据用户的描述生成SQL Where 子句,用于查询交易记录。例如:Reason LIKE '%关键词%' Table Schema: TransactionRecord ( Id LONG, Reason STRING, Amount DECIMAL, RefundAmount DECIMAL, Balance DECIMAL, OccurredAt DATETIME, EmailMessageId LONG, Type INT, Classify STRING, ImportNo STRING, ImportFrom STRING ) 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.Fail("AI分析失败,请检查AI配置"); } // 解析AI返回的JSON NlpAnalysisInfo? analysisInfo; try { analysisInfo = JsonSerializer.Deserialize(aiResponse); if (analysisInfo == null) { return BaseResponse.Fail("AI返回格式错误"); } } catch (Exception ex) { logger.LogError(ex, "解析AI返回结果失败,返回内容: {Response}", aiResponse); return BaseResponse.Fail($"AI返回格式错误: {ex.Message}"); } // 根据关键词查询交易记录 var allRecords = await transactionRepository.QueryBySqlAsync(analysisInfo.Sql); logger.LogInformation("NLP分析查询到 {Count} 条记录,SQL: {Sql}", allRecords.Count, analysisInfo.Sql); // 为每条记录预设分类 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 { 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.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(); } private static string GetTypeName(TransactionType type) { return type switch { TransactionType.Expense => "支出", TransactionType.Income => "收入", TransactionType.None => "不计入收支", _ => "未知" }; } } /// /// 创建交易记录DTO /// public record CreateTransactionDto( string OccurredAt, string? Reason, decimal Amount, TransactionType Type, string? Classify ); /// /// 更新交易记录DTO /// public record UpdateTransactionDto( long Id, string? Reason, decimal Amount, decimal Balance, TransactionType Type, string? Classify ); /// /// 日历统计响应DTO /// public record DailyStatisticsDto( string Date, int Count, decimal Amount ); /// /// 智能分类请求DTO /// public record SmartClassifyRequest( List? TransactionIds = null ); /// /// 批量更新分类项DTO /// public record BatchUpdateClassifyItem( long Id, string? Classify, TransactionType? Type = null ); /// /// 按摘要批量更新DTO /// public record BatchUpdateByReasonDto( string Reason, TransactionType Type, string Classify ); /// /// NLP分析请求DTO /// public record NlpAnalysisRequest( string UserInput ); /// /// NLP分析结果DTO /// public record NlpAnalysisResult { public List Records { get; set; } = new(); public TransactionType TargetType { get; set; } public string TargetClassify { get; set; } = string.Empty; public string SearchKeyword { get; set; } = string.Empty; } /// /// 带分类信息的交易记录DTO /// 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; } /// /// AI分析信息DTO(内部使用) /// 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; }