新增:统计功能
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 20s
Docker Build & Deploy / Deploy to Production (push) Successful in 5s

This commit is contained in:
孙诚
2025-12-26 17:13:57 +08:00
parent cb11d80d1f
commit 09393f8ee5
7 changed files with 1955 additions and 9 deletions

View File

@@ -1,5 +1,7 @@
namespace WebApi.Controllers;
using Repository;
[ApiController]
[Route("api/[controller]/[action]")]
public class TransactionRecordController(
@@ -230,13 +232,244 @@ public class TransactionRecordController(
}
}
/// <summary>
/// 获取月度统计数据
/// </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>
/// 获取趋势统计数据
/// </summary>
[HttpGet]
public async Task<BaseResponse<List<TrendStatistics>>> GetTrendStatisticsAsync(
[FromQuery] int startYear,
[FromQuery] int startMonth,
[FromQuery] int monthCount = 6
)
{
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);
// 清理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生成分析报告
var dataJson = System.Text.Json.JsonSerializer.Serialize(queryResults, new System.Text.Json.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. 支出金额用 <span class='expense-value'>金额</span> 包裹
6. 收入金额用 <span class='income-value'>金额</span> 包裹
7. 重要结论用 <span class='highlight'>内容</span> 高亮
【样式限制(重要)】
8. 不要包含 html、body、head 标签
9. 不要使用任何 style 属性或 <style> 标签
10. 不要设置 background、background-color、color 等样式属性
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]
public async Task<BaseResponse<List<Entity.TransactionRecord>>> GetByDateAsync(
[FromQuery] string date
)
public async Task<BaseResponse<List<Entity.TransactionRecord>>> GetByDateAsync([FromQuery] string date)
{
try
{
@@ -380,11 +613,13 @@ public class TransactionRecordController(
JSON
""";
var userPrompt = $@"请为以下账单进行分类:
var userPrompt = $$"""
请为以下账单进行分类:
{billsInfo}
{{billsInfo}}
请逐个输出分类结果。";
请逐个输出分类结果。
""";
// 流式调用AI
await WriteEventAsync("start", $"开始分类 {records.Count} 条账单");
@@ -587,7 +822,7 @@ public class TransactionRecordController(
}
// 根据关键词查询交易记录
var allRecords = await transactionRepository.QueryBySqlAsync(analysisInfo.Sql);
var allRecords = await transactionRepository.QueryByWhereAsync(analysisInfo.Sql);
logger.LogInformation("NLP分析查询到 {Count} 条记录SQL: {Sql}", allRecords.Count, analysisInfo.Sql);
// 为每条记录预设分类
@@ -757,3 +992,49 @@ public record NlpAnalysisInfo
[JsonPropertyName("targetClassify")]
public string TargetClassify { get; set; } = string.Empty;
}
/// <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; }
}