Skip to content

Commit c815986

Browse files
pythonHuangclaude
andcommitted
feat: 流程设计器新增Redis缓存查询/设置节点(key支持变量模板/JSON自动解析/过期秒数)+系统设置新增Redis配置页(地址/端口/密码/库+测试连接),未配置时节点执行给出明确报错,附完整帮助弹窗
Co-Authored-By: Claude Code <noreply@anthropic.com>
1 parent cd17207 commit c815986

10 files changed

Lines changed: 425 additions & 8 deletions

File tree

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
using Juggle.Application.Models.Response;
2+
using Juggle.Infrastructure.Persistence;
3+
using Microsoft.AspNetCore.Authorization;
4+
using Microsoft.AspNetCore.Mvc;
5+
using Microsoft.EntityFrameworkCore;
6+
using StackExchange.Redis;
7+
8+
namespace Juggle.Api.Controllers.Api;
9+
10+
/// <summary>Redis 配置:流程 REDIS_GET/REDIS_SET 节点与缓存能力使用。</summary>
11+
[ApiController]
12+
[Route("api/system/redis")]
13+
[Authorize]
14+
public class RedisConfigController : ControllerBase
15+
{
16+
private readonly JuggleDbContext _db;
17+
18+
public RedisConfigController(JuggleDbContext db) => _db = db;
19+
20+
[HttpGet("config")]
21+
public async Task<ApiResult> GetConfig()
22+
{
23+
var configs = await _db.SystemConfigs
24+
.Where(c => c.Deleted == 0 && c.ConfigKey!.StartsWith("redis."))
25+
.ToListAsync();
26+
string? Get(string key) => configs.FirstOrDefault(c => c.ConfigKey == key)?.ConfigValue;
27+
return ApiResult.Success(new
28+
{
29+
host = Get("redis.host") ?? "",
30+
port = Get("redis.port") ?? "6379",
31+
password = Get("redis.password") ?? "",
32+
db = Get("redis.db") ?? "0"
33+
});
34+
}
35+
36+
[HttpPost("config")]
37+
public async Task<ApiResult> SaveConfig([FromBody] RedisConfigRequest req)
38+
{
39+
await UpsertAsync("redis.host", req.Host?.Trim() ?? "", "Redis地址");
40+
await UpsertAsync("redis.port", req.Port?.Trim() ?? "6379", "Redis端口");
41+
await UpsertAsync("redis.password", req.Password?.Trim() ?? "", "Redis密码");
42+
await UpsertAsync("redis.db", req.Db?.Trim() ?? "0", "Redis数据库");
43+
return ApiResult.Success();
44+
}
45+
46+
/// <summary>测试 Redis 连接</summary>
47+
[HttpPost("test")]
48+
public async Task<ApiResult> Test([FromBody] RedisConfigRequest req)
49+
{
50+
try
51+
{
52+
var connStr = $"{req.Host?.Trim()}:{req.Port?.Trim() ?? "6379"},password={req.Password?.Trim()},defaultDatabase={req.Db?.Trim() ?? "0"},abortConnect=false,connectTimeout=5000";
53+
var conn = await ConnectionMultiplexer.ConnectAsync(connStr);
54+
var pong = await conn.GetDatabase().PingAsync();
55+
await conn.CloseAsync();
56+
return ApiResult.Success($"连接成功(PING {pong.TotalMilliseconds}ms)");
57+
}
58+
catch (Exception ex)
59+
{
60+
return ApiResult.Fail($"连接失败: {ex.Message}");
61+
}
62+
}
63+
64+
private async Task UpsertAsync(string key, string value, string name)
65+
{
66+
var entity = await _db.SystemConfigs.FirstOrDefaultAsync(c => c.ConfigKey == key);
67+
if (entity == null)
68+
{
69+
_db.SystemConfigs.Add(new Domain.Entities.SystemConfigEntity
70+
{
71+
ConfigKey = key, ConfigValue = value, ConfigName = name, ConfigGroup = "Redis",
72+
CreatedAt = DateTime.Now.ToString("o")
73+
});
74+
}
75+
else
76+
{
77+
entity.ConfigValue = value;
78+
entity.UpdatedAt = DateTime.Now.ToString("o");
79+
}
80+
await _db.SaveChangesAsync();
81+
}
82+
}
83+
84+
public class RedisConfigRequest
85+
{
86+
public string Host { get; set; } = "";
87+
public string Port { get; set; } = "6379";
88+
public string Password { get; set; } = "";
89+
public string Db { get; set; } = "0";
90+
}

‎Juggle.Application/Services/Flow/FlowExecutionService.cs‎

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,26 @@ public FlowExecutionService(JuggleDbContext db, IHttpClientFactory httpClientFac
2828
_aiService = aiService;
2929
}
3030

31-
/// <summary>构建引擎(注入大模型对话函数,供 AI 节点使用)。</summary>
32-
private FlowEngine BuildEngine(Dictionary<string, DataSourceInfo> dsInfos,
31+
/// <summary>读取 Redis 连接配置(系统配置表),未配置返回 null。</summary>
32+
private async Task<string?> GetRedisConnStrAsync()
33+
{
34+
var configs = await _db.SystemConfigs
35+
.Where(c => c.Deleted == 0 && c.ConfigKey!.StartsWith("redis."))
36+
.ToListAsync();
37+
var host = configs.FirstOrDefault(c => c.ConfigKey == "redis.host")?.ConfigValue;
38+
if (string.IsNullOrWhiteSpace(host)) return null;
39+
var port = configs.FirstOrDefault(c => c.ConfigKey == "redis.port")?.ConfigValue ?? "6379";
40+
var password = configs.FirstOrDefault(c => c.ConfigKey == "redis.password")?.ConfigValue ?? "";
41+
var dbIndex = configs.FirstOrDefault(c => c.ConfigKey == "redis.db")?.ConfigValue ?? "0";
42+
return $"{host}:{port},password={password},defaultDatabase={dbIndex},abortConnect=false,connectTimeout=5000";
43+
}
44+
45+
/// <summary>构建引擎(注入大模型对话函数与 Redis 连接,供 AI/Redis 节点使用)。</summary>
46+
private async Task<FlowEngine> BuildEngineAsync(Dictionary<string, DataSourceInfo> dsInfos,
3347
Dictionary<string, string?> staticVars, Func<string, Task<string?>> flowContentLoader)
3448
=> new FlowEngine(_httpClientFactory, dsInfos, staticVars, flowContentLoader,
35-
aiChatFunc: (systemPrompt, userInput) => _aiService.ChatAsync(systemPrompt, userInput));
49+
aiChatFunc: (systemPrompt, userInput) => _aiService.ChatAsync(systemPrompt, userInput),
50+
redisConnStr: await GetRedisConnStrAsync());
3651

3752
// ────────────────────────────────────────────────────────────────
3853
// 数据源
@@ -202,7 +217,7 @@ public async Task<FlowResult> RunAsync(
202217
return ver?.FlowContent;
203218
}
204219

205-
var engine = BuildEngine(dsInfos, staticVars, FlowContentLoader);
220+
var engine = await BuildEngineAsync(dsInfos, staticVars, FlowContentLoader);
206221

207222
var inputJson = JsonSerializer.Serialize(inputParams);
208223
var startTime = DateTime.Now;
@@ -372,7 +387,7 @@ public async Task RunAsyncWithLog(
372387
return ver?.FlowContent;
373388
}
374389

375-
var engine = BuildEngine(dsInfos, staticVars, FlowContentLoader);
390+
var engine = await BuildEngineAsync(dsInfos, staticVars, FlowContentLoader);
376391
var startTime = DateTime.Now;
377392
FlowResult result;
378393
try

‎Juggle.Domain/Engine/FlowEngine.cs‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,22 @@ public class FlowEngine
1919
private readonly Func<string, Task<string?>>? _flowContentLoader;
2020
/// <summary>大模型对话函数(systemPrompt, userInput → reply),供 AI 节点调用,可为 null(未接入大模型)</summary>
2121
private readonly Func<string, string, Task<string>>? _aiChatFunc;
22+
/// <summary>Redis 连接串(供 REDIS_GET/REDIS_SET 节点使用),可为 null(未配置 Redis)</summary>
23+
private readonly string? _redisConnStr;
2224

2325
public FlowEngine(IHttpClientFactory httpClientFactory,
2426
Dictionary<string, DataSourceInfo>? dataSources = null,
2527
Dictionary<string, string?>? staticVariables = null,
2628
Func<string, Task<string?>>? flowContentLoader = null,
27-
Func<string, string, Task<string>>? aiChatFunc = null)
29+
Func<string, string, Task<string>>? aiChatFunc = null,
30+
string? redisConnStr = null)
2831
{
2932
_httpClientFactory = httpClientFactory;
3033
_dataSources = dataSources ?? new();
3134
_staticVarSnapshot = staticVariables ?? new(StringComparer.OrdinalIgnoreCase);
3235
_flowContentLoader = flowContentLoader;
3336
_aiChatFunc = aiChatFunc;
37+
_redisConnStr = redisConnStr;
3438
}
3539

3640
public async Task<FlowResult> ExecuteAsync(
@@ -157,6 +161,10 @@ public async Task<FlowResult> ExecuteAsync(
157161
"FILE_PARSE" => new FileParseNodeExecutor(),
158162
"EXCEL_READ" => new ExcelReadNodeExecutor(),
159163
"FILE_WRITE" => new FileWriteNodeExecutor(),
164+
"REDIS_GET" => new RedisGetNodeExecutor(
165+
_redisConnStr ?? throw new InvalidOperationException("未配置 Redis(系统设置 → Redis 配置),无法执行 Redis 查询节点")),
166+
"REDIS_SET" => new RedisSetNodeExecutor(
167+
_redisConnStr ?? throw new InvalidOperationException("未配置 Redis(系统设置 → Redis 配置),无法执行 Redis 设置节点")),
160168
_ => throw new InvalidOperationException($"未知节点类型: {currentNode.ElementType}")
161169
};
162170

‎Juggle.Domain/Engine/FlowModels.cs‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ public class FlowNode
4747
[JsonPropertyName("fileWriteConfig")]
4848
public FileWriteConfig? FileWriteConfig { get; set; }
4949

50+
[JsonPropertyName("redisGetConfig")]
51+
public RedisGetConfig? RedisGetConfig { get; set; }
52+
53+
[JsonPropertyName("redisSetConfig")]
54+
public RedisSetConfig? RedisSetConfig { get; set; }
55+
5056
[JsonPropertyName("subFlowConfig")]
5157
public SubFlowConfig? SubFlowConfig { get; set; }
5258

@@ -326,6 +332,38 @@ public class FileWriteConfig
326332
public string Output { get; set; } = "";
327333
}
328334

335+
/// <summary>Redis 缓存查询节点配置(REDIS_GET 节点)</summary>
336+
public class RedisGetConfig
337+
{
338+
/// <summary>key(支持 ${变量} 模板)</summary>
339+
[JsonPropertyName("key")]
340+
public string Key { get; set; } = "";
341+
342+
/// <summary>输出变量(JSON 字符串自动解析为对象,无值时为 null)</summary>
343+
[JsonPropertyName("output")]
344+
public string Output { get; set; } = "";
345+
}
346+
347+
/// <summary>Redis 缓存设置节点配置(REDIS_SET 节点)</summary>
348+
public class RedisSetConfig
349+
{
350+
/// <summary>key(支持 ${变量} 模板)</summary>
351+
[JsonPropertyName("key")]
352+
public string Key { get; set; } = "";
353+
354+
/// <summary>值变量(对象自动序列化为 JSON 字符串)</summary>
355+
[JsonPropertyName("value")]
356+
public string Value { get; set; } = "";
357+
358+
/// <summary>过期秒数(0=永不过期)</summary>
359+
[JsonPropertyName("expireSeconds")]
360+
public int ExpireSeconds { get; set; } = 0;
361+
362+
/// <summary>输出变量(写入是否成功 true/false)</summary>
363+
[JsonPropertyName("output")]
364+
public string Output { get; set; } = "";
365+
}
366+
329367
/// <summary>MySQL 节点配置(MYSQL 节点)</summary>
330368
public class MysqlConfig
331369
{
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
using System.Text.Json;
2+
using StackExchange.Redis;
3+
4+
namespace Juggle.Domain.Engine.NodeExecutors;
5+
6+
/// <summary>Redis 连接管理(按连接串缓存复用)。</summary>
7+
public static class RedisConnectionPool
8+
{
9+
private static readonly Dictionary<string, ConnectionMultiplexer> _cache = new();
10+
11+
public static ConnectionMultiplexer Get(string connStr)
12+
{
13+
lock (_cache)
14+
{
15+
if (!_cache.TryGetValue(connStr, out var conn) || !conn.IsConnected)
16+
{
17+
conn?.Dispose();
18+
conn = ConnectionMultiplexer.Connect(connStr);
19+
_cache[connStr] = conn;
20+
}
21+
return conn;
22+
}
23+
}
24+
}
25+
26+
/// <summary>Redis 缓存查询节点:按 key 取值(JSON 字符串自动解析为对象)。</summary>
27+
public class RedisGetNodeExecutor : INodeExecutor
28+
{
29+
private readonly string _connStr;
30+
31+
public RedisGetNodeExecutor(string connStr) => _connStr = connStr;
32+
33+
public async Task<string?> ExecuteAsync(FlowNode node, FlowContext context)
34+
{
35+
var cfg = node.RedisGetConfig
36+
?? throw new InvalidOperationException($"Redis 查询节点 [{node.Key}] 未配置 redisGetConfig。");
37+
if (string.IsNullOrWhiteSpace(cfg.Key))
38+
throw new InvalidOperationException($"Redis 查询节点 [{node.Key}] 未配置 key。");
39+
40+
var key = RenderTemplate(cfg.Key, context);
41+
var db = RedisConnectionPool.Get(_connStr).GetDatabase();
42+
43+
var value = await db.StringGetAsync(key);
44+
object? result = null;
45+
if (value.HasValue)
46+
{
47+
var str = value.ToString();
48+
// JSON 字符串自动解析为对象/数组
49+
try
50+
{
51+
using var doc = JsonDocument.Parse(str);
52+
result = FileParseNodeExecutor.CloneToNative(doc.RootElement);
53+
}
54+
catch { result = str; }
55+
}
56+
57+
if (!string.IsNullOrWhiteSpace(cfg.Output))
58+
context.SetVariable(cfg.Output, result);
59+
60+
return node.Outgoings.FirstOrDefault();
61+
}
62+
63+
internal static string RenderTemplate(string template, FlowContext context)
64+
=> System.Text.RegularExpressions.Regex.Replace(template, @"\$\{([^}]+)\}", m =>
65+
{
66+
var name = m.Groups[1].Value.Trim();
67+
return context.GetVariable(name)?.ToString() ?? "";
68+
});
69+
}
70+
71+
/// <summary>Redis 缓存设置节点:写入 key-value(可设过期秒数),结果写入输出变量。</summary>
72+
public class RedisSetNodeExecutor : INodeExecutor
73+
{
74+
private readonly string _connStr;
75+
76+
public RedisSetNodeExecutor(string connStr) => _connStr = connStr;
77+
78+
public async Task<string?> ExecuteAsync(FlowNode node, FlowContext context)
79+
{
80+
var cfg = node.RedisSetConfig
81+
?? throw new InvalidOperationException($"Redis 设置节点 [{node.Key}] 未配置 redisSetConfig。");
82+
if (string.IsNullOrWhiteSpace(cfg.Key))
83+
throw new InvalidOperationException($"Redis 设置节点 [{node.Key}] 未配置 key。");
84+
85+
var key = RedisGetNodeExecutor.RenderTemplate(cfg.Key, context);
86+
var val = string.IsNullOrWhiteSpace(cfg.Value)
87+
? ""
88+
: context.GetVariable(cfg.Value) switch
89+
{
90+
string s => s,
91+
var v => JsonSerializer.Serialize(v)
92+
};
93+
var db = RedisConnectionPool.Get(_connStr).GetDatabase();
94+
95+
var ok = cfg.ExpireSeconds > 0
96+
? await db.StringSetAsync(key, val, TimeSpan.FromSeconds(cfg.ExpireSeconds))
97+
: await db.StringSetAsync(key, val);
98+
99+
if (!string.IsNullOrWhiteSpace(cfg.Output))
100+
context.SetVariable(cfg.Output, ok);
101+
102+
return node.Outgoings.FirstOrDefault();
103+
}
104+
}

‎Juggle.Domain/Juggle.Domain.csproj‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
<ItemGroup>
1010
<PackageReference Include="ClosedXML" Version="0.104.2" />
11+
<PackageReference Include="StackExchange.Redis" Version="2.8.16" />
1112
<PackageReference Include="DM.DmProvider" Version="8.3.1.47463" />
1213
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.1" />
1314
<PackageReference Include="Microsoft.Data.Sqlite" Version="8.0.4" />

‎JuggleNet6.Frontend/src/router/index.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ const routes = [
4141
{ path: 'report/designer/:id', component: () => import('../views/report/ReportDesigner.vue') },
4242
{ path: 'report/view/:id', component: () => import('../views/report/ReportView.vue') },
4343
{ path: 'system/ai-provider', component: () => import('../views/system/AiProviderList.vue') },
44+
{ path: 'system/redis', component: () => import('../views/system/RedisConfig.vue') },
4445
{ path: 'ai/flow-assistant', component: () => import('../views/ai/FlowAssistant.vue') },
4546
{ path: 'ai/api-assistant', component: () => import('../views/ai/ApiAssistant.vue') },
4647
{ path: 'ai/assistants', component: () => import('../views/ai/AiAssistantList.vue') },

‎JuggleNet6.Frontend/src/views/Layout.vue‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@
8585
<el-menu-item index="/system/tenant" v-if="hasMenu('/system/tenant')">租户管理</el-menu-item>
8686
<el-menu-item index="/system/config" v-if="hasMenu('/system/config')">系统配置</el-menu-item>
8787
<el-menu-item index="/system/ai-provider" v-if="hasMenu('/system/config')">大模型设置</el-menu-item>
88+
<el-menu-item index="/system/redis" v-if="hasMenu('/system/config')">Redis 配置</el-menu-item>
8889
<el-menu-item index="/system/login-log" v-if="hasMenu('/system/login-log')">登录日志</el-menu-item>
8990
<el-menu-item index="/system/audit-log" v-if="hasMenu('/system/audit-log')">审计日志</el-menu-item>
9091
</el-sub-menu>

0 commit comments

Comments
 (0)