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

325 lines
12 KiB
Markdown

# Phase 3: 调整网关部分的需求 - 技术研究
**日期:** 2026-03-03
**状态:** 研究完成
---
## 1. EF Core Owned Entity 配置模式
### GwDestination 内嵌实体
```csharp
// 在 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 内嵌值对象
```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<GwTenant> GwTenants => Set<GwTenant>(); // 删除
public DbSet<GwServiceInstance> GwServiceInstances => Set<GwServiceInstance>(); // 删除
// 2. 移除 OnModelCreating 中的配置
// modelBuilder.Entity<GwTenant>(...) - 删除
// modelBuilder.Entity<GwServiceInstance>(...) - 删除
```
### 创建 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<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 映射
```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<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 接口设计
```csharp
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. 会话亲和实现
### 中间件:设置会话键
```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*