using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using RainOpsMini.Helpers;
using RainOpsMini.Helpers.SquadGameLog;
using RainOpsMini.Plugins;
using SqlSugar;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
namespace RainOpsMini.Services
{
///
/// 玩家数据维护服务
/// 负责定时更新玩家的游戏时长、昵称等信息
///
public class PlayerMaintenanceService : BackgroundService
{
private readonly IServiceProvider _serviceProvider;
private readonly GameTimeQueryPlugin _gameTimePlugin;
///
/// 每次批量更新的最大玩家数量
///
private const int BATCH_SIZE = 150;
///
/// 玩家游戏时长更新的冷却时间(6小时)
///
private readonly TimeSpan UPDATE_COOLDOWN = TimeSpan.FromHours(6);
///
/// 构造函数
///
/// 服务提供者
/// 游戏时长查询插件实例
public PlayerMaintenanceService(IServiceProvider serviceProvider, GameTimeQueryPlugin gameTimePlugin)
{
_serviceProvider = serviceProvider;
_gameTimePlugin = gameTimePlugin;
}
///
/// 服务执行主逻辑
///
/// 取消令牌
/// Task
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
RainOpsLog.Log("[玩家维护服务] 服务已启动。");
// 等待系统完全启动并连接到 RCON
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
while (!stoppingToken.IsCancellationRequested)
{
try
{
// 1. 根据在线玩家更新未知昵称(保持此操作,因为它快速且仅涉及本地数据)
await UpdateUnknownNicknames();
// 2. 按优先级处理游戏时长更新
await ProcessGameTimeUpdates(stoppingToken);
}
catch (Exception ex)
{
RainOpsLog.Log($"[玩家维护服务] 全局错误: {ex.Message}");
}
// 每5分钟运行一次以检查优先级,但单个玩家受冷却时间限制
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
}
}
///
/// 处理游戏时长更新逻辑
/// 优先级:
/// 1. 在线玩家(优先检查无时长或时长为0的)
/// 2. 近期加入服务器的玩家
/// 3. 玩家库中时长为0的玩家
///
/// 取消令牌
/// Task
private async Task ProcessGameTimeUpdates(CancellationToken token)
{
var candidates = new List();
var processedInThisRun = new HashSet();
// 1. 优先级:在线玩家
// 获取在线玩家列表(完全拷贝一份数据以确保线程安全)
var onlinePlayers = Program.RconCache.GetData().Players;
if (onlinePlayers != null)
{
foreach (var p in onlinePlayers)
{
if (string.IsNullOrEmpty(p.SteamId)) continue;
// 检查是否满足更新条件(冷却时间)
if (CanUpdate(p.SteamId))
{
candidates.Add(p.SteamId);
processedInThisRun.Add(p.SteamId);
}
}
}
// 2. 优先级:近期加入的玩家(从数据库查询)
// 对应要求:检查玩家库中近期加入服务器的玩家
if (candidates.Count < BATCH_SIZE)
{
using (var db = DbHelper.GetInstance())
{
var recentPlayers = db.Queryable()
.OrderBy(p => p.LastSeen, OrderByType.Desc)
.Take(100) // 查看最近的100名玩家
.Select(p => p.SteamID)
.ToList();
foreach (var id in recentPlayers)
{
if (candidates.Count >= BATCH_SIZE) break;
if (!processedInThisRun.Contains(id) && CanUpdate(id))
{
candidates.Add(id);
processedInThisRun.Add(id);
}
}
}
}
// 3. 优先级:游戏时长为0的玩家(从数据库查询)
// 对应要求:检查玩家库中时长为0的玩家
if (candidates.Count < BATCH_SIZE)
{
using (var db = DbHelper.GetInstance())
{
var zeroTimePlayers = db.Queryable()
.Where(p => p.SquadPlaytimeHours == 0)
.OrderBy(p => p.LastSeen, OrderByType.Desc) // 在时长为0的玩家中优先处理最近活跃的
.Take(100)
.Select(p => p.SteamID)
.ToList();
foreach (var id in zeroTimePlayers)
{
if (candidates.Count >= BATCH_SIZE) break;
if (!processedInThisRun.Contains(id) && CanUpdate(id))
{
candidates.Add(id);
processedInThisRun.Add(id);
}
}
}
}
if (candidates.Count > 0)
{
RainOpsLog.Log($"[玩家维护服务] 正在处理 {candidates.Count} 名玩家的游戏时长更新。");
foreach (var steamId in candidates)
{
if (token.IsCancellationRequested) break;
try
{
// 再次检查冷却时间以防万一
if (!CanUpdate(steamId)) continue;
// 调用插件刷新游戏时长
int minutes = await _gameTimePlugin.RefreshPlayerGameTime(steamId);
// 更新最近尝试时间缓存,防止短时间内重复尝试(即使失败或结果为0)
_recentAttempts[steamId] = DateTime.Now;
// 每次请求间隔1秒,避免触发API速率限制
await Task.Delay(1000, token);
}
catch (Exception ex)
{
// 检查是否为429错误 (Too Many Requests)
if (ex.Message.Contains("429") || ex.Message.Contains("Too Many Requests"))
{
RainOpsLog.Log($"[玩家维护服务] API请求受限 (429),尝试使用爬虫查询玩家 {steamId}...");
var (hours, nickname) = SteamCrawlerHelper.GetHtmlSquadRunTime(steamId);
if (hours > 0)
{
// 更新插件缓存和时长
_gameTimePlugin.SetPlayerGameTime(steamId, hours * 60, nickname);
_recentAttempts[steamId] = DateTime.Now;
RainOpsLog.Log($"[玩家维护服务] 爬虫更新成功: {nickname}, 时长: {hours}小时");
}
else
{
RainOpsLog.Log($"[玩家维护服务] 爬虫未查询到Squad时长或玩家未公开资料。");
}
}
else
{
RainOpsLog.Log($"[玩家维护服务] 更新玩家 {steamId} 时出错: {ex.Message}");
}
}
}
}
}
///
/// 内存缓存,用于跟踪尝试记录(即使失败或返回0),以强制执行6小时冷却时间
///
private static Dictionary _recentAttempts = new Dictionary();
///
/// 检查是否可以更新该玩家(基于冷却时间)
///
/// 玩家SteamID
/// 如果可以更新返回true,否则返回false
private bool CanUpdate(string steamId)
{
if (string.IsNullOrEmpty(steamId)) return false;
// 1. 检查本地尝试缓存(重启后清除,但在运行时循环中有效)
if (_recentAttempts.TryGetValue(steamId, out var lastAttempt))
{
if ((DateTime.Now - lastAttempt) < UPDATE_COOLDOWN) return false;
}
// 2. 检查插件的成功更新时间
var lastUpdate = _gameTimePlugin.GetLastUpdateTime(steamId);
if (lastUpdate.HasValue)
{
if ((DateTime.Now - lastUpdate.Value) < UPDATE_COOLDOWN) return false;
}
return true;
}
///
/// 更新未知昵称
/// 遍历在线玩家,如果数据库中该玩家昵称为Unknown,则更新为当前在线昵称
///
/// Task
private async Task UpdateUnknownNicknames()
{
try
{
// 获取在线玩家副本(Program.RconCache.GetData()返回的是副本吗?根据规则4,我们需要注意)
// 假设 GetData() 返回的是引用,这里我们应该注意。
// 查看 Program.RconCache 实现通常是返回一个新的对象或者我们需要手动处理。
// 此处我们直接使用 GetData().Players,如果是 List 引用,遍历是安全的(只要不在遍历时修改集合)。
// 为了符合规则4 "数据使用的时候需要完全拷贝一份",我们创建一个新列表。
var rconData = Program.RconCache.GetData();
if (rconData?.Players == null) return;
var onlinePlayers = new List(rconData.Players); // 创建副本
if (onlinePlayers.Count == 0) return;
int updateCount = 0;
using (var db = DbHelper.GetInstance())
{
foreach (var player in onlinePlayers)
{
if (string.IsNullOrEmpty(player.SteamId) || string.IsNullOrEmpty(player.Name) || player.Name == "Unknown") continue;
// 检查数据库中是否记录该玩家为 "Unknown"
var dbPlayer = db.Queryable()
.Where(p => p.SteamID == player.SteamId && (p.LastName == "Unknown" || p.LastName == null || p.LastName == ""))
.First();
if (dbPlayer != null)
{
dbPlayer.LastName = player.Name;
dbPlayer.LastSeen = DateTime.Now;
db.Updateable(dbPlayer).ExecuteCommand();
updateCount++;
}
}
}
if (updateCount > 0)
{
RainOpsLog.Log($"[玩家维护服务] 已从在线玩家中更新了 {updateCount} 个未知昵称。");
}
}
catch (Exception ex)
{
RainOpsLog.Log($"[玩家维护服务] 更新昵称时出错: {ex.Message}");
}
}
}
}