Files
squad-rain-ops-mini/Services/PlayerMaintenanceService.cs

298 lines
12 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
{
/// <summary>
/// 玩家数据维护服务
/// 负责定时更新玩家的游戏时长、昵称等信息
/// </summary>
public class PlayerMaintenanceService : BackgroundService
{
private readonly IServiceProvider _serviceProvider;
private readonly GameTimeQueryPlugin _gameTimePlugin;
/// <summary>
/// 每次批量更新的最大玩家数量
/// </summary>
private const int BATCH_SIZE = 150;
/// <summary>
/// 玩家游戏时长更新的冷却时间6小时
/// </summary>
private readonly TimeSpan UPDATE_COOLDOWN = TimeSpan.FromHours(6);
/// <summary>
/// 构造函数
/// </summary>
/// <param name="serviceProvider">服务提供者</param>
/// <param name="gameTimePlugin">游戏时长查询插件实例</param>
public PlayerMaintenanceService(IServiceProvider serviceProvider, GameTimeQueryPlugin gameTimePlugin)
{
_serviceProvider = serviceProvider;
_gameTimePlugin = gameTimePlugin;
}
/// <summary>
/// 服务执行主逻辑
/// </summary>
/// <param name="stoppingToken">取消令牌</param>
/// <returns>Task</returns>
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);
}
}
/// <summary>
/// 处理游戏时长更新逻辑
/// 优先级:
/// 1. 在线玩家优先检查无时长或时长为0的
/// 2. 近期加入服务器的玩家
/// 3. 玩家库中时长为0的玩家
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>Task</returns>
private async Task ProcessGameTimeUpdates(CancellationToken token)
{
var candidates = new List<string>();
var processedInThisRun = new HashSet<string>();
// 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<PlayerProfile>()
.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<PlayerProfile>()
.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}");
}
}
}
}
}
/// <summary>
/// 内存缓存用于跟踪尝试记录即使失败或返回0以强制执行6小时冷却时间
/// </summary>
private static Dictionary<string, DateTime> _recentAttempts = new Dictionary<string, DateTime>();
/// <summary>
/// 检查是否可以更新该玩家(基于冷却时间)
/// </summary>
/// <param name="steamId">玩家SteamID</param>
/// <returns>如果可以更新返回true否则返回false</returns>
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;
}
/// <summary>
/// 更新未知昵称
/// 遍历在线玩家如果数据库中该玩家昵称为Unknown则更新为当前在线昵称
/// </summary>
/// <returns>Task</returns>
private async Task UpdateUnknownNicknames()
{
try
{
// 获取在线玩家副本Program.RconCache.GetData()返回的是副本吗根据规则4我们需要注意
// 假设 GetData() 返回的是引用,这里我们应该注意。
// 查看 Program.RconCache 实现通常是返回一个新的对象或者我们需要手动处理。
// 此处我们直接使用 GetData().Players如果是 List<Player> 引用,遍历是安全的(只要不在遍历时修改集合)。
// 为了符合规则4 "数据使用的时候需要完全拷贝一份",我们创建一个新列表。
var rconData = Program.RconCache.GetData();
if (rconData?.Players == null) return;
var onlinePlayers = new List<RainOpsMini.Models.PlayerInfo>(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<PlayerProfile>()
.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}");
}
}
}
}