268 lines
9.9 KiB
C#
268 lines
9.9 KiB
C#
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<ChessBotService> _logger;
|
|
private readonly IServiceScopeFactory _scopeFactory;
|
|
private readonly IHubContext<ChessGameHub> _hubContext;
|
|
private static readonly Random _random = new();
|
|
|
|
public ChessBotService(
|
|
ILogger<ChessBotService> logger,
|
|
IServiceScopeFactory scopeFactory,
|
|
IHubContext<ChessGameHub> 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<IRedisService>();
|
|
var distributedLock = scope.ServiceProvider.GetRequiredService<IDistributedLock>();
|
|
var emptyRoomRateRepo = scope.ServiceProvider.GetRequiredService<IMongoBaseRepository<EmptyRoomRateConfig>>();
|
|
|
|
var rooms = redisService.GetAllChessRooms();
|
|
if (rooms == null) rooms = new List<ChessGameDomain.ChessGameRoom>();
|
|
|
|
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<IRedisService>();
|
|
var distributedLock = scope.ServiceProvider.GetRequiredService<IDistributedLock>();
|
|
|
|
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<ChessGameDomain.RedisChessGameUser>();
|
|
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<int> EnsureChessEmptyRoomRate(
|
|
IMongoBaseRepository<EmptyRoomRateConfig> emptyRoomRateRepo,
|
|
IRedisService redisService,
|
|
IDistributedLock distributedLock,
|
|
List<ChessGameRoom> 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<RedisChessGameUser>();
|
|
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);
|
|
}
|
|
}
|