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 GetStatisticsAsync(); Task> GetServicesAsync(bool globalOnly = false, string? tenantCode = null); Task GetServiceAsync(string serviceName, string? tenantCode = null); Task RegisterServiceAsync(CreateGatewayServiceDto dto); Task UnregisterServiceAsync(string serviceName, string? tenantCode = null); Task> GetRoutesAsync(bool globalOnly = false); Task CreateRouteAsync(CreateGatewayRouteDto dto); Task> GetInstancesAsync(string clusterId); Task AddInstanceAsync(CreateGatewayInstanceDto dto); Task RemoveInstanceAsync(long instanceId); Task UpdateInstanceWeightAsync(long instanceId, int weight); Task ReloadGatewayAsync(); } public class GatewayService : IGatewayService { private readonly GatewayDbContext _dbContext; private readonly ILogger _logger; public GatewayService(GatewayDbContext dbContext, ILogger logger) { _dbContext = dbContext; _logger = logger; } public async Task GetStatisticsAsync() { var routes = await _dbContext.TenantRoutes.Where(r => !r.IsDeleted).ToListAsync(); var instances = await _dbContext.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> GetServicesAsync(bool globalOnly = false, string? tenantCode = null) { var query = _dbContext.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 _dbContext.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 GetServiceAsync(string serviceName, string? tenantCode = null) { var route = await _dbContext.TenantRoutes .FirstOrDefaultAsync(r => r.ServiceName == serviceName && r.IsDeleted == false && (r.IsGlobal || r.TenantCode == tenantCode)); if (route == null) return null; var instances = await _dbContext.ServiceInstances .CountAsync(i => i.ClusterId == route.ClusterId && !i.IsDeleted); return MapToServiceDto(route, instances); } public async Task RegisterServiceAsync(CreateGatewayServiceDto dto) { 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 _dbContext.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 _dbContext.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 _dbContext.TenantRoutes.AddAsync(route); await _dbContext.SaveChangesAsync(); _logger.LogInformation("Registered service {Service} at {Address}", dto.ServicePrefix, dto.ServiceAddress); return MapToServiceDto(route, 1); } public async Task UnregisterServiceAsync(string serviceName, string? tenantCode = null) { var route = await _dbContext.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 _dbContext.ServiceInstances .Where(i => i.ClusterId == route.ClusterId && !i.IsDeleted) .ToListAsync(); foreach (var instance in instances) { instance.IsDeleted = true; instance.UpdatedTime = DateTime.UtcNow; } await _dbContext.SaveChangesAsync(); _logger.LogInformation("Unregistered service {Service}", serviceName); return true; } public async Task> GetRoutesAsync(bool globalOnly = false) { var query = _dbContext.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 _dbContext.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 CreateRouteAsync(CreateGatewayRouteDto dto) { var existing = await _dbContext.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 _dbContext.TenantRoutes.AddAsync(route); await _dbContext.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> GetInstancesAsync(string clusterId) { var instances = await _dbContext.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 AddInstanceAsync(CreateGatewayInstanceDto dto) { var existing = await _dbContext.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 _dbContext.ServiceInstances.AddAsync(instance); await _dbContext.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 RemoveInstanceAsync(long instanceId) { var instance = await _dbContext.ServiceInstances.FindAsync(instanceId); if (instance == null) return false; instance.IsDeleted = true; instance.UpdatedTime = DateTime.UtcNow; await _dbContext.SaveChangesAsync(); return true; } public async Task UpdateInstanceWeightAsync(long instanceId, int weight) { var instance = await _dbContext.ServiceInstances.FindAsync(instanceId); if (instance == null) return false; instance.Weight = weight; instance.UpdatedTime = DateTime.UtcNow; await _dbContext.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 }; } }