fengling-console/Services/GatewayService.cs
Sam c8cb7c06bc feat: 添加Console API认证和OpenIddict集成
- 配置AuthService使用OpenIddict reference tokens
- 添加fengling-api客户端用于introspection验证
- 配置Console API通过OpenIddict验证reference tokens
- 实现Tenant/Users/Roles/OAuthClients CRUD API
- 添加GatewayController服务注册API
- 重构Repository和Service层支持多租户

BREAKING CHANGE: API认证现在使用OpenIddict reference tokens
2026-02-08 19:01:25 +08:00

387 lines
13 KiB
C#

using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using YarpGateway.Data;
using YarpGateway.Models;
using Fengling.Console.Models.Dtos;
namespace Fengling.Console.Services;
public interface IGatewayService
{
Task<GatewayStatisticsDto> GetStatisticsAsync();
Task<List<GatewayServiceDto>> GetServicesAsync(bool globalOnly = false, string? tenantCode = null);
Task<GatewayServiceDto?> GetServiceAsync(string serviceName, string? tenantCode = null);
Task<GatewayServiceDto> RegisterServiceAsync(CreateGatewayServiceDto dto);
Task<bool> UnregisterServiceAsync(string serviceName, string? tenantCode = null);
Task<List<GatewayRouteDto>> GetRoutesAsync(bool globalOnly = false);
Task<GatewayRouteDto> CreateRouteAsync(CreateGatewayRouteDto dto);
Task<List<GatewayInstanceDto>> GetInstancesAsync(string clusterId);
Task<GatewayInstanceDto> AddInstanceAsync(CreateGatewayInstanceDto dto);
Task<bool> RemoveInstanceAsync(long instanceId);
Task<bool> UpdateInstanceWeightAsync(long instanceId, int weight);
Task ReloadGatewayAsync();
}
public class GatewayService : IGatewayService
{
private readonly IDbContextFactory<GatewayDbContext> _dbContextFactory;
private readonly ILogger<GatewayService> _logger;
public GatewayService(IDbContextFactory<GatewayDbContext> dbContextFactory, ILogger<GatewayService> logger)
{
_dbContextFactory = dbContextFactory;
_logger = logger;
}
public async Task<GatewayStatisticsDto> GetStatisticsAsync()
{
await using var db = await _dbContextFactory.CreateDbContextAsync();
var routes = await db.TenantRoutes.Where(r => !r.IsDeleted).ToListAsync();
var instances = await db.ServiceInstances.Where(i => !i.IsDeleted).ToListAsync();
return new GatewayStatisticsDto
{
TotalServices = routes.Select(r => r.ServiceName).Distinct().Count(),
GlobalRoutes = routes.Count(r => r.IsGlobal),
TenantRoutes = routes.Count(r => !r.IsGlobal),
TotalInstances = instances.Count,
HealthyInstances = instances.Count(i => i.Health == 1),
RecentServices = routes
.OrderByDescending(r => r.CreatedTime)
.Take(5)
.Select(MapToServiceDto)
.ToList()
};
}
public async Task<List<GatewayServiceDto>> GetServicesAsync(bool globalOnly = false, string? tenantCode = null)
{
await using var db = await _dbContextFactory.CreateDbContextAsync();
var query = db.TenantRoutes.Where(r => !r.IsDeleted);
if (globalOnly)
query = query.Where(r => r.IsGlobal);
else if (!string.IsNullOrEmpty(tenantCode))
query = query.Where(r => r.TenantCode == tenantCode);
var routes = await query.OrderByDescending(r => r.CreatedTime).ToListAsync();
var clusters = routes.Select(r => r.ClusterId).Distinct().ToList();
var instances = await db.ServiceInstances
.Where(i => clusters.Contains(i.ClusterId) && !i.IsDeleted)
.GroupBy(i => i.ClusterId)
.ToDictionaryAsync(g => g.Key, g => g.Count());
return routes.Select(r => MapToServiceDto(r, instances.GetValueOrDefault(r.ClusterId, 0))).ToList();
}
public async Task<GatewayServiceDto?> GetServiceAsync(string serviceName, string? tenantCode = null)
{
await using var db = await _dbContextFactory.CreateDbContextAsync();
var route = await db.TenantRoutes
.FirstOrDefaultAsync(r =>
r.ServiceName == serviceName &&
r.IsDeleted == false &&
(r.IsGlobal || r.TenantCode == tenantCode));
if (route == null) return null;
var instances = await db.ServiceInstances
.CountAsync(i => i.ClusterId == route.ClusterId && !i.IsDeleted);
return MapToServiceDto(route, instances);
}
public async Task<GatewayServiceDto> RegisterServiceAsync(CreateGatewayServiceDto dto)
{
await using var db = await _dbContextFactory.CreateDbContextAsync();
var clusterId = $"{dto.ServicePrefix}-service";
var pathPattern = $"/{dto.ServicePrefix}/{dto.Version}/{{**path}}";
var destinationId = string.IsNullOrEmpty(dto.DestinationId)
? $"{dto.ServicePrefix}-1"
: dto.DestinationId;
// Check if route already exists
var existingRoute = await db.TenantRoutes
.FirstOrDefaultAsync(r =>
r.ServiceName == dto.ServicePrefix &&
r.IsGlobal == dto.IsGlobal &&
(dto.IsGlobal || r.TenantCode == dto.TenantCode));
if (existingRoute != null)
{
throw new InvalidOperationException($"Service {dto.ServicePrefix} already registered");
}
// Add instance
var instanceId = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
var instance = new GwServiceInstance
{
Id = instanceId,
ClusterId = clusterId,
DestinationId = destinationId,
Address = dto.ServiceAddress,
Weight = dto.Weight,
Health = 1,
Status = 1,
CreatedTime = DateTime.UtcNow
};
await db.ServiceInstances.AddAsync(instance);
// Add route
var routeId = instanceId + 1;
var route = new GwTenantRoute
{
Id = routeId,
TenantCode = dto.IsGlobal ? "" : dto.TenantCode ?? "",
ServiceName = dto.ServicePrefix,
ClusterId = clusterId,
PathPattern = pathPattern,
Priority = dto.IsGlobal ? 0 : 10,
Status = 1,
IsGlobal = dto.IsGlobal,
CreatedTime = DateTime.UtcNow
};
await db.TenantRoutes.AddAsync(route);
await db.SaveChangesAsync();
_logger.LogInformation("Registered service {Service} at {Address}", dto.ServicePrefix, dto.ServiceAddress);
return MapToServiceDto(route, 1);
}
public async Task<bool> UnregisterServiceAsync(string serviceName, string? tenantCode = null)
{
await using var db = await _dbContextFactory.CreateDbContextAsync();
var route = await db.TenantRoutes
.FirstOrDefaultAsync(r =>
r.ServiceName == serviceName &&
r.IsDeleted == false &&
(r.IsGlobal || r.TenantCode == tenantCode));
if (route == null) return false;
// Soft delete route
route.IsDeleted = true;
route.UpdatedTime = DateTime.UtcNow;
// Soft delete instances
var instances = await db.ServiceInstances
.Where(i => i.ClusterId == route.ClusterId && !i.IsDeleted)
.ToListAsync();
foreach (var instance in instances)
{
instance.IsDeleted = true;
instance.UpdatedTime = DateTime.UtcNow;
}
await db.SaveChangesAsync();
_logger.LogInformation("Unregistered service {Service}", serviceName);
return true;
}
public async Task<List<GatewayRouteDto>> GetRoutesAsync(bool globalOnly = false)
{
await using var db = await _dbContextFactory.CreateDbContextAsync();
var query = db.TenantRoutes.Where(r => !r.IsDeleted);
if (globalOnly)
query = query.Where(r => r.IsGlobal);
var routes = await query.OrderByDescending(r => r.Priority).ToListAsync();
var clusters = routes.Select(r => r.ClusterId).Distinct().ToList();
var instances = await db.ServiceInstances
.Where(i => clusters.Contains(i.ClusterId) && !i.IsDeleted)
.GroupBy(i => i.ClusterId)
.ToDictionaryAsync(g => g.Key, g => g.Count());
return routes.Select(r => new GatewayRouteDto
{
Id = r.Id,
ServiceName = r.ServiceName,
ClusterId = r.ClusterId,
PathPattern = r.PathPattern,
Priority = r.Priority,
IsGlobal = r.IsGlobal,
TenantCode = r.TenantCode,
Status = r.Status,
InstanceCount = instances.GetValueOrDefault(r.ClusterId, 0)
}).ToList();
}
public async Task<GatewayRouteDto> CreateRouteAsync(CreateGatewayRouteDto dto)
{
await using var db = await _dbContextFactory.CreateDbContextAsync();
var existing = await db.TenantRoutes
.FirstOrDefaultAsync(r =>
r.ServiceName == dto.ServiceName &&
r.IsGlobal == dto.IsGlobal &&
(dto.IsGlobal || r.TenantCode == dto.TenantCode));
if (existing != null)
{
throw new InvalidOperationException($"Route for {dto.ServiceName} already exists");
}
var route = new GwTenantRoute
{
Id = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
TenantCode = dto.IsGlobal ? "" : dto.TenantCode ?? "",
ServiceName = dto.ServiceName,
ClusterId = dto.ClusterId,
PathPattern = dto.PathPattern,
Priority = dto.Priority,
Status = 1,
IsGlobal = dto.IsGlobal,
CreatedTime = DateTime.UtcNow
};
await db.TenantRoutes.AddAsync(route);
await db.SaveChangesAsync();
return new GatewayRouteDto
{
Id = route.Id,
ServiceName = route.ServiceName,
ClusterId = route.ClusterId,
PathPattern = route.PathPattern,
Priority = route.Priority,
IsGlobal = route.IsGlobal,
TenantCode = route.TenantCode,
Status = route.Status,
InstanceCount = 0
};
}
public async Task<List<GatewayInstanceDto>> GetInstancesAsync(string clusterId)
{
await using var db = await _dbContextFactory.CreateDbContextAsync();
var instances = await db.ServiceInstances
.Where(i => i.ClusterId == clusterId && !i.IsDeleted)
.OrderByDescending(i => i.Weight)
.ToListAsync();
return instances.Select(i => new GatewayInstanceDto
{
Id = i.Id,
ClusterId = i.ClusterId,
DestinationId = i.DestinationId,
Address = i.Address,
Weight = i.Weight,
Health = i.Health,
Status = i.Status,
CreatedAt = i.CreatedTime
}).ToList();
}
public async Task<GatewayInstanceDto> AddInstanceAsync(CreateGatewayInstanceDto dto)
{
await using var db = await _dbContextFactory.CreateDbContextAsync();
var existing = await db.ServiceInstances
.FirstOrDefaultAsync(i =>
i.ClusterId == dto.ClusterId &&
i.DestinationId == dto.DestinationId &&
!i.IsDeleted);
if (existing != null)
{
throw new InvalidOperationException($"Instance {dto.DestinationId} already exists in cluster {dto.ClusterId}");
}
var instance = new GwServiceInstance
{
Id = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
ClusterId = dto.ClusterId,
DestinationId = dto.DestinationId,
Address = dto.Address,
Weight = dto.Weight,
Health = 1,
Status = 1,
CreatedTime = DateTime.UtcNow
};
await db.ServiceInstances.AddAsync(instance);
await db.SaveChangesAsync();
return new GatewayInstanceDto
{
Id = instance.Id,
ClusterId = instance.ClusterId,
DestinationId = instance.DestinationId,
Address = instance.Address,
Weight = instance.Weight,
Health = instance.Health,
Status = instance.Status,
CreatedAt = instance.CreatedTime
};
}
public async Task<bool> RemoveInstanceAsync(long instanceId)
{
await using var db = await _dbContextFactory.CreateDbContextAsync();
var instance = await db.ServiceInstances.FindAsync(instanceId);
if (instance == null) return false;
instance.IsDeleted = true;
instance.UpdatedTime = DateTime.UtcNow;
await db.SaveChangesAsync();
return true;
}
public async Task<bool> UpdateInstanceWeightAsync(long instanceId, int weight)
{
await using var db = await _dbContextFactory.CreateDbContextAsync();
var instance = await db.ServiceInstances.FindAsync(instanceId);
if (instance == null) return false;
instance.Weight = weight;
instance.UpdatedTime = DateTime.UtcNow;
await db.SaveChangesAsync();
return true;
}
public async Task ReloadGatewayAsync()
{
_logger.LogInformation("Gateway configuration reloaded");
await Task.CompletedTask;
}
private static GatewayServiceDto MapToServiceDto(GwTenantRoute route, int instanceCount = 0)
{
return new GatewayServiceDto
{
Id = route.Id,
ServicePrefix = route.ServiceName,
ServiceName = route.ServiceName,
ClusterId = route.ClusterId,
PathPattern = route.PathPattern,
ServiceAddress = "",
DestinationId = "",
Weight = 1,
InstanceCount = instanceCount,
IsGlobal = route.IsGlobal,
TenantCode = route.TenantCode,
Status = route.Status,
CreatedAt = route.CreatedTime
};
}
}