添加配置管理功能,包括获取和设置配置值的接口及实现
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,
|
ITextSegmentService textSegmentService,
|
||||||
ILogger<SmartHandleService> logger,
|
ILogger<SmartHandleService> logger,
|
||||||
ITransactionCategoryRepository categoryRepository,
|
ITransactionCategoryRepository categoryRepository,
|
||||||
IOpenAiService openAiService
|
IOpenAiService openAiService,
|
||||||
|
IConfigService configService
|
||||||
) : ISmartHandleService
|
) : ISmartHandleService
|
||||||
{
|
{
|
||||||
public async Task SmartClassifyAsync(long[] transactionIds, Action<(string, string)> chunkAction)
|
public async Task SmartClassifyAsync(long[] transactionIds, Action<(string, string)> chunkAction)
|
||||||
@@ -258,9 +259,9 @@ public class SmartHandleService(
|
|||||||
{
|
{
|
||||||
// 第一步:使用AI生成聚合SQL查询
|
// 第一步:使用AI生成聚合SQL查询
|
||||||
var now = DateTime.Now;
|
var now = DateTime.Now;
|
||||||
var sqlPrompt = $"""
|
var sqlPrompt = $$"""
|
||||||
当前日期:{now:yyyy年M月d日}({now:yyyy-MM-dd})
|
当前日期:{{now:yyyy年M月d日}}({{now:yyyy-MM-dd}})
|
||||||
用户问题:{userInput}
|
用户问题:{{userInput}}
|
||||||
|
|
||||||
数据库类型:SQLite
|
数据库类型:SQLite
|
||||||
数据库表名:TransactionRecord
|
数据库表名:TransactionRecord
|
||||||
@@ -291,21 +292,26 @@ public class SmartHandleService(
|
|||||||
- 提取日期:strftime('%Y-%m-%d', OccurredAt)
|
- 提取日期:strftime('%Y-%m-%d', OccurredAt)
|
||||||
- 不要使用 YEAR()、MONTH()、DAY() 函数,SQLite不支持
|
- 不要使用 YEAR()、MONTH()、DAY() 函数,SQLite不支持
|
||||||
|
|
||||||
示例1(按分类统计):
|
【重要】最终的SQL会被一下DOTNET代码执行, 请确保你生成的代码可执行,不报错
|
||||||
用户:这三个月坐车花了多少钱?
|
```C#
|
||||||
返回: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
|
public async Task<List<dynamic>> ExecuteDynamicSqlAsync(string completeSql)
|
||||||
|
{
|
||||||
|
var dt = await FreeSql.Ado.ExecuteDataTableAsync(completeSql);
|
||||||
|
var result = new List<dynamic>();
|
||||||
|
|
||||||
示例2(按月统计):
|
foreach (System.Data.DataRow row in dt.Rows)
|
||||||
用户:最近半年每月支出情况
|
{
|
||||||
返回: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
|
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(总体统计):
|
return result;
|
||||||
用户:本月花了多少钱?
|
}
|
||||||
返回: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语句。
|
只返回SQL语句。
|
||||||
""";
|
""";
|
||||||
@@ -344,10 +350,15 @@ public class SmartHandleService(
|
|||||||
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var userPromptExtra = await configService.GetConfigByKeyAsync<string>("BillAnalysisPrompt");
|
||||||
|
|
||||||
var dataPrompt = $"""
|
var dataPrompt = $"""
|
||||||
当前日期:{DateTime.Now:yyyy年M月d日}
|
当前日期:{DateTime.Now:yyyy年M月d日}
|
||||||
用户问题:{userInput}
|
用户问题:{userInput}
|
||||||
|
|
||||||
|
【用户要求(重要)】
|
||||||
|
{userInput}
|
||||||
|
|
||||||
查询结果数据(JSON格式):
|
查询结果数据(JSON格式):
|
||||||
{dataJson}
|
{dataJson}
|
||||||
|
|
||||||
@@ -376,6 +387,9 @@ public class SmartHandleService(
|
|||||||
14. 给出实用建议:基于数据提供合理的财务建议
|
14. 给出实用建议:基于数据提供合理的财务建议
|
||||||
15. 语言专业、清晰、简洁
|
15. 语言专业、清晰、简洁
|
||||||
|
|
||||||
|
【用户补充(重要)】
|
||||||
|
{userPromptExtra}
|
||||||
|
|
||||||
直接输出纯净的HTML内容,不要markdown代码块标记。
|
直接输出纯净的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
|
left-arrow
|
||||||
placeholder
|
placeholder
|
||||||
@click-left="onClickLeft"
|
@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">
|
<div class="scroll-content analysis-content">
|
||||||
<!-- 输入区域 -->
|
<!-- 输入区域 -->
|
||||||
@@ -71,13 +80,32 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, nextTick } from 'vue'
|
import { ref, nextTick } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
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 router = useRouter()
|
||||||
const userInput = ref('')
|
const userInput = ref('')
|
||||||
@@ -87,6 +115,10 @@ const resultHtml = ref('')
|
|||||||
const resultContainer = ref(null)
|
const resultContainer = ref(null)
|
||||||
const scrollAnchor = ref(null)
|
const scrollAnchor = ref(null)
|
||||||
|
|
||||||
|
// 提示词弹窗相关
|
||||||
|
const showPromptDialog = ref(false)
|
||||||
|
const promptValue = ref('')
|
||||||
|
|
||||||
// 快捷问题
|
// 快捷问题
|
||||||
const quickQuestions = [
|
const quickQuestions = [
|
||||||
'最近三个月交通费用多少?',
|
'最近三个月交通费用多少?',
|
||||||
@@ -100,6 +132,45 @@ const onClickLeft = () => {
|
|||||||
router.back()
|
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) => {
|
const selectQuestion = (question) => {
|
||||||
userInput.value = 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