代码优化
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 23s
Docker Build & Deploy / Deploy to Production (push) Successful in 6s

This commit is contained in:
2025-12-31 11:10:10 +08:00
parent 4b322494ba
commit e7d5c076d4
7 changed files with 259 additions and 406 deletions

View File

@@ -21,7 +21,8 @@ public class EmailHandleService(
IEmailMessageRepository emailRepo,
ITransactionRecordRepository trxRepo,
IEnumerable<IEmailParseServices> emailParsers,
IMessageRecordService messageRecordService
IMessageRecordService messageRecordService,
ISmartHandleService smartHandleService
) : IEmailHandleService
{
public async Task<bool> HandleEmailAsync(
@@ -79,11 +80,12 @@ public class EmailHandleService(
// 目前已经
bool allSuccess = true;
var records = new List<TransactionRecord>();
foreach (var (card, reason, amount, balance, type, occurredAt) in parsed)
{
logger.LogInformation("处理交易记录: 卡号 {Card}, 交易原因 {Reason}, 金额 {Amount}, 余额 {Balance}, 类型 {Type}", card, reason, amount, balance, type);
var success = await SaveTransactionRecordAsync(
var record = await SaveTransactionRecordAsync(
card,
reason,
amount,
@@ -93,12 +95,17 @@ public class EmailHandleService(
emailMessage.Id
);
if (!success)
if (record == null)
{
allSuccess = false;
continue;
}
records.Add(record);
}
_ = await AnalyzeClassifyAsync(records.ToArray());
return allSuccess;
}
@@ -141,11 +148,12 @@ public class EmailHandleService(
logger.LogInformation("成功解析邮件,共 {Count} 条交易记录", parsed.Length);
bool allSuccess = true;
var records = new List<TransactionRecord>();
foreach (var (card, reason, amount, balance, type, occurredAt) in parsed)
{
logger.LogInformation("刷新交易记录: 卡号 {Card}, 交易原因 {Reason}, 金额 {Amount}, 余额 {Balance}, 类型 {Type}", card, reason, amount, balance, type);
var success = await SaveTransactionRecordAsync(
var record = await SaveTransactionRecordAsync(
card,
reason,
amount,
@@ -155,12 +163,17 @@ public class EmailHandleService(
emailMessage.Id
);
if (!success)
if (record == null)
{
allSuccess = false;
continue;
}
records.Add(record);
}
_ = await AnalyzeClassifyAsync(records.ToArray());
return allSuccess;
}
@@ -221,7 +234,7 @@ public class EmailHandleService(
}
}
private async Task<bool> SaveTransactionRecordAsync(
private async Task<TransactionRecord?> SaveTransactionRecordAsync(
string card,
string reason,
decimal amount,
@@ -251,9 +264,10 @@ public class EmailHandleService(
else
{
logger.LogWarning("交易记录更新失败,卡号 {Card}, 金额 {Amount}", card, amount);
return null;
}
return updated;
return existing;
}
var trx = new TransactionRecord
@@ -276,9 +290,10 @@ public class EmailHandleService(
else
{
logger.LogWarning("交易记录落库失败,卡号 {Card}, 金额 {Amount}", card, amount);
return null;
}
return inserted;
return trx;
}
private async Task<(string card, string reason, decimal amount, decimal balance, TransactionType type, DateTime? occurredAt)[]?> ParseEmailBodyAsync(string from, string subject, string body)
@@ -293,4 +308,59 @@ public class EmailHandleService(
return await service.ParseAsync(body);
}
private async Task<TransactionRecord[]> AnalyzeClassifyAsync(TransactionRecord[] records)
{
var result = new List<TransactionRecord>();
await smartHandleService.SmartClassifyAsync(records.Select(r => r.Id).ToArray(), chunk =>
{
// 处理分类结果
var (type, data) = chunk;
if (type != "data")
{
logger.LogWarning("未知的分类结果类型: {Type}, {Data}. 跳过分类", type, data);
return;
}
try
{
var item = JsonSerializer.Deserialize<JsonObject>(data);
var recordId = item?["id"]?.GetValue<long>();
var classify = item?["Classify"]?.GetValue<string>();
var recordType = item?["Type"]?.GetValue<int>();
if (recordId == null || string.IsNullOrEmpty(classify) || recordType == null)
{
logger.LogWarning("AI分类结果数据不完整跳过分类: {Data}", data);
return;
}
if (recordType < (int)TransactionType.Expense || recordType > (int)TransactionType.None)
{
logger.LogWarning("AI分类结果交易类型无效跳过分类: {Data}", data);
return;
}
var record = records.FirstOrDefault(r => r.Id == recordId);
if (record == null)
{
logger.LogWarning("未找到对应的交易记录(AI返回内容有误)跳过分类ID: {Id}", recordId);
return;
}
record.Classify = classify;
record.Type = (TransactionType)recordType;
result.Add(record);
}
catch (Exception ex)
{
logger.LogWarning(ex, "解析AI分类结果失败跳过分类: {Data}", data);
}
});
return result.ToArray();
}
}

View File

@@ -35,7 +35,13 @@ public class EmailParseForm95555(
DateTime? occurredAt
)[]> ParseEmailContentAsync(string emailContent)
{
var pattern = "您账户(?<card>\\d+)于.*?(?<type>收入|支出|消费|转入|转出)?.*?在?(?<reason>.+?)(?<amount>\\d+\\.\\d{1,2})元,余额(?<balance>\\d+\\.\\d{1,2})";
var pattern =
"您账户(?<card>\\d+)" +
"于.*?" + // 时间等信息统统吞掉
"(?:(?<type>收入|支出|消费|转入|转出).*?)?" + // 可选 type
"(?:在(?<reason>.*?))?" + // 可选 reason“财付通-微信支付-这有电快捷支付”)
"(?<amount>\\d+\\.\\d{1,2})元,余额" +
"(?<balance>\\d+\\.\\d{1,2})";
var matches = Regex.Matches(emailContent, pattern);

View File

@@ -144,6 +144,17 @@ public class EmailParseFormCCSVC(
reason = string.Join(" ", parts.Skip(2));
}
// 招商信用卡特殊,消费金额为正数,退款为负数
if(amount > 0)
{
type = TransactionType.Expense;
}
else
{
type = TransactionType.Income;
amount = Math.Abs(amount);
}
result.Add((card, reason, amount, balance, type, occurredAt));
}
catch (Exception ex)

View File

@@ -11,3 +11,4 @@ global using FreeSql;
global using System.Linq;
global using Service.AppSettingModel;
global using System.Text.Json.Serialization;
global using System.Text.Json.Nodes;

View File

@@ -2,7 +2,9 @@
public interface ISmartHandleService
{
Task SmartClassifyAsync(long[] transactionIds, Action<(string , string)> chunkAction);
Task SmartClassifyAsync(long[] transactionIds, Action<(string type, string data)> chunkAction);
Task AnalyzeBillAsync(string userInput, Action<string> chunkAction);
}
public class SmartHandleService(
@@ -13,7 +15,7 @@ public class SmartHandleService(
IOpenAiService openAiService
) : ISmartHandleService
{
public async Task SmartClassifyAsync(long[] transactionIds, Action<(string , string)> chunkAction)
public async Task SmartClassifyAsync(long[] transactionIds, Action<(string, string)> chunkAction)
{
try
{
@@ -199,6 +201,151 @@ public class SmartHandleService(
}
}
public async Task AnalyzeBillAsync(string userInput, Action<string> chunkAction)
{
try
{
// 第一步使用AI生成聚合SQL查询
var now = DateTime.Now;
var sqlPrompt = $"""
{now:yyyy年M月d日}{now:yyyy-MM-dd}
{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 = JsonSerializer.Serialize(new { content = "<div class='error-message'>SQL执行失败请重新描述您的问题</div>" });
chunkAction(errorData);
return;
}
// 第三步将查询结果序列化为JSON直接传递给AI生成分析报告
var dataJson = JsonSerializer.Serialize(queryResults, new JsonSerializerOptions
{
WriteIndented = true,
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
});
var dataPrompt = $"""
{DateTime.Now:yyyy年M月d日}
{userInput}
JSON格式
{dataJson}
1. 使HTML格式H5页面风格
2.
3. 使table > thead/tbody > tr > th/td
4. 使HTML标签h2h3ptableul/listrong
5. <span class='expense-value'></span>
6. <span class='income-value'></span>
7. <span class='highlight'></span>
8. htmlbodyhead
9. 使 style <style>
10. backgroundbackground-colorcolor
11. 使 div
12. JSON数据转换为易读的表格和文字说明
13.
14.
15.
HTML内容markdown代码块标记
""";
// 第四步流式输出AI分析结果
await foreach (var chunk in openAiService.ChatStreamAsync(dataPrompt))
{
var sseData = JsonSerializer.Serialize(new { content = chunk });
chunkAction(sseData);
}
// 发送完成标记
chunkAction("[DONE]");
}
catch (Exception ex)
{
logger.LogError(ex, "智能分析账单失败");
var errorData = JsonSerializer.Serialize(new { content = $"<div class='error-message'>分析失败:{ex.Message}</div>" });
chunkAction(errorData);
}
}
/// <summary>
/// 查找匹配的右括号
/// </summary>
@@ -243,4 +390,3 @@ public record GroupClassifyResult
[JsonPropertyName("type")]
public TransactionType Type { get; set; }
}

View File

@@ -21,7 +21,7 @@
<van-cell title="编辑分类" is-link @click="handleEditClassification" />
<van-cell title="批量分类" is-link @click="handleBatchClassification" />
<van-cell title="智能分类" is-link @click="handleSmartClassification" />
<van-cell title="自然语言分类" is-link @click="handleNaturalLanguageClassification" />
<!-- <van-cell title="自然语言分类" is-link @click="handleNaturalLanguageClassification" /> -->
</van-cell-group>
<div class="detail-header" style="padding-bottom: 5px;">
<p>开发者</p>

View File

@@ -6,8 +6,6 @@ using Repository;
[Route("api/[controller]/[action]")]
public class TransactionRecordController(
ITransactionRecordRepository transactionRepository,
ITransactionCategoryRepository categoryRepository,
IOpenAiService openAiService,
ISmartHandleService smartHandleService,
ILogger<TransactionRecordController> logger
) : ControllerBase
@@ -339,151 +337,16 @@ public class TransactionRecordController(
Response.Headers.Append("Cache-Control", "no-cache");
Response.Headers.Append("Connection", "keep-alive");
try
if (string.IsNullOrWhiteSpace(request.UserInput))
{
// 第一步使用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();
await WriteEventAsync("<div class='error-message'>请输入分析内容</div>");
return;
}
// 第三步将查询结果序列化为JSON直接传递给AI生成分析报告
var dataJson = System.Text.Json.JsonSerializer.Serialize(queryResults, new JsonSerializerOptions
await smartHandleService.AnalyzeBillAsync(request.UserInput, async (chunk) =>
{
WriteIndented = true,
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
await WriteEventAsync(chunk);
});
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>
@@ -683,147 +546,6 @@ public class TransactionRecordController(
}
}
/// <summary>
/// 自然语言分析 - 根据用户输入的自然语言查询交易记录并预设分类
/// </summary>
[HttpPost]
public async Task<BaseResponse<NlpAnalysisResult>> NlpAnalysisAsync([FromBody] NlpAnalysisRequest request)
{
try
{
if (string.IsNullOrWhiteSpace(request.UserInput))
{
return BaseResponse<NlpAnalysisResult>.Fail("请输入查询条件");
}
// 获取所有分类
var categories = await categoryRepository.GetAllAsync();
var categoryInfo = new StringBuilder();
foreach (var type in new[] { 0, 1, 2 })
{
var typeName = GetTypeName((TransactionType)type);
categoryInfo.AppendLine($"{typeName}: ");
var categoriesOfType = categories.Where(c => (int)c.Type == type).ToList();
foreach (var category in categoriesOfType)
{
categoryInfo.AppendLine($"- {category.Name}");
}
}
var systemPrompt = $$"""
你是一个专业的交易记录查询助手。用户会用自然语言描述他想要查询和分类的交易记录。
可用的分类列表:
{{categoryInfo}}
你需要分析用户的需求,提取以下信息:
1. 查询SQL, 根据用户的描述生成完整SQL语句用于查询交易记录。例如SELECT * FROM TransactionRecord WHERE Reason LIKE '%关键词%' OR Classify LIKE '%关键词2%' LIMIT 500
[重要Table Schema:]
```
TransactionRecord (
Id LONG,
Reason STRING NOT NULL,
Amount DECIMAL,
RefundAmount DECIMAL,
Balance DECIMAL,
OccurredAt DATETIME,
EmailMessageId LONG,
Type INT,
Classify STRING NOT NULL,
ImportNo STRING NOT NULL,
ImportFrom STRING NOT NULL
)
```
[重要]
如果用户没有限制则最多查询500条记录如果用户指定了时间范围请在SQL中加入时间过滤条件。
[重要SQL限制]
必须是SELECT * FROM TransactionRecord 开头的SQL语句。
当前日期:{{DateTime.Now:yyyy年M月d日}}
[重要SQLite日期函数]
- 提取年份strftime('%Y', OccurredAt)
- 提取月份strftime('%m', OccurredAt)
- 提取日期strftime('%Y-%m-%d', OccurredAt)
- 不要使用 YEAR()、MONTH()、DAY() 函数SQLite不支持
2. 目标交易类型0:支出, 1:收入, 2:不计入收支)
3. 目标分类名称(必须从上面的分类列表中选择)
请以JSON格式输出格式如下
{
"sql": "SQL",
"targetType": ,
"targetClassify": "分类名称"
}
JSON
""";
var userPrompt = $"用户输入:{request.UserInput}";
// 调用AI分析
var aiResponse = await openAiService.ChatAsync(systemPrompt, userPrompt);
logger.LogInformation("NLP分析AI返回结果: {Response}", aiResponse);
if (string.IsNullOrWhiteSpace(aiResponse))
{
return BaseResponse<NlpAnalysisResult>.Fail("AI分析失败请检查AI配置");
}
// 解析AI返回的JSON
NlpAnalysisInfo? analysisInfo;
try
{
analysisInfo = JsonSerializer.Deserialize<NlpAnalysisInfo>(aiResponse);
if (analysisInfo == null)
{
return BaseResponse<NlpAnalysisResult>.Fail("AI返回格式错误");
}
}
catch (Exception ex)
{
logger.LogError(ex, "解析AI返回结果失败返回内容: {Response}", aiResponse);
return BaseResponse<NlpAnalysisResult>.Fail($"AI返回格式错误: {ex.Message}");
}
// 根据关键词查询交易记录
var allRecords = await transactionRepository.ExecuteRawSqlAsync(analysisInfo.Sql);
logger.LogInformation("NLP分析查询到 {Count} 条记录SQL: {Sql}", allRecords.Count, analysisInfo.Sql);
// 为每条记录预设分类
var recordsWithClassify = allRecords.Select(r => new TransactionRecordWithClassify
{
Id = r.Id,
Reason = r.Reason ?? string.Empty,
Amount = r.Amount,
Balance = r.Balance,
Card = r.Card ?? string.Empty,
OccurredAt = r.OccurredAt,
CreateTime = r.CreateTime,
ImportFrom = r.ImportFrom ?? string.Empty,
RefundAmount = r.RefundAmount,
UpsetedType = analysisInfo.TargetType,
UpsetedClassify = analysisInfo.TargetClassify,
TargetType = r.Type,
TargetClassify = r.Classify ?? string.Empty
}).ToList();
return new BaseResponse<NlpAnalysisResult>
{
Success = true,
Data = new NlpAnalysisResult
{
Records = recordsWithClassify,
TargetType = analysisInfo.TargetType,
TargetClassify = analysisInfo.TargetClassify,
SearchKeyword = analysisInfo.Sql
}
};
}
catch (Exception ex)
{
logger.LogError(ex, "NLP分析失败用户输入: {Input}", request.UserInput);
return BaseResponse<NlpAnalysisResult>.Fail($"NLP分析失败: {ex.Message}");
}
}
private async Task WriteEventAsync(string eventType, string data)
{
var message = $"event: {eventType}\ndata: {data}\n\n";
@@ -831,22 +553,11 @@ public class TransactionRecordController(
await Response.Body.FlushAsync();
}
/// <summary>
/// 查找匹配的右括号
/// </summary>
private static int FindMatchingBrace(string str, int startPos)
private async Task WriteEventAsync(string data)
{
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;
var message = $"data: {data}\n\n";
await Response.WriteAsync(message);
await Response.Body.FlushAsync();
}
private static string GetTypeName(TransactionType type)
@@ -918,101 +629,9 @@ public record BatchUpdateByReasonDto(
string Classify
);
/// <summary>
/// NLP分析请求DTO
/// </summary>
public record NlpAnalysisRequest(
string UserInput
);
/// <summary>
/// NLP分析结果DTO
/// </summary>
public record NlpAnalysisResult
{
public List<TransactionRecordWithClassify> Records { get; set; } = new();
public TransactionType TargetType { get; set; }
public string TargetClassify { get; set; } = string.Empty;
public string SearchKeyword { get; set; } = string.Empty;
}
/// <summary>
/// 带分类信息的交易记录DTO
/// </summary>
public record TransactionRecordWithClassify
{
public long Id { get; set; }
public string Reason { get; set; } = string.Empty;
public decimal Amount { get; set; }
public decimal Balance { get; set; }
public string Card { get; set; } = string.Empty;
public DateTime OccurredAt { get; set; }
public DateTime CreateTime { get; set; }
public string ImportFrom { get; set; } = string.Empty;
public decimal RefundAmount { get; set; }
public TransactionType UpsetedType { get; set; }
public string UpsetedClassify { get; set; } = string.Empty;
public TransactionType TargetType { get; set; }
public string TargetClassify { get; set; } = string.Empty;
}
/// <summary>
/// AI分析信息DTO内部使用
/// </summary>
public record NlpAnalysisInfo
{
[JsonPropertyName("sql")]
public string Sql { get; set; } = string.Empty;
[JsonPropertyName("targetType")]
public TransactionType TargetType { get; set; }
[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; }
}