网站首页 网站源码
using System.Net.Http;
using System.Text.Json.Nodes;
using Dpz.Core.Public.Entity.Steam;
using Dpz.Core.Public.ViewModel.Steam;
using Microsoft.AspNetCore.WebUtilities;
namespace Dpz.Core.Service.RepositoryServiceImpl;
public class SteamGameService(
IRepository<SteamGame> repository,
IMapper mapper,
HttpClient httpClient,
IConfiguration configuration,
ILogger<SteamGameService> logger,
IFusionCache fusionCache
) : AbstractCacheService(fusionCache), ISteamGameService
{
protected override string CachePrefixKey =>
"Dpz.Core.Service.RepositoryServiceImpl.SteamGameService";
protected override TimeSpan CacheDefaultExpiration => TimeSpan.FromDays(1);
private readonly IFusionCache _fusionCache = fusionCache;
private record SteamConfig(string AppKey = "", string SteamId = "");
private record CdnConfig
{
public string BaseUrl { get; init; } = "https://cdn.dpangzi.com";
public string LogoUrlTemplate { get; init; } =
"https://media.st.dl.eccdnx.com/steam/apps/{0}/capsule_231x87.jpg";
}
private readonly SteamConfig _steamConfig =
configuration.GetSection("Steam").Get<SteamConfig>()
?? throw new InvalidConfigurationException(
"Steam 配置缺失:请检查 AppKey 和 SteamId 是否配置。"
);
private readonly CdnConfig _cdnConfig =
configuration.GetSection("Cdn").Get<CdnConfig>() ?? new CdnConfig();
// API请求延迟时间,防止请求过于频繁
private const int ApiDelayMs = 500;
public event LogoDownload? OnLogoDownloadComplete;
public event AchievementIconGrayDownload? OnAchievementIconGrayDownloadComplete;
public event AchievementIconDownload? OnAchievementIconDownloadComplete;
public async Task UpdateGamesAsync()
{
logger.LogInformation("开始更新Steam游戏数据");
var dbGames = await repository.SearchFor(x => true).ToListAsync();
var games = await FetchGamesAsync(dbGames);
// 新增游戏
var newGames = games.ExceptBy(dbGames.Select(x => x.Id), x => x.Id).ToList();
if (newGames.Count > 0)
{
logger.LogInformation("发现{Count}个新游戏,准备添加", newGames.Count);
await repository.InsertAsync(newGames);
}
// DB已有游戏,待更新
var updateGames = games.IntersectBy(dbGames.Select(x => x.Id), x => x.Id).ToList();
logger.LogInformation("开始更新{Count}个已有游戏", updateGames.Count);
await Parallel.ForEachAsync(
updateGames,
new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount * 2 },
async (game, _) => await UpdateGameAsync(game.Id, game.PlayTime, game.Achievements)
);
await _fusionCache.RemoveByTagAsync(
[CachePrefixKey + nameof(GetGamesAsync), CachePrefixKey + nameof(GetGameAsync)]
);
logger.LogInformation("Steam游戏数据更新完成");
}
private async Task UpdateGameAsync(
int id,
uint playTime,
IReadOnlyCollection<AchievementDetail> achievements
)
{
logger.LogInformation("更新游戏 {GameId} 的数据", id);
var update = Builders<SteamGame>
.Update.Set(x => x.PlayTime, playTime)
.Set(x => x.LastUpdateTime, DateTime.Now);
if (achievements.Count > 0)
{
update = update.Set(x => x.Achievements, achievements.ToList());
}
await repository.UpdateAsync(x => x.Id == id, update);
}
private async Task<List<AchievementDetail>> FetchAchievementsAsync(
int id,
List<SteamGame> dbGames
)
{
logger.LogInformation("获取游戏 {GameId} 的成就数据", id);
var queryString = new Dictionary<string, string?>
{
{ "steamid", _steamConfig.SteamId },
{ "key", _steamConfig.AppKey },
{ "format", "json" },
{ "appid", id.ToString() },
{ "l", "schinese" },
};
// 添加API请求延迟
await Task.Delay(ApiDelayMs);
var achievements =
await ExecuteHttpRequestAsync<List<AchievementDetail>>(
"/ISteamUserStats/GetSchemaForGame/v2/",
queryString,
root => root?["game"]?["availableGameStats"]?["achievements"]
) ?? [];
var unlockAchievements = await FetchUnlockAchievementAsync(queryString);
await Parallel.ForEachAsync(
achievements,
async (achievement, _) =>
{
if (
unlockAchievements.TryGetValue(achievement.Name, out var unlockTime)
&& unlockTime > 0
)
{
achievement.UnlockTime = unlockTime.ToDateTime();
}
await DownloadAchievementAsync(dbGames, id, achievement);
await DownloadAchievementGrayAsync(dbGames, id, achievement);
}
);
logger.LogInformation("游戏 {GameId} 共获取到 {Count} 个成就", id, achievements.Count);
return achievements;
}
private async Task<Dictionary<string, long>> FetchUnlockAchievementAsync(
Dictionary<string, string?> queryString
)
{
logger.LogInformation("获取已解锁的成就数据");
// 添加API请求延迟
await Task.Delay(ApiDelayMs);
return await ExecuteHttpRequestAsync<Dictionary<string, long>>(
"/ISteamUserStats/GetPlayerAchievements/v0001/",
queryString,
root =>
{
var achievements = root
?["playerstats"]?["achievements"]?.AsArray()
.Select(x =>
{
var name = x?["apiname"]?.GetValue<string>();
if (name == null)
{
logger.LogError(
"Achievement name is null for {SteamId}",
_steamConfig.SteamId
);
return (KeyValuePair<string, long>?)null;
}
var unlockTime = x?["unlocktime"]?.GetValue<long>() ?? 0;
return x?["achieved"]?.GetValue<int>() == 1
? KeyValuePair.Create(name, unlockTime)
: KeyValuePair.Create(name, 0L);
})
.Where(x => x != null)
.Select(x => x!.Value)
.ToDictionary(x => x.Key, x => x.Value);
return JsonValue.Create(achievements);
}
) ?? new Dictionary<string, long>();
}
private async Task<List<SteamGame>> FetchGamesAsync(List<SteamGame> dbGames)
{
logger.LogInformation("开始获取Steam游戏列表");
var queryString = new Dictionary<string, string?>
{
{ "steamid", _steamConfig.SteamId },
{ "key", _steamConfig.AppKey },
{ "include_appinfo", "1" },
{ "format", "json" },
};
// 添加API请求延迟
await Task.Delay(ApiDelayMs);
var games =
await ExecuteHttpRequestAsync<List<SteamGame>>(
"/IPlayerService/GetOwnedGames/v0001/",
queryString,
root => root?["response"]?["games"]?.AsArray()) ?? [];
logger.LogInformation("获取到 {Count} 个Steam游戏", games.Count);
await Parallel.ForEachAsync(games, async (game, _) => await UpdateGame(dbGames, game));
return games;
}
private async Task UpdateGame(List<SteamGame> dbGames, SteamGame game)
{
logger.LogInformation("更新游戏 {GameId} 的基本信息", game.Id);
game.LastUpdateTime = DateTime.Now;
game.Achievements = await FetchAchievementsAsync(game.Id, dbGames);
if (!IsLogoDownload(dbGames, game.Id))
{
logger.LogInformation("下载游戏 {GameId} 的Logo", game.Id);
// 添加API请求延迟
await Task.Delay(ApiDelayMs);
await using var stream = await HttpDownloadAsync(
string.Format(_cdnConfig.LogoUrlTemplate, game.Id)
);
if (stream != null && OnLogoDownloadComplete != null)
{
var url = await OnLogoDownloadComplete(stream, game.Id);
if (!string.IsNullOrWhiteSpace(url))
{
game.ImageLogo = url;
game.ImageIcon = url;
}
}
}
}
private async Task<Stream?> HttpDownloadAsync(string uri)
{
try
{
var response = await httpClient.GetAsync(uri);
response.EnsureSuccessStatusCode();
var memoryStream = new MemoryStream();
await response.Content.CopyToAsync(memoryStream);
memoryStream.Position = 0;
return memoryStream;
}
catch (Exception e)
{
logger.LogError(e, "Download failed for {Uri}", uri);
return null;
}
}
private bool IsLogoDownload(IEnumerable<SteamGame> games, int id)
{
var game = games.FirstOrDefault(x => x.Id == id);
return game != null
&& game.ImageIcon.StartsWith(_cdnConfig.BaseUrl, StringComparison.OrdinalIgnoreCase)
&& game.ImageLogo.StartsWith(_cdnConfig.BaseUrl, StringComparison.OrdinalIgnoreCase);
}
private async Task DownloadAchievementAsync(
List<SteamGame> dbGames,
int id,
AchievementDetail achievement
)
{
var dbAchievement = dbGames
.FirstOrDefault(x => x.Id == id)
?.Achievements.FirstOrDefault(x => x.Name == achievement.Name);
if (
dbAchievement?.Icon.StartsWith(_cdnConfig.BaseUrl, StringComparison.OrdinalIgnoreCase)
?? false
)
{
achievement.Icon = dbAchievement.Icon;
return;
}
logger.LogInformation(
"下载游戏 {GameId} 成就 {AchievementName} 的图标",
id,
achievement.Name
);
// 添加API请求延迟
await Task.Delay(ApiDelayMs);
await using var icon = await HttpDownloadAsync(achievement.Icon);
var handler = OnAchievementIconDownloadComplete;
if (icon != null && handler != null)
{
var url = await handler.Invoke(icon, id, achievement.Name);
if (!string.IsNullOrWhiteSpace(url))
{
achievement.Icon = url;
}
}
}
private async Task DownloadAchievementGrayAsync(
IEnumerable<SteamGame> dbGames,
int id,
AchievementDetail achievement
)
{
var dbAchievement = dbGames
.FirstOrDefault(x => x.Id == id)
?.Achievements.FirstOrDefault(x => x.Name == achievement.Name);
if (
dbAchievement?.IconGray.StartsWith(
_cdnConfig.BaseUrl,
StringComparison.OrdinalIgnoreCase
) ?? false
)
{
achievement.IconGray = dbAchievement.IconGray;
return;
}
logger.LogInformation(
"下载游戏 {GameId} 成就 {AchievementName} 的灰色图标",
id,
achievement.Name
);
// 添加API请求延迟
await Task.Delay(ApiDelayMs);
await using var icon = await HttpDownloadAsync(achievement.IconGray);
var handler = OnAchievementIconGrayDownloadComplete;
if (icon != null && handler != null)
{
var url = await handler.Invoke(icon, id, achievement.Name);
if (!string.IsNullOrWhiteSpace(url))
{
achievement.IconGray = url;
}
}
}
public async Task<List<VmSteamGame>> GetGamesAsync()
{
return await GetOrSetCacheAsync<List<VmSteamGame>>(
nameof(GetGamesAsync),
async (_, cancellationToken) =>
{
var list = await repository.SearchFor(x => true).ToListAsync(cancellationToken);
var result = mapper.Map<List<VmSteamGame>>(list);
return result;
}
);
}
public async Task<VmSteamGame?> GetGameAsync(int id)
{
return await GetOrSetCacheAsync<VmSteamGame?>(
nameof(GetGameAsync),
async (_, cancellationToken) =>
{
var game = await repository
.SearchFor(x => x.Id == id)
.FirstOrDefaultAsync(cancellationToken);
if (game == null)
{
logger.LogWarning("未找到游戏 {GameId} 的信息", id);
return null;
}
var result = mapper.Map<VmSteamGame>(game);
return result;
},
new { id }
);
}
private async Task<T?> ExecuteHttpRequestAsync<T>(
string endpoint,
Dictionary<string, string?> queryParams,
Func<JsonNode?, JsonNode?>? dataSelector = null
)
{
var requestUri = QueryHelpers.AddQueryString(endpoint, queryParams);
try
{
var response = await httpClient.GetAsync(requestUri);
response.EnsureSuccessStatusCode();
var jsonContent = await response.Content.ReadAsStringAsync();
var jsonNode = JsonNode.Parse(jsonContent);
if (jsonNode == null)
{
return default;
}
var data = dataSelector?.Invoke(jsonNode) ?? jsonNode;
return data.Deserialize<T>();
}
catch (Exception e)
{
logger.LogError(e, "HTTP request failed for {Endpoint}", endpoint);
return default;
}
}
}
上述代码是一个 C# 类 SteamGameService
的实现,主要用于与 Steam API 交互,管理和更新 Steam 游戏的信息。以下是该类的主要功能和结构的详细解释:
游戏信息更新:
UpdateGamesAsync
方法用于更新数据库中的游戏信息。它首先从数据库中获取所有游戏,然后通过 Steam API 获取最新的游戏数据。游戏成就获取:
FetchAchievementsAsync
方法用于获取特定游戏的成就信息。它会调用 Steam API 获取成就数据,并下载成就图标。FetchUnlockAchievementAsync
方法用于获取玩家已解锁的成就。游戏库获取:
FetchGamesAsync
方法从 Steam API 获取用户的游戏库列表,并为每个游戏获取成就信息和下载游戏的图标。图像下载:
HttpDownloadAsync
方法用于下载游戏或成就的图像。DownloadAchievementAsync
和 DownloadAchievementGrayAsync
方法用于下载成就的图标和灰色图标。缓存管理:
IHybridCachingProvider
进行缓存管理,以提高性能,避免频繁访问数据库和 Steam API。GetGamesAsync
和 GetGameAsync
方法从缓存中获取游戏信息,如果缓存不存在,则从数据库中获取并更新缓存。事件处理:
OnLogoDownloadComplete
和 OnAchievementIconDownloadComplete
),用于在下载完成后执行特定操作。测试方法:
Test
方法用于测试 MongoDB 更新操作的序列化,输出更新操作的渲染结果。ActivatorUtilitiesConstructor
特性,允许依赖注入。_prefixKey
用于缓存键的前缀。async
和 await
关键字,以支持非阻塞的 I/O 操作。ExceptBy
和 IntersectBy
,以便于处理游戏的新增和更新。SteamGameService
类是一个功能丰富的服务类,主要用于管理 Steam 游戏的信息,包括获取、更新和缓存游戏及其成就数据。它通过与 Steam API 的交互,提供了一个完整的游戏信息管理解决方案,并通过事件和缓存机制提高了性能和可扩展性。