using System.Collections.Specialized;
using System.Net.Http;
using System.Net.Http.Json;
using Dpz.Core.Public.ViewModel.Response;
using Dpz.Core.Shard.Service;

namespace Dpz.Core.Web.Library.Api.Service;

public class HomeCacheService(
    IArticleService articleService,
    IPictureRecordService pictureRecordService,
    IHttpContextAccessor httpContextAccessor,
    IHttpClientFactory httpClientFactory,
    IFusionCache fusionCache,
    IVideoShardService videoShardService,
    ILogger<HomeCacheService> logger
) : IHomeCacheService
{
    /// <summary>
    /// 过期时间
    /// </summary>
    private readonly TimeSpan _expired = TimeSpan.FromDays(1);

    private async Task<IList<PictureRecordResponse>> GetBannersAsync()
    {
        var banner = await pictureRecordService.GetBannerAsync();
        return banner.ToList();
    }

    public async Task<HomeContainerModel> GetCacheDataAsync()
    {
        var model = await fusionCache.GetOrSetAsync<HomeContainerModel>(
            CacheKey.HomeContainerKey,
            async (_, cancellationToken) =>
            {
                var m = new HomeContainerModel
                {
                    Blogs = await GetTopArticlesAsync(),
                    LikeArticle = await GetRandomArticlesAsync(cancellationToken),
                    News = await GetLatestArticlesAsync(cancellationToken),
                    Pictures = await GetBannersAsync(),
                    Tags = await GetArticleTagsAsync(cancellationToken),
                };
                return m;
            },
            options => options.SetDuration(_expired)
        );

        var video = await GetRandomVideoAsync();
        model.Video = video;
        return model;
    }

    private async Task<VmVideo?> GetRandomVideoAsync()
    {
        var videos = await videoShardService.GetVideosAsync();
        if (videos.Count == 0)
        {
            return null;
        }

        return videos.OrderBy(_ => Guid.NewGuid()).First();
    }

    public async Task<List<string>> GetArticleTagsAsync(
        CancellationToken cancellationToken = default
    )
    {
        return await articleService.GetAllTagsAsync(cancellationToken);
    }

    public async Task<List<ArticleMiniResponse>> GetLatestArticlesAsync(
        CancellationToken cancellationToken = default
    )
    {
        return await articleService.GetLatestAsync(cancellationToken: cancellationToken);
    }

    public async Task<List<ArticleMiniResponse>> GetRandomArticlesAsync(
        CancellationToken cancellationToken = default
    )
    {
        return await articleService.GetRandomArticlesAsync(cancellationToken: cancellationToken);
    }

    public async Task<TodayNewsModel?> GetTodayNewsAsync()
    {
        var ipAddress = httpContextAccessor.HttpContext?.Request.GetIpAddress() ?? "";
        var todayNewsKey = $"home:news:{ipAddress.Replace(":", "-")}";
        return await fusionCache.GetOrSetAsync<TodayNewsModel?>(
            todayNewsKey,
            async (_, cancellationToke) =>
            {
                var httpClient = httpClientFactory.CreateClient("edge");

                try
                {
                    var parameters = new NameValueCollection { { "ip", ipAddress } };
                    var request = new HttpRequestMessage(
                        HttpMethod.Get,
                        "https://news.topurl.cn/api?" + parameters.ToQueryString()
                    );
                    var response = await httpClient.SendAsync(request, cancellationToke);
                    if (!response.IsSuccessStatusCode)
                    {
                        logger.LogWarning(
                            "get today news fail,status code:{Code}",
                            response.StatusCode
                        );
                        return null;
                    }
                    var data = await response.Content.ReadFromJsonAsync<NewsResponse>(
                        cancellationToken: cancellationToke
                    );
                    return data?.Data;
                }
                catch (Exception ex)
                {
                    logger.LogError(ex, "get today news fail");
                    return null;
                }
            },
            options => options.SetDuration(TimeSpan.FromHours(5))
        );
    }

    private async Task<List<ArticleMiniResponse>> GetTopArticlesAsync()
    {
        var articleMinis = await articleService.GetTopArticlesAsync(-3, 10);
        return articleMinis;
    }
}
⚠⚠    以下内容为AI分析的结果,请根据实际情况进行判断。

代码解释

这是一个 ASP.NET Core 应用程序中的首页缓存服务类,主要功能是提供首页所需的各种数据,并通过缓存机制优化性能。

核心功能概览

1. 类定义与依赖注入

public class HomeCacheService(...) : IHomeCacheService
  • 使用了 C# 12 主构造函数语法,直接在类声明中注入依赖
  • 注入的服务包括:
    • IArticleService: 文章服务
    • IPictureRecordService: 图片记录服务
    • IHttpContextAccessor: HTTP 上下文访问器
    • IHttpClientFactory: HTTP 客户端工厂
    • IFusionCache: 缓存服务(FusionCache 库)
    • IVideoShardService: 视频分片服务
    • ILogger: 日志记录器

2. 主要方法详解

GetCacheDataAsync() - 获取首页容器数据

public async Task<HomeContainerModel> GetCacheDataAsync()
  • 核心方法,返回首页所有数据的容器模型
  • 使用 fusionCache.GetOrSetAsync 实现缓存穿透保护
    • 缓存键:CacheKey.HomeContainerKey
    • 缓存时长:1天(_expired
    • 缓存内容包括:
      • Blogs: 热门文章
      • LikeArticle: 随机推荐文章
      • News: 最新文章
      • Pictures: 轮播图
      • Tags: 文章标签
  • 视频数据特殊处理:每次都重新获取随机视频(不使用缓存)

GetRandomVideoAsync() - 获取随机视频

private async Task<VmVideo?> GetRandomVideoAsync()
  • 获取所有视频后,使用 OrderBy(_ => Guid.NewGuid()) 随机排序
  • 返回第一个视频或 null(无视频时)

GetTodayNewsAsync() - 获取今日新闻

public async Task<TodayNewsModel?> GetTodayNewsAsync()
  • 重要特性
    • 根据用户 IP 地址 构建缓存键(避免不同 IP 用户共享新闻缓存)
    • 调用外部 API:https://news.topurl.cn/api
    • 缓存时长:5 小时
    • 异常处理:捕获并记录所有异常,返回 null

辅助方法

  • GetBannersAsync(): 获取轮播图
  • GetTopArticlesAsync(): 获取置顶文章(前3天内,最多10条)
  • GetLatestArticlesAsync(): 获取最新文章
  • GetRandomArticlesAsync(): 获取随机推荐文章
  • GetArticleTagsAsync(): 获取所有文章标签

设计亮点

  1. 分层缓存策略

    • 首页容器数据缓存 1 天
    • 今日新闻缓存 5 小时
    • 视频数据不缓存(实时随机)
  2. IP 级别缓存隔离:新闻数据按 IP 缓存,避免不同地区用户看到相同内容

  3. 容错处理:外部 API 调用失败时返回 null,不影响其他功能

  4. 异步编程:全面使用 async/await,支持取消令牌(CancellationToken

使用场景

这个服务典型应用于博客或新闻网站的首页,通过缓存减少数据库查询和外部 API 调用,提升响应速度。

评论加载中...