Skip to content

Commit 7e6de5b

Browse files
committed
feat: 系统配置中心 - 邮件SMTP/告警Webhook/系统参数可视化配置(需求16)
1 parent c69b413 commit 7e6de5b

4 files changed

Lines changed: 313 additions & 0 deletions

File tree

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
using Juggle.Application.Models.Request;
2+
using Juggle.Application.Models.Response;
3+
using Juggle.Domain.Entities;
4+
using Juggle.Infrastructure.Persistence;
5+
using Microsoft.AspNetCore.Authorization;
6+
using Microsoft.AspNetCore.Mvc;
7+
using Microsoft.EntityFrameworkCore;
8+
9+
namespace Juggle.Api.Controllers.Api;
10+
11+
[ApiController]
12+
[Route("api/system/config")]
13+
[Authorize]
14+
public class SystemConfigController : ControllerBase
15+
{
16+
private readonly JuggleDbContext _db;
17+
18+
// 预定义的配置项(首次访问时自动初始化)
19+
private static readonly List<(string key, string name, string group, string defaultVal)> DefaultConfigs = new()
20+
{
21+
// 邮件配置
22+
("email.smtp.host", "SMTP服务器地址", "email", "smtp.example.com"),
23+
("email.smtp.port", "SMTP端口", "email", "465"),
24+
("email.smtp.ssl", "启用SSL", "email", "true"),
25+
("email.smtp.username", "SMTP用户名", "email", ""),
26+
("email.smtp.password", "SMTP密码", "email", ""),
27+
("email.from.address", "发件人地址", "email", ""),
28+
("email.from.name", "发件人名称", "email", "Juggle告警"),
29+
// 告警配置
30+
("alert.enabled", "启用全局告警", "alert", "false"),
31+
("alert.webhook.url", "告警Webhook地址", "alert", ""),
32+
("alert.webhook.secret", "告警Webhook密钥", "alert", ""),
33+
("alert.email.to", "告警收件人", "alert", ""),
34+
("alert.on.fail.enabled", "流程失败时告警", "alert", "true"),
35+
("alert.min.cost.ms", "慢执行告警阈值(ms)","alert", "0"),
36+
// 系统配置
37+
("system.page.size", "默认分页大小", "system", "10"),
38+
("system.log.keep.days","日志保留天数", "system", "30"),
39+
};
40+
41+
public SystemConfigController(JuggleDbContext db)
42+
{
43+
_db = db;
44+
}
45+
46+
[HttpGet("all")]
47+
public async Task<ApiResult> GetAll()
48+
{
49+
// 确保默认配置项存在
50+
await EnsureDefaultsAsync();
51+
52+
var configs = await _db.SystemConfigs
53+
.Where(c => c.Deleted == 0)
54+
.OrderBy(c => c.ConfigGroup)
55+
.ThenBy(c => c.Id)
56+
.ToListAsync();
57+
58+
// 按分组返回
59+
var grouped = configs.GroupBy(c => c.ConfigGroup ?? "system")
60+
.ToDictionary(g => g.Key, g => g.Select(c => new
61+
{
62+
c.Id, c.ConfigKey, c.ConfigName, c.ConfigValue, c.Remark
63+
}).ToList());
64+
65+
return ApiResult.Success(grouped);
66+
}
67+
68+
[HttpPost("save")]
69+
public async Task<ApiResult> Save([FromBody] List<SystemConfigSaveRequest> items)
70+
{
71+
foreach (var item in items)
72+
{
73+
var cfg = await _db.SystemConfigs
74+
.FirstOrDefaultAsync(c => c.ConfigKey == item.ConfigKey && c.Deleted == 0);
75+
if (cfg != null)
76+
{
77+
cfg.ConfigValue = item.ConfigValue;
78+
cfg.UpdatedAt = DateTime.Now.ToString("o");
79+
}
80+
}
81+
await _db.SaveChangesAsync();
82+
return ApiResult.Success();
83+
}
84+
85+
private async Task EnsureDefaultsAsync()
86+
{
87+
foreach (var (key, name, group, defVal) in DefaultConfigs)
88+
{
89+
var exists = await _db.SystemConfigs.AnyAsync(c => c.ConfigKey == key && c.Deleted == 0);
90+
if (!exists)
91+
{
92+
_db.SystemConfigs.Add(new SystemConfigEntity
93+
{
94+
ConfigKey = key,
95+
ConfigName = name,
96+
ConfigGroup = group,
97+
ConfigValue = defVal,
98+
Deleted = 0,
99+
CreatedAt = DateTime.Now.ToString("o")
100+
});
101+
}
102+
}
103+
await _db.SaveChangesAsync();
104+
}
105+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
namespace Juggle.Domain.Entities;
2+
3+
/// <summary>系统配置项(键值对)</summary>
4+
public class SystemConfigEntity : BaseEntity
5+
{
6+
public string ConfigKey { get; set; } = ""; // 配置键,全局唯一
7+
public string? ConfigValue { get; set; } // 配置值
8+
public string? ConfigName { get; set; } // 显示名称
9+
public string? ConfigGroup { get; set; } // 分组(email/alert/system)
10+
public string? Remark { get; set; }
11+
}

Juggle.Infrastructure/Persistence/JuggleDbContext.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ public JuggleDbContext(DbContextOptions<JuggleDbContext> options) : base(options
2424
public DbSet<StaticVariableEntity> StaticVariables { get; set; } = null!;
2525
public DbSet<ScheduleTaskEntity> ScheduleTasks { get; set; } = null!;
2626
public DbSet<WebhookEntity> Webhooks { get; set; } = null!;
27+
public DbSet<SystemConfigEntity> SystemConfigs { get; set; } = null!;
28+
public DbSet<FlowTestCaseEntity> FlowTestCases { get; set; } = null!;
2729

2830
protected override void OnModelCreating(ModelBuilder modelBuilder)
2931
{
@@ -47,6 +49,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
4749
modelBuilder.Entity<TokenPermissionEntity>().ToTable("t_token_permission");
4850
modelBuilder.Entity<ScheduleTaskEntity>().ToTable("t_schedule_task");
4951
modelBuilder.Entity<WebhookEntity>().ToTable("t_webhook");
52+
modelBuilder.Entity<SystemConfigEntity>().ToTable("t_system_config");
53+
modelBuilder.Entity<FlowTestCaseEntity>().ToTable("t_flow_test_case");
5054

5155
// 列名映射(snake_case)
5256
modelBuilder.Entity<UserEntity>(e => {
@@ -326,6 +330,37 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
326330
e.Property(p => p.Remark).HasColumnName("remark");
327331
});
328332

333+
modelBuilder.Entity<SystemConfigEntity>(e => {
334+
e.Property(p => p.Id).HasColumnName("id");
335+
e.Property(p => p.Deleted).HasColumnName("deleted");
336+
e.Property(p => p.CreatedAt).HasColumnName("created_at");
337+
e.Property(p => p.CreatedBy).HasColumnName("created_by");
338+
e.Property(p => p.UpdatedAt).HasColumnName("updated_at");
339+
e.Property(p => p.UpdatedBy).HasColumnName("updated_by");
340+
e.Property(p => p.ConfigKey).HasColumnName("config_key");
341+
e.Property(p => p.ConfigValue).HasColumnName("config_value");
342+
e.Property(p => p.ConfigName).HasColumnName("config_name");
343+
e.Property(p => p.ConfigGroup).HasColumnName("config_group");
344+
e.Property(p => p.Remark).HasColumnName("remark");
345+
});
346+
347+
modelBuilder.Entity<FlowTestCaseEntity>(e => {
348+
e.Property(p => p.Id).HasColumnName("id");
349+
e.Property(p => p.Deleted).HasColumnName("deleted");
350+
e.Property(p => p.CreatedAt).HasColumnName("created_at");
351+
e.Property(p => p.CreatedBy).HasColumnName("created_by");
352+
e.Property(p => p.UpdatedAt).HasColumnName("updated_at");
353+
e.Property(p => p.UpdatedBy).HasColumnName("updated_by");
354+
e.Property(p => p.FlowKey).HasColumnName("flow_key");
355+
e.Property(p => p.CaseName).HasColumnName("case_name");
356+
e.Property(p => p.InputJson).HasColumnName("input_json");
357+
e.Property(p => p.AssertJson).HasColumnName("assert_json");
358+
e.Property(p => p.LastRunStatus).HasColumnName("last_run_status");
359+
e.Property(p => p.LastRunTime).HasColumnName("last_run_time");
360+
e.Property(p => p.LastRunResult).HasColumnName("last_run_result");
361+
e.Property(p => p.Remark).HasColumnName("remark");
362+
});
363+
329364
// 初始数据
330365
modelBuilder.Entity<UserEntity>().HasData(new UserEntity
331366
{
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
<template>
2+
<div class="page-container">
3+
<div class="page-header">
4+
<h2>系统配置中心</h2>
5+
<el-button type="primary" icon="Check" @click="saveAll" :loading="saving">保存配置</el-button>
6+
</div>
7+
8+
<div v-loading="loading">
9+
<!-- 邮件配置 -->
10+
<el-card class="config-card">
11+
<template #header>
12+
<div class="card-title"><el-icon><Message /></el-icon> 邮件 SMTP 配置</div>
13+
</template>
14+
<el-form :model="form" label-width="130px">
15+
<el-row :gutter="24">
16+
<el-col :span="12">
17+
<el-form-item label="SMTP服务器">
18+
<el-input v-model="form['email.smtp.host']" placeholder="smtp.example.com" />
19+
</el-form-item>
20+
</el-col>
21+
<el-col :span="6">
22+
<el-form-item label="端口">
23+
<el-input v-model="form['email.smtp.port']" placeholder="465" />
24+
</el-form-item>
25+
</el-col>
26+
<el-col :span="6">
27+
<el-form-item label="启用SSL">
28+
<el-switch v-model="form['email.smtp.ssl']" active-value="true" inactive-value="false" />
29+
</el-form-item>
30+
</el-col>
31+
<el-col :span="12">
32+
<el-form-item label="SMTP用户名">
33+
<el-input v-model="form['email.smtp.username']" />
34+
</el-form-item>
35+
</el-col>
36+
<el-col :span="12">
37+
<el-form-item label="SMTP密码">
38+
<el-input v-model="form['email.smtp.password']" type="password" show-password />
39+
</el-form-item>
40+
</el-col>
41+
<el-col :span="12">
42+
<el-form-item label="发件人地址">
43+
<el-input v-model="form['email.from.address']" placeholder="noreply@example.com" />
44+
</el-form-item>
45+
</el-col>
46+
<el-col :span="12">
47+
<el-form-item label="发件人名称">
48+
<el-input v-model="form['email.from.name']" placeholder="Juggle告警" />
49+
</el-form-item>
50+
</el-col>
51+
</el-row>
52+
</el-form>
53+
</el-card>
54+
55+
<!-- 告警配置 -->
56+
<el-card class="config-card">
57+
<template #header>
58+
<div class="card-title"><el-icon><Bell /></el-icon> 告警配置</div>
59+
</template>
60+
<el-form :model="form" label-width="150px">
61+
<el-row :gutter="24">
62+
<el-col :span="8">
63+
<el-form-item label="启用全局告警">
64+
<el-switch v-model="form['alert.enabled']" active-value="true" inactive-value="false" />
65+
</el-form-item>
66+
</el-col>
67+
<el-col :span="8">
68+
<el-form-item label="流程失败时告警">
69+
<el-switch v-model="form['alert.on.fail.enabled']" active-value="true" inactive-value="false" />
70+
</el-form-item>
71+
</el-col>
72+
<el-col :span="8">
73+
<el-form-item label="慢执行阈值(ms)">
74+
<el-input v-model="form['alert.min.cost.ms']" placeholder="0=不限" />
75+
</el-form-item>
76+
</el-col>
77+
<el-col :span="12">
78+
<el-form-item label="告警Webhook地址">
79+
<el-input v-model="form['alert.webhook.url']" placeholder="https://..." />
80+
</el-form-item>
81+
</el-col>
82+
<el-col :span="12">
83+
<el-form-item label="告警Webhook密钥">
84+
<el-input v-model="form['alert.webhook.secret']" />
85+
</el-form-item>
86+
</el-col>
87+
<el-col :span="12">
88+
<el-form-item label="告警邮件收件人">
89+
<el-input v-model="form['alert.email.to']" placeholder="admin@example.com" />
90+
</el-form-item>
91+
</el-col>
92+
</el-row>
93+
</el-form>
94+
</el-card>
95+
96+
<!-- 系统配置 -->
97+
<el-card class="config-card">
98+
<template #header>
99+
<div class="card-title"><el-icon><Setting /></el-icon> 系统配置</div>
100+
</template>
101+
<el-form :model="form" label-width="130px">
102+
<el-row :gutter="24">
103+
<el-col :span="8">
104+
<el-form-item label="默认分页大小">
105+
<el-input-number v-model.number="form['system.page.size']" :min="5" :max="100" />
106+
</el-form-item>
107+
</el-col>
108+
<el-col :span="8">
109+
<el-form-item label="日志保留天数">
110+
<el-input-number v-model.number="form['system.log.keep.days']" :min="1" :max="365" />
111+
</el-form-item>
112+
</el-col>
113+
</el-row>
114+
</el-form>
115+
</el-card>
116+
</div>
117+
</div>
118+
</template>
119+
120+
<script setup lang="ts">
121+
import { ref, reactive, onMounted } from 'vue'
122+
import { ElMessage } from 'element-plus'
123+
import { Message, Bell, Setting } from '@element-plus/icons-vue'
124+
import request from '../../utils/request'
125+
126+
const loading = ref(false)
127+
const saving = ref(false)
128+
const form = reactive<Record<string, any>>({})
129+
130+
onMounted(() => loadConfig())
131+
132+
async function loadConfig() {
133+
loading.value = true
134+
try {
135+
const res: any = await request.get('/system/config/all')
136+
const grouped = res.data || {}
137+
// 展平所有分组的配置项到 form
138+
for (const group of Object.values(grouped) as any[]) {
139+
for (const item of group) {
140+
form[item.configKey] = item.configValue ?? ''
141+
}
142+
}
143+
} finally { loading.value = false }
144+
}
145+
146+
async function saveAll() {
147+
saving.value = true
148+
try {
149+
const items = Object.entries(form).map(([configKey, configValue]) => ({ configKey, configValue: String(configValue) }))
150+
await request.post('/system/config/save', items)
151+
ElMessage.success('配置保存成功')
152+
} finally { saving.value = false }
153+
}
154+
</script>
155+
156+
<style scoped>
157+
.page-container { padding: 20px; }
158+
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
159+
.page-header h2 { font-size: 20px; color: #333; }
160+
.config-card { margin-bottom: 16px; }
161+
.card-title { display: flex; align-items: center; gap: 8px; font-weight: 600; }
162+
</style>

0 commit comments

Comments
 (0)