refactor: major project restructuring and cleanup

Changes:

- Remove deprecated Fengling.Activity and YarpGateway.Admin projects

- Add points processing services with distributed lock support

- Update Vben frontend with gateway management pages

- Add gateway config controller and database listener

- Update routing to use header-mixed-nav layout

- Add comprehensive test suites for Member services

- Add YarpGateway integration tests

- Update package versions in Directory.Packages.props

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
sam 2026-02-15 10:34:07 +08:00
parent f14bf019f1
commit 9516e1cd93
2 changed files with 31 additions and 51 deletions

View File

@ -39,6 +39,7 @@ builder.Services.AddScoped<IRoleRepository, RoleRepository>();
builder.Services.AddScoped<IUserService, UserService>(); builder.Services.AddScoped<IUserService, UserService>();
builder.Services.AddScoped<ITenantService, TenantService>(); builder.Services.AddScoped<ITenantService, TenantService>();
builder.Services.AddScoped<IRoleService, RoleService>(); builder.Services.AddScoped<IRoleService, RoleService>();
builder.Services.AddScoped<IGatewayService, GatewayService>();
builder.Services.AddOpenIddict() builder.Services.AddOpenIddict()
.AddCore(options => .AddCore(options =>
@ -79,6 +80,7 @@ builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c => builder.Services.AddSwaggerGen(c =>
{ {
c.SwaggerDoc("v1", new() { Title = "Fengling.Console API", Version = "v1" }); c.SwaggerDoc("v1", new() { Title = "Fengling.Console API", Version = "v1" });
c.CustomSchemaIds(type => type.FullName); // Use full name to avoid conflicts with YarpGateway DTOs
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
if (File.Exists(xmlPath)) if (File.Exists(xmlPath))

View File

@ -24,21 +24,19 @@ public interface IGatewayService
public class GatewayService : IGatewayService public class GatewayService : IGatewayService
{ {
private readonly IDbContextFactory<GatewayDbContext> _dbContextFactory; private readonly GatewayDbContext _dbContext;
private readonly ILogger<GatewayService> _logger; private readonly ILogger<GatewayService> _logger;
public GatewayService(IDbContextFactory<GatewayDbContext> dbContextFactory, ILogger<GatewayService> logger) public GatewayService(GatewayDbContext dbContext, ILogger<GatewayService> logger)
{ {
_dbContextFactory = dbContextFactory; _dbContext = dbContext;
_logger = logger; _logger = logger;
} }
public async Task<GatewayStatisticsDto> GetStatisticsAsync() public async Task<GatewayStatisticsDto> GetStatisticsAsync()
{ {
await using var db = await _dbContextFactory.CreateDbContextAsync(); var routes = await _dbContext.TenantRoutes.Where(r => !r.IsDeleted).ToListAsync();
var instances = await _dbContext.ServiceInstances.Where(i => !i.IsDeleted).ToListAsync();
var routes = await db.TenantRoutes.Where(r => !r.IsDeleted).ToListAsync();
var instances = await db.ServiceInstances.Where(i => !i.IsDeleted).ToListAsync();
return new GatewayStatisticsDto return new GatewayStatisticsDto
{ {
@ -57,9 +55,7 @@ public class GatewayService : IGatewayService
public async Task<List<GatewayServiceDto>> GetServicesAsync(bool globalOnly = false, string? tenantCode = null) public async Task<List<GatewayServiceDto>> GetServicesAsync(bool globalOnly = false, string? tenantCode = null)
{ {
await using var db = await _dbContextFactory.CreateDbContextAsync(); var query = _dbContext.TenantRoutes.Where(r => !r.IsDeleted);
var query = db.TenantRoutes.Where(r => !r.IsDeleted);
if (globalOnly) if (globalOnly)
query = query.Where(r => r.IsGlobal); query = query.Where(r => r.IsGlobal);
@ -69,7 +65,7 @@ public class GatewayService : IGatewayService
var routes = await query.OrderByDescending(r => r.CreatedTime).ToListAsync(); var routes = await query.OrderByDescending(r => r.CreatedTime).ToListAsync();
var clusters = routes.Select(r => r.ClusterId).Distinct().ToList(); var clusters = routes.Select(r => r.ClusterId).Distinct().ToList();
var instances = await db.ServiceInstances var instances = await _dbContext.ServiceInstances
.Where(i => clusters.Contains(i.ClusterId) && !i.IsDeleted) .Where(i => clusters.Contains(i.ClusterId) && !i.IsDeleted)
.GroupBy(i => i.ClusterId) .GroupBy(i => i.ClusterId)
.ToDictionaryAsync(g => g.Key, g => g.Count()); .ToDictionaryAsync(g => g.Key, g => g.Count());
@ -79,9 +75,7 @@ public class GatewayService : IGatewayService
public async Task<GatewayServiceDto?> GetServiceAsync(string serviceName, string? tenantCode = null) public async Task<GatewayServiceDto?> GetServiceAsync(string serviceName, string? tenantCode = null)
{ {
await using var db = await _dbContextFactory.CreateDbContextAsync(); var route = await _dbContext.TenantRoutes
var route = await db.TenantRoutes
.FirstOrDefaultAsync(r => .FirstOrDefaultAsync(r =>
r.ServiceName == serviceName && r.ServiceName == serviceName &&
r.IsDeleted == false && r.IsDeleted == false &&
@ -89,7 +83,7 @@ public class GatewayService : IGatewayService
if (route == null) return null; if (route == null) return null;
var instances = await db.ServiceInstances var instances = await _dbContext.ServiceInstances
.CountAsync(i => i.ClusterId == route.ClusterId && !i.IsDeleted); .CountAsync(i => i.ClusterId == route.ClusterId && !i.IsDeleted);
return MapToServiceDto(route, instances); return MapToServiceDto(route, instances);
@ -97,8 +91,6 @@ public class GatewayService : IGatewayService
public async Task<GatewayServiceDto> RegisterServiceAsync(CreateGatewayServiceDto dto) public async Task<GatewayServiceDto> RegisterServiceAsync(CreateGatewayServiceDto dto)
{ {
await using var db = await _dbContextFactory.CreateDbContextAsync();
var clusterId = $"{dto.ServicePrefix}-service"; var clusterId = $"{dto.ServicePrefix}-service";
var pathPattern = $"/{dto.ServicePrefix}/{dto.Version}/{{**path}}"; var pathPattern = $"/{dto.ServicePrefix}/{dto.Version}/{{**path}}";
var destinationId = string.IsNullOrEmpty(dto.DestinationId) var destinationId = string.IsNullOrEmpty(dto.DestinationId)
@ -106,7 +98,7 @@ public class GatewayService : IGatewayService
: dto.DestinationId; : dto.DestinationId;
// Check if route already exists // Check if route already exists
var existingRoute = await db.TenantRoutes var existingRoute = await _dbContext.TenantRoutes
.FirstOrDefaultAsync(r => .FirstOrDefaultAsync(r =>
r.ServiceName == dto.ServicePrefix && r.ServiceName == dto.ServicePrefix &&
r.IsGlobal == dto.IsGlobal && r.IsGlobal == dto.IsGlobal &&
@ -130,7 +122,7 @@ public class GatewayService : IGatewayService
Status = 1, Status = 1,
CreatedTime = DateTime.UtcNow CreatedTime = DateTime.UtcNow
}; };
await db.ServiceInstances.AddAsync(instance); await _dbContext.ServiceInstances.AddAsync(instance);
// Add route // Add route
var routeId = instanceId + 1; var routeId = instanceId + 1;
@ -146,9 +138,9 @@ public class GatewayService : IGatewayService
IsGlobal = dto.IsGlobal, IsGlobal = dto.IsGlobal,
CreatedTime = DateTime.UtcNow CreatedTime = DateTime.UtcNow
}; };
await db.TenantRoutes.AddAsync(route); await _dbContext.TenantRoutes.AddAsync(route);
await db.SaveChangesAsync(); await _dbContext.SaveChangesAsync();
_logger.LogInformation("Registered service {Service} at {Address}", dto.ServicePrefix, dto.ServiceAddress); _logger.LogInformation("Registered service {Service} at {Address}", dto.ServicePrefix, dto.ServiceAddress);
@ -157,9 +149,7 @@ public class GatewayService : IGatewayService
public async Task<bool> UnregisterServiceAsync(string serviceName, string? tenantCode = null) public async Task<bool> UnregisterServiceAsync(string serviceName, string? tenantCode = null)
{ {
await using var db = await _dbContextFactory.CreateDbContextAsync(); var route = await _dbContext.TenantRoutes
var route = await db.TenantRoutes
.FirstOrDefaultAsync(r => .FirstOrDefaultAsync(r =>
r.ServiceName == serviceName && r.ServiceName == serviceName &&
r.IsDeleted == false && r.IsDeleted == false &&
@ -172,7 +162,7 @@ public class GatewayService : IGatewayService
route.UpdatedTime = DateTime.UtcNow; route.UpdatedTime = DateTime.UtcNow;
// Soft delete instances // Soft delete instances
var instances = await db.ServiceInstances var instances = await _dbContext.ServiceInstances
.Where(i => i.ClusterId == route.ClusterId && !i.IsDeleted) .Where(i => i.ClusterId == route.ClusterId && !i.IsDeleted)
.ToListAsync(); .ToListAsync();
@ -182,7 +172,7 @@ public class GatewayService : IGatewayService
instance.UpdatedTime = DateTime.UtcNow; instance.UpdatedTime = DateTime.UtcNow;
} }
await db.SaveChangesAsync(); await _dbContext.SaveChangesAsync();
_logger.LogInformation("Unregistered service {Service}", serviceName); _logger.LogInformation("Unregistered service {Service}", serviceName);
@ -191,9 +181,7 @@ public class GatewayService : IGatewayService
public async Task<List<GatewayRouteDto>> GetRoutesAsync(bool globalOnly = false) public async Task<List<GatewayRouteDto>> GetRoutesAsync(bool globalOnly = false)
{ {
await using var db = await _dbContextFactory.CreateDbContextAsync(); var query = _dbContext.TenantRoutes.Where(r => !r.IsDeleted);
var query = db.TenantRoutes.Where(r => !r.IsDeleted);
if (globalOnly) if (globalOnly)
query = query.Where(r => r.IsGlobal); query = query.Where(r => r.IsGlobal);
@ -201,7 +189,7 @@ public class GatewayService : IGatewayService
var routes = await query.OrderByDescending(r => r.Priority).ToListAsync(); var routes = await query.OrderByDescending(r => r.Priority).ToListAsync();
var clusters = routes.Select(r => r.ClusterId).Distinct().ToList(); var clusters = routes.Select(r => r.ClusterId).Distinct().ToList();
var instances = await db.ServiceInstances var instances = await _dbContext.ServiceInstances
.Where(i => clusters.Contains(i.ClusterId) && !i.IsDeleted) .Where(i => clusters.Contains(i.ClusterId) && !i.IsDeleted)
.GroupBy(i => i.ClusterId) .GroupBy(i => i.ClusterId)
.ToDictionaryAsync(g => g.Key, g => g.Count()); .ToDictionaryAsync(g => g.Key, g => g.Count());
@ -222,9 +210,7 @@ public class GatewayService : IGatewayService
public async Task<GatewayRouteDto> CreateRouteAsync(CreateGatewayRouteDto dto) public async Task<GatewayRouteDto> CreateRouteAsync(CreateGatewayRouteDto dto)
{ {
await using var db = await _dbContextFactory.CreateDbContextAsync(); var existing = await _dbContext.TenantRoutes
var existing = await db.TenantRoutes
.FirstOrDefaultAsync(r => .FirstOrDefaultAsync(r =>
r.ServiceName == dto.ServiceName && r.ServiceName == dto.ServiceName &&
r.IsGlobal == dto.IsGlobal && r.IsGlobal == dto.IsGlobal &&
@ -248,8 +234,8 @@ public class GatewayService : IGatewayService
CreatedTime = DateTime.UtcNow CreatedTime = DateTime.UtcNow
}; };
await db.TenantRoutes.AddAsync(route); await _dbContext.TenantRoutes.AddAsync(route);
await db.SaveChangesAsync(); await _dbContext.SaveChangesAsync();
return new GatewayRouteDto return new GatewayRouteDto
{ {
@ -267,9 +253,7 @@ public class GatewayService : IGatewayService
public async Task<List<GatewayInstanceDto>> GetInstancesAsync(string clusterId) public async Task<List<GatewayInstanceDto>> GetInstancesAsync(string clusterId)
{ {
await using var db = await _dbContextFactory.CreateDbContextAsync(); var instances = await _dbContext.ServiceInstances
var instances = await db.ServiceInstances
.Where(i => i.ClusterId == clusterId && !i.IsDeleted) .Where(i => i.ClusterId == clusterId && !i.IsDeleted)
.OrderByDescending(i => i.Weight) .OrderByDescending(i => i.Weight)
.ToListAsync(); .ToListAsync();
@ -289,9 +273,7 @@ public class GatewayService : IGatewayService
public async Task<GatewayInstanceDto> AddInstanceAsync(CreateGatewayInstanceDto dto) public async Task<GatewayInstanceDto> AddInstanceAsync(CreateGatewayInstanceDto dto)
{ {
await using var db = await _dbContextFactory.CreateDbContextAsync(); var existing = await _dbContext.ServiceInstances
var existing = await db.ServiceInstances
.FirstOrDefaultAsync(i => .FirstOrDefaultAsync(i =>
i.ClusterId == dto.ClusterId && i.ClusterId == dto.ClusterId &&
i.DestinationId == dto.DestinationId && i.DestinationId == dto.DestinationId &&
@ -314,8 +296,8 @@ public class GatewayService : IGatewayService
CreatedTime = DateTime.UtcNow CreatedTime = DateTime.UtcNow
}; };
await db.ServiceInstances.AddAsync(instance); await _dbContext.ServiceInstances.AddAsync(instance);
await db.SaveChangesAsync(); await _dbContext.SaveChangesAsync();
return new GatewayInstanceDto return new GatewayInstanceDto
{ {
@ -332,29 +314,25 @@ public class GatewayService : IGatewayService
public async Task<bool> RemoveInstanceAsync(long instanceId) public async Task<bool> RemoveInstanceAsync(long instanceId)
{ {
await using var db = await _dbContextFactory.CreateDbContextAsync(); var instance = await _dbContext.ServiceInstances.FindAsync(instanceId);
var instance = await db.ServiceInstances.FindAsync(instanceId);
if (instance == null) return false; if (instance == null) return false;
instance.IsDeleted = true; instance.IsDeleted = true;
instance.UpdatedTime = DateTime.UtcNow; instance.UpdatedTime = DateTime.UtcNow;
await db.SaveChangesAsync(); await _dbContext.SaveChangesAsync();
return true; return true;
} }
public async Task<bool> UpdateInstanceWeightAsync(long instanceId, int weight) public async Task<bool> UpdateInstanceWeightAsync(long instanceId, int weight)
{ {
await using var db = await _dbContextFactory.CreateDbContextAsync(); var instance = await _dbContext.ServiceInstances.FindAsync(instanceId);
var instance = await db.ServiceInstances.FindAsync(instanceId);
if (instance == null) return false; if (instance == null) return false;
instance.Weight = weight; instance.Weight = weight;
instance.UpdatedTime = DateTime.UtcNow; instance.UpdatedTime = DateTime.UtcNow;
await db.SaveChangesAsync(); await _dbContext.SaveChangesAsync();
return true; return true;
} }