# Phase 3: 调整网关部分的需求 - 技术研究 **日期:** 2026-03-03 **状态:** 研究完成 --- ## 1. EF Core Owned Entity 配置模式 ### GwDestination 内嵌实体 ```csharp // 在 GwCluster 中配置 Owned Entity modelBuilder.Entity(entity => { entity.HasKey(e => e.Id); entity.Property(e => e.ClusterId).HasMaxLength(100).IsRequired(); entity.HasIndex(e => e.ClusterId).IsUnique(); // 配置内嵌的 Destinations 列表 entity.OwnsMany(e => e.Destinations, dest => { dest.WithOwner().HasForeignKey("GwClusterId"); dest.Property(d => d.DestinationId).HasMaxLength(100).IsRequired(); dest.Property(d => d.Address).HasMaxLength(200).IsRequired(); dest.Property(d => d.Health).HasMaxLength(200); dest.Property(d => d.Weight).HasDefaultValue(1); dest.Property(d => d.HealthStatus).HasDefaultValue(1); dest.Property(d => d.Status).HasDefaultValue(1); // 复合唯一索引 dest.HasIndex(d => new { d.DestinationId }).IsUnique(); }); }); ``` ### GwHealthCheckConfig 内嵌值对象 ```csharp entity.OwnsOne(e => e.HealthCheck, hc => { hc.Property(h => h.Enabled).HasDefaultValue(false); hc.Property(h => h.Path).HasMaxLength(100).HasDefaultValue("/health"); hc.Property(h => h.IntervalSeconds).HasDefaultValue(30); hc.Property(h => h.TimeoutSeconds).HasDefaultValue(10); }); ``` ### GwSessionAffinityConfig 内嵌值对象 ```csharp entity.OwnsOne(e => e.SessionAffinity, sa => { sa.Property(s => s.Enabled).HasDefaultValue(false); sa.Property(s => s.Policy).HasMaxLength(50).HasDefaultValue("Header"); sa.Property(s => s.AffinityKeyName).HasMaxLength(100).HasDefaultValue("X-Session-Key"); }); ``` --- ## 2. 实体迁移策略 ### 删除现有实体 ```csharp // 1. 从 PlatformDbContext 中移除 DbSet public DbSet GwTenants => Set(); // 删除 public DbSet GwServiceInstances => Set(); // 删除 // 2. 移除 OnModelCreating 中的配置 // modelBuilder.Entity(...) - 删除 // modelBuilder.Entity(...) - 删除 ``` ### 创建 EF Core 迁移 ```bash # 创建迁移 dotnet ef migrations add RestructureGatewayEntities --project Fengling.Platform.Infrastructure # 迁移将执行: # - DROP TABLE GwTenants # - DROP TABLE GwServiceInstances # - CREATE TABLE GwClusters (包含 Destinations 作为 JSON 或关联表) # - ALTER TABLE GwTenantRoutes (添加新字段) ``` --- ## 3. GwCluster → YARP ClusterConfig 映射 ```csharp public static ClusterConfig ToClusterConfig(this GwCluster cluster) { return new ClusterConfig { ClusterId = cluster.ClusterId, LoadBalancingPolicy = cluster.LoadBalancingPolicy ?? "PowerOfTwoChoices", Destinations = cluster.Destinations .Where(d => d.Status == 1) .ToDictionary( d => d.DestinationId, d => new DestinationConfig { Address = d.Address, Health = d.Health, Metadata = new Dictionary { ["Weight"] = d.Weight.ToString() } } ), HealthCheck = cluster.HealthCheck?.Enabled == true ? new HealthCheckConfig { Active = new ActiveHealthCheckConfig { Enabled = true, Path = cluster.HealthCheck.Path ?? "/health", Interval = TimeSpan.FromSeconds(cluster.HealthCheck.IntervalSeconds), Timeout = TimeSpan.FromSeconds(cluster.HealthCheck.TimeoutSeconds) } } : null, SessionAffinity = cluster.SessionAffinity?.Enabled == true ? new SessionAffinityConfig { Enabled = true, Policy = cluster.SessionAffinity.Policy, AffinityKeyName = cluster.SessionAffinity.AffinityKeyName } : null }; } ``` --- ## 4. GwTenantRoute → YARP RouteConfig 映射 ```csharp public static RouteConfig ToRouteConfig(this GwTenantRoute route) { return new RouteConfig { RouteId = route.Id, Match = new RouteMatch { Path = route.PathPattern, Methods = route.Methods?.Split(',').ToList(), Hosts = route.Hosts?.Split(',').ToList(), Headers = ParseHeaderMatch(route.Headers) }, ClusterId = route.ClusterId, Order = route.Priority, Metadata = new Dictionary { ["TenantCode"] = route.TenantCode, ["ServiceName"] = route.ServiceName, ["IsGlobal"] = route.IsGlobal.ToString() }, Transforms = ParseTransforms(route.Transforms) }; } private static IReadOnlyList? ParseHeaderMatch(string? headersJson) { if (string.IsNullOrEmpty(headersJson)) return null; // 解析 JSON 格式的 Header 匹配规则 // [{"Name":"X-Custom","Values":["value1"],"Mode":"ExactHeader"}] return JsonSerializer.Deserialize>(headersJson); } private static IReadOnlyList>? ParseTransforms(string? transformsJson) { if (string.IsNullOrEmpty(transformsJson)) return null; // 解析 JSON 格式的转换规则 return JsonSerializer.Deserialize>>(transformsJson); } ``` --- ## 5. IClusterStore 接口设计 ```csharp public interface IClusterStore { // 基础查询 Task FindByIdAsync(string id, CancellationToken cancellationToken = default); Task FindByClusterIdAsync(string clusterId, CancellationToken cancellationToken = default); Task> GetAllAsync(CancellationToken cancellationToken = default); // 分页查询 Task> GetPagedAsync( int page, int pageSize, string? name = null, ClusterStatus? status = null, CancellationToken cancellationToken = default); Task GetCountAsync( string? name = null, ClusterStatus? status = null, CancellationToken cancellationToken = default); // CRUD 操作 Task CreateAsync(GwCluster cluster, CancellationToken cancellationToken = default); Task UpdateAsync(GwCluster cluster, CancellationToken cancellationToken = default); Task DeleteAsync(GwCluster cluster, CancellationToken cancellationToken = default); // Destination 管理 Task AddDestinationAsync(string clusterId, GwDestination destination, CancellationToken cancellationToken = default); Task UpdateDestinationAsync(string clusterId, GwDestination destination, CancellationToken cancellationToken = default); Task RemoveDestinationAsync(string clusterId, string destinationId, CancellationToken cancellationToken = default); } ``` --- ## 6. 会话亲和实现 ### 中间件:设置会话键 ```csharp public class SessionAffinityMiddleware { private readonly RequestDelegate _next; public SessionAffinityMiddleware(RequestDelegate next) { _next = next; } public async Task InvokeAsync(HttpContext context) { // 优先使用 UserId var userId = context.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value; // 其次使用 TenantCode var tenantCode = context.User?.FindFirst("TenantCode")?.Value ?? context.Request.Headers["X-Tenant-Code"].FirstOrDefault(); // 设置会话亲和键 var sessionKey = userId ?? tenantCode ?? "anonymous"; context.Items["SessionAffinityKey"] = sessionKey; // 添加到请求头供 YARP 使用 context.Request.Headers["X-Session-Key"] = sessionKey; await _next(context); } } ``` --- ## 7. 依赖关系图 ``` ┌─────────────────────────────────────────────────────────────┐ │ Wave 1 │ │ ┌─────────────────┐ ┌─────────────────┐ │ │ │ GwCluster.cs │ │ GwTenantRoute │ │ │ │ GwDestination │ │ (扩展字段) │ │ │ │ 值对象 │ │ │ │ │ └────────┬────────┘ └────────┬────────┘ │ │ │ │ │ └───────────┼────────────────────┼────────────────────────────┘ │ │ ▼ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Wave 2 │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ PlatformDbContext.cs │ │ │ │ - 移除 GwTenant, GwServiceInstance DbSet │ │ │ │ - 添加 GwCluster DbSet │ │ │ │ - 配置 Owned Entities │ │ │ └─────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Wave 3 │ │ ┌─────────────────┐ ┌─────────────────┐ │ │ │ IClusterStore │ │ IRouteStore │ │ │ │ ClusterStore │ │ RouteStore │ │ │ │ (替换Instance) │ │ (更新) │ │ │ └────────┬────────┘ └────────┬────────┘ │ │ │ │ │ │ ▼ ▼ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ Extensions.cs │ │ │ │ - 移除 IInstanceStore 注册 │ │ │ │ - 添加 IClusterStore 注册 │ │ │ └─────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ ``` --- ## 8. 风险评估 | 风险 | 影响 | 缓解措施 | |------|------|----------| | 删除 GwTenant 导致数据丢失 | 高 | 确认无数据后再删除,或提供迁移脚本 | | 删除 GwServiceInstance 导致数据丢失 | 高 | 同上 | | EF Core 迁移失败 | 中 | 手动编写 SQL 迁移脚本 | | Owned Entity 查询性能 | 低 | EF Core 8+ 已优化 | --- ## 9. 参考资源 - [EF Core Owned Entities](https://learn.microsoft.com/ef/core/modeling/owned-entities) - [YARP Configuration](https://microsoft.github.io/reverse-proxy/) - [YARP GitHub](https://github.com/microsoft/reverse-proxy) - 项目文档: `docs/yarp-configuration-model.md` --- *研究完成日期: 2026-03-03*