diff --git a/Entity/ConfigEntity.cs b/Entity/ConfigEntity.cs new file mode 100644 index 0000000..bcbba1f --- /dev/null +++ b/Entity/ConfigEntity.cs @@ -0,0 +1,31 @@ +namespace Entity; + +/// +/// 配置实体 +/// +[Table(Name = "Config")] +public class ConfigEntity : BaseEntity +{ + /// + /// 配置Key + /// + public string Key { get; set; } = string.Empty; + + /// + /// 配置Value + /// + public string Value { get; set; } = string.Empty; + + /// + /// 配置类型 + /// + public ConfigType Type { get; set; } +} + +public enum ConfigType +{ + Boolean, + String, + Json, + Number +} diff --git a/Repository/ConfigRepository.cs b/Repository/ConfigRepository.cs new file mode 100644 index 0000000..5fed37b --- /dev/null +++ b/Repository/ConfigRepository.cs @@ -0,0 +1,19 @@ +namespace Repository; + +public interface IConfigRepository : IBaseRepository +{ + /// + /// 根据Key获取配置 + /// + Task GetByKeyAsync(string key); +} + +public class ConfigRepository(IFreeSql freeSql) : BaseRepository(freeSql), IConfigRepository +{ + public async Task GetByKeyAsync(string key) + { + return await FreeSql.Select() + .Where(c => c.Key == key) + .FirstAsync(); + } +} diff --git a/Service/ConfigService.cs b/Service/ConfigService.cs new file mode 100644 index 0000000..8550e27 --- /dev/null +++ b/Service/ConfigService.cs @@ -0,0 +1,77 @@ +namespace Service; + +public interface IConfigService +{ + /// + /// 根据Key获取配置值 + /// + Task GetConfigByKeyAsync(string key); + + /// + /// 设置配置值 + /// + Task SetConfigByKeyAsync(string key, T value); +} + +public class ConfigService(IConfigRepository configRepository) : IConfigService +{ + public async Task GetConfigByKeyAsync(string key) + { + var config = await configRepository.GetByKeyAsync(key); + if (config == null || string.IsNullOrEmpty(config.Value)) + { + return default; + } + + return config.Type switch + { + ConfigType.Boolean => (T)(object)bool.Parse(config.Value), + ConfigType.String => (T)(object)config.Value, + ConfigType.Number => (T)Convert.ChangeType(config.Value, typeof(T)), + ConfigType.Json => JsonSerializer.Deserialize(config.Value) ?? default, + _ => default + }; + } + + public async Task SetConfigByKeyAsync(string key, T value) + { + if (value == null) + { + return false; + } + + var config = await configRepository.GetByKeyAsync(key); + var type = typeof(T) switch + { + Type t when t == typeof(bool) => ConfigType.Boolean, + Type t when t == typeof(int) + || t == typeof(double) + || t == typeof(float) + || t == typeof(decimal) => ConfigType.Number, + Type t when t == typeof(string) => ConfigType.String, + _ => ConfigType.Json + }; + var valueStr = type switch + { + ConfigType.Boolean => value.ToString()!.ToLower(), + ConfigType.Number => value.ToString()!, + ConfigType.String => value as string ?? string.Empty, + ConfigType.Json => JsonSerializer.Serialize(value), + _ => throw new InvalidOperationException("Unsupported config type") + }; + if (config == null) + { + config = new ConfigEntity + { + Key = key, + Type = type, + + }; + return await configRepository.AddAsync(config); + } + + config.Value = valueStr; + config.Type = type; + return await configRepository.UpdateAsync(config); + } +} diff --git a/Service/SmartHandleService.cs b/Service/SmartHandleService.cs index 5c60a43..e9d9e96 100644 --- a/Service/SmartHandleService.cs +++ b/Service/SmartHandleService.cs @@ -14,7 +14,8 @@ public class SmartHandleService( ITextSegmentService textSegmentService, ILogger logger, ITransactionCategoryRepository categoryRepository, - IOpenAiService openAiService + IOpenAiService openAiService, + IConfigService configService ) : ISmartHandleService { public async Task SmartClassifyAsync(long[] transactionIds, Action<(string, string)> chunkAction) @@ -258,9 +259,9 @@ public class SmartHandleService( { // 第一步:使用AI生成聚合SQL查询 var now = DateTime.Now; - var sqlPrompt = $""" - 当前日期:{now:yyyy年M月d日}({now:yyyy-MM-dd}) - 用户问题:{userInput} + var sqlPrompt = $$""" + 当前日期:{{now:yyyy年M月d日}}({{now:yyyy-MM-dd}}) + 用户问题:{{userInput}} 数据库类型:SQLite 数据库表名:TransactionRecord @@ -291,22 +292,27 @@ public class SmartHandleService( - 提取日期: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会被一下DOTNET代码执行, 请确保你生成的代码可执行,不报错 + ```C# + public async Task> ExecuteDynamicSqlAsync(string completeSql) + { + var dt = await FreeSql.Ado.ExecuteDataTableAsync(completeSql); + var result = new List(); + + foreach (System.Data.DataRow row in dt.Rows) + { + var expando = new System.Dynamic.ExpandoObject() as IDictionary; + foreach (System.Data.DataColumn column in dt.Columns) + { + expando[column.ColumnName] = row[column]; + } + result.Add(expando); + } + + return result; + } + ``` + 只返回SQL语句。 """; @@ -344,10 +350,15 @@ public class SmartHandleService( Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping }); + var userPromptExtra = await configService.GetConfigByKeyAsync("BillAnalysisPrompt"); + var dataPrompt = $""" 当前日期:{DateTime.Now:yyyy年M月d日} 用户问题:{userInput} + 【用户要求(重要)】 + {userInput} + 查询结果数据(JSON格式): {dataJson} @@ -375,6 +386,9 @@ public class SmartHandleService( 13. 提供洞察分析:根据数据给出有价值的发现和趋势分析 14. 给出实用建议:基于数据提供合理的财务建议 15. 语言专业、清晰、简洁 + + 【用户补充(重要)】 + {userPromptExtra} 直接输出纯净的HTML内容,不要markdown代码块标记。 """; diff --git a/Web/src/api/config.js b/Web/src/api/config.js new file mode 100644 index 0000000..c9a4826 --- /dev/null +++ b/Web/src/api/config.js @@ -0,0 +1,28 @@ +import request from './request' + +/** + * 获取配置值 + * @param {string} key - 配置的key + * @returns {Promise<{success: boolean, data: string}>} + */ +export const getConfig = (key) => { + return request({ + url: '/Config/GetConfig', + method: 'get', + params: { key } + }) +} + +/** + * 设置配置值 + * @param {string} key - 配置的key + * @param {string} value - 配置的值 + * @returns {Promise<{success: boolean}>} + */ +export const setConfig = (key, value) => { + return request({ + url: '/Config/SetConfig', + method: 'post', + params: { key, value } + }) +} diff --git a/Web/src/views/BillAnalysisView.vue b/Web/src/views/BillAnalysisView.vue index f52f2c0..8ecc1b4 100644 --- a/Web/src/views/BillAnalysisView.vue +++ b/Web/src/views/BillAnalysisView.vue @@ -6,7 +6,16 @@ left-arrow placeholder @click-left="onClickLeft" - /> + > + +
@@ -71,13 +80,32 @@
+ + + + +