diff --git a/src/Controllers/GlobalUsing.cs b/src/Controllers/GlobalUsing.cs
index 737ce24..a2acf50 100644
--- a/src/Controllers/GlobalUsing.cs
+++ b/src/Controllers/GlobalUsing.cs
@@ -1,5 +1,6 @@
-global using OpenIddict.Validation.AspNetCore;
+global using OpenIddict.Validation.AspNetCore;
global using Fengling.Console.Models.Dtos;
global using Fengling.Console.Services;
+global using Fengling.Platform.Infrastructure;
global using Microsoft.AspNetCore.Authorization;
global using Microsoft.AspNetCore.Mvc;
\ No newline at end of file
diff --git a/src/Controllers/PendingConfigController.cs b/src/Controllers/PendingConfigController.cs
new file mode 100644
index 0000000..294a233
--- /dev/null
+++ b/src/Controllers/PendingConfigController.cs
@@ -0,0 +1,491 @@
+using System.Security.Claims;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using Fengling.Console.Models.Dtos;
+using Fengling.Console.Models.Entities;
+using Fengling.Console.Models.K8s;
+using Fengling.Platform.Domain.AggregatesModel.GatewayAggregate;
+
+namespace Fengling.Console.Controllers;
+
+///
+/// 待确认配置管理控制器
+/// 提供从 K8s 服务发现或手动添加的待确认网关配置的确认、拒绝、修改等功能
+///
+[ApiController]
+[Route("api/console/pending-configs")]
+[Authorize(AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme)]
+public class PendingConfigController : ControllerBase
+{
+ private readonly PendingConfigCache _pendingConfigCache;
+ private readonly INotificationService _notificationService;
+ private readonly IClusterStore _clusterStore;
+ private readonly IRouteStore _routeStore;
+ private readonly ILogger _logger;
+
+ public PendingConfigController(
+ PendingConfigCache pendingConfigCache,
+ INotificationService notificationService,
+ IClusterStore clusterStore,
+ IRouteStore routeStore,
+ ILogger logger)
+ {
+ _pendingConfigCache = pendingConfigCache;
+ _notificationService = notificationService;
+ _clusterStore = clusterStore;
+ _routeStore = routeStore;
+ _logger = logger;
+ }
+
+ ///
+ /// 获取待确认配置统计信息
+ ///
+ /// 待确认配置的统计数据,包括总数、新配置数、变更数
+ /// 成功返回统计数据
+ /// 服务器内部错误
+ [HttpGet("statistics")]
+ [Produces("application/json")]
+ [ProducesResponseType(typeof(PendingConfigStatisticsDto), StatusCodes.Status200OK)]
+ [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)]
+ public ActionResult GetStatistics()
+ {
+ try
+ {
+ var allConfigs = _pendingConfigCache.GetAll().ToList();
+ var result = new PendingConfigStatisticsDto
+ {
+ TotalCount = allConfigs.Count,
+ NewCount = allConfigs.Count(c => c.IsNew),
+ ModifiedCount = allConfigs.Count(c => c.IsModified)
+ };
+ return Ok(result);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error getting pending config statistics");
+ return StatusCode(500, new { message = ex.Message });
+ }
+ }
+
+ ///
+ /// 获取待确认配置列表
+ /// 支持分页、按状态筛选和按服务名搜索
+ ///
+ /// 查询参数,包括分页、状态筛选和服务名搜索
+ /// 分页的待确认配置列表
+ /// 成功返回配置列表
+ /// 服务器内部错误
+ [HttpGet]
+ [Produces("application/json")]
+ [ProducesResponseType(typeof(PagedResultDto), StatusCodes.Status200OK)]
+ [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)]
+ public ActionResult> GetList([FromQuery] PendingConfigListRequest request)
+ {
+ try
+ {
+ var query = _pendingConfigCache.GetAll().AsEnumerable();
+
+ // 应用状态筛选
+ if (request.Status.HasValue)
+ {
+ query = query.Where(c => c.Status == request.Status.Value);
+ }
+
+ // 应用服务名称搜索
+ if (!string.IsNullOrWhiteSpace(request.ServiceName))
+ {
+ query = query.Where(c => c.ServiceName.Contains(request.ServiceName, StringComparison.OrdinalIgnoreCase));
+ }
+
+ // 应用租户编码筛选
+ if (!string.IsNullOrWhiteSpace(request.TenantCode))
+ {
+ query = query.Where(c => c.TenantCode == request.TenantCode);
+ }
+
+ // 应用来源筛选
+ if (request.Source.HasValue)
+ {
+ query = query.Where(c => c.Source == request.Source.Value);
+ }
+
+ var allItems = query.ToList();
+ var totalCount = allItems.Count;
+
+ // 应用分页
+ var items = allItems
+ .Skip((request.Page - 1) * request.PageSize)
+ .Take(request.PageSize)
+ .Select(MapToDto)
+ .ToList();
+
+ var result = new PagedResultDto
+ {
+ Items = items,
+ TotalCount = totalCount,
+ Page = request.Page,
+ PageSize = request.PageSize
+ };
+
+ return Ok(result);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error getting pending config list");
+ return StatusCode(500, new { message = ex.Message });
+ }
+ }
+
+ ///
+ /// 获取单个待确认配置详情
+ ///
+ /// 配置ID
+ /// 待确认配置的详细信息,包括 JSON 配置内容
+ /// 成功返回配置详情
+ /// 配置不存在
+ /// 服务器内部错误
+ [HttpGet("{id}")]
+ [Produces("application/json")]
+ [ProducesResponseType(typeof(PendingConfigDto), StatusCodes.Status200OK)]
+ [ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)]
+ [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)]
+ public ActionResult GetDetail(string id)
+ {
+ try
+ {
+ var config = FindConfigById(id);
+ if (config == null)
+ {
+ return NotFound(new { message = $"Pending config {id} not found" });
+ }
+
+ return Ok(MapToDto(config));
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error getting pending config {ConfigId}", id);
+ return StatusCode(500, new { message = ex.Message });
+ }
+ }
+
+ ///
+ /// 确认待确认配置
+ /// 将配置写入数据库(GwTenantRoute/GwCluster/GwDestination),发送 PostgreSQL NOTIFY 通知,并从缓存中移除
+ ///
+ /// 配置ID
+ /// 确认请求,包含可选的备注信息
+ /// 操作结果
+ /// 成功确认配置
+ /// 配置不存在
+ /// 配置格式无效或保存失败
+ /// 服务器内部错误
+ [HttpPost("{id}/confirm")]
+ [Authorize(Roles = "Admin")]
+ [Produces("application/json")]
+ [ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
+ [ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)]
+ [ProducesResponseType(typeof(object), StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)]
+ public async Task Confirm(string id, [FromBody] ConfirmPendingConfigRequest? request)
+ {
+ try
+ {
+ var config = FindConfigById(id);
+ if (config == null)
+ {
+ return NotFound(new { message = $"Pending config {id} not found" });
+ }
+
+ // 解析配置 JSON
+ var configModel = JsonSerializer.Deserialize(config.ConfigJson, new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase
+ });
+
+ if (configModel == null)
+ {
+ return BadRequest(new { message = "Invalid config format" });
+ }
+
+ // 获取当前用户
+ var confirmedBy = User.FindFirst(ClaimTypes.Name)?.Value ?? User.Identity?.Name ?? "system";
+
+ // 写入数据库
+ await SaveConfigToDatabaseAsync(configModel);
+
+ // 发送 PostgreSQL NOTIFY
+ await _notificationService.PublishConfigChangeAsync(
+ "route",
+ "create",
+ new { configId = id, serviceName = config.ServiceName, clusterId = config.ClusterId }
+ );
+
+ // 从缓存中移除
+ _pendingConfigCache.Remove(config.SourceId);
+
+ _logger.LogInformation(
+ "Pending config {ConfigId} confirmed by {ConfirmedBy}. Service: {ServiceName}",
+ id, confirmedBy, config.ServiceName);
+
+ return Ok(new { message = "Config confirmed successfully", configId = id });
+ }
+ catch (JsonException ex)
+ {
+ _logger.LogError(ex, "Error parsing config JSON for {ConfigId}", id);
+ return BadRequest(new { message = "Invalid config JSON format" });
+ }
+ catch (InvalidOperationException ex)
+ {
+ _logger.LogWarning(ex, "Validation error confirming config {ConfigId}", id);
+ return BadRequest(new { message = ex.Message });
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error confirming pending config {ConfigId}", id);
+ return StatusCode(500, new { message = ex.Message });
+ }
+ }
+
+ ///
+ /// 拒绝待确认配置
+ /// 从缓存中移除配置,并可选择记录拒绝原因
+ ///
+ /// 配置ID
+ /// 拒绝请求,包含可选的拒绝原因
+ /// 操作结果
+ /// 成功拒绝配置
+ /// 配置不存在
+ /// 服务器内部错误
+ [HttpPost("{id}/reject")]
+ [Authorize(Roles = "Admin")]
+ [Produces("application/json")]
+ [ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
+ [ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)]
+ [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)]
+ public ActionResult Reject(string id, [FromBody] RejectPendingConfigRequest? request)
+ {
+ try
+ {
+ var config = FindConfigById(id);
+ if (config == null)
+ {
+ return NotFound(new { message = $"Pending config {id} not found" });
+ }
+
+ // 从缓存中移除
+ var removed = _pendingConfigCache.Remove(config.SourceId);
+ if (!removed)
+ {
+ return NotFound(new { message = $"Pending config {id} not found in cache" });
+ }
+
+ // 记录拒绝原因(可选)
+ var reason = request?.Reason ?? "No reason provided";
+ _logger.LogInformation(
+ "Pending config {ConfigId} rejected. Reason: {Reason}. Service: {ServiceName}",
+ id, reason, config.ServiceName);
+
+ return Ok(new { message = "Config rejected successfully", configId = id, reason });
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error rejecting pending config {ConfigId}", id);
+ return StatusCode(500, new { message = ex.Message });
+ }
+ }
+
+ ///
+ /// 修改待确认配置
+ /// 允许用户调整配置内容,更新缓存中的配置,并标记为 Modified 状态
+ ///
+ /// 配置ID
+ /// 修改请求,包含新的配置内容和修改原因
+ /// 更新后的配置详情
+ /// 成功修改配置
+ /// 配置不存在
+ /// 配置格式无效
+ /// 服务器内部错误
+ [HttpPut("{id}/modify")]
+ [Authorize(Roles = "Admin")]
+ [Produces("application/json")]
+ [ProducesResponseType(typeof(PendingConfigDto), StatusCodes.Status200OK)]
+ [ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)]
+ [ProducesResponseType(typeof(object), StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)]
+ public ActionResult Modify(string id, [FromBody] ModifyPendingConfigRequest request)
+ {
+ try
+ {
+ if (request?.Config == null)
+ {
+ return BadRequest(new { message = "Config content is required" });
+ }
+
+ var config = FindConfigById(id);
+ if (config == null)
+ {
+ return NotFound(new { message = $"Pending config {id} not found" });
+ }
+
+ // 将新的配置内容序列化为 JSON
+ var newConfigJson = request.Config.ToJsonString(new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase
+ });
+
+ // 更新配置
+ config.ConfigJson = newConfigJson;
+ config.Status = PendingConfigStatus.Modified;
+ config.IsModified = true;
+
+ // 尝试解析配置以更新相关字段
+ try
+ {
+ var configModel = JsonSerializer.Deserialize(newConfigJson, new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase
+ });
+
+ if (configModel != null)
+ {
+ config.ServiceName = configModel.ServiceName;
+ config.TenantCode = configModel.TenantCode;
+ config.ClusterId = configModel.ClusterId;
+ }
+ }
+ catch (JsonException ex)
+ {
+ _logger.LogWarning(ex, "Failed to parse modified config for {ConfigId}", id);
+ // 即使解析失败,仍然保存 JSON 内容
+ }
+
+ // 更新缓存
+ _pendingConfigCache.AddOrUpdate(config);
+
+ _logger.LogInformation(
+ "Pending config {ConfigId} modified. Reason: {Reason}. Service: {ServiceName}",
+ id, request.Reason ?? "No reason provided", config.ServiceName);
+
+ return Ok(MapToDto(config));
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error modifying pending config {ConfigId}", id);
+ return StatusCode(500, new { message = ex.Message });
+ }
+ }
+
+ ///
+ /// 根据 ID 查找配置
+ ///
+ private PendingConfig? FindConfigById(string id)
+ {
+ return _pendingConfigCache.GetAll().FirstOrDefault(c => c.Id == id);
+ }
+
+ ///
+ /// 将 PendingConfig 映射为 PendingConfigDto
+ ///
+ private static PendingConfigDto MapToDto(PendingConfig config)
+ {
+ JsonNode? configNode = null;
+ try
+ {
+ if (!string.IsNullOrWhiteSpace(config.ConfigJson))
+ {
+ configNode = JsonNode.Parse(config.ConfigJson);
+ }
+ }
+ catch (JsonException)
+ {
+ // 忽略解析错误,返回 null
+ }
+
+ return new PendingConfigDto
+ {
+ Id = config.Id,
+ Type = config.Type.ToString(),
+ Source = config.Source.ToString(),
+ SourceId = config.SourceId,
+ Config = configNode,
+ ServiceName = config.ServiceName,
+ TenantCode = config.TenantCode,
+ ClusterId = config.ClusterId,
+ Status = config.Status.ToString(),
+ CreatedAt = config.CreatedAt,
+ ConfirmedAt = config.ConfirmedAt,
+ ConfirmedBy = config.ConfirmedBy,
+ IsNew = config.IsNew,
+ IsModified = config.IsModified
+ };
+ }
+
+ ///
+ /// 将配置保存到数据库
+ ///
+ private async Task SaveConfigToDatabaseAsync(GatewayConfigModel config)
+ {
+ // 1. 创建或更新 Cluster
+ var cluster = await _clusterStore.FindByClusterIdAsync(config.ClusterId);
+ if (cluster == null)
+ {
+ cluster = new GwCluster
+ {
+ Id = Guid.CreateVersion7().ToString("N"),
+ ClusterId = config.ClusterId,
+ Name = $"{config.ServiceName} Cluster",
+ Destinations = new List()
+ };
+ await _clusterStore.CreateAsync(cluster);
+ _logger.LogInformation("Created new cluster: {ClusterId}", config.ClusterId);
+ }
+
+ // 2. 添加 Destination 到 Cluster
+ var serviceAddress = config.ServiceAddress ?? $"http://{config.ServiceName}";
+ var port = config.Port ?? 80;
+ var destinationAddress = $"{serviceAddress}:{port}";
+
+ var destination = new GwDestination
+ {
+ DestinationId = config.DestinationId,
+ Address = destinationAddress,
+ Weight = config.Weight,
+ HealthStatus = 1, // Healthy
+ Status = 1, // Active
+ TenantCode = config.TenantCode ?? ""
+ };
+
+ await _clusterStore.AddDestinationAsync(config.ClusterId, destination);
+ _logger.LogInformation(
+ "Added destination {DestinationId} to cluster {ClusterId}: {Address}",
+ config.DestinationId, config.ClusterId, destinationAddress);
+
+ // 3. 创建 Route
+ var pathPattern = string.IsNullOrEmpty(config.PathPrefix)
+ ? $"/{config.ServiceName}/{{**path}}"
+ : config.PathPrefix.TrimEnd('/') + "/{**path}";
+
+ var routeId = Guid.CreateVersion7().ToString("N");
+ var route = new GwTenantRoute
+ {
+ Id = routeId,
+ TenantCode = config.IsGlobal ? "" : (config.TenantCode ?? ""),
+ ServiceName = config.ServiceName,
+ ClusterId = config.ClusterId,
+ Match = new GwRouteMatch
+ {
+ Path = pathPattern,
+ Hosts = !string.IsNullOrEmpty(config.Host) ? new List { config.Host } : null
+ },
+ Priority = config.IsGlobal ? 0 : 10,
+ Status = (int)RouteStatus.Active,
+ IsGlobal = config.IsGlobal,
+ CreatedTime = DateTime.UtcNow
+ };
+
+ await _routeStore.CreateAsync(route);
+ _logger.LogInformation(
+ "Created new route: {RouteId} for service {ServiceName} with path {PathPattern}",
+ routeId, config.ServiceName, pathPattern);
+ }
+}
diff --git a/src/Models/Dtos/PendingConfigDtos.cs b/src/Models/Dtos/PendingConfigDtos.cs
new file mode 100644
index 0000000..b91222b
--- /dev/null
+++ b/src/Models/Dtos/PendingConfigDtos.cs
@@ -0,0 +1,176 @@
+using System.Text.Json.Nodes;
+using Fengling.Console.Models.Entities;
+
+namespace Fengling.Console.Models.Dtos;
+
+///
+/// 待确认配置 DTO
+/// 返回给前端的配置详情
+///
+public class PendingConfigDto
+{
+ ///
+ /// 唯一标识
+ ///
+ public string Id { get; set; } = "";
+
+ ///
+ /// 配置类型
+ ///
+ public string Type { get; set; } = "";
+
+ ///
+ /// 配置来源
+ ///
+ public string Source { get; set; } = "";
+
+ ///
+ /// 来源标识(如 K8s Service UID)
+ ///
+ public string SourceId { get; set; } = "";
+
+ ///
+ /// 配置内容(JSON 对象)
+ ///
+ public JsonNode? Config { get; set; }
+
+ ///
+ /// 服务名称
+ ///
+ public string ServiceName { get; set; } = "";
+
+ ///
+ /// 租户编码
+ ///
+ public string? TenantCode { get; set; }
+
+ ///
+ /// 集群ID
+ ///
+ public string ClusterId { get; set; } = "";
+
+ ///
+ /// 配置状态
+ ///
+ public string Status { get; set; } = "";
+
+ ///
+ /// 创建时间
+ ///
+ public DateTime CreatedAt { get; set; }
+
+ ///
+ /// 确认时间
+ ///
+ public DateTime? ConfirmedAt { get; set; }
+
+ ///
+ /// 确认人
+ ///
+ public string? ConfirmedBy { get; set; }
+
+ ///
+ /// 是否为新配置
+ ///
+ public bool IsNew { get; set; }
+
+ ///
+ /// 是否已修改
+ ///
+ public bool IsModified { get; set; }
+}
+
+///
+/// 待确认配置列表查询请求
+///
+public class PendingConfigListRequest
+{
+ ///
+ /// 页码,从 1 开始
+ ///
+ public int Page { get; set; } = 1;
+
+ ///
+ /// 每页大小,默认 10
+ ///
+ public int PageSize { get; set; } = 10;
+
+ ///
+ /// 按状态筛选(Pending, Confirmed, Rejected, Modified)
+ ///
+ public PendingConfigStatus? Status { get; set; }
+
+ ///
+ /// 按服务名称搜索(支持模糊匹配)
+ ///
+ public string? ServiceName { get; set; }
+
+ ///
+ /// 按租户编码筛选
+ ///
+ public string? TenantCode { get; set; }
+
+ ///
+ /// 按配置来源筛选(K8sDiscovery, Manual)
+ ///
+ public PendingConfigSource? Source { get; set; }
+}
+
+///
+/// 确认待确认配置请求
+///
+public class ConfirmPendingConfigRequest
+{
+ ///
+ /// 确认备注信息
+ ///
+ public string? Remark { get; set; }
+}
+
+///
+/// 拒绝待确认配置请求
+///
+public class RejectPendingConfigRequest
+{
+ ///
+ /// 拒绝原因
+ ///
+ public string? Reason { get; set; }
+}
+
+///
+/// 修改待确认配置请求
+///
+public class ModifyPendingConfigRequest
+{
+ ///
+ /// 修改后的配置内容(JSON 对象)
+ ///
+ public JsonNode? Config { get; set; }
+
+ ///
+ /// 修改原因
+ ///
+ public string? Reason { get; set; }
+}
+
+///
+/// 待确认配置统计信息
+///
+public class PendingConfigStatisticsDto
+{
+ ///
+ /// 待确认总数
+ ///
+ public int TotalCount { get; set; }
+
+ ///
+ /// 新配置数量
+ ///
+ public int NewCount { get; set; }
+
+ ///
+ /// 变更配置数量
+ ///
+ public int ModifiedCount { get; set; }
+}
diff --git a/src/Program.cs b/src/Program.cs
index d92e988..bafebfd 100644
--- a/src/Program.cs
+++ b/src/Program.cs
@@ -96,6 +96,9 @@ builder.Services.AddSwaggerGen(c =>
});
+// 添加通知服务
+builder.Services.AddNotificationService();
+
// 添加 K8s 服务监视
builder.Services.AddServiceLabelParser();
builder.Services.AddPendingConfigCache();