添加智能分类功能,支持获取未分类账单数量和列表;实现AI分类逻辑;更新相关API和前端视图
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 20s
Docker Build & Deploy / Deploy to Production (push) Successful in 8s

This commit is contained in:
孙诚
2025-12-25 15:40:50 +08:00
parent a9dfcdaa5c
commit bbcb630401
9 changed files with 714 additions and 3 deletions

View File

@@ -4,6 +4,8 @@
[Route("api/[controller]/[action]")]
public class TransactionRecordController(
ITransactionRecordRepository transactionRepository,
ITransactionCategoryRepository categoryRepository,
IOpenAiService openAiService,
ILogger<TransactionRecordController> logger
) : ControllerBase
{
@@ -264,6 +266,187 @@ public class TransactionRecordController(
}
}
/// <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]
public async Task<BaseResponse<List<Entity.TransactionRecord>>> GetUnclassifiedAsync([FromQuery] int pageSize = 10)
{
try
{
var records = await transactionRepository.GetUnclassifiedAsync(pageSize);
return new BaseResponse<List<Entity.TransactionRecord>>
{
Success = true,
Data = records
};
}
catch (Exception ex)
{
logger.LogError(ex, "获取未分类账单列表失败");
return BaseResponse<List<Entity.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
{
// 获取要分类的账单
var records = await transactionRepository.GetUnclassifiedAsync(request.PageSize);
if (records.Count == 0)
{
await WriteEventAsync("error", "没有需要分类的账单");
return;
}
// 获取所有分类
var categories = await categoryRepository.GetAllAsync();
// 构建分类信息
var categoryInfo = string.Join("\n", categories
.Select(c =>
{
var children = categories.Where(x => x.ParentId == c.Id).ToList();
var childrenStr = children.Count > 0
? $",子分类:{string.Join("", children.Select(x => x.Name))}"
: "";
return $"- {c.Name} ({(c.Type == TransactionType.Expense ? "" : "")}){childrenStr}";
}));
// 构建账单信息
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. 如果有合适的子分类,也要指定子分类
3. 如果无法确定分类,可以选择""其他""
请对每个账单进行分类,每次输出一个账单的分类结果,格式如下:
{{""id"": 账单ID, ""classify"": ""一级分类"", ""subClassify"": ""子分类""}}
只输出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}");
}
}
/// <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;
record.SubClassify = item.SubClassify ?? string.Empty;
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}");
}
}
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 => "不计入收支",
_ => "未知"
};
}
}
@@ -299,4 +482,20 @@ public record DailyStatisticsDto(
string Date,
int Count,
decimal Amount
);
/// <summary>
/// 智能分类请求DTO
/// </summary>
public record SmartClassifyRequest(
int PageSize = 10
);
/// <summary>
/// 批量更新分类项DTO
/// </summary>
public record BatchUpdateClassifyItem(
long Id,
string? Classify,
string? SubClassify
);