添加功能
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 29s
Docker Build & Deploy / Deploy to Production (push) Successful in 6s

This commit is contained in:
孙诚
2025-12-29 20:30:15 +08:00
parent a13e1fe9e8
commit 0d94276a0d
22 changed files with 560706 additions and 138 deletions

View File

@@ -154,7 +154,7 @@ public class LogController(ILogger<LogController> logger) : ControllerBase
// 使用正则表达式解析
var match = System.Text.RegularExpressions.Regex.Match(
line,
@"^\[(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\.\d{3}\s+[+-]\d{2}:\d{2})\]\s+\[(\w+)\]\s+(.*)$"
@"^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} [+-]\d{2}:\d{2})\] \[([A-Z]{3})\] (.*)$"
);
if (match.Success)

View File

@@ -8,6 +8,7 @@ public class TransactionRecordController(
ITransactionRecordRepository transactionRepository,
ITransactionCategoryRepository categoryRepository,
IOpenAiService openAiService,
ITextSegmentService textSegmentService,
ILogger<TransactionRecordController> logger
) : ControllerBase
{
@@ -31,18 +32,18 @@ public class TransactionRecordController(
TransactionType? transactionType = type.HasValue ? (TransactionType)type.Value : null;
var list = await transactionRepository.GetPagedListAsync(
pageIndex,
pageSize,
searchKeyword,
classify,
transactionType,
year,
pageSize,
searchKeyword,
classify,
transactionType,
year,
month,
sortByAmount);
var total = await transactionRepository.GetTotalCountAsync(
searchKeyword,
classify,
transactionType,
year,
searchKeyword,
classify,
transactionType,
year,
month);
return new PagedResponse<TransactionRecord>
@@ -392,7 +393,7 @@ public class TransactionRecordController(
""";
var sqlText = await openAiService.ChatAsync(sqlPrompt);
// 清理SQL文本
sqlText = sqlText?.Trim() ?? "";
sqlText = sqlText.TrimStart('`').TrimEnd('`');
@@ -421,11 +422,11 @@ public class TransactionRecordController(
// 第三步将查询结果序列化为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}
@@ -577,23 +578,64 @@ public class TransactionRecordController(
return;
}
// 获取指定ID的账单
var records = new List<TransactionRecord>();
foreach (var id in request.TransactionIds)
{
var record = await transactionRepository.GetByIdAsync(id);
if (record != null && record.Classify == string.Empty)
{
records.Add(record);
}
}
// 获取指定ID的账单(作为样本)
var sampleRecords = await transactionRepository.GetByIdsAsync(request.TransactionIds.ToArray());
if (records.Count == 0)
if (sampleRecords.Length == 0)
{
await WriteEventAsync("error", "找不到指定的账单");
return;
}
// 重新按Reason分组所有待分类账单
var groupedRecords = sampleRecords
.GroupBy(r => r.Reason)
.Select(g => new
{
Reason = g.Key,
Ids = g.Select(r => r.Id).ToList(),
Count = g.Count(),
TotalAmount = g.Sum(r => r.Amount),
SampleType = g.First().Type
})
.OrderByDescending(g => Math.Abs(g.TotalAmount))
.ToList();
// 【增强功能】对每个分组的摘要进行分词,查询已分类的相似账单
var referenceRecords = new Dictionary<string, List<TransactionRecord>>();
foreach (var group in groupedRecords)
{
// 使用专业分词库提取关键词
var keywords = textSegmentService.ExtractKeywords(group.Reason);
if (keywords.Count > 0)
{
// 查询包含这些关键词且已分类的账单(带相关度评分)
// minMatchRate=0.4 表示至少匹配40%的关键词才被认为是相似的
var similarClassifiedWithScore = await transactionRepository.GetClassifiedByKeywordsWithScoreAsync(keywords, minMatchRate: 0.4, limit: 10);
if (similarClassifiedWithScore.Count > 0)
{
// 只取前5个最相关的
var topSimilar = similarClassifiedWithScore.Take(5).Select(x => x.record).ToList();
referenceRecords[group.Reason] = topSimilar;
// 记录调试信息
logger.LogDebug("摘要 '{Reason}' 提取关键词: {Keywords}, 找到 {Count} 个相似账单,相关度分数: {Scores}",
group.Reason,
string.Join(", ", keywords),
similarClassifiedWithScore.Count,
string.Join(", ", similarClassifiedWithScore.Select(x => $"{x.record.Reason}({x.relevanceScore:F2})")));
}
else
{
logger.LogDebug("摘要 '{Reason}' 提取关键词: {Keywords}, 未找到高相关度的相似账单",
group.Reason,
string.Join(", ", keywords));
}
}
}
// 获取所有分类
var categories = await categoryRepository.GetAllAsync();
@@ -610,28 +652,43 @@ public class TransactionRecordController(
}
}
// 构建账单信息
var billsInfo = string.Join("\n", records.Select((r, i) =>
$"{i + 1}. ID={r.Id}, 摘要={r.Reason}, 金额={r.Amount}, 类型={GetTypeName(r.Type)}"));
// 构建账单分组信息
var billsInfo = new StringBuilder();
foreach (var (group, index) in groupedRecords.Select((g, i) => (g, i)))
{
billsInfo.AppendLine($"{index + 1}. 摘要={group.Reason}, 当前类型={GetTypeName(group.SampleType)}, 当前分类={(string.IsNullOrEmpty(group.SampleType.ToString()) ? "" : group.SampleType.ToString())}, 涉及金额={group.TotalAmount}");
// 如果有相似的已分类账单,添加参考信息
if (referenceRecords.TryGetValue(group.Reason, out var references))
{
billsInfo.AppendLine(" 【参考】相似且已分类的账单:");
foreach (var refer in references.Take(3)) // 最多显示3个参考
{
billsInfo.AppendLine($" - 摘要={refer.Reason}, 分类={refer.Classify}, 类型={GetTypeName(refer.Type)}, 金额={refer.Amount}");
}
}
}
var systemPrompt = $$"""
你是一个专业的账单分类助手。请根据提供的账单信息和分类列表,为每个账单选择最合适的分类。
你是一个专业的账单分类助手。请根据提供的账单分组信息和分类列表,为每个分组选择最合适的分类。
可用的分类列表:
{{categoryInfo}}
分类规则:
1. 根据账单的摘要和金额,选择最匹配的分类
2. 如果无法确定分类,可以选择""其他""
1. 根据账单的摘要和涉及金额,选择最匹配的分类
2. 如果提供了【参考】信息,优先参考相似账单的分类,这些是历史上已分类的相似账单
3. 如果无法确定分类,可以选择""
4.
请对每个账单进行分类,每次输出一个账单的分类结果,格式如下:
{"id": 账单ID, "type": 0:支出/1:收入/2:不计入收支(Type为Number枚举值) ,"classify": 分类名称}
{"reason": "交易摘要", "type": 0:/1:/2:(Type为Number枚举值) ,"classify": "分类名称"}
JSON
""";
var userPrompt = $$"""
请为以下账单进行分类:
请为以下账单分组进行分类:
{{billsInfo}}
@@ -639,11 +696,58 @@ public class TransactionRecordController(
""";
// 流式调用AI
await WriteEventAsync("start", $"开始分类 {records.Count} 条账单");
await WriteEventAsync("start", $"开始分类,共 {sampleRecords.Length} 条账单");
// 用于存储AI返回的分组分类结果
var classifyResults = new List<(string Reason, string Classify, TransactionType Type)>();
var buffer = new StringBuilder();
var sendedIds = new HashSet<long>();
await foreach (var chunk in openAiService.ChatStreamAsync(systemPrompt, userPrompt))
{
await WriteEventAsync("data", chunk);
buffer.Append(chunk);
// 尝试解析完整的JSON对象
var bufferStr = buffer.ToString();
var startIdx = 0;
while (startIdx < bufferStr.Length)
{
var openBrace = bufferStr.IndexOf('{', startIdx);
if (openBrace == -1) break;
var closeBrace = FindMatchingBrace(bufferStr, openBrace);
if (closeBrace == -1) break;
var jsonStr = bufferStr.Substring(openBrace, closeBrace - openBrace + 1);
try
{
var result = JsonSerializer.Deserialize<GroupClassifyResult>(jsonStr);
if (result != null && !string.IsNullOrEmpty(result.Reason))
{
classifyResults.Add((result.Reason, result.Classify ?? "", result.Type));
// 每一条结果单独通知
var group = groupedRecords.FirstOrDefault(g => g.Reason == result.Reason);
if (group != null)
{
// 为该分组的所有账单ID返回分类结果
foreach (var id in group.Ids)
{
if (!sendedIds.Contains(id))
{
sendedIds.Add(id);
var resultJson = JsonSerializer.Serialize(new { id, result.Classify, result.Type });
await WriteEventAsync("data", resultJson);
}
}
}
}
}
catch (Exception ex)
{
logger.LogWarning(ex, "解析AI分类结果失败: {JsonStr}", jsonStr);
}
startIdx = closeBrace + 1;
}
}
await WriteEventAsync("end", "分类完成");
@@ -853,7 +957,7 @@ public class TransactionRecordController(
// 根据关键词查询交易记录
var allRecords = await transactionRepository.ExecuteRawSqlAsync(analysisInfo.Sql);
logger.LogInformation("NLP分析查询到 {Count} 条记录SQL: {Sql}", allRecords.Count, analysisInfo.Sql);
// 为每条记录预设分类
var recordsWithClassify = allRecords.Select(r => new TransactionRecordWithClassify
{
@@ -898,6 +1002,24 @@ public class TransactionRecordController(
await Response.Body.FlushAsync();
}
/// <summary>
/// 查找匹配的右括号
/// </summary>
private static int FindMatchingBrace(string str, int startPos)
{
int braceCount = 0;
for (int i = startPos; i < str.Length; i++)
{
if (str[i] == '{') braceCount++;
else if (str[i] == '}')
{
braceCount--;
if (braceCount == 0) return i;
}
}
return -1;
}
private static string GetTypeName(TransactionType type)
{
return type switch
@@ -949,6 +1071,21 @@ public record SmartClassifyRequest(
List<long>? TransactionIds = null
);
/// <summary>
/// 分组分类结果DTO用于AI返回结果解析
/// </summary>
public record GroupClassifyResult
{
[JsonPropertyName("reason")]
public string Reason { get; set; } = string.Empty;
[JsonPropertyName("classify")]
public string? Classify { get; set; }
[JsonPropertyName("type")]
public TransactionType Type { get; set; }
}
/// <summary>
/// 批量更新分类项DTO
/// </summary>

File diff suppressed because it is too large Load Diff

349046
WebApi/Resources.bak/dict.txt Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,258 @@
{
"E-e": -3.14e+100,
"E-d": -3.14e+100,
"E-g": -3.14e+100,
"E-f": -3.14e+100,
"E-a": -3.14e+100,
"E-c": -3.14e+100,
"E-b": -3.14e+100,
"E-m": -3.14e+100,
"S-rg": -10.275268591948773,
"E-o": -3.14e+100,
"E-n": -3.14e+100,
"E-i": -3.14e+100,
"E-h": -3.14e+100,
"E-k": -3.14e+100,
"E-j": -3.14e+100,
"E-u": -3.14e+100,
"E-t": -3.14e+100,
"E-w": -3.14e+100,
"E-v": -3.14e+100,
"E-q": -3.14e+100,
"E-p": -3.14e+100,
"E-s": -3.14e+100,
"M-bg": -3.14e+100,
"M-uj": -3.14e+100,
"E-y": -3.14e+100,
"E-x": -3.14e+100,
"E-z": -3.14e+100,
"B-uz": -3.14e+100,
"S-d": -3.903919764181873,
"M-rg": -3.14e+100,
"E-nt": -3.14e+100,
"B-d": -3.9750475297585357,
"B-uv": -3.14e+100,
"E-vi": -3.14e+100,
"B-mq": -6.78695300139688,
"M-rr": -3.14e+100,
"S-ag": -6.954113917960154,
"M-jn": -3.14e+100,
"E-l": -3.14e+100,
"M-rz": -3.14e+100,
"B-ud": -3.14e+100,
"S-an": -12.84021794941031,
"B-qg": -3.14e+100,
"B-ug": -3.14e+100,
"M-y": -3.14e+100,
"S-qg": -3.14e+100,
"S-z": -3.14e+100,
"S-y": -6.1970794699489575,
"S-x": -8.427419656069674,
"S-w": -3.14e+100,
"S-v": -3.053292303412302,
"S-u": -6.940320595827818,
"S-t": -3.14e+100,
"B-nrt": -4.985642733519195,
"S-r": -2.7635336784127853,
"S-q": -4.888658618255058,
"M-zg": -3.14e+100,
"S-o": -8.464460927750023,
"S-n": -3.8551483897645107,
"B-zg": -3.14e+100,
"S-l": -3.14e+100,
"S-k": -6.940320595827818,
"S-in": -3.14e+100,
"S-i": -3.14e+100,
"S-h": -8.650563207383884,
"S-g": -6.507826815331734,
"B-f": -5.491630418482717,
"S-e": -5.942513006281674,
"M-en": -3.14e+100,
"S-c": -4.786966795861212,
"S-b": -6.472888763970454,
"S-a": -3.9025396831295227,
"B-g": -3.14e+100,
"B-b": -5.018374362109218,
"B-c": -3.423880184954888,
"M-ug": -3.14e+100,
"B-a": -4.762305214596967,
"E-qe": -3.14e+100,
"M-x": -3.14e+100,
"E-nz": -3.14e+100,
"M-z": -3.14e+100,
"M-u": -3.14e+100,
"B-k": -3.14e+100,
"M-w": -3.14e+100,
"B-jn": -3.14e+100,
"S-yg": -13.533365129970255,
"B-o": -8.433498702146057,
"B-l": -4.905883584659895,
"B-m": -3.6524299819046386,
"M-m": -3.14e+100,
"M-l": -3.14e+100,
"M-o": -3.14e+100,
"M-n": -3.14e+100,
"M-i": -3.14e+100,
"M-h": -3.14e+100,
"B-t": -3.3647479094528574,
"M-ul": -3.14e+100,
"B-z": -7.045681111485645,
"M-d": -3.14e+100,
"M-mg": -3.14e+100,
"B-y": -9.844485675856319,
"M-a": -3.14e+100,
"S-nrt": -3.14e+100,
"M-c": -3.14e+100,
"M-uz": -3.14e+100,
"E-mg": -3.14e+100,
"B-i": -6.1157847275557105,
"M-b": -3.14e+100,
"E-uz": -3.14e+100,
"B-n": -1.6966257797548328,
"E-uv": -3.14e+100,
"M-ud": -3.14e+100,
"M-p": -3.14e+100,
"E-ul": -3.14e+100,
"E-mq": -3.14e+100,
"M-s": -3.14e+100,
"M-yg": -3.14e+100,
"E-uj": -3.14e+100,
"E-ud": -3.14e+100,
"S-ln": -3.14e+100,
"M-r": -3.14e+100,
"E-ng": -3.14e+100,
"B-r": -3.4098187790818413,
"E-en": -3.14e+100,
"M-qg": -3.14e+100,
"B-s": -5.522673590839954,
"S-rr": -3.14e+100,
"B-p": -4.200984132085048,
"B-dg": -3.14e+100,
"M-uv": -3.14e+100,
"S-zg": -3.14e+100,
"B-v": -2.6740584874265685,
"S-tg": -6.272842531880403,
"B-w": -3.14e+100,
"B-e": -8.563551830394255,
"M-k": -3.14e+100,
"M-j": -3.14e+100,
"B-df": -8.888974230828882,
"M-e": -3.14e+100,
"E-tg": -3.14e+100,
"M-t": -3.14e+100,
"E-nr": -3.14e+100,
"M-nrfg": -3.14e+100,
"B-nr": -2.2310495913769506,
"E-df": -3.14e+100,
"E-dg": -3.14e+100,
"S-jn": -3.14e+100,
"M-q": -3.14e+100,
"B-mg": -3.14e+100,
"B-ln": -3.14e+100,
"M-f": -3.14e+100,
"E-ln": -3.14e+100,
"E-yg": -3.14e+100,
"S-bg": -3.14e+100,
"E-ns": -3.14e+100,
"B-tg": -3.14e+100,
"E-qg": -3.14e+100,
"S-nr": -4.483663103956885,
"S-ns": -3.14e+100,
"M-vn": -3.14e+100,
"S-nt": -12.147070768850364,
"S-nz": -3.14e+100,
"S-ad": -11.048458480182255,
"B-yg": -3.14e+100,
"M-v": -3.14e+100,
"E-vn": -3.14e+100,
"S-ng": -4.913434861102905,
"M-g": -3.14e+100,
"M-nt": -3.14e+100,
"S-en": -3.14e+100,
"M-nr": -3.14e+100,
"M-ns": -3.14e+100,
"S-vq": -3.14e+100,
"B-uj": -3.14e+100,
"M-nz": -3.14e+100,
"B-qe": -3.14e+100,
"M-in": -3.14e+100,
"M-ng": -3.14e+100,
"S-vn": -11.453923588290419,
"E-zg": -3.14e+100,
"S-vi": -3.14e+100,
"S-vg": -5.9430181843676895,
"S-vd": -3.14e+100,
"B-ad": -6.680066036784177,
"E-rz": -3.14e+100,
"B-ag": -3.14e+100,
"B-vd": -9.044728760238115,
"S-mq": -3.14e+100,
"B-vi": -12.434752841302146,
"E-rr": -3.14e+100,
"B-rr": -12.434752841302146,
"M-vq": -3.14e+100,
"E-jn": -3.14e+100,
"B-vn": -4.3315610890163585,
"S-mg": -10.825314928868044,
"B-in": -3.14e+100,
"M-vi": -3.14e+100,
"M-an": -3.14e+100,
"M-vd": -3.14e+100,
"B-rg": -3.14e+100,
"M-vg": -3.14e+100,
"M-ad": -3.14e+100,
"M-ag": -3.14e+100,
"E-rg": -3.14e+100,
"S-uz": -9.299258625372996,
"B-en": -3.14e+100,
"S-uv": -8.15808672228609,
"S-df": -3.14e+100,
"S-dg": -8.948397651299683,
"M-qe": -3.14e+100,
"B-ng": -3.14e+100,
"E-bg": -3.14e+100,
"S-ul": -8.4153713175535,
"S-uj": -6.85251045118004,
"S-ug": -7.5394037026636855,
"B-ns": -2.8228438314969213,
"S-ud": -7.728230161053767,
"B-nt": -4.846091668182416,
"B-ul": -3.14e+100,
"E-in": -3.14e+100,
"B-bg": -3.14e+100,
"M-df": -3.14e+100,
"M-dg": -3.14e+100,
"M-nrt": -3.14e+100,
"B-j": -5.0576191284681915,
"E-ug": -3.14e+100,
"E-vq": -3.14e+100,
"B-vg": -3.14e+100,
"B-nz": -3.94698846057672,
"S-qe": -3.14e+100,
"B-rz": -7.946116471570005,
"B-nrfg": -5.873722175405573,
"E-ad": -3.14e+100,
"E-ag": -3.14e+100,
"B-u": -9.163917277503234,
"M-ln": -3.14e+100,
"B-an": -8.697083223018778,
"M-mq": -3.14e+100,
"E-an": -3.14e+100,
"S-s": -3.14e+100,
"B-q": -6.998123858956596,
"E-nrt": -3.14e+100,
"B-h": -13.533365129970255,
"E-r": -3.14e+100,
"S-p": -2.9868401813596317,
"M-tg": -3.14e+100,
"S-rz": -3.14e+100,
"S-nrfg": -3.14e+100,
"B-vq": -12.147070768850364,
"B-x": -3.14e+100,
"E-vd": -3.14e+100,
"E-nrfg": -3.14e+100,
"S-m": -3.269200652116097,
"E-vg": -3.14e+100,
"S-f": -5.194820249981676,
"S-j": -4.911992119644354
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,18 @@
{
"M": {
"M": -1.2603623820268226,
"E": -0.33344856811948514
},
"S": {
"S": -0.6658631448798212,
"B": -0.7211965654669841
},
"B": {
"M": -0.916290731874155,
"E": -0.51082562376599
},
"E": {
"S": -0.8085250474669937,
"B": -0.5897149736854513
}
}

View File

@@ -0,0 +1,330 @@
一个
没有
自己
可以
因为
所以
如果
已经
什么
怎么
为什么
多少
这个
那个
这些
那些
哪些
什么样
怎样
如何
多么
何等
若干
左右
前后
上下
东西
南北
里外
内外
大小
高低
长短
多少
好坏
新旧
早晚
前面
后面
上面
下面
左边
右边
里面
外面
中间
旁边
附近
周围
全部
所有
一切
任何
每个
各自
彼此
相互
互相
共同
一起
同时
当时
平时
随时
及时
准时
按时
到时
届时
临时
暂时
长期
短期
永远
从来
向来
素来
一向
历来
总是
常常
往往
通常
一般
大概
大约
左右
上下
几乎
差不多
可能
也许
大概
或许
恐怕
难道
究竟
到底
果然
居然
竟然
偏偏
简直
实在
的确
确实
真正
真是
果真
当真
委实
着实
十分
非常
稍微
一点
有点
一些
若干
许多
很多
好多
大量
少量
大批
成批
整个
全部
部分
多半
大半
少半
绝大部分
绝大多数
极少数
等等
之类
以及
及其
乃至
甚至
甚而
进而
从而
因而
所以
因此
于是
然后
接着
随后
继而
终于
最后
最终
结果
总之
综上所述
由此可见
显而易见
众所周知
不言而喭
毫无疑问
毋庸置疑
无可置疑
不容置疑
无庸讳言
何必
何况
何妨
反正
不过
只是
但是
然而
可是
只不过
不外
无非
虽然
尽管
即使
就算
哪怕
纵使
纵然
既然
假如
如果
倘若
要是
万一
除非
不管
无论
不论
任凭
只要
只有
除了
除去
除开
撇开
此外
另外
以外
之外
不仅
不但
不只
不光
而且
并且
况且
何况
甚至
以至
乃至
与其
宁可
宁愿
还是
或者
抑或
要么
不是
就是
是否
难道
莫非

View File

@@ -20,6 +20,17 @@
</Content>
</ItemGroup>
<ItemGroup>
<!-- 移除默认包含的 Resources JSON 文件,然后显式添加并设置复制规则 -->
<Content Remove="Resources\*.json" />
<Content Include="Resources\*.txt">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Resources\*.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<Watch Remove="logs/**" />
</ItemGroup>