refactor(gateway): adapt to Platform 1.0.12 entity changes
All checks were successful
Build and Push Docker / build (push) Successful in 6m44s

- 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
This commit is contained in:
movingsam 2026-03-04 13:00:11 +08:00
parent da1048c3ae
commit 154484d2dc
4 changed files with 216 additions and 115 deletions

View File

@ -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

View File

@ -24,21 +24,11 @@ public class ConsoleDbContext : PlatformDbContext
base.OnModelCreating(modelBuilder); base.OnModelCreating(modelBuilder);
// ========== Gateway 模块 ========== // ========== Gateway 模块 ==========
modelBuilder.Entity<GwTenant>(entity =>
{
entity.ToTable("gw_tenants");
});
modelBuilder.Entity<GwTenantRoute>(entity => modelBuilder.Entity<GwTenantRoute>(entity =>
{ {
entity.ToTable("gw_tenant_routes"); entity.ToTable("gw_tenant_routes");
}); });
modelBuilder.Entity<GwServiceInstance>(entity =>
{
entity.ToTable("gw_service_instances");
});
// ========== Tenant 模块 ========== // ========== Tenant 模块 ==========
modelBuilder.Entity<Tenant>(entity => modelBuilder.Entity<Tenant>(entity =>
{ {

View File

@ -1,7 +1,6 @@
using System.Reflection; using System.Reflection;
using Fengling.Console.Data; using Fengling.Console.Data;
using Fengling.Console.Services; using Fengling.Console.Services;
using Fengling.Console.Services;
using Fengling.Platform.Domain.AggregatesModel.UserAggregate; using Fengling.Platform.Domain.AggregatesModel.UserAggregate;
using Fengling.Platform.Domain.AggregatesModel.RoleAggregate; using Fengling.Platform.Domain.AggregatesModel.RoleAggregate;
using Fengling.Platform.Infrastructure; using Fengling.Platform.Infrastructure;
@ -57,11 +56,8 @@ builder.Services.AddScoped<ITenantManager, TenantManager>();
// Register Gateway managers // Register Gateway managers
builder.Services.AddScoped<IRouteStore, RouteStore<ConsoleDbContext>>(); builder.Services.AddScoped<IRouteStore, RouteStore<ConsoleDbContext>>();
builder.Services.AddScoped<IInstanceStore, InstanceStore<ConsoleDbContext>>(); builder.Services.AddScoped<IClusterStore, ClusterStore<PlatformDbContext>>();
builder.Services.AddScoped<IInstanceStore, InstanceStore<PlatformDbContext>>();
builder.Services.AddScoped<IRouteManager, RouteManager>(); builder.Services.AddScoped<IRouteManager, RouteManager>();
builder.Services.AddScoped<ITenantStore, TenantStore<ConsoleDbContext>>();
builder.Services.AddScoped<ITenantManager, TenantManager>();
builder.Services.AddScoped<IUserService, UserService>(); builder.Services.AddScoped<IUserService, UserService>();
builder.Services.AddScoped<ITenantService, TenantService>(); builder.Services.AddScoped<ITenantService, TenantService>();

View File

@ -23,38 +23,46 @@ public interface IGatewayService
public class GatewayService : IGatewayService public class GatewayService : IGatewayService
{ {
private readonly IRouteStore _routeStore; private readonly IRouteStore _routeStore;
private readonly IInstanceStore _instanceStore; private readonly IClusterStore _clusterStore;
private readonly ILogger<GatewayService> _logger; private readonly ILogger<GatewayService> _logger;
public GatewayService( public GatewayService(
IRouteStore routeStore, IRouteStore routeStore,
IInstanceStore instanceStore, IClusterStore clusterStore,
ILogger<GatewayService> logger) ILogger<GatewayService> logger)
{ {
_routeStore = routeStore; _routeStore = routeStore;
_instanceStore = instanceStore; _clusterStore = clusterStore;
_logger = logger; _logger = logger;
} }
public async Task<GatewayStatisticsDto> GetStatisticsAsync() public async Task<GatewayStatisticsDto> GetStatisticsAsync()
{ {
var routes = await _routeStore.GetAllAsync(); var routes = await _routeStore.GetAllAsync();
var instances = await _instanceStore.GetAllAsync(); var clusters = await _clusterStore.GetAllAsync();
var activeRoutes = routes.Where(r => !r.IsDeleted).ToList(); 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 return new GatewayStatisticsDto
{ {
TotalServices = activeRoutes.Select(r => r.ServiceName).Distinct().Count(), TotalServices = activeRoutes.Select(r => r.ServiceName).Distinct().Count(),
GlobalRoutes = activeRoutes.Count(r => r.IsGlobal), GlobalRoutes = activeRoutes.Count(r => r.IsGlobal),
TenantRoutes = activeRoutes.Count(r => !r.IsGlobal), TenantRoutes = activeRoutes.Count(r => !r.IsGlobal),
TotalInstances = activeInstances.Count, TotalInstances = totalInstances,
HealthyInstances = activeInstances.Count(i => i.Health == (int)InstanceHealth.Healthy), HealthyInstances = healthyInstances,
RecentServices = activeRoutes RecentServices = activeRoutes
.OrderByDescending(r => r.CreatedTime) .OrderByDescending(r => r.CreatedTime)
.Take(5) .Take(5)
.Select(MapToServiceDto) .Select(r => MapToServiceDto(r, 0))
.ToList() .ToList()
}; };
} }
@ -62,7 +70,7 @@ public class GatewayService : IGatewayService
public async Task<List<GatewayServiceDto>> GetServicesAsync(bool globalOnly = false, string? tenantCode = null) public async Task<List<GatewayServiceDto>> GetServicesAsync(bool globalOnly = false, string? tenantCode = null)
{ {
var routes = await _routeStore.GetAllAsync(); var routes = await _routeStore.GetAllAsync();
var instances = await _instanceStore.GetAllAsync(); var clusters = await _clusterStore.GetAllAsync();
var query = routes.Where(r => !r.IsDeleted); var query = routes.Where(r => !r.IsDeleted);
@ -72,12 +80,14 @@ public class GatewayService : IGatewayService
query = query.Where(r => r.TenantCode == tenantCode); query = query.Where(r => r.TenantCode == tenantCode);
var routeList = query.OrderByDescending(r => r.CreatedTime).ToList(); var routeList = query.OrderByDescending(r => r.CreatedTime).ToList();
var clusters = routeList.Select(r => r.ClusterId).Distinct().ToList();
// Build instance count dict from clusters
var instancesDict = instances var instancesDict = clusters
.Where(i => clusters.Contains(i.ClusterId) && !i.IsDeleted) .Where(c => !c.IsDeleted && routeList.Any(r => r.ClusterId == c.ClusterId))
.GroupBy(i => i.ClusterId) .ToDictionary(
.ToDictionary(g => g.Key, g => g.Count()); c => c.ClusterId,
c => c.Destinations?.Count(d => d.Status == 1) ?? 0
);
return routeList.Select(r => MapToServiceDto(r, instancesDict.GetValueOrDefault(r.ClusterId, 0))).ToList(); 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; if (route == null) return null;
var instances = await _instanceStore.GetAllAsync(); var cluster = await _clusterStore.FindByClusterIdAsync(route.ClusterId);
var instanceCount = instances.Count(i => i.ClusterId == route.ClusterId && !i.IsDeleted); var instanceCount = cluster?.Destinations?.Count(d => d.Status == 1) ?? 0;
return MapToServiceDto(route, instanceCount); return MapToServiceDto(route, instanceCount);
} }
@ -118,20 +128,30 @@ public class GatewayService : IGatewayService
throw new InvalidOperationException($"Service {dto.ServicePrefix} already registered"); throw new InvalidOperationException($"Service {dto.ServicePrefix} already registered");
} }
// Add instance // Create or get cluster
var instanceId = Guid.CreateVersion7().ToString("N"); var cluster = await _clusterStore.FindByClusterIdAsync(clusterId);
var instance = new GwServiceInstance if (cluster == null)
{
cluster = new GwCluster
{
Id = Guid.CreateVersion7().ToString("N"),
ClusterId = clusterId,
Name = $"{dto.ServicePrefix} Service",
Destinations = new List<GwDestination>()
};
await _clusterStore.CreateAsync(cluster);
}
// Add destination to cluster
var destination = new GwDestination
{ {
Id = instanceId,
ClusterId = clusterId,
DestinationId = destinationId, DestinationId = destinationId,
Address = dto.ServiceAddress, Address = dto.ServiceAddress,
Weight = dto.Weight, Weight = dto.Weight,
Health = (int)InstanceHealth.Healthy, HealthStatus = 1, // Healthy
Status = (int)InstanceStatus.Active, Status = 1 // Active
CreatedTime = DateTime.UtcNow
}; };
await _instanceStore.CreateAsync(instance); await _clusterStore.AddDestinationAsync(clusterId, destination);
// Add route // Add route
var routeId = Guid.CreateVersion7().ToString("N"); var routeId = Guid.CreateVersion7().ToString("N");
@ -141,7 +161,7 @@ public class GatewayService : IGatewayService
TenantCode = dto.IsGlobal ? "" : dto.TenantCode ?? "", TenantCode = dto.IsGlobal ? "" : dto.TenantCode ?? "",
ServiceName = dto.ServicePrefix, ServiceName = dto.ServicePrefix,
ClusterId = clusterId, ClusterId = clusterId,
PathPattern = pathPattern, Match = new GwRouteMatch { Path = pathPattern },
Priority = dto.IsGlobal ? 0 : 10, Priority = dto.IsGlobal ? 0 : 10,
Status = (int)RouteStatus.Active, Status = (int)RouteStatus.Active,
IsGlobal = dto.IsGlobal, IsGlobal = dto.IsGlobal,
@ -169,16 +189,8 @@ public class GatewayService : IGatewayService
route.UpdatedTime = DateTime.UtcNow; route.UpdatedTime = DateTime.UtcNow;
await _routeStore.UpdateAsync(route); await _routeStore.UpdateAsync(route);
// Soft delete instances // Note: We don't delete destinations when unregistering a service
var instances = await _instanceStore.GetAllAsync(); // The cluster and its destinations persist until explicitly deleted
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);
}
_logger.LogInformation("Unregistered service {Service}", serviceName); _logger.LogInformation("Unregistered service {Service}", serviceName);
@ -188,7 +200,7 @@ public class GatewayService : IGatewayService
public async Task<List<GatewayRouteDto>> GetRoutesAsync(bool globalOnly = false) public async Task<List<GatewayRouteDto>> GetRoutesAsync(bool globalOnly = false)
{ {
var routes = await _routeStore.GetAllAsync(); var routes = await _routeStore.GetAllAsync();
var instances = await _instanceStore.GetAllAsync(); var clusters = await _clusterStore.GetAllAsync();
var query = routes.Where(r => !r.IsDeleted); var query = routes.Where(r => !r.IsDeleted);
@ -196,19 +208,21 @@ public class GatewayService : IGatewayService
query = query.Where(r => r.IsGlobal); query = query.Where(r => r.IsGlobal);
var routeList = query.OrderByDescending(r => r.Priority).ToList(); var routeList = query.OrderByDescending(r => r.Priority).ToList();
var clusters = routeList.Select(r => r.ClusterId).Distinct().ToList();
var instancesDict = instances // Build instance count dict from clusters
.Where(i => clusters.Contains(i.ClusterId) && !i.IsDeleted) var instancesDict = clusters
.GroupBy(i => i.ClusterId) .Where(c => !c.IsDeleted && routeList.Any(r => r.ClusterId == c.ClusterId))
.ToDictionary(g => g.Key, g => g.Count()); .ToDictionary(
c => c.ClusterId,
c => c.Destinations?.Count(d => d.Status == 1) ?? 0
);
return routeList.Select(r => new GatewayRouteDto return routeList.Select(r => new GatewayRouteDto
{ {
Id = r.Id, Id = r.Id,
ServiceName = r.ServiceName, ServiceName = r.ServiceName,
ClusterId = r.ClusterId, ClusterId = r.ClusterId,
PathPattern = r.PathPattern, PathPattern = r.Match.Path ?? "",
Priority = r.Priority, Priority = r.Priority,
IsGlobal = r.IsGlobal, IsGlobal = r.IsGlobal,
TenantCode = r.TenantCode, TenantCode = r.TenantCode,
@ -236,7 +250,7 @@ public class GatewayService : IGatewayService
TenantCode = dto.IsGlobal ? "" : dto.TenantCode ?? "", TenantCode = dto.IsGlobal ? "" : dto.TenantCode ?? "",
ServiceName = dto.ServiceName, ServiceName = dto.ServiceName,
ClusterId = dto.ClusterId, ClusterId = dto.ClusterId,
PathPattern = dto.PathPattern, Match = new GwRouteMatch { Path = dto.PathPattern },
Priority = dto.Priority, Priority = dto.Priority,
Status = (int)RouteStatus.Active, Status = (int)RouteStatus.Active,
IsGlobal = dto.IsGlobal, IsGlobal = dto.IsGlobal,
@ -250,7 +264,7 @@ public class GatewayService : IGatewayService
Id = route.Id, Id = route.Id,
ServiceName = route.ServiceName, ServiceName = route.ServiceName,
ClusterId = route.ClusterId, ClusterId = route.ClusterId,
PathPattern = route.PathPattern, PathPattern = route.Match.Path ?? "",
Priority = route.Priority, Priority = route.Priority,
IsGlobal = route.IsGlobal, IsGlobal = route.IsGlobal,
TenantCode = route.TenantCode, TenantCode = route.TenantCode,
@ -261,80 +275,103 @@ public class GatewayService : IGatewayService
public async Task<List<GatewayInstanceDto>> GetInstancesAsync(string clusterId) public async Task<List<GatewayInstanceDto>> GetInstancesAsync(string clusterId)
{ {
var instances = await _instanceStore.GetAllAsync(); var cluster = await _clusterStore.FindByClusterIdAsync(clusterId);
var clusterInstances = instances if (cluster == null || cluster.Destinations == null)
.Where(i => i.ClusterId == clusterId && !i.IsDeleted) return new List<GatewayInstanceDto>();
.OrderByDescending(i => i.Weight)
.ToList();
return clusterInstances.Select(i => new GatewayInstanceDto return cluster.Destinations
{ .Where(d => d.Status == 1)
Id = i.Id, .OrderByDescending(d => d.Weight)
ClusterId = i.ClusterId, .Select(d => new GatewayInstanceDto
DestinationId = i.DestinationId, {
Address = i.Address, Id = d.DestinationId,
Weight = i.Weight, ClusterId = clusterId,
Health = (int)i.Health, DestinationId = d.DestinationId,
Status = (int)i.Status, Address = d.Address ?? "",
CreatedAt = i.CreatedTime Weight = d.Weight,
}).ToList(); Health = d.HealthStatus,
Status = d.Status,
CreatedAt = DateTime.UtcNow
}).ToList();
} }
public async Task<GatewayInstanceDto> AddInstanceAsync(CreateGatewayInstanceDto dto) public async Task<GatewayInstanceDto> AddInstanceAsync(CreateGatewayInstanceDto dto)
{ {
var existing = await _instanceStore.FindByDestinationAsync(dto.ClusterId, dto.DestinationId); var destination = new GwDestination
if (existing != null && !existing.IsDeleted)
{ {
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, ClusterId = dto.ClusterId,
DestinationId = dto.DestinationId, DestinationId = dto.DestinationId,
Address = dto.Address, Address = dto.Address,
Weight = dto.Weight, Weight = dto.Weight,
Health = (int)InstanceHealth.Healthy, Health = 1,
Status = (int)InstanceStatus.Active, Status = 1,
CreatedTime = DateTime.UtcNow CreatedAt = 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
}; };
} }
public async Task<bool> RemoveInstanceAsync(string instanceId) public async Task<bool> RemoveInstanceAsync(string instanceId)
{ {
var instance = await _instanceStore.FindByIdAsync(instanceId); // We need to find the cluster and destination
if (instance == null) return false; // Since we don't have direct lookup, iterate through clusters
var clusters = await _clusterStore.GetAllAsync();
instance.IsDeleted = true;
instance.UpdatedTime = DateTime.UtcNow; foreach (var cluster in clusters)
await _instanceStore.UpdateAsync(instance); {
return true; 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<bool> UpdateInstanceWeightAsync(string instanceId, int weight) public async Task<bool> UpdateInstanceWeightAsync(string instanceId, int weight)
{ {
var instance = await _instanceStore.FindByIdAsync(instanceId); // Find the cluster containing this destination
if (instance == null) return false; var clusters = await _clusterStore.GetAllAsync();
instance.Weight = weight; foreach (var cluster in clusters)
instance.UpdatedTime = DateTime.UtcNow; {
await _instanceStore.UpdateAsync(instance); if (cluster.Destinations == null) continue;
return true;
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() public async Task ReloadGatewayAsync()
@ -351,7 +388,7 @@ public class GatewayService : IGatewayService
ServicePrefix = route.ServiceName, ServicePrefix = route.ServiceName,
ServiceName = route.ServiceName, ServiceName = route.ServiceName,
ClusterId = route.ClusterId, ClusterId = route.ClusterId,
PathPattern = route.PathPattern, PathPattern = route.Match.Path ?? "",
ServiceAddress = "", ServiceAddress = "",
DestinationId = "", DestinationId = "",
Weight = 1, Weight = 1,