using Service.AI; using Service.Transaction; namespace WebApi.Controllers; [ApiController] [Route("api/[controller]/[action]")] public class TransactionRecordController( ITransactionRecordRepository transactionRepository, ITransactionStatisticsService transactionStatisticsService, ISmartHandleService smartHandleService, ILogger logger, IConfigService configService ) : ControllerBase { /// /// 获取交易记录列表(分页) /// [HttpGet] public async Task> GetListAsync( [FromQuery] int pageIndex = 1, [FromQuery] int pageSize = 20, [FromQuery] string? searchKeyword = null, [FromQuery] string? classify = null, [FromQuery] int? type = null, [FromQuery] int? year = null, [FromQuery] int? month = null, [FromQuery] DateTime? startDate = null, [FromQuery] DateTime? endDate = null, [FromQuery] string? reason = null, [FromQuery] bool sortByAmount = false ) { try { var classifies = string.IsNullOrWhiteSpace(classify) ? null : classify.Split(',', StringSplitOptions.RemoveEmptyEntries); TransactionType? transactionType = type.HasValue ? (TransactionType)type.Value : null; var list = await transactionRepository.QueryAsync( year: year, month: month, startDate: startDate, endDate: endDate, type: transactionType, classifies: classifies, searchKeyword: searchKeyword, reason: reason, pageIndex: pageIndex, pageSize: pageSize, sortByAmount: sortByAmount); var total = await transactionRepository.CountAsync( year: year, month: month, startDate: startDate, endDate: endDate, type: transactionType, classifies: classifies, searchKeyword: searchKeyword, reason: reason); return new PagedResponse { Success = true, Data = list.ToArray(), Total = (int)total }; } catch (Exception ex) { logger.LogError(ex, "获取交易记录列表失败,页码: {PageIndex}, 页大小: {PageSize}", pageIndex, pageSize); return PagedResponse.Fail($"获取交易记录列表失败: {ex.Message}"); } } /// /// 获取待确认分类的交易记录列表 /// [HttpGet] public async Task>> GetUnconfirmedListAsync() { try { var list = await transactionRepository.GetUnconfirmedRecordsAsync(); return list.Ok(); } catch (Exception ex) { logger.LogError(ex, "获取待确认分类交易列表失败"); return $"获取待确认分类交易列表失败: {ex.Message}".Fail>(); } } /// /// 全部确认待确认的交易分类 /// [HttpPost] public async Task> ConfirmAllUnconfirmedAsync([FromBody] ConfirmAllUnconfirmedRequestDto request) { try { var count = await transactionRepository.ConfirmAllUnconfirmedAsync(request.Ids); return count.Ok(); } catch (Exception ex) { logger.LogError(ex, "全部确认待确认分类失败"); return $"全部确认待确认分类失败: {ex.Message}".Fail(); } } /// /// 根据ID获取交易记录详情 /// [HttpGet("{id}")] public async Task> GetByIdAsync(long id) { try { var transaction = await transactionRepository.GetByIdAsync(id); if (transaction == null) { return "交易记录不存在".Fail(); } return transaction.Ok(); } catch (Exception ex) { logger.LogError(ex, "获取交易记录详情失败,交易ID: {TransactionId}", id); return $"获取交易记录详情失败: {ex.Message}".Fail(); } } /// /// 根据邮件ID获取交易记录列表 /// [HttpGet("{emailId}")] public async Task>> GetByEmailIdAsync(long emailId) { try { var transactions = await transactionRepository.GetByEmailIdAsync(emailId); return transactions.Ok(); } catch (Exception ex) { logger.LogError(ex, "获取邮件交易记录失败,邮件ID: {EmailId}", emailId); return $"获取邮件交易记录失败: {ex.Message}".Fail>(); } } /// /// 创建交易记录 /// [HttpPost] public async Task CreateAsync([FromBody] CreateTransactionDto dto) { try { // 解析日期字符串 if (!DateTime.TryParse(dto.OccurredAt, out var occurredAt)) { return "交易时间格式不正确".Fail(); } var transaction = new TransactionRecord { OccurredAt = occurredAt, Reason = dto.Reason ?? string.Empty, Amount = dto.Amount, Type = dto.Type, Classify = dto.Classify ?? string.Empty, ImportFrom = "手动录入", ImportNo = Guid.NewGuid().ToString("N"), Card = "手动", EmailMessageId = 0 // 手动录入的记录,EmailMessageId 设为 0 }; var result = await transactionRepository.AddAsync(transaction); if (result) { return BaseResponse.Done(); } return "创建交易记录失败".Fail(); } catch (Exception ex) { logger.LogError(ex, "创建交易记录失败,交易信息: {@TransactionDto}", dto); return $"创建交易记录失败: {ex.Message}".Fail(); } } /// /// 更新交易记录 /// [HttpPost] public async Task UpdateAsync([FromBody] UpdateTransactionDto dto) { try { var transaction = await transactionRepository.GetByIdAsync(dto.Id); if (transaction == null) { return "交易记录不存在".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; // 更新交易时间 if (!string.IsNullOrEmpty(dto.OccurredAt) && DateTime.TryParse(dto.OccurredAt, out var occurredAt)) { transaction.OccurredAt = occurredAt; } // 清除待确认状态 transaction.UnconfirmedClassify = null; transaction.UnconfirmedType = null; var success = await transactionRepository.UpdateAsync(transaction); if (success) { return BaseResponse.Done(); } return "更新交易记录失败".Fail(); } catch (Exception ex) { logger.LogError(ex, "更新交易记录失败,交易ID: {TransactionId}, 交易信息: {@TransactionDto}", dto.Id, dto); return $"更新交易记录失败: {ex.Message}".Fail(); } } /// /// 删除交易记录 /// [HttpPost] public async Task DeleteByIdAsync([FromQuery] long id) { try { var success = await transactionRepository.DeleteAsync(id); if (success) { return BaseResponse.Done(); } return "删除交易记录失败,记录不存在".Fail(); } catch (Exception ex) { logger.LogError(ex, "删除交易记录失败,交易ID: {TransactionId}", id); return $"删除交易记录失败: {ex.Message}".Fail(); } } /// /// 获取累积余额统计数据(用于余额卡片图表) /// [HttpGet] public async Task>> GetBalanceStatisticsAsync( [FromQuery] int year, [FromQuery] int month ) { try { // 获取存款分类 var savingClassify = await configService.GetConfigByKeyAsync("SavingsCategories"); // 获取每日统计数据 var statistics = await transactionStatisticsService.GetDailyStatisticsAsync(year, month, savingClassify); // 按日期排序并计算累积余额 var sortedStats = statistics.OrderBy(s => s.Key).ToList(); var result = new List(); decimal cumulativeBalance = 0; foreach (var item in sortedStats) { var dailyBalance = item.Value.income - item.Value.expense; cumulativeBalance += dailyBalance; result.Add(new BalanceStatisticsDto( item.Key, cumulativeBalance )); } return result.Ok(); } catch (Exception ex) { logger.LogError(ex, "获取累积余额统计失败,年份: {Year}, 月份: {Month}", year, month); return $"获取累积余额统计失败: {ex.Message}".Fail>(); } } /// /// 获取指定月份每天的消费统计 /// [HttpGet] public async Task>> GetDailyStatisticsAsync( [FromQuery] int year, [FromQuery] int month ) { try { // 获取存款分类 var savingClassify = await configService.GetConfigByKeyAsync("SavingsCategories"); var statistics = await transactionStatisticsService.GetDailyStatisticsAsync(year, month, savingClassify); var result = statistics.Select(s => new DailyStatisticsDto( s.Key, s.Value.count, s.Value.expense, s.Value.income, s.Value.saving )).ToList(); return result.Ok(); } catch (Exception ex) { logger.LogError(ex, "获取日历统计数据失败,年份: {Year}, 月份: {Month}", year, month); return $"获取日历统计数据失败: {ex.Message}".Fail>(); } } /// /// 获取月度统计数据 /// [HttpGet] public async Task> GetMonthlyStatisticsAsync( [FromQuery] int year, [FromQuery] int month ) { try { var statistics = await transactionStatisticsService.GetMonthlyStatisticsAsync(year, month); return statistics.Ok(); } catch (Exception ex) { logger.LogError(ex, "获取月度统计数据失败,年份: {Year}, 月份: {Month}", year, month); return $"获取月度统计数据失败: {ex.Message}".Fail(); } } /// /// 获取分类统计数据 /// [HttpGet] public async Task>> GetCategoryStatisticsAsync( [FromQuery] int year, [FromQuery] int month, [FromQuery] TransactionType type ) { try { var statistics = await transactionStatisticsService.GetCategoryStatisticsAsync(year, month, type); return statistics.Ok(); } catch (Exception ex) { logger.LogError(ex, "获取分类统计数据失败,年份: {Year}, 月份: {Month}, 类型: {Type}", year, month, type); return $"获取分类统计数据失败: {ex.Message}".Fail>(); } } /// /// 获取趋势统计数据 /// [HttpGet] public async Task>> GetTrendStatisticsAsync( [FromQuery] int startYear, [FromQuery] int startMonth, [FromQuery] int monthCount = 6 ) { try { var statistics = await transactionStatisticsService.GetTrendStatisticsAsync(startYear, startMonth, monthCount); return statistics.Ok(); } catch (Exception ex) { logger.LogError(ex, "获取趋势统计数据失败,开始年份: {Year}, 开始月份: {Month}, 月份数: {Count}", startYear, startMonth, monthCount); return $"获取趋势统计数据失败: {ex.Message}".Fail>(); } } /// /// 智能分析账单(流式输出) /// 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"); if (string.IsNullOrWhiteSpace(request.UserInput)) { await WriteEventAsync("
请输入分析内容
"); return; } await smartHandleService.AnalyzeBillAsync(request.UserInput, async void (chunk) => { try { await WriteEventAsync(chunk); } catch (Exception e) { logger.LogError(e, "流式写入账单分析结果失败"); } }); } /// /// 获取指定日期的交易记录 /// [HttpGet] public async Task>> GetByDateAsync([FromQuery] string date) { try { if (!DateTime.TryParse(date, out var targetDate)) { return "日期格式不正确".Fail>(); } // 获取当天的开始和结束时间 var startDate = targetDate.Date; var endDate = startDate.AddDays(1); var records = await transactionRepository.QueryAsync(startDate: startDate, endDate: endDate); return records.Ok(); } catch (Exception ex) { logger.LogError(ex, "获取指定日期的交易记录失败,日期: {Date}", date); return $"获取指定日期的交易记录失败: {ex.Message}".Fail>(); } } /// /// 获取未分类的账单数量 /// [HttpGet] public async Task> GetUnclassifiedCountAsync() { try { var count = (int)await transactionRepository.CountAsync(); return count.Ok(); } catch (Exception ex) { logger.LogError(ex, "获取未分类账单数量失败"); return $"获取未分类账单数量失败: {ex.Message}".Fail(); } } /// /// 获取未分类的账单列表 /// [HttpGet] public async Task>> GetUnclassifiedAsync([FromQuery] int pageSize = 10) { try { var records = await transactionRepository.GetUnclassifiedAsync(pageSize); return records.Ok(); } catch (Exception ex) { logger.LogError(ex, "获取未分类账单列表失败"); return $"获取未分类账单列表失败: {ex.Message}".Fail>(); } } /// /// 智能分类 - 使用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"); // 验证账单ID列表 if (request.TransactionIds == null || request.TransactionIds.Count == 0) { await WriteEventAsync("error", "请提供要分类的账单ID"); return; } await smartHandleService.SmartClassifyAsync(request.TransactionIds.ToArray(), async void (chunk) => { try { var (eventType, content) = chunk; await TrySetUnconfirmedAsync(eventType, content); await WriteEventAsync(eventType, content); } catch (Exception e) { logger.LogError(e, "流式写入智能分类结果失败"); } }); await Response.Body.FlushAsync(); } private async Task TrySetUnconfirmedAsync(string eventType, string content) { if (eventType != "data") { return; } try { var jsonObject = JsonSerializer.Deserialize(content); var id = jsonObject?["id"]?.GetValue() ?? 0; var classify = jsonObject?["Classify"]?.GetValue() ?? string.Empty; var typeValue = jsonObject?["Type"]?.GetValue() ?? -1; if (id == 0 || typeValue == -1 || string.IsNullOrEmpty(classify)) { logger.LogWarning("解析智能分类结果时,发现无效数据,内容: {Content}", content); return; } var record = await transactionRepository.GetByIdAsync(id); if (record == null) { logger.LogWarning("解析智能分类结果时,未找到对应的交易记录,ID: {Id}", id); return; } record.UnconfirmedClassify = classify; record.UnconfirmedType = (TransactionType)typeValue; var success = await transactionRepository.UpdateAsync(record); if (!success) { logger.LogWarning("解析智能分类结果时,更新交易记录失败,ID: {Id}", id); } } catch (Exception ex) { logger.LogError(ex, "解析智能分类结果失败,内容: {Content}", content); } } /// /// 批量更新账单分类 /// [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; } if (!string.IsNullOrEmpty(record.Classify)) { record.UnconfirmedClassify = null; } if (record.Type == item.Type) { record.UnconfirmedType = TransactionType.None; } var success = await transactionRepository.UpdateAsync(record); if (success) successCount++; else failCount++; } else { failCount++; } } return $"批量更新完成,成功 {successCount} 条,失败 {failCount} 条".Ok(); } catch (Exception ex) { logger.LogError(ex, "批量更新分类失败"); return $"批量更新分类失败: {ex.Message}".Fail(); } } /// /// 获取按交易摘要分组的统计信息(支持分页) /// [HttpGet] public async Task> GetReasonGroupsAsync( [FromQuery] int pageIndex = 1, [FromQuery] int pageSize = 20) { try { var (list, total) = await transactionStatisticsService.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 count.Ok($"成功更新 {count} 条记录"); } catch (Exception ex) { logger.LogError(ex, "按摘要批量更新分类失败,摘要: {Reason}", dto.Reason); return $"按摘要批量更新分类失败: {ex.Message}".Fail(); } } /// /// 一句话录账解析 /// [HttpPost] public async Task> ParseOneLine([FromBody] ParseOneLineRequestDto request) { if (string.IsNullOrEmpty(request.Text)) { return "请求参数缺失:text".Fail(); } try { var result = await smartHandleService.ParseOneLineBillAsync(request.Text); if (result == null) { return "AI解析失败".Fail(); } return result.Ok(); } catch (Exception ex) { logger.LogError(ex, "一句话录账解析失败,文本: {Text}", request.Text); return ("AI解析失败: " + ex.Message).Fail(); } } 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 async Task WriteEventAsync(string data) { var message = $"data: {data}\n\n"; await Response.WriteAsync(message); await Response.Body.FlushAsync(); } } /// /// 创建交易记录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, string? OccurredAt = null ); /// /// 日历统计响应DTO /// public record DailyStatisticsDto( string Date, int Count, decimal Expense, decimal Income, decimal Balance ); /// /// 累积余额统计DTO /// public record BalanceStatisticsDto( string Date, decimal CumulativeBalance ); /// /// 智能分类请求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 ); /// /// 账单分析请求DTO /// public record BillAnalysisRequest( string UserInput ); public record ParseOneLineRequestDto( string Text ); public record ConfirmAllUnconfirmedRequestDto( long[] Ids );