添加配置管理功能,包括获取和设置配置值的接口及实现
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 24s
Docker Build & Deploy / Deploy to Production (push) Successful in 7s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s

This commit is contained in:
孙诚
2026-01-05 15:21:13 +08:00
parent 5a824dac91
commit d44cceb6e4
8 changed files with 306 additions and 22 deletions

31
Entity/ConfigEntity.cs Normal file
View 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
}

View 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
View 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);
}
}

View File

@@ -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,21 +292,26 @@ 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
SQL会被一下DOTNET代码执行,
```C#
public async Task<List<dynamic>> ExecuteDynamicSqlAsync(string completeSql)
{
var dt = await FreeSql.Ado.ExecuteDataTableAsync(completeSql);
var result = new List<dynamic>();
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
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);
}
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
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}
@@ -376,6 +387,9 @@ public class SmartHandleService(
14. 给出实用建议:基于数据提供合理的财务建议
15. 语言专业、清晰、简洁
【用户补充(重要)】
{userPromptExtra}
直接输出纯净的HTML内容不要markdown代码块标记。
""";

28
Web/src/api/config.js Normal file
View 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 }
})
}

View File

@@ -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

View 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
View File

@@ -0,0 +1,3 @@
cd Web; pnpm i ;pnpm dev;
cd ../WebApi; dotnet watch run;