using Dpz.Core.Public.ViewModel.Request;
using Dpz.Core.Public.ViewModel.RequestEvent;
using Dpz.Core.Public.ViewModel.Response;
using Medallion.Threading;

namespace Dpz.Core.Service.RepositoryServiceImpl;

public class MumbleService(
    IRepository<Mumble> repository,
    IMapper mapper,
    IMediator mediator,
    IFusionCache fusionCache,
    IDistributedLockProvider distributedLockProvider
) : AbstractCacheService(fusionCache), IMumbleService
{
    protected override string CachePrefixKey => ServiceCacheKeyPrefixes.MumbleService;
    protected override TimeSpan CacheDefaultExpiration => TimeSpan.FromDays(3);

    private readonly IFusionCache _fusionCache = fusionCache;

    /// <summary>
    /// 点赞缓存标签前缀
    /// </summary>
    private string LikeTag => CachePrefixKey + ":Like";

    /// <summary>
    /// 生成点赞缓存键,形如 {Tag}:{Id}
    /// </summary>
    private string BuildLikeCacheKey(string mumbleId) => $"{LikeTag}:{mumbleId}";

    /// <summary>
    /// 生成点赞更新锁键,避免并发冲突
    /// </summary>
    private string BuildLikeLockKey(string mumbleId) => $"{LikeTag}:Lock:{mumbleId}";

    /// <summary>
    /// 评论数缓存标签前缀
    /// </summary>
    private string CommentTag => CachePrefixKey + ":Comment";

    /// <summary>
    /// 生成评论数缓存键,形如 {Tag}:{Id}
    /// </summary>
    private string BuildCommentCacheKey(string mumbleId) => $"{CommentTag}:{mumbleId}";

    private async Task ClearCacheAsync()
    {
        var tags = new[]
        {
            CachePrefixKey + nameof(GetPagesAsync),
            CachePrefixKey + nameof(GetRandomMumblesAsync),
            CachePrefixKey + nameof(FindAsync),
            LikeTag,
            CommentTag,
        };
        await _fusionCache.RemoveByTagAsync(tags);
    }

    public async Task<MumbleResponse> CreateAsync(CreateMumbleRequest request)
    {
        var entity = mapper.Map<Mumble>(request);
        await repository.InsertAsync(entity);
        await ClearCacheAsync();
        return mapper.Map<MumbleResponse>(entity);
    }

    public async Task<MumbleResponse?> EditContentAsync(EditMumbleRequest request)
    {
        if (request == null)
        {
            throw new ArgumentNullException(nameof(request));
        }

        if (request.Markdown == null)
        {
            throw new ArgumentNullException(nameof(request.Markdown));
        }

        var entity = await repository.TryGetAsync(request.Id);

        if (entity == null)
        {
            return null;
        }

        var editMarkdownRequest = new EditMarkdownRequest
        {
            Markdown = request.Markdown,
            OriginalMarkdown = entity.Markdown,
        };
        await mediator.Send(editMarkdownRequest);

        var update = Builders<Mumble>
            .Update.Set(x => x.Markdown, request.Markdown)
            .Set(x => x.LastUpdateTime, DateTime.Now);
        await repository.UpdateAsync(x => x.Id == entity.Id, update);
        await ClearCacheAsync();
        return mapper.Map<MumbleResponse>(await repository.FindAsync(entity.Id));
    }

    public async Task<IPagedList<MumbleResponse>> GetPagesAsync(
        int pageIndex,
        int pageSize,
        string? content = null,
        string? account = null
    )
    {
        var pagedList = await GetOrSetPagedListAsync(
            nameof(GetPagesAsync),
            async _ =>
            {
                var predicate = repository.SearchFor(x => true);
                if (!string.IsNullOrEmpty(content))
                {
                    predicate = predicate.Where(x => x.Markdown.Contains(content));
                }

                if (!string.IsNullOrEmpty(account))
                {
                    predicate = predicate.Where(x => x.Author.Id == account);
                }

                return await predicate
                    .OrderByDescending(x => x.CreateTime)
                    .ToPagedListAsync<Mumble, MumbleResponse>(pageIndex, pageSize);
            },
            new
            {
                content,
                account,
                pageIndex,
                pageSize,
            }
        );
        await ApplyLikeCountsAsync(pagedList);
        return pagedList;
    }

    public async Task<List<MumbleResponse>> GetRandomMumblesAsync(int count)
    {
        var cache = await GetOrSetCacheAsync<List<MumbleResponse>>(
            nameof(GetRandomMumblesAsync),
            async (_, cancellationToken) =>
            {
                var date = DateTime.Now;
                var data = await repository
                    .SearchFor(x =>
                        x.CreateTime.Month == date.Month
                        && x.CreateTime.Day == date.Day
                        && x.CreateTime.Year != date.Year
                    )
                    .Take(count)
                    .OrderByDescending(x => x.CreateTime)
                    .ToListAsync(cancellationToken);
                if (data.Count == 0)
                {
                    data = await repository
                        .SearchFor(x => true)
                        .Sample(count)
                        .ToListAsync(cancellationToken: cancellationToken);
                }
                return mapper.Map<List<MumbleResponse>>(data);
            },
            new { count },
            x => x.SetDuration(TimeSpan.FromHours(3))
        );
        await ApplyLikeCountsAsync(cache);
        return cache;
    }

    public async Task<MumbleResponse?> FindAsync(string id)
    {
        var cache = await GetOrSetCacheAsync<MumbleResponse?>(
            nameof(FindAsync),
            async (_, _) => mapper.Map<MumbleResponse?>(await repository.TryGetAsync(id)),
            new { id }
        );
        await ApplyLikeCountAsync(cache);
        return cache;
    }

    public async Task DeleteAsync(params string[] id)
    {
        var list = await repository.TryGetListAsync(id);
        var request = new DeleteMarkdownRequest
        {
            Markdown = list.Select(x => x.Markdown).ToList(),
        };
        await mediator.Send(request);
        await repository.TryDeleteAsync(id);
        await ClearCacheAsync();
    }

    public async Task<MumbleResponse?> LikeAsync(string id)
    {
        var entity = await repository.TryGetAsync(id);

        if (entity is null)
        {
            return null;
        }

        await using (await distributedLockProvider.AcquireLockAsync(BuildLikeLockKey(id)))
        {
            var current = entity.Like;
            var update = Builders<Mumble>.Update.Inc(x => x.Like, 1);
            await repository.UpdateAsync(x => x.Id == entity.Id, update);
            var next = checked(current + 1);
            entity.Like = next;
            await SetLikeCacheAsync(id, next);
            await UpdateCachedMumbleLikeAsync(id, next);
            return mapper.Map<MumbleResponse>(entity);
        }
    }

    /// <summary>
    /// 应用最新的点赞数、评论数到列表中,保证缓存命中也能看到最新值
    /// </summary>
    private async Task ApplyLikeCountsAsync(IEnumerable<MumbleResponse>? mumbles)
    {
        if (mumbles == null)
        {
            return;
        }

        foreach (var mumble in mumbles)
        {
            if (string.IsNullOrWhiteSpace(mumble.Id))
            {
                continue;
            }
            mumble.Like = await EnsureLikeCacheAsync(mumble.Id, mumble.Like);
            mumble.CommentCount = await EnsureCommentCacheAsync(mumble.Id, mumble.CommentCount);
        }
    }

    /// <summary>
    /// 应用最新的点赞数、评论数到单个对象
    /// </summary>
    private async Task ApplyLikeCountAsync(MumbleResponse? mumble)
    {
        if (mumble == null || string.IsNullOrWhiteSpace(mumble.Id))
        {
            return;
        }

        mumble.Like = await EnsureLikeCacheAsync(mumble.Id, mumble.Like);
        mumble.CommentCount = await EnsureCommentCacheAsync(mumble.Id, mumble.CommentCount);
    }

    /// <summary>
    /// 保证点赞缓存存在,若缺失则使用后备值初始化
    /// </summary>
    private async Task<int> EnsureLikeCacheAsync(string mumbleId, int fallbackValue)
    {
        var cache = await _fusionCache.TryGetAsync<int>(BuildLikeCacheKey(mumbleId));
        if (cache.HasValue)
        {
            return cache.Value;
        }

        await SetLikeCacheAsync(mumbleId, fallbackValue);
        return fallbackValue;
    }

    /// <summary>
    /// 设置点赞缓存,并带上点赞相关标签
    /// </summary>
    private ValueTask SetLikeCacheAsync(string mumbleId, int likeCount)
    {
        return _fusionCache.SetAsync(
            BuildLikeCacheKey(mumbleId),
            likeCount,
            options => options.SetDuration(CacheDefaultExpiration),
            new[] { CachePrefixKey, LikeTag }
        );
    }

    /// <summary>
    /// 保证评论缓存存在,若缺失则使用后备值初始化
    /// </summary>
    private async Task<int> EnsureCommentCacheAsync(string mumbleId, int fallbackValue)
    {
        var cache = await _fusionCache.TryGetAsync<int>(BuildCommentCacheKey(mumbleId));
        if (cache.HasValue)
        {
            return cache.Value;
        }

        await SetCommentCacheAsync(mumbleId, fallbackValue);
        return fallbackValue;
    }

    /// <summary>
    /// 设置评论缓存,并带上评论相关标签
    /// </summary>
    private ValueTask SetCommentCacheAsync(string mumbleId, int commentCount)
    {
        return _fusionCache.SetAsync(
            BuildCommentCacheKey(mumbleId),
            commentCount,
            options => options.SetDuration(CacheDefaultExpiration),
            [CachePrefixKey, CommentTag]
        );
    }

    /// <summary>
    /// 更新已缓存的碎碎念详情中的点赞值
    /// </summary>
    private async Task UpdateCachedMumbleLikeAsync(string mumbleId, int likeCount)
    {
        var cached = await TryGetFromCacheAsync<MumbleResponse?>(
            nameof(FindAsync),
            new { id = mumbleId }
        );
        if (cached is { HasValue: true, Value: not null })
        {
            cached.Value.Like = likeCount;
            await SetCacheAsync(nameof(FindAsync), cached.Value, new { id = mumbleId });
        }
    }
}
⚠⚠    以下内容为AI分析的结果,请根据实际情况进行判断。

MumbleService 代码解析

这个 MumbleService 类是一个处理"碎碎念"(Mumble)相关业务逻辑的服务类,主要功能包括创建、编辑、查询和删除碎碎念,以及点赞和评论计数管理等。

主要功能

  1. 基础CRUD操作:

    • CreateAsync: 创建新的碎碎念
    • EditContentAsync: 编辑碎碎念内容
    • FindAsync: 根据ID查找单个碎碎念
    • DeleteAsync: 删除碎碎念
  2. 查询功能:

    • GetPagesAsync: 分页获取碎碎念列表,支持按内容和作者过滤
    • GetRandomMumblesAsync: 获取随机碎碎念(优先获取历史同月同日的碎碎念)
  3. 互动功能:

    • LikeAsync: 点赞碎碎念,使用分布式锁防止并发问题
  4. 缓存管理:

    • 使用FusionCache进行多级缓存
    • 点赞数和评论数单独缓存
    • 缓存自动失效和更新机制

缓存设计

  1. 多级缓存:

    • 继承自AbstractCacheService,提供基础缓存功能
    • 默认缓存过期时间为3天(CacheDefaultExpiration)
  2. 缓存键设计:

    • 点赞缓存: {CachePrefixKey}:Like:{Id}
    • 评论数缓存: {CachePrefixKey}:Comment:{Id}
    • 分布式锁键: {CachePrefixKey}:Like:Lock:{Id}
  3. 缓存失效:

    • 任何修改操作后调用ClearCacheAsync清除相关缓存
    • 使用标签批量清除相关缓存

点赞系统实现

  1. 并发控制:

    • 使用IDistributedLockProvider确保点赞操作的原子性
    • 通过AcquireLockAsync获取分布式锁
  2. 缓存一致性:

    • 点赞后更新数据库和缓存
    • 通过UpdateCachedMumbleLikeAsync保持缓存数据最新
  3. 计数保证:

    • EnsureLikeCacheAsync确保缓存中存在点赞数
    • ApplyLikeCountsAsyncApplyLikeCountAsync确保返回的数据包含最新计数

其他特点

  1. 使用MediatR:

    • 通过IMediator发送EditMarkdownRequestDeleteMarkdownRequest等事件
  2. 自动映射:

    • 使用IMapper进行DTO和实体之间的转换
  3. 异常处理:

    • 对空参数进行校验并抛出ArgumentNullException

这个服务类设计合理,考虑了性能(缓存)、并发安全(分布式锁)和数据一致性,是一个典型的中大型应用中的服务层实现。

评论加载中...