feat(03-gateway-infrastructure-update): update Infrastructure layer for GwCluster

- Updated PlatformDbContext: removed GwTenant/GwServiceInstance DbSets, added GwCluster with EF Core config
- Created IClusterStore interface with CRUD and Destination management methods
- Created ClusterStore<TContext> implementation with soft delete and embedded Destinations support
- Deleted obsolete IInstanceStore and InstanceStore (replaced by IClusterStore)
- Updated Extensions.cs and GatewayExtensions.cs to register IClusterStore

Plan 03 of Phase 03 complete.
This commit is contained in:
movingsam 2026-03-03 15:46:57 +08:00
parent b058c3ea56
commit a6558137af
7 changed files with 217 additions and 150 deletions

View File

@ -0,0 +1,153 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Fengling.Platform.Domain.AggregatesModel.GatewayAggregate;
namespace Fengling.Platform.Infrastructure;
/// <summary>
/// 集群存储实现
/// </summary>
public class ClusterStore<TContext> : IClusterStore
where TContext : PlatformDbContext
{
private readonly TContext _context;
private readonly DbSet<GwCluster> _clusters;
public ClusterStore(TContext context)
{
_context = context;
_clusters = context.GwClusters;
}
public void Dispose() { }
public virtual Task<GwCluster?> FindByIdAsync(string? id, CancellationToken cancellationToken = default)
{
if (id == null) return Task.FromResult<GwCluster?>(null);
return _clusters.FirstOrDefaultAsync(c => c.Id == id, cancellationToken);
}
public virtual Task<GwCluster?> FindByClusterIdAsync(string clusterId, CancellationToken cancellationToken = default)
{
return _clusters.FirstOrDefaultAsync(c => c.ClusterId == clusterId && !c.IsDeleted, cancellationToken);
}
public virtual async Task<IList<GwCluster>> GetAllAsync(CancellationToken cancellationToken = default)
{
return await _clusters.Where(c => !c.IsDeleted).ToListAsync(cancellationToken);
}
public virtual async Task<IList<GwCluster>> GetPagedAsync(int page, int pageSize, string? clusterId = null,
string? name = null, int? status = null, CancellationToken cancellationToken = default)
{
var query = _clusters.AsQueryable();
if (!string.IsNullOrEmpty(clusterId))
query = query.Where(c => c.ClusterId.Contains(clusterId));
if (!string.IsNullOrEmpty(name))
query = query.Where(c => c.Name.Contains(name));
if (status.HasValue)
query = query.Where(c => c.Status == status.Value);
return await query
.Where(c => !c.IsDeleted)
.OrderByDescending(c => c.CreatedTime)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync(cancellationToken);
}
public virtual async Task<int> GetCountAsync(string? clusterId = null, string? name = null,
int? status = null, CancellationToken cancellationToken = default)
{
var query = _clusters.AsQueryable();
if (!string.IsNullOrEmpty(clusterId))
query = query.Where(c => c.ClusterId.Contains(clusterId));
if (!string.IsNullOrEmpty(name))
query = query.Where(c => c.Name.Contains(name));
if (status.HasValue)
query = query.Where(c => c.Status == status.Value);
return await query.Where(c => !c.IsDeleted).CountAsync(cancellationToken);
}
public virtual async Task<IdentityResult> CreateAsync(GwCluster cluster, CancellationToken cancellationToken = default)
{
_clusters.Add(cluster);
await _context.SaveChangesAsync(cancellationToken);
return IdentityResult.Success;
}
public virtual async Task<IdentityResult> UpdateAsync(GwCluster cluster, CancellationToken cancellationToken = default)
{
cluster.UpdatedTime = DateTime.UtcNow;
_clusters.Update(cluster);
await _context.SaveChangesAsync(cancellationToken);
return IdentityResult.Success;
}
public virtual async Task<IdentityResult> DeleteAsync(GwCluster cluster, CancellationToken cancellationToken = default)
{
// 软删除
cluster.IsDeleted = true;
cluster.UpdatedTime = DateTime.UtcNow;
_clusters.Update(cluster);
await _context.SaveChangesAsync(cancellationToken);
return IdentityResult.Success;
}
public virtual async Task<GwCluster?> AddDestinationAsync(string clusterId, GwDestination destination, CancellationToken cancellationToken = default)
{
var cluster = await _clusters.FirstOrDefaultAsync(c => c.ClusterId == clusterId && !c.IsDeleted, cancellationToken);
if (cluster == null) return null;
cluster.Destinations.Add(destination);
cluster.UpdatedTime = DateTime.UtcNow;
await _context.SaveChangesAsync(cancellationToken);
return cluster;
}
public virtual async Task<GwCluster?> UpdateDestinationAsync(string clusterId, string destinationId, GwDestination destination, CancellationToken cancellationToken = default)
{
var cluster = await _clusters
.Include(c => c.Destinations)
.FirstOrDefaultAsync(c => c.ClusterId == clusterId && !c.IsDeleted, cancellationToken);
if (cluster == null) return null;
var existingDest = cluster.Destinations.FirstOrDefault(d => d.DestinationId == destinationId);
if (existingDest == null) return null;
existingDest.Address = destination.Address;
existingDest.Health = destination.Health;
existingDest.Weight = destination.Weight;
existingDest.HealthStatus = destination.HealthStatus;
existingDest.Status = destination.Status;
cluster.UpdatedTime = DateTime.UtcNow;
await _context.SaveChangesAsync(cancellationToken);
return cluster;
}
public virtual async Task<GwCluster?> RemoveDestinationAsync(string clusterId, string destinationId, CancellationToken cancellationToken = default)
{
var cluster = await _clusters
.Include(c => c.Destinations)
.FirstOrDefaultAsync(c => c.ClusterId == clusterId && !c.IsDeleted, cancellationToken);
if (cluster == null) return null;
var destination = cluster.Destinations.FirstOrDefault(d => d.DestinationId == destinationId);
if (destination == null) return null;
cluster.Destinations.Remove(destination);
cluster.UpdatedTime = DateTime.UtcNow;
await _context.SaveChangesAsync(cancellationToken);
return cluster;
}
}

View File

@ -25,11 +25,11 @@ public static class Extensions
// Gateway 服务
services.AddScoped<IRouteStore, RouteStore<TContext>>();
services.AddScoped<IInstanceStore, InstanceStore<TContext>>();
services.AddScoped<IClusterStore, ClusterStore<TContext>>();
services.AddScoped<IRouteManager, RouteManager>();
serviceAction?.Invoke(services);
return services;
}
}
}

View File

@ -18,11 +18,11 @@ public static class GatewayExtensions
{
// 注册 Gateway stores
services.AddScoped<IRouteStore, RouteStore<TContext>>();
services.AddScoped<IInstanceStore, InstanceStore<TContext>>();
services.AddScoped<IClusterStore, ClusterStore<TContext>>();
// 注册 Gateway managers
services.AddScoped<IRouteManager, RouteManager>();
return services;
}
}
}

View File

@ -0,0 +1,27 @@
using Microsoft.AspNetCore.Identity;
using Fengling.Platform.Domain.AggregatesModel.GatewayAggregate;
namespace Fengling.Platform.Infrastructure;
/// <summary>
/// 集群存储接口
/// </summary>
public interface IClusterStore
{
Task<GwCluster?> FindByIdAsync(string? id, CancellationToken cancellationToken = default);
Task<GwCluster?> FindByClusterIdAsync(string clusterId, CancellationToken cancellationToken = default);
Task<IList<GwCluster>> GetAllAsync(CancellationToken cancellationToken = default);
Task<IList<GwCluster>> GetPagedAsync(int page, int pageSize, string? clusterId = null,
string? name = null, int? status = null, CancellationToken cancellationToken = default);
Task<int> GetCountAsync(string? clusterId = null, string? name = null,
int? status = null, CancellationToken cancellationToken = default);
Task<IdentityResult> CreateAsync(GwCluster cluster, CancellationToken cancellationToken = default);
Task<IdentityResult> UpdateAsync(GwCluster cluster, CancellationToken cancellationToken = default);
Task<IdentityResult> DeleteAsync(GwCluster cluster, CancellationToken cancellationToken = default);
// Destination management
Task<GwCluster?> AddDestinationAsync(string clusterId, GwDestination destination, CancellationToken cancellationToken = default);
Task<GwCluster?> UpdateDestinationAsync(string clusterId, string destinationId, GwDestination destination, CancellationToken cancellationToken = default);
Task<GwCluster?> RemoveDestinationAsync(string clusterId, string destinationId, CancellationToken cancellationToken = default);
}

View File

@ -1,23 +0,0 @@
using Microsoft.AspNetCore.Identity;
using Fengling.Platform.Domain.AggregatesModel.GatewayAggregate;
namespace Fengling.Platform.Infrastructure;
/// <summary>
/// 服务实例存储接口
/// </summary>
public interface IInstanceStore
{
Task<GwServiceInstance?> FindByIdAsync(string? id, CancellationToken cancellationToken = default);
Task<GwServiceInstance?> FindByClusterIdAsync(string clusterId, CancellationToken cancellationToken = default);
Task<GwServiceInstance?> FindByDestinationAsync(string clusterId, string destinationId, CancellationToken cancellationToken = default);
Task<IList<GwServiceInstance>> GetAllAsync(CancellationToken cancellationToken = default);
Task<IList<GwServiceInstance>> GetPagedAsync(int page, int pageSize, string? clusterId = null,
InstanceHealth? health = null, InstanceStatus? status = null, CancellationToken cancellationToken = default);
Task<int> GetCountAsync(string? clusterId = null,
InstanceHealth? health = null, InstanceStatus? status = null, CancellationToken cancellationToken = default);
Task<IdentityResult> CreateAsync(GwServiceInstance instance, CancellationToken cancellationToken = default);
Task<IdentityResult> UpdateAsync(GwServiceInstance instance, CancellationToken cancellationToken = default);
Task<IdentityResult> DeleteAsync(GwServiceInstance instance, CancellationToken cancellationToken = default);
}

View File

@ -1,108 +0,0 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Fengling.Platform.Domain.AggregatesModel.GatewayAggregate;
namespace Fengling.Platform.Infrastructure;
/// <summary>
/// 服务实例存储实现
/// </summary>
public class InstanceStore<TContext> : IInstanceStore
where TContext : PlatformDbContext
{
private readonly TContext _context;
private readonly DbSet<GwServiceInstance> _instances;
public InstanceStore(TContext context)
{
_context = context;
_instances = context.GwServiceInstances;
}
public void Dispose() { }
public virtual Task<GwServiceInstance?> FindByIdAsync(string? id, CancellationToken cancellationToken = default)
{
if (id == null) return Task.FromResult<GwServiceInstance?>(null);
return _instances.FirstOrDefaultAsync(i => i.Id == id, cancellationToken);
}
public virtual Task<GwServiceInstance?> FindByClusterIdAsync(string clusterId, CancellationToken cancellationToken = default)
{
return _instances.FirstOrDefaultAsync(i => i.ClusterId == clusterId && !i.IsDeleted, cancellationToken);
}
public virtual Task<GwServiceInstance?> FindByDestinationAsync(string clusterId, string destinationId, CancellationToken cancellationToken = default)
{
return _instances.FirstOrDefaultAsync(i => i.ClusterId == clusterId && i.DestinationId == destinationId && !i.IsDeleted, cancellationToken);
}
public virtual async Task<IList<GwServiceInstance>> GetAllAsync(CancellationToken cancellationToken = default)
{
return await _instances.Where(i => !i.IsDeleted).ToListAsync(cancellationToken);
}
public virtual async Task<IList<GwServiceInstance>> GetPagedAsync(int page, int pageSize, string? clusterId = null,
InstanceHealth? health = null, InstanceStatus? status = null, CancellationToken cancellationToken = default)
{
var query = _instances.AsQueryable();
if (!string.IsNullOrEmpty(clusterId))
query = query.Where(i => i.ClusterId.Contains(clusterId));
if (health.HasValue)
query = query.Where(i => i.Health == (int)health.Value);
if (status.HasValue)
query = query.Where(i => i.Status == (int)status.Value);
return await query
.Where(i => !i.IsDeleted)
.OrderByDescending(i => i.CreatedTime)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync(cancellationToken);
}
public virtual async Task<int> GetCountAsync(string? clusterId = null,
InstanceHealth? health = null, InstanceStatus? status = null, CancellationToken cancellationToken = default)
{
var query = _instances.AsQueryable();
if (!string.IsNullOrEmpty(clusterId))
query = query.Where(i => i.ClusterId.Contains(clusterId));
if (health.HasValue)
query = query.Where(i => i.Health == (int)health.Value);
if (status.HasValue)
query = query.Where(i => i.Status == (int)status.Value);
return await query.Where(i => !i.IsDeleted).CountAsync(cancellationToken);
}
public virtual async Task<IdentityResult> CreateAsync(GwServiceInstance instance, CancellationToken cancellationToken = default)
{
_instances.Add(instance);
await _context.SaveChangesAsync(cancellationToken);
return IdentityResult.Success;
}
public virtual async Task<IdentityResult> UpdateAsync(GwServiceInstance instance, CancellationToken cancellationToken = default)
{
instance.UpdatedTime = DateTime.UtcNow;
_instances.Update(instance);
await _context.SaveChangesAsync(cancellationToken);
return IdentityResult.Success;
}
public virtual async Task<IdentityResult> DeleteAsync(GwServiceInstance instance, CancellationToken cancellationToken = default)
{
// 软删除
instance.IsDeleted = true;
instance.UpdatedTime = DateTime.UtcNow;
_instances.Update(instance);
await _context.SaveChangesAsync(cancellationToken);
return IdentityResult.Success;
}
}

View File

@ -18,9 +18,8 @@ public class PlatformDbContext(DbContextOptions options)
public DbSet<AuditLog> AuditLogs => Set<AuditLog>();
// Gateway 实体
public DbSet<GwTenant> GwTenants => Set<GwTenant>();
public DbSet<GwTenantRoute> GwTenantRoutes => Set<GwTenantRoute>();
public DbSet<GwServiceInstance> GwServiceInstances => Set<GwServiceInstance>();
public DbSet<GwCluster> GwClusters => Set<GwCluster>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
@ -86,14 +85,6 @@ public class PlatformDbContext(DbContextOptions options)
});
// Gateway 实体配置
modelBuilder.Entity<GwTenant>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.TenantCode).HasMaxLength(50).IsRequired();
entity.Property(e => e.TenantName).HasMaxLength(100).IsRequired();
entity.HasIndex(e => e.TenantCode).IsUnique();
});
modelBuilder.Entity<GwTenantRoute>(entity =>
{
entity.HasKey(e => e.Id);
@ -107,14 +98,41 @@ public class PlatformDbContext(DbContextOptions options)
entity.HasIndex(e => new { e.ServiceName, e.IsGlobal, e.Status });
});
modelBuilder.Entity<GwServiceInstance>(entity =>
// GwCluster 聚合根配置 - 使用 Owned 类型
modelBuilder.Entity<GwCluster>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.ClusterId).HasMaxLength(100).IsRequired();
entity.Property(e => e.DestinationId).HasMaxLength(100).IsRequired();
entity.Property(e => e.Address).HasMaxLength(200).IsRequired();
entity.HasIndex(e => new { e.ClusterId, e.DestinationId }).IsUnique();
entity.HasIndex(e => e.Health);
entity.Property(e => e.Name).HasMaxLength(100).IsRequired();
entity.Property(e => e.Description).HasMaxLength(500);
entity.Property(e => e.LoadBalancingPolicy).HasMaxLength(50);
entity.HasIndex(e => e.ClusterId).IsUnique();
entity.HasIndex(e => e.Name);
entity.HasIndex(e => e.Status);
// 配置内嵌的目标端点列表 - 使用 OwnedMany
entity.OwnsMany(e => e.Destinations, owned =>
{
owned.WithOwner().HasForeignKey("ClusterId");
owned.Property<string>("ClusterId").HasMaxLength(100);
owned.Property(d => d.DestinationId).HasMaxLength(100).IsRequired();
owned.Property(d => d.Address).HasMaxLength(200).IsRequired();
owned.Property(d => d.Health).HasMaxLength(200);
owned.HasIndex("ClusterId", "DestinationId");
});
// 配置内嵌健康检查配置
entity.OwnsOne(e => e.HealthCheck, owned =>
{
owned.Property(h => h.Path).HasMaxLength(200);
});
// 配置内嵌会话亲和配置
entity.OwnsOne(e => e.SessionAffinity, owned =>
{
owned.Property(s => s.Policy).HasMaxLength(50);
owned.Property(s => s.AffinityKeyName).HasMaxLength(50);
});
});
modelBuilder.ApplyConfigurationsFromAssembly(typeof(PlatformDbContext).Assembly);