using ChessGameDomain; using ChessGameService; using CommonDomain; using CommonDomain.BaseModel; using CommonDomain.Enum; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using MongoRepository; using RedisHelper; using RedisService; using Common.Redis; namespace ChessGameHost.Services; public class ChessBotService : BackgroundService { private readonly ILogger _logger; private readonly IServiceScopeFactory _scopeFactory; private readonly IHubContext _hubContext; private static readonly Random _random = new(); public ChessBotService( ILogger logger, IServiceScopeFactory scopeFactory, IHubContext hubContext) { _logger = logger; _scopeFactory = scopeFactory; _hubContext = hubContext; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken); await CleanStaleRooms(); while (!stoppingToken.IsCancellationRequested) { try { using var scope = _scopeFactory.CreateScope(); var redisService = scope.ServiceProvider.GetRequiredService(); var distributedLock = scope.ServiceProvider.GetRequiredService(); var emptyRoomRateRepo = scope.ServiceProvider.GetRequiredService>(); var rooms = redisService.GetAllChessRooms(); if (rooms == null) rooms = new List(); if (rooms.Count > 0) { var robotRoomAllowance = await EnsureChessEmptyRoomRate( emptyRoomRateRepo, redisService, distributedLock, rooms); var candidateRooms = rooms.Where(r => r != null && r.state == 0 && (r.users == null || r.users.Count == 0) && r.robotCount == 0 ).ToList(); int maxBatch = Math.Min(Math.Min(candidateRooms.Count, robotRoomAllowance), 5); for (int i = 0; i < maxBatch; i++) { var candidate = candidateRooms[i]; ChessGameRoom? updatedRoom = null; await distributedLock.TryExecuteAsync( $"{RedisKey.DistributedLockPrefix}chess:room:{candidate.RoomId}", () => { var target = redisService.GetChessRoom(candidate.RoomId); if (target == null || target.state == 3 || (target.users != null && target.users.Count > 0)) return Task.CompletedTask; target.robotCount = 1; target.state = 1; if (string.IsNullOrEmpty(target.password)) target.password = _random.Next(100000, 999999).ToString(); redisService.SetChessRoom(target); updatedRoom = target; return Task.CompletedTask; }, expiry: 10); if (updatedRoom != null) await _hubContext.Clients.Group(ChessGameHub.LobbyGroup).SendAsync("JoinRoomNotice", updatedRoom); } } } catch (Exception ex) { _logger.LogError(ex, "ChessBotService 执行异常"); } await Task.Delay(TimeSpan.FromMinutes(2), stoppingToken); } } private async Task CleanStaleRooms() { try { using var scope = _scopeFactory.CreateScope(); var redisService = scope.ServiceProvider.GetRequiredService(); var distributedLock = scope.ServiceProvider.GetRequiredService(); var rooms = redisService.GetAllChessRooms(); if (rooms == null) return; var staleRooms = rooms.Where(r => r != null && (r.users == null || r.users.Count == 0) && (r.state == 3 || (r.state == 1 && string.IsNullOrWhiteSpace(r.createUserId)))) .ToList(); if (staleRooms.Count == 0) return; foreach (var room in staleRooms) { await distributedLock.TryExecuteAsync( $"{RedisKey.DistributedLockPrefix}chess:room:{room.RoomId}", () => { var target = redisService.GetChessRoom(room.RoomId); if (target == null) return Task.CompletedTask; if (target.state == 1 && string.IsNullOrWhiteSpace(target.createUserId) && (target.users == null || target.users.Count == 0)) { redisService.RemoveChessRoom(target.RoomId); return Task.CompletedTask; } target.users = new List(); target.state = 0; target.password = ""; target.createUserId = ""; target.gold = 0; target.gameType = 0; target.robotCount = 0; target.IsGameEnded = false; target.EndGameRequests?.Clear(); target.PendingWinUserId = null; target.TimeoutLoserUserId = ""; target.LastActiveTime = 0; redisService.SetChessRoom(target); return Task.CompletedTask; }, expiry: 5); await Task.Delay(50); } } catch (Exception ex) { _logger.LogError(ex, "[ChessBot] 启动清理异常"); } } private async Task EnsureChessEmptyRoomRate( IMongoBaseRepository emptyRoomRateRepo, IRedisService redisService, IDistributedLock distributedLock, List rooms) { try { var chessConfigs = emptyRoomRateRepo .FilterBy(c => c.GameType == GameTypeEnum.中国象棋 && !c.IsDelete) .ToList(); if (chessConfigs == null || chessConfigs.Count == 0) return 0; var threshold = chessConfigs.Max(c => c.EmptyRoomRate); if (threshold <= 0) return rooms.Count; if (rooms == null || rooms.Count == 0) return 0; var total = rooms.Count(r => r != null); var requiredEmpty = (int)Math.Ceiling(total * Math.Clamp(threshold, 0, 100) / 100.0); var empty = rooms.Count(IsAvailableEmptyRoom); var needRelease = Math.Max(0, requiredEmpty - empty); if (needRelease > 0) { var robotOnlyRooms = rooms .Where(r => r != null && !HasActiveHuman(r) && r.robotCount > 0) .OrderBy(r => r.CreateTime) .Take(needRelease) .ToList(); foreach (var room in robotOnlyRooms) { ChessGameRoom? releasedRoom = null; await distributedLock.TryExecuteAsync( $"{RedisKey.DistributedLockPrefix}chess:room:{room.RoomId}", () => { var target = redisService.GetChessRoom(room.RoomId); if (target == null || HasActiveHuman(target) || target.robotCount <= 0) return Task.CompletedTask; ResetToEmptyRoom(target); redisService.SetChessRoom(target); releasedRoom = target; return Task.CompletedTask; }, expiry: 10, retry: true, retryDelay: 50); if (releasedRoom != null) { empty++; await _hubContext.Clients.Group(ChessGameHub.LobbyGroup) .SendAsync("ExitRoomNotice", releasedRoom); } } } return Math.Max(0, empty - requiredEmpty); } catch (Exception ex) { _logger.LogError(ex, "ChessBotService 维护空房率失败"); return 0; } } private static bool IsAvailableEmptyRoom(ChessGameRoom room) { return room != null && !HasActiveHuman(room) && room.robotCount == 0 && room.state != 2; } private static void ResetToEmptyRoom(ChessGameRoom room) { room.users = new List(); room.state = 0; room.password = ""; room.createUserId = ""; room.gold = 0; room.gameType = 0; room.robotCount = 0; room.IsGameEnded = false; room.EndGameRequests?.Clear(); room.PendingWinUserId = null; room.TimeoutLoserUserId = ""; room.LastActiveTime = 0; } private static bool HasActiveHuman(ChessGameRoom room) { return room?.users != null && room.users.Any(u => u != null && !u.IsLeave); } }