添加配置管理功能,包括获取和设置配置值的接口及实现
This commit is contained in:
31
Entity/ConfigEntity.cs
Normal file
31
Entity/ConfigEntity.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
namespace Entity;
|
||||
|
||||
/// <summary>
|
||||
/// 配置实体
|
||||
/// </summary>
|
||||
[Table(Name = "Config")]
|
||||
public class ConfigEntity : BaseEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// 配置Key
|
||||
/// </summary>
|
||||
public string Key { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 配置Value
|
||||
/// </summary>
|
||||
public string Value { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 配置类型
|
||||
/// </summary>
|
||||
public ConfigType Type { get; set; }
|
||||
}
|
||||
|
||||
public enum ConfigType
|
||||
{
|
||||
Boolean,
|
||||
String,
|
||||
Json,
|
||||
Number
|
||||
}
|
||||
19
Repository/ConfigRepository.cs
Normal file
19
Repository/ConfigRepository.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
namespace Repository;
|
||||
|
||||
public interface IConfigRepository : IBaseRepository<ConfigEntity>
|
||||
{
|
||||
/// <summary>
|
||||
/// 根据Key获取配置
|
||||
/// </summary>
|
||||
Task<ConfigEntity?> GetByKeyAsync(string key);
|
||||
}
|
||||
|
||||
public class ConfigRepository(IFreeSql freeSql) : BaseRepository<ConfigEntity>(freeSql), IConfigRepository
|
||||
{
|
||||
public async Task<ConfigEntity?> GetByKeyAsync(string key)
|
||||
{
|
||||
return await FreeSql.Select<ConfigEntity>()
|
||||
.Where(c => c.Key == key)
|
||||
.FirstAsync();
|
||||
}
|
||||
}
|
||||
77
Service/ConfigService.cs
Normal file
77
Service/ConfigService.cs
Normal file
@@ -0,0 +1,77 @@
|
||||
namespace Service;
|
||||
|
||||
public interface IConfigService
|
||||
{
|
||||
/// <summary>
|
||||
/// 根据Key获取配置值
|
||||
/// </summary>
|
||||
Task<T?> GetConfigByKeyAsync<T>(string key);
|
||||
|
||||
/// <summary>
|
||||
/// 设置配置值
|
||||
/// </summary>
|
||||
Task<bool> SetConfigByKeyAsync<T>(string key, T value);
|
||||
}
|
||||
|
||||
public class ConfigService(IConfigRepository configRepository) : IConfigService
|
||||
{
|
||||
public async Task<T?> GetConfigByKeyAsync<T>(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<T>(config.Value) ?? default,
|
||||
_ => default
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<bool> SetConfigByKeyAsync<T>(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);
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,8 @@ public class SmartHandleService(
|
||||
ITextSegmentService textSegmentService,
|
||||
ILogger<SmartHandleService> 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<List<dynamic>> ExecuteDynamicSqlAsync(string completeSql)
|
||||
{
|
||||
var dt = await FreeSql.Ado.ExecuteDataTableAsync(completeSql);
|
||||
var result = new List<dynamic>();
|
||||
|
||||
foreach (System.Data.DataRow row in dt.Rows)
|
||||
{
|
||||
var expando = new System.Dynamic.ExpandoObject() as IDictionary<string, object>;
|
||||
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<string>("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代码块标记。
|
||||
""";
|
||||
|
||||
28
Web/src/api/config.js
Normal file
28
Web/src/api/config.js
Normal file
@@ -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 }
|
||||
})
|
||||
}
|
||||
@@ -6,7 +6,16 @@
|
||||
left-arrow
|
||||
placeholder
|
||||
@click-left="onClickLeft"
|
||||
/>
|
||||
>
|
||||
<template #right>
|
||||
<van-icon
|
||||
name="question-o"
|
||||
size="20"
|
||||
@click="onClickPrompt"
|
||||
style="cursor: pointer; padding-right: 12px;"
|
||||
/>
|
||||
</template>
|
||||
</van-nav-bar>
|
||||
|
||||
<div class="scroll-content analysis-content">
|
||||
<!-- 输入区域 -->
|
||||
@@ -71,13 +80,32 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 提示词设置弹窗 -->
|
||||
<van-dialog
|
||||
v-model:show="showPromptDialog"
|
||||
title="编辑分析提示词"
|
||||
:show-cancel-button="true"
|
||||
@confirm="confirmPrompt"
|
||||
>
|
||||
<van-field
|
||||
v-model="promptValue"
|
||||
rows="4"
|
||||
autosize
|
||||
type="textarea"
|
||||
maxlength="2000"
|
||||
placeholder="输入自定义的分析提示词..."
|
||||
show-word-limit
|
||||
/>
|
||||
</van-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, nextTick } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { showToast } from 'vant'
|
||||
import { showToast, showLoadingToast, closeToast } from 'vant'
|
||||
import { getConfig, setConfig } from '@/api/config'
|
||||
|
||||
const router = useRouter()
|
||||
const userInput = ref('')
|
||||
@@ -87,6 +115,10 @@ const resultHtml = ref('')
|
||||
const resultContainer = ref(null)
|
||||
const scrollAnchor = ref(null)
|
||||
|
||||
// 提示词弹窗相关
|
||||
const showPromptDialog = ref(false)
|
||||
const promptValue = ref('')
|
||||
|
||||
// 快捷问题
|
||||
const quickQuestions = [
|
||||
'最近三个月交通费用多少?',
|
||||
@@ -100,6 +132,45 @@ const onClickLeft = () => {
|
||||
router.back()
|
||||
}
|
||||
|
||||
// 点击提示词按钮
|
||||
const onClickPrompt = async () => {
|
||||
try {
|
||||
const response = await getConfig('BillAnalysisPrompt')
|
||||
if (response.success) {
|
||||
promptValue.value = response.data || ''
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取提示词失败:', error)
|
||||
}
|
||||
showPromptDialog.value = true
|
||||
}
|
||||
|
||||
// 确认提示词
|
||||
const confirmPrompt = async () => {
|
||||
if (!promptValue.value.trim()) {
|
||||
showToast('请输入提示词')
|
||||
return
|
||||
}
|
||||
|
||||
showLoadingToast({
|
||||
message: '保存中...',
|
||||
forbidClick: true
|
||||
})
|
||||
|
||||
try {
|
||||
const response = await setConfig('BillAnalysisPrompt', promptValue.value)
|
||||
if (response.success) {
|
||||
showToast('提示词已保存')
|
||||
showPromptDialog.value = false
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存提示词失败:', error)
|
||||
showToast('保存失败,请重试')
|
||||
} finally {
|
||||
closeToast()
|
||||
}
|
||||
}
|
||||
|
||||
// 选择快捷问题
|
||||
const selectQuestion = (question) => {
|
||||
userInput.value = question
|
||||
|
||||
41
WebApi/Controllers/ConfigController.cs
Normal file
41
WebApi/Controllers/ConfigController.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
namespace WebApi.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]/[action]")]
|
||||
public class ConfigController(
|
||||
IConfigService configService
|
||||
) : ControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取配置值
|
||||
/// </summary>
|
||||
public async Task<BaseResponse<string>> GetConfig(string key)
|
||||
{
|
||||
try
|
||||
{
|
||||
var config = await configService.GetConfigByKeyAsync<string>(key);
|
||||
var value = config ?? string.Empty;
|
||||
return value.Ok("配置获取成功");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return $"获取{key}配置失败: {ex.Message}".Fail<string>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置配置值
|
||||
/// </summary>
|
||||
public async Task<BaseResponse> SetConfig(string key, string value)
|
||||
{
|
||||
try
|
||||
{
|
||||
await configService.SetConfigByKeyAsync(key, value);
|
||||
return "配置设置成功".Ok();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return $"设置{key}配置失败: {ex.Message}".Fail();
|
||||
}
|
||||
}
|
||||
}
|
||||
3
launch.bat
Normal file
3
launch.bat
Normal file
@@ -0,0 +1,3 @@
|
||||
cd Web; pnpm i ;pnpm dev;
|
||||
|
||||
cd ../WebApi; dotnet watch run;
|
||||
Reference in New Issue
Block a user