namespace Dpz.Core.Service.RepositoryServiceImpl;

public class VideoService(IRepository<Video> repository, IMapper mapper, IFusionCache fusionCache)
    : IVideoService
{
    public async Task<List<VmVideo>> GetVideosAsync()
    {
        var list = await repository.SearchFor(x => true).ToListAsync();
        return list == null || list.Count == 0 ? [] : mapper.Map<List<VmVideo>>(list);
    }

    public async Task SaveInformationAsync(VmVideo video)
    {
        if (video == null || string.IsNullOrEmpty(video.Id))
        {
            throw new ArgumentNullException(
                nameof(video),
                "argument video or video.id is null or empty"
            );
        }

        var inVideo = mapper.Map<Video>(video);

        var filter = Builders<Video>.Filter.Eq(x => x.Id, inVideo.Id);
        var find = await repository.Collection.FindAsync(filter);
        var entity = await find.FirstOrDefaultAsync();
        if (entity == null)
        {
            inVideo.CommentCount = 0;
            inVideo.DanmakuCount = 0;
            inVideo.PlayCount = 0;
            await repository.InsertAsync(inVideo);
            return;
        }

        var update = Builders<Video>
            .Update.Set(x => x.Name, inVideo.Name)
            .Set(x => x.Description, inVideo.Description)
            .Set(x => x.Tags, inVideo.Tags)
            .Set(x => x.Title, inVideo.Title)
            .Set(x => x.SubTitle, inVideo.SubTitle)
            .Set(x => x.VideoTitle, inVideo.VideoTitle);

        await repository.UpdateAsync(x => x.Id == inVideo.Id, update);
    }

    public async Task<VmVideo?> GetVideoAsync(string id)
    {
        var entity = await repository.SearchFor(x => x.Id == id).FirstOrDefaultAsync();
        if (entity == null)
        {
            return null;
        }

        return mapper.Map<VmVideo>(entity);
    }

    public async Task AddPlayAsync(string id)
    {
        var update = Builders<Video>.Update.Inc(x => x.PlayCount, 1);
        await repository.UpdateAsync(x => x.Id == id, update);
    }

    /// <summary>
    /// 更新视频评论数(仅由消息队列使用)
    /// </summary>
    public async Task UpdateCommentCountAsync(
        string videoId,
        int count,
        CancellationToken cancellationToken = default
    )
    {
        // 视频使用字符串ID,直接更新
        var update = Builders<Video>.Update.Set(x => x.CommentCount, count);
        await repository.UpdateAsync(x => x.Id == videoId, update, cancellationToken);
    }

    public async Task UpdateDanmakuCountAsync(
        string videoId,
        int count,
        CancellationToken cancellationToken = default
    )
    {
        // 更新数据库
        var update = Builders<Video>.Update.Set(x => x.DanmakuCount, count);
        await repository.UpdateAsync(x => x.Id == videoId, update, cancellationToken);

        // 更新单个视频缓存
        var singleVideoKey = $"{CacheKey.VideoListKey}:{videoId}";
        var cachedVideo = await fusionCache.TryGetAsync<VmVideo>(
            singleVideoKey,
            token: cancellationToken
        );
        if (cachedVideo.HasValue)
        {
            cachedVideo.Value.DanmakuCount = count;
            await fusionCache.SetAsync(
                singleVideoKey,
                cachedVideo.Value,
                TimeSpan.FromDays(1),
                [CacheKey.VideoListKey],
                token: cancellationToken
            );
        }

        // 更新视频列表缓存
        var listCache = await fusionCache.TryGetAsync<List<VmVideo>>(
            CacheKey.VideoListKey,
            token: cancellationToken
        );
        if (listCache.HasValue)
        {
            var video = listCache.Value.FirstOrDefault(x => x.Id == videoId);
            if (video != null)
            {
                video.DanmakuCount = count;
                await fusionCache.SetAsync(
                    CacheKey.VideoListKey,
                    listCache.Value,
                    TimeSpan.FromDays(1),
                    [CacheKey.VideoListKey],
                    token: cancellationToken
                );
            }
        }
    }
}
⚠⚠    以下内容为AI分析的结果,请根据实际情况进行判断。

这是一个基于 .NET 的视频服务实现类,使用了依赖注入、MongoDB 和缓存技术。让我详细解释一下这个代码:

类结构和依赖

public class VideoService(IRepository<Video> repository, IMapper mapper, IFusionCache fusionCache)
    : IVideoService

这个类使用了 C# 12 的主构造函数语法,注入了三个依赖:

  • IRepository<Video>: MongoDB 仓储模式,用于数据库操作
  • IMapper: AutoMapper,用于对象映射(Video ↔ VmVideo)
  • IFusionCache: 缓存组件,用于提高性能

主要功能方法

1. 获取所有视频

public async Task<List<VmVideo>> GetVideosAsync()
  • 从数据库获取所有视频记录
  • 使用 AutoMapper 将实体对象转换为视图模型
  • 返回空列表而不是 null(防御性编程)

2. 保存视频信息

public async Task SaveInformationAsync(VmVideo video)
  • 实现了 Upsert 操作(插入或更新)
  • 参数验证:检查视频对象和ID是否为空
  • 如果视频不存在,创建新记录并初始化计数器
  • 如果存在,只更新基本信息字段(名称、描述、标签等)

3. 获取单个视频

public async Task<VmVideo?> GetVideoAsync(string id)
  • 根据ID查询单个视频
  • 返回可空类型,遵循现代 C# 最佳实践

4. 增加播放次数

public async Task AddPlayAsync(string id)
  • 使用 MongoDB 的原子操作 $inc 增加播放计数
  • 避免了读取-修改-写入的竞态条件

5. 更新评论数

public async Task UpdateCommentCountAsync(...)
  • 专门用于消息队列调用
  • 直接设置评论数量,而不是增量操作

6. 更新弹幕数(最复杂的方法)

public async Task UpdateDanmakuCountAsync(...)

这个方法展示了多层缓存策略:

  1. 更新数据库:直接设置弹幕数量
  2. 更新单个视频缓存:
    • 尝试获取单个视频的缓存
    • 如果存在,更新弹幕数并重新缓存
  3. 更新视频列表缓存:
    • 获取整个视频列表缓存
    • 找到对应视频并更新弹幕数
    • 重新缓存整个列表

技术特点

1. 异步编程

  • 所有方法都是异步的,提高并发性能
  • 支持 CancellationToken 用于取消操作

2. MongoDB 特性

  • 使用 Builders<Video> 构建查询和更新操作
  • 利用 MongoDB 的原子操作($inc, $set)

3. 缓存策略

  • 使用 FusionCache 进行多级缓存
  • 缓存过期时间设置为1天
  • 使用缓存标签进行批量失效

4. 错误处理

  • 参数验证和异常抛出
  • 空值检查和防御性编程

设计模式

  1. 仓储模式: 通过 IRepository<Video> 抽象数据访问
  2. 依赖注入: 构造函数注入所有依赖
  3. 映射模式: 使用 AutoMapper 分离领域模型和视图模型
  4. 缓存模式: 多层缓存提高性能

这个服务类展现了现代 .NET 应用的典型架构模式,具有良好的可测试性、可维护性和性能优化。

评论加载中...