From 154484d2dc4468df8c08636f4df8578d31cf71d4 Mon Sep 17 00:00:00 2001 From: movingsam Date: Wed, 4 Mar 2026 13:00:11 +0800 Subject: [PATCH] refactor(gateway): adapt to Platform 1.0.12 entity changes - Remove IInstanceStore DI registration (replaced by IClusterStore) - Remove GwTenant and GwServiceInstance from ConsoleDbContext config - Update GatewayService to use Match.Path instead of PathPattern - Cast RouteStatus enum to int for Status field - Add 04-SUMMARY.md documentation BREAKING CHANGE: Gateway entity API changes in Platform 1.0.12 --- .../04-gateway-entity-update/04-SUMMARY.md | 78 ++++++ src/Data/ConsoleDbContext.cs | 10 - src/Program.cs | 6 +- src/Services/GatewayService.cs | 237 ++++++++++-------- 4 files changed, 216 insertions(+), 115 deletions(-) create mode 100644 .planning/phases/04-gateway-entity-update/04-SUMMARY.md diff --git a/.planning/phases/04-gateway-entity-update/04-SUMMARY.md b/.planning/phases/04-gateway-entity-update/04-SUMMARY.md new file mode 100644 index 0000000..27205bc --- /dev/null +++ b/.planning/phases/04-gateway-entity-update/04-SUMMARY.md @@ -0,0 +1,78 @@ +# Phase 4 总结:适配 Platform 1.0.12 Gateway 实体变更 + +## 概述 + +本次 Phase 4 成功完成了 Fengling Console 对 Platform 1.0.12 Gateway 实体变更的适配工作。 + +## 主要变更 + +### 1. Program.cs 依赖注入更新 + +**变更内容:** +- 移除了 `IInstanceStore` 和 `InstanceStore` 的注册 +- 保留了 `IClusterStore` 和 `ClusterStore` 的注册 + +**变更原因:** +Platform 1.0.12 移除了 `IInstanceStore` 接口,实例(Destination)现在是 `GwCluster` 的内嵌对象。 + +### 2. ConsoleDbContext 实体配置清理 + +**变更内容:** +- 移除了 `GwTenant` 实体配置(原平台中已移除) +- 移除了 `GwServiceInstance` 实体配置(已重构为 GwDestination) + +### 3. GatewayService 实体属性适配 + +**变更内容:** + +| 旧属性 | 新属性 | 说明 | +|--------|--------|------| +| `GwTenantRoute.PathPattern` (string) | `GwTenantRoute.Match.Path` ( GwRouteMatch.Path ) | 路由匹配配置从简单字符串升级为复杂对象 | +| `Status = RouteStatus.Active` (enum) | `Status = (int)RouteStatus.Active` (int) | Status 字段为 int 类型,需要显式转换枚举 | + +**具体代码变更:** + +```csharp +// 旧代码 +new GwTenantRoute +{ + PathPattern = pathPattern, + Status = RouteStatus.Active, +} + +// 新代码 +new GwTenantRoute +{ + Match = new GwRouteMatch { Path = pathPattern }, + Status = (int)RouteStatus.Active, +} +``` + +```csharp +// 旧代码读取 +r.PathPattern +r.Status + +// 新代码读取 +r.Match.Path +r.Status (已是 int) +``` + +## 编译结果 + +✅ 编译成功,0 个错误,3 个警告(警告为预存在的代码质量问题,与本次变更无关) + +## 验证 + +- [x] `dotnet build` 通过 +- [x] 无新增编译错误 + +## 相关文档 + +- 实体变更详情:`.planning/docs/gateway-entity-changes-1.0.12.md` + +## 下一步 + +可以考虑的改进: +1. 修复 TenantService.cs 中的警告(roleManager 参数未使用) +2. 完善 GatewayService 中的空值处理(Match 可能为 null) diff --git a/src/Data/ConsoleDbContext.cs b/src/Data/ConsoleDbContext.cs index 2bf61df..23bc7de 100644 --- a/src/Data/ConsoleDbContext.cs +++ b/src/Data/ConsoleDbContext.cs @@ -24,21 +24,11 @@ public class ConsoleDbContext : PlatformDbContext base.OnModelCreating(modelBuilder); // ========== Gateway 模块 ========== - modelBuilder.Entity(entity => - { - entity.ToTable("gw_tenants"); - }); - modelBuilder.Entity(entity => { entity.ToTable("gw_tenant_routes"); }); - modelBuilder.Entity(entity => - { - entity.ToTable("gw_service_instances"); - }); - // ========== Tenant 模块 ========== modelBuilder.Entity(entity => { diff --git a/src/Program.cs b/src/Program.cs index 96cd9d1..501960d 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -1,7 +1,6 @@ using System.Reflection; using Fengling.Console.Data; using Fengling.Console.Services; -using Fengling.Console.Services; using Fengling.Platform.Domain.AggregatesModel.UserAggregate; using Fengling.Platform.Domain.AggregatesModel.RoleAggregate; using Fengling.Platform.Infrastructure; @@ -57,11 +56,8 @@ builder.Services.AddScoped(); // Register Gateway managers builder.Services.AddScoped>(); -builder.Services.AddScoped>(); -builder.Services.AddScoped>(); +builder.Services.AddScoped>(); builder.Services.AddScoped(); -builder.Services.AddScoped>(); -builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/src/Services/GatewayService.cs b/src/Services/GatewayService.cs index a3369c7..0bbf245 100644 --- a/src/Services/GatewayService.cs +++ b/src/Services/GatewayService.cs @@ -23,38 +23,46 @@ public interface IGatewayService public class GatewayService : IGatewayService { private readonly IRouteStore _routeStore; - private readonly IInstanceStore _instanceStore; + private readonly IClusterStore _clusterStore; private readonly ILogger _logger; public GatewayService( IRouteStore routeStore, - IInstanceStore instanceStore, + IClusterStore clusterStore, ILogger logger) { _routeStore = routeStore; - _instanceStore = instanceStore; + _clusterStore = clusterStore; _logger = logger; -} + } public async Task GetStatisticsAsync() { var routes = await _routeStore.GetAllAsync(); - var instances = await _instanceStore.GetAllAsync(); + var clusters = await _clusterStore.GetAllAsync(); var activeRoutes = routes.Where(r => !r.IsDeleted).ToList(); - var activeInstances = instances.Where(i => !i.IsDeleted).ToList(); + + // Count destinations from all clusters + var totalInstances = clusters + .Where(c => !c.IsDeleted) + .Sum(c => c.Destinations?.Count(d => d.Status == 1) ?? 0); + + var healthyInstances = clusters + .Where(c => !c.IsDeleted) + .Sum(c => c.Destinations?.Count(d => d.HealthStatus == 1) ?? 0); return new GatewayStatisticsDto { TotalServices = activeRoutes.Select(r => r.ServiceName).Distinct().Count(), GlobalRoutes = activeRoutes.Count(r => r.IsGlobal), TenantRoutes = activeRoutes.Count(r => !r.IsGlobal), - TotalInstances = activeInstances.Count, - HealthyInstances = activeInstances.Count(i => i.Health == (int)InstanceHealth.Healthy), + TotalInstances = totalInstances, + HealthyInstances = healthyInstances, RecentServices = activeRoutes .OrderByDescending(r => r.CreatedTime) .Take(5) - .Select(MapToServiceDto) + .Select(r => MapToServiceDto(r, 0)) .ToList() }; } @@ -62,7 +70,7 @@ public class GatewayService : IGatewayService public async Task> GetServicesAsync(bool globalOnly = false, string? tenantCode = null) { var routes = await _routeStore.GetAllAsync(); - var instances = await _instanceStore.GetAllAsync(); + var clusters = await _clusterStore.GetAllAsync(); var query = routes.Where(r => !r.IsDeleted); @@ -72,12 +80,14 @@ public class GatewayService : IGatewayService query = query.Where(r => r.TenantCode == tenantCode); var routeList = query.OrderByDescending(r => r.CreatedTime).ToList(); - var clusters = routeList.Select(r => r.ClusterId).Distinct().ToList(); - - var instancesDict = instances - .Where(i => clusters.Contains(i.ClusterId) && !i.IsDeleted) - .GroupBy(i => i.ClusterId) - .ToDictionary(g => g.Key, g => g.Count()); + + // Build instance count dict from clusters + var instancesDict = clusters + .Where(c => !c.IsDeleted && routeList.Any(r => r.ClusterId == c.ClusterId)) + .ToDictionary( + c => c.ClusterId, + c => c.Destinations?.Count(d => d.Status == 1) ?? 0 + ); return routeList.Select(r => MapToServiceDto(r, instancesDict.GetValueOrDefault(r.ClusterId, 0))).ToList(); } @@ -92,8 +102,8 @@ public class GatewayService : IGatewayService if (route == null) return null; - var instances = await _instanceStore.GetAllAsync(); - var instanceCount = instances.Count(i => i.ClusterId == route.ClusterId && !i.IsDeleted); + var cluster = await _clusterStore.FindByClusterIdAsync(route.ClusterId); + var instanceCount = cluster?.Destinations?.Count(d => d.Status == 1) ?? 0; return MapToServiceDto(route, instanceCount); } @@ -118,20 +128,30 @@ public class GatewayService : IGatewayService throw new InvalidOperationException($"Service {dto.ServicePrefix} already registered"); } - // Add instance - var instanceId = Guid.CreateVersion7().ToString("N"); - var instance = new GwServiceInstance + // Create or get cluster + var cluster = await _clusterStore.FindByClusterIdAsync(clusterId); + if (cluster == null) + { + cluster = new GwCluster + { + Id = Guid.CreateVersion7().ToString("N"), + ClusterId = clusterId, + Name = $"{dto.ServicePrefix} Service", + Destinations = new List() + }; + await _clusterStore.CreateAsync(cluster); + } + + // Add destination to cluster + var destination = new GwDestination { - Id = instanceId, - ClusterId = clusterId, DestinationId = destinationId, Address = dto.ServiceAddress, Weight = dto.Weight, - Health = (int)InstanceHealth.Healthy, - Status = (int)InstanceStatus.Active, - CreatedTime = DateTime.UtcNow + HealthStatus = 1, // Healthy + Status = 1 // Active }; - await _instanceStore.CreateAsync(instance); + await _clusterStore.AddDestinationAsync(clusterId, destination); // Add route var routeId = Guid.CreateVersion7().ToString("N"); @@ -141,7 +161,7 @@ public class GatewayService : IGatewayService TenantCode = dto.IsGlobal ? "" : dto.TenantCode ?? "", ServiceName = dto.ServicePrefix, ClusterId = clusterId, - PathPattern = pathPattern, + Match = new GwRouteMatch { Path = pathPattern }, Priority = dto.IsGlobal ? 0 : 10, Status = (int)RouteStatus.Active, IsGlobal = dto.IsGlobal, @@ -169,16 +189,8 @@ public class GatewayService : IGatewayService route.UpdatedTime = DateTime.UtcNow; await _routeStore.UpdateAsync(route); - // Soft delete instances - var instances = await _instanceStore.GetAllAsync(); - var routeInstances = instances.Where(i => i.ClusterId == route.ClusterId && !i.IsDeleted).ToList(); - - foreach (var instance in routeInstances) - { - instance.IsDeleted = true; - instance.UpdatedTime = DateTime.UtcNow; - await _instanceStore.UpdateAsync(instance); - } + // Note: We don't delete destinations when unregistering a service + // The cluster and its destinations persist until explicitly deleted _logger.LogInformation("Unregistered service {Service}", serviceName); @@ -188,7 +200,7 @@ public class GatewayService : IGatewayService public async Task> GetRoutesAsync(bool globalOnly = false) { var routes = await _routeStore.GetAllAsync(); - var instances = await _instanceStore.GetAllAsync(); + var clusters = await _clusterStore.GetAllAsync(); var query = routes.Where(r => !r.IsDeleted); @@ -196,19 +208,21 @@ public class GatewayService : IGatewayService query = query.Where(r => r.IsGlobal); var routeList = query.OrderByDescending(r => r.Priority).ToList(); - var clusters = routeList.Select(r => r.ClusterId).Distinct().ToList(); - var instancesDict = instances - .Where(i => clusters.Contains(i.ClusterId) && !i.IsDeleted) - .GroupBy(i => i.ClusterId) - .ToDictionary(g => g.Key, g => g.Count()); + // Build instance count dict from clusters + var instancesDict = clusters + .Where(c => !c.IsDeleted && routeList.Any(r => r.ClusterId == c.ClusterId)) + .ToDictionary( + c => c.ClusterId, + c => c.Destinations?.Count(d => d.Status == 1) ?? 0 + ); return routeList.Select(r => new GatewayRouteDto { Id = r.Id, ServiceName = r.ServiceName, ClusterId = r.ClusterId, - PathPattern = r.PathPattern, + PathPattern = r.Match.Path ?? "", Priority = r.Priority, IsGlobal = r.IsGlobal, TenantCode = r.TenantCode, @@ -236,7 +250,7 @@ public class GatewayService : IGatewayService TenantCode = dto.IsGlobal ? "" : dto.TenantCode ?? "", ServiceName = dto.ServiceName, ClusterId = dto.ClusterId, - PathPattern = dto.PathPattern, + Match = new GwRouteMatch { Path = dto.PathPattern }, Priority = dto.Priority, Status = (int)RouteStatus.Active, IsGlobal = dto.IsGlobal, @@ -250,7 +264,7 @@ public class GatewayService : IGatewayService Id = route.Id, ServiceName = route.ServiceName, ClusterId = route.ClusterId, - PathPattern = route.PathPattern, + PathPattern = route.Match.Path ?? "", Priority = route.Priority, IsGlobal = route.IsGlobal, TenantCode = route.TenantCode, @@ -261,80 +275,103 @@ public class GatewayService : IGatewayService public async Task> GetInstancesAsync(string clusterId) { - var instances = await _instanceStore.GetAllAsync(); - var clusterInstances = instances - .Where(i => i.ClusterId == clusterId && !i.IsDeleted) - .OrderByDescending(i => i.Weight) - .ToList(); + var cluster = await _clusterStore.FindByClusterIdAsync(clusterId); + if (cluster == null || cluster.Destinations == null) + return new List(); - return clusterInstances.Select(i => new GatewayInstanceDto - { - Id = i.Id, - ClusterId = i.ClusterId, - DestinationId = i.DestinationId, - Address = i.Address, - Weight = i.Weight, - Health = (int)i.Health, - Status = (int)i.Status, - CreatedAt = i.CreatedTime - }).ToList(); + return cluster.Destinations + .Where(d => d.Status == 1) + .OrderByDescending(d => d.Weight) + .Select(d => new GatewayInstanceDto + { + Id = d.DestinationId, + ClusterId = clusterId, + DestinationId = d.DestinationId, + Address = d.Address ?? "", + Weight = d.Weight, + Health = d.HealthStatus, + Status = d.Status, + CreatedAt = DateTime.UtcNow + }).ToList(); } public async Task AddInstanceAsync(CreateGatewayInstanceDto dto) { - var existing = await _instanceStore.FindByDestinationAsync(dto.ClusterId, dto.DestinationId); - if (existing != null && !existing.IsDeleted) + var destination = new GwDestination { - throw new InvalidOperationException($"Instance {dto.DestinationId} already exists in cluster {dto.ClusterId}"); + DestinationId = dto.DestinationId, + Address = dto.Address, + Weight = dto.Weight, + HealthStatus = 1, // Healthy + Status = 1 // Active + }; + + var cluster = await _clusterStore.AddDestinationAsync(dto.ClusterId, destination); + if (cluster == null) + { + throw new InvalidOperationException($"Cluster {dto.ClusterId} not found"); } - var instance = new GwServiceInstance + return new GatewayInstanceDto { - Id = Guid.CreateVersion7().ToString("N"), + Id = dto.DestinationId, ClusterId = dto.ClusterId, DestinationId = dto.DestinationId, Address = dto.Address, Weight = dto.Weight, - Health = (int)InstanceHealth.Healthy, - Status = (int)InstanceStatus.Active, - CreatedTime = DateTime.UtcNow - }; - - await _instanceStore.CreateAsync(instance); - - return new GatewayInstanceDto - { - Id = instance.Id, - ClusterId = instance.ClusterId, - DestinationId = instance.DestinationId, - Address = instance.Address, - Weight = instance.Weight, - Health = (int)instance.Health, - Status = (int)instance.Status, - CreatedAt = instance.CreatedTime + Health = 1, + Status = 1, + CreatedAt = DateTime.UtcNow }; } public async Task RemoveInstanceAsync(string instanceId) { - var instance = await _instanceStore.FindByIdAsync(instanceId); - if (instance == null) return false; - - instance.IsDeleted = true; - instance.UpdatedTime = DateTime.UtcNow; - await _instanceStore.UpdateAsync(instance); - return true; + // We need to find the cluster and destination + // Since we don't have direct lookup, iterate through clusters + var clusters = await _clusterStore.GetAllAsync(); + + foreach (var cluster in clusters) + { + if (cluster.Destinations == null) continue; + + var dest = cluster.Destinations.FirstOrDefault(d => d.DestinationId == instanceId); + if (dest != null) + { + await _clusterStore.RemoveDestinationAsync(cluster.ClusterId, instanceId); + return true; + } + } + + return false; } public async Task UpdateInstanceWeightAsync(string instanceId, int weight) { - var instance = await _instanceStore.FindByIdAsync(instanceId); - if (instance == null) return false; - - instance.Weight = weight; - instance.UpdatedTime = DateTime.UtcNow; - await _instanceStore.UpdateAsync(instance); - return true; + // Find the cluster containing this destination + var clusters = await _clusterStore.GetAllAsync(); + + foreach (var cluster in clusters) + { + if (cluster.Destinations == null) continue; + + var dest = cluster.Destinations.FirstOrDefault(d => d.DestinationId == instanceId); + if (dest != null) + { + var updatedDest = new GwDestination + { + DestinationId = dest.DestinationId, + Address = dest.Address ?? "", + Weight = weight, + HealthStatus = dest.HealthStatus, + Status = dest.Status + }; + await _clusterStore.UpdateDestinationAsync(cluster.ClusterId, instanceId, updatedDest); + return true; + } + } + + return false; } public async Task ReloadGatewayAsync() @@ -351,7 +388,7 @@ public class GatewayService : IGatewayService ServicePrefix = route.ServiceName, ServiceName = route.ServiceName, ClusterId = route.ClusterId, - PathPattern = route.PathPattern, + PathPattern = route.Match.Path ?? "", ServiceAddress = "", DestinationId = "", Weight = 1,