fengling-platform/.planning/phases/03-/03-RESEARCH.md

12 KiB

Phase 3: 调整网关部分的需求 - 技术研究

日期: 2026-03-03 状态: 研究完成


1. EF Core Owned Entity 配置模式

GwDestination 内嵌实体

// 在 GwCluster 中配置 Owned Entity
modelBuilder.Entity<GwCluster>(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 内嵌值对象

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 内嵌值对象

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. 实体迁移策略

删除现有实体

// 1. 从 PlatformDbContext 中移除 DbSet
public DbSet<GwTenant> GwTenants => Set<GwTenant>();  // 删除
public DbSet<GwServiceInstance> GwServiceInstances => Set<GwServiceInstance>();  // 删除

// 2. 移除 OnModelCreating 中的配置
// modelBuilder.Entity<GwTenant>(...) - 删除
// modelBuilder.Entity<GwServiceInstance>(...) - 删除

创建 EF Core 迁移

# 创建迁移
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 映射

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<string, string>
                    {
                        ["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 映射

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<string, string>
        {
            ["TenantCode"] = route.TenantCode,
            ["ServiceName"] = route.ServiceName,
            ["IsGlobal"] = route.IsGlobal.ToString()
        },
        Transforms = ParseTransforms(route.Transforms)
    };
}

private static IReadOnlyList<RouteHeader>? ParseHeaderMatch(string? headersJson)
{
    if (string.IsNullOrEmpty(headersJson)) return null;
    
    // 解析 JSON 格式的 Header 匹配规则
    // [{"Name":"X-Custom","Values":["value1"],"Mode":"ExactHeader"}]
    return JsonSerializer.Deserialize<List<RouteHeader>>(headersJson);
}

private static IReadOnlyList<IReadOnlyDictionary<string, string>>? ParseTransforms(string? transformsJson)
{
    if (string.IsNullOrEmpty(transformsJson)) return null;
    
    // 解析 JSON 格式的转换规则
    return JsonSerializer.Deserialize<List<Dictionary<string, string>>>(transformsJson);
}

5. IClusterStore 接口设计

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? name = null, 
        ClusterStatus? status = null,
        CancellationToken cancellationToken = default);
    
    Task<int> GetCountAsync(
        string? name = null, 
        ClusterStatus? status = null,
        CancellationToken cancellationToken = default);
    
    // CRUD 操作
    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 管理
    Task<IdentityResult> AddDestinationAsync(string clusterId, GwDestination destination, CancellationToken cancellationToken = default);
    Task<IdentityResult> UpdateDestinationAsync(string clusterId, GwDestination destination, CancellationToken cancellationToken = default);
    Task<IdentityResult> RemoveDestinationAsync(string clusterId, string destinationId, CancellationToken cancellationToken = default);
}

6. 会话亲和实现

中间件:设置会话键

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. 参考资源


研究完成日期: 2026-03-03