添加智能分类功能,支持获取未分类账单数量和列表;实现AI分类逻辑;更新相关API和前端视图
This commit is contained in:
@@ -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
|
||||
);
|
||||
Reference in New Issue
Block a user