namespace WebApi.Controllers; using Repository; [ApiController] [Route("api/[controller]/[action]")] public class TransactionRecordController( ITransactionRecordRepository transactionRepository, ITransactionCategoryRepository categoryRepository, IOpenAiService openAiService, ITextSegmentService textSegmentService, ILogger logger ) : 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] bool sortByAmount = false ) { try { TransactionType? transactionType = type.HasValue ? (TransactionType)type.Value : null; var list = await transactionRepository.GetPagedListAsync( pageIndex, pageSize, searchKeyword, classify, transactionType, year, month, sortByAmount); var total = await transactionRepository.GetTotalCountAsync( searchKeyword, classify, transactionType, year, month); 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}"); } } /// /// 根据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 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> GetMonthlyStatisticsAsync( [FromQuery] int year, [FromQuery] int month ) { try { var statistics = await transactionRepository.GetMonthlyStatisticsAsync(year, month); return new BaseResponse { Success = true, Data = statistics }; } catch (Exception ex) { logger.LogError(ex, "获取月度统计数据失败,年份: {Year}, 月份: {Month}", year, month); return BaseResponse.Fail($"获取月度统计数据失败: {ex.Message}"); } } /// /// 获取分类统计数据 /// [HttpGet] public async Task>> GetCategoryStatisticsAsync( [FromQuery] int year, [FromQuery] int month, [FromQuery] TransactionType type ) { try { var statistics = await transactionRepository.GetCategoryStatisticsAsync(year, month, type); return new BaseResponse> { Success = true, Data = statistics }; } catch (Exception ex) { logger.LogError(ex, "获取分类统计数据失败,年份: {Year}, 月份: {Month}, 类型: {Type}", year, month, type); return BaseResponse>.Fail($"获取分类统计数据失败: {ex.Message}"); } } /// /// 获取趋势统计数据 /// [HttpGet] public async Task>> GetTrendStatisticsAsync( [FromQuery] int startYear, [FromQuery] int startMonth, [FromQuery] int monthCount = 6 ) { try { var statistics = await transactionRepository.GetTrendStatisticsAsync(startYear, startMonth, monthCount); return new BaseResponse> { Success = true, Data = statistics }; } catch (Exception ex) { logger.LogError(ex, "获取趋势统计数据失败,开始年份: {Year}, 开始月份: {Month}, 月份数: {Count}", startYear, startMonth, monthCount); return BaseResponse>.Fail($"获取趋势统计数据失败: {ex.Message}"); } } /// /// 智能分析账单(流式输出) /// 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); // 清理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 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 = "
SQL执行失败,请重新描述您的问题
" }); await Response.WriteAsync($"data: {errorData}\n\n"); await Response.Body.FlushAsync(); return; } // 第三步:将查询结果序列化为JSON,直接传递给AI生成分析报告 var dataJson = System.Text.Json.JsonSerializer.Serialize(queryResults, new JsonSerializerOptions { WriteIndented = true, Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping }); var dataPrompt = $""" 当前日期:{DateTime.Now:yyyy年M月d日} 用户问题:{request.UserInput} 查询结果数据(JSON格式): {dataJson} 说明:以上数据是根据用户问题查询出的聚合统计结果,请基于这些数据生成分析报告。 请生成一份专业的数据分析报告,严格遵守以下要求: 【格式要求】 1. 使用HTML格式(移动端H5页面风格) 2. 生成清晰的报告标题(基于用户问题) 3. 使用表格展示统计数据(table > thead/tbody > tr > th/td) 4. 使用合适的HTML标签:h2(标题)、h3(小节)、p(段落)、table(表格)、ul/li(列表)、strong(强调) 5. 支出金额用 金额 包裹 6. 收入金额用 金额 包裹 7. 重要结论用 内容 高亮 【样式限制(重要)】 8. 不要包含 html、body、head 标签 9. 不要使用任何 style 属性或