using Dpz.Core.EnumLibrary;

namespace Dpz.Core.Web.Controllers;

public class SteamController(ISteamGameService steamGameService) : Controller
{
    public async Task<IActionResult> Index(CancellationToken cancellationToken)
    {
        this.SetTitle("我的Steam游戏");
        var games = await steamGameService.GetGameSummariesAsync(cancellationToken);
        return View(games);
    }

    public async Task<IActionResult> Achievements(
        int? id = null,
        CancellationToken cancellationToken = default
    )
    {
        if (id == null)
        {
            return NotFound();
        }

        var model = await steamGameService.GetGameAsync(id.Value, cancellationToken);

        if (model == null)
        {
            return NotFound();
        }

        var pageMetaPage = new VmPageMetadata
        {
            Description =
                "游戏成就:" + string.Join(",", model.Achievements.Select(x => x.DisplayName)),
            Keywords = [$"{model.Name},{model.Name}游戏成就"],
            Relations = ["Steam", "Achievements", id.Value.ToString()],
        };
        ViewData["PageMetadata"] = pageMetaPage;
        this.SetTitle($"{model.Name} 成就");
        return View(model);
    }

    [HttpGet, CheckAuthorize(Permissions = Permissions.System)]
    public async Task<IActionResult> ClearCache(CancellationToken cancellationToken)
    {
        await steamGameService.ClearCacheAsync(cancellationToken);
        return RedirectToAction(nameof(Index));
    }
}
⚠⚠    以下内容为AI分析的结果,请根据实际情况进行判断。

文件定位

SteamController.cs 是 MVC 页面控制器(继承 Controller,返回 View),位于 src/Dpz.Core.Web/Controllers/ 目录下,属于"我的 Steam 游戏库"这一展示型站点功能。它与 WebApi 侧的 SteamController 同名但职责不同:WebApi 那个是 [ApiController]/ControllerBase,返回 JSON;本文件返回 Razor 视图。

依赖注入采用 C# 主构造函数写法:SteamController(ISteamGameService steamGameService),即只依赖一个查询服务。命名空间以外的 using 只有 Dpz.Core.EnumLibrary,其余符号(Controller、ISteamGameService、VmPageMetadata、SetTitle 等)来自 Web 项目的 Usings.cs 全局 using。


三个 Action 的职责

1. Index(游戏库列表)

this.SetTitle("我的Steam游戏");
var games = await steamGameService.GetGameSummariesAsync(cancellationToken);
return View(games);

2. Achievements(int? id)(单游戏成就详情)

处理流程:

  1. id == null → NotFound()。
  2. 调 steamGameService.GetGameAsync(id.Value, ...);返回 null → NotFound()(服务实现中找不到时会记 LogWarning)。
  3. 组装 VmPageMetadata(VmPageMetadata.cs)写入 ViewData["PageMetadata"]:
    • Description = "游戏成就:" + 所有成就 DisplayName 逗号拼接;
    • Keywords = ["{游戏名},{游戏名}游戏成就"];
    • Relations = ["Steam", "Achievements", id],即注释所说的 {controller, action, id...} 三元组(该类注释标明"仅用于兼容旧数据")。
  4. SetTitle($"{model.Name} 成就"),返回 View(model),模型为 SteamGameResponse,由 Views/Steam/Achievements.cshtml 渲染(区分已解锁/未解锁/隐藏成就)。

PageMetadata 并不是渲染时直接读取就完事:全局过滤器 GlobalFilter.cs 在 Action 执行后会调用 IPageMetadataService.ResolvePageMetadataAsync(route, explicitMetadata, ...),把 Action 里显式设置的 VmPageMetadata 与路由元数据、公共元数据合并后回写 ViewData["PageMetadata"],最终由 Views/Shared/_PageMetadataPartial.cshtml 输出 SEO 标签。同一个 GlobalFilter 还负责把 SteamController 的 ViewBag.Menu 设为 MenuItem.Steam(导航高亮),以及 X-PJAX 请求时把 ViewResult 转成 PartialViewResult 并把标题 Base64 写入响应头。

3. ClearCache(管理端缓存清理)

[HttpGet, CheckAuthorize(Permissions = Permissions.System)]
public async Task<IActionResult> ClearCache(CancellationToken cancellationToken)
{
    await steamGameService.ClearCacheAsync(cancellationToken);
    return RedirectToAction(nameof(Index));
}
  • CheckAuthorize(CheckAuthorizeAttribute.cs)是基于 IFilterFactory 的权限过滤器:它先检查 [AllowAnonymous] 跳过,再校验是否已登录、会话是否被撤销(IAccountSessionService)、用户身份、安全戳(IAccountService + IMediator 的 ValidateUserSecurityStampRequest)、账号是否启用、以及 Permissions 按位与是否包含所需位;失败时按是否期望 JSON 返回 JsonResult 或重定向到登录/Account.ReLogin,并把 VmUserInfo 塞进 ViewData["CurrentUser"]。这里要求 Permissions.System。
  • 真正"清缓存"的逻辑在服务实现里:ClearCacheAsync 本身只返回 Task.CompletedTask,靠方法上的 [InvalidateCache(Methods = [nameof(GetGamesAsync), nameof(GetGameSummariesAsync), nameof(GetGameAsync)])] 特性使这三个被 [Cache] 标记的查询失效,属于 AOP/特性驱动的缓存失效。
  • 执行完重定向回 Index,即用户看到刷新后的游戏库。

协作关系概览(含过滤器时序)

flowchart TD
    A[浏览器请求 /Steam/...] --> B[GlobalFilter.OnActionExecuting: 设置 SessionId / ViewBag.Menu = MenuItem.Steam]
    B --> C{目标 Action}
    C -->|Index| D[SetTitle + GetGameSummariesAsync]
    C -->|Achievements| E[CheckAuthorize? 无<br/>id 为空或模型为空 -> NotFound<br/>否则构造 VmPageMetadata + SetTitle]
    C -->|ClearCache| F[CheckAuthorize: 登录/会话/安全戳/启用/Permissions.System]
    F -->|不通过| G[重定向登录 / ReLogin / 403 / JsonResult]
    F -->|通过| H[steamGameService.ClearCacheAsync -> InvalidateCache 3 个查询]
    H --> I[RedirectToAction Index]
    D --> J[View: Views/Steam/Index.cshtml]
    E --> K[View: Views/Steam/Achievements.cshtml]
    J --> L[GlobalFilter.OnActionExecuted: ResolvePageMetadataAsync 合并 SEO 元数据; PJAX 时转 PartialViewResult]
    K --> L
    L --> M[_PageMetadataPartial 输出 meta 标签]

分层上:控制器只做参数校验 + 视图模型/元数据装配;数据查询(含 MongoDB 投影、Mapster 映射、缓存)在 SteamGameService.cs,接口契约在 ISteamGameService.cs;游戏库与成就的同步由注释所述的 Dpz.Core.Service.Mediator 的 Steam 模块(如 SyncSteamGamesHandler.cs)和 SteamActivator.cs 定时任务负责,本控制器不参与写入。


需要留意的点

  • Achievements 只判断了 model == null,没有判空 model.Achievements;视图里直接 Model.Achievements.Count。该字段是否在 SteamGameResponse 上默认初始化为空集合,我未在本次工具调用中读取该文件确认,因此不做断言。
  • ClearCache 使用 [HttpGet] 执行有副作用的操作(使缓存失效),实际属于应走 POST 的场景,且未加防伪令牌;这是设计上的注意点而非 bug。
  • id == null / model == null 都返回 NotFound(),而 GlobalFilter.cs 对 NotFoundResult 会提前 return,跳过 SEO 元数据解析——行为一致。
  • Achievements 中 Relations 与 Keywords 的写法(Relations 用于旧数据兼容)说明该页面的 SEO 归属既可能来自显式设置的 VmPageMetadata,也可能来自数据库中的 PageMetadata 记录,最终结果由 IPageMetadataService.ResolvePageMetadataAsync 合并决定。
评论加载中...