using Dpz.Core.MessageQueue.Abstractions;
using Dpz.Core.Public.ViewModel.Messages;
using Dpz.Core.Public.ViewModel.Request;
using Dpz.Core.Public.ViewModel.Response;

namespace Dpz.Core.Service.RepositoryServiceImpl;

public class PictureRecordService(
    IRepository<PictureRecord> repository,
    IMapper mapper,
    IFusionCache fusionCache,
    IMessagePublisher<RemoveImagesMessage> removeImagesPublisher
) : IPictureRecordService
{
    private readonly IFusionCache _fusionCache = fusionCache;

    [Cache(ExpirationSeconds = 1 * ExpirationTime.Day)]
    public async Task<List<string>> GetTagsAsync(CancellationToken cancellationToken = default)
    {
        var tags = await repository
            .MongodbQueryable
            //.Select(x => x.Tags)
            .SelectMany(x => x.Tags)
            .GroupBy(x => x)
            .Select(x => x.Key)
            .ToListAsync(cancellationToken);
        return tags;
    }

    [Cache(ExpirationSeconds = 1 * ExpirationTime.Day)]
    public async Task<ICollection<PictureRecordResponse>> GetBannerAsync()
    {
        var pagedList = await repository
            .SearchFor(x => x.Tags.Any(y => y == "banner"))
            .OrderByDescending(x => x.UploadTime)
            .ToListAsync<PictureRecord, PictureRecordResponse>();
        pagedList.ForEach(x => x.AccessUrl += "!banner");
        return pagedList;
    }

    [Cache(ExpirationSeconds = 1 * ExpirationTime.Day)]
    public async Task<IPagedList<PictureRecordResponse>> GetPagesAsync(
        List<string>? tags,
        string? description,
        int pageIndex = 1,
        int pageSize = 20,
        string? account = null,
        CancellationToken cancellationToken = default
    )
    {
        var filterEmpty = Builders<PictureRecord>.Filter.Empty;
        var filters = new List<FilterDefinition<PictureRecord>>();

        var clearTags = tags?.Where(x => !string.IsNullOrEmpty(x)).Select(x => x).ToList() ?? [];
        if (clearTags.Count > 0)
        {
            var filter = Builders<PictureRecord>.Filter.AnyIn(x => x.Tags, clearTags);
            filters.Add(filter);
        }

        if (!string.IsNullOrEmpty(description))
        {
            var filter = Builders<PictureRecord>.Filter.Regex(
                x => x.Description,
                new BsonRegularExpression(description, "i")
            );
            filters.Add(filter);
        }

        if (!string.IsNullOrEmpty(account))
        {
            var filter = Builders<PictureRecord>.Filter.Eq(x => x.Creator.Id, account);
            filters.Add(filter);
        }

        var filterResult =
            filters.Count > 0 ? Builders<PictureRecord>.Filter.And(filters) : filterEmpty;

        return await repository
            .SearchFor(filterResult)
            .SortByDescending(x => x.Id)
            .ToPagedListAsync<PictureRecord, PictureRecordResponse>(
                pageIndex,
                pageSize,
                cancellationToken: cancellationToken
            );
    }

    [Cache(ExpirationSeconds = 1 * ExpirationTime.Day)]
    public async Task<PictureRecordResponse?> GetPictureRecordAsync(string id)
    {
        var entity = await repository.TryGetAsync(id);
        return entity == null ? null : mapper.Map<PictureRecordResponse>(entity);
    }

    [InvalidateCache(
        Methods = [
            nameof(GetTagsAsync),
            nameof(GetPagesAsync),
            nameof(GetBannerAsync),
            nameof(GetPictureRecordAsync),
        ]
    )]
    public async Task<PictureRecordResponse> CreateRecordAsync(
        CreatePictureRecordRequest request,
        CancellationToken cancellationToken = default
    )
    {
        var entity = mapper.Map<PictureRecord>(request);
        await repository.InsertAsync(entity, cancellationToken);
        await RemoveHomeContainerIfBannerAsync(entity.Tags, cancellationToken);
        return mapper.Map<PictureRecordResponse>(entity);
    }

    [InvalidateCache(
        Methods = [
            nameof(GetTagsAsync),
            nameof(GetPagesAsync),
            nameof(GetBannerAsync),
            nameof(GetPictureRecordAsync),
        ]
    )]
    public async Task<PictureRecordResponse?> ChangeInformationAsync(
        EditPictureRecordRequest request,
        CancellationToken cancellationToken = default
    )
    {
        if (!ObjectId.TryParse(request.Id, out var oid))
        {
            return null;
        }

        var update = Builders<PictureRecord>
            .Update.Set(x => x.Category, request.Category)
            .Set(x => x.Description, request.Description)
            .Set(x => x.Tags, request.Tags);

        await repository.UpdateAsync(x => x.Id == oid, update, cancellationToken);
        await RemoveHomeContainerIfBannerAsync(request.Tags, cancellationToken);

        var entity = await repository.FindAsync(oid, cancellationToken);
        return entity == null ? null : mapper.Map<PictureRecordResponse>(entity);
    }

    [InvalidateCache(
        Methods = [
            nameof(GetTagsAsync),
            nameof(GetPagesAsync),
            nameof(GetBannerAsync),
            nameof(GetPictureRecordAsync),
        ]
    )]
    public async Task DeleteAsync(string id)
    {
        var entity = await repository.TryGetAsync(id);
        if (entity == null)
        {
            return;
        }
        await repository.DeleteAsync(entity.Id);

        // 发布消息到队列,异步删除图片
        if (!string.IsNullOrEmpty(entity.AccessUrl))
        {
            await removeImagesPublisher.PublishAsync(
                new RemoveImagesMessage { ImageUrl = entity.AccessUrl }
            );
        }
    }

    [InvalidateCache(
        Methods = [
            nameof(GetTagsAsync),
            nameof(GetPagesAsync),
            nameof(GetBannerAsync),
            nameof(GetPictureRecordAsync),
        ]
    )]
    public Task ClearCacheAsync(CancellationToken cancellationToken = default)
    {
        return Task.CompletedTask;
    }

    [InvalidateCache(
        Methods = [
            nameof(GetTagsAsync),
            nameof(GetPagesAsync),
            nameof(GetBannerAsync),
            nameof(GetPictureRecordAsync),
        ]
    )]
    public async Task ClearPublishCacheAsync(
        IReadOnlyCollection<string> tags,
        CancellationToken cancellationToken = default
    )
    {
        await RemoveHomeContainerIfBannerAsync(tags, cancellationToken);
    }

    private async Task RemoveHomeContainerIfBannerAsync(
        IReadOnlyCollection<string> tags,
        CancellationToken cancellationToken = default
    )
    {
        if (tags.Any(x => string.Equals(x, "banner", StringComparison.OrdinalIgnoreCase)))
        {
            await _fusionCache.RemoveAsync(CacheKey.HomeContainerKey, token: cancellationToken);
        }
    }
}
⚠⚠    以下内容为AI分析的结果,请根据实际情况进行判断。

代码解释

这是一个图片记录服务类(PictureRecordService),实现了 IPictureRecordService 接口,用于管理图片记录的 CRUD 操作和缓存管理。

核心功能模块

1. 依赖注入(构造函数)

public PictureRecordService(
    IRepository<PictureRecord> repository,        // 数据仓储
    IMapper mapper,                               // 对象映射器
    IFusionCache fusionCache,                     // 缓存管理
    IMessagePublisher<RemoveImagesMessage> removeImagesPublisher  // 消息发布器
)

2. 主要方法说明

GetTagsAsync - 获取所有标签

  • 使用 MongoDB 查询所有图片的标签
  • 通过 SelectMany 展平标签数组,GroupBy 去重
  • 缓存1天

GetBannerAsync - 获取轮播图

  • 筛选包含 "banner" 标签的图片
  • 按上传时间倒序排列
  • 为访问 URL 添加 !banner 后缀(可能用于图片裁剪/样式)
  • 缓存1天

GetPagesAsync - 分页查询图片

参数:
- tags: 标签过滤(支持多个)
- description: 描述模糊搜索(正则表达式,忽略大小写)
- account: 创建者筛选
- pageIndex/pageSize: 分页参数

使用 MongoDB FilterDefinition 构建动态查询条件。

GetPictureRecordAsync - 获取单个图片记录

  • 根据 ID 查询
  • 缓存1天

CreateRecordAsync - 创建图片记录

  • 映射请求对象到实体
  • 插入数据库
  • 如果包含 "banner" 标签,清除首页容器缓存
  • 清除相关缓存(通过 InvalidateCache 特性)

ChangeInformationAsync - 修改图片信息

  • 验证 ObjectId 格式
  • 使用 MongoDB Update Builder 更新字段:
    • Category(分类)
    • Description(描述)
    • Tags(标签)
  • 清除相关缓存

DeleteAsync - 删除图片记录

  • 删除数据库记录
  • 异步发布消息到队列,由后台服务删除实际图片文件
  • 清除相关缓存

ClearCacheAsync - 手动清除缓存

  • 触发缓存失效(通过 InvalidateCache 特性实现)

ClearPublishCacheAsync - 清除发布相关缓存

  • 根据标签判断是否需要清除首页容器缓存

3. 关键设计模式

缓存策略

  • 使用 [Cache] 特性标记需要缓存的方法
  • 使用 [InvalidateCache] 特性自动清除相关缓存
  • 写操作(Create/Update/Delete)会使所有读操作的缓存失效

消息队列

await removeImagesPublisher.PublishAsync(
    new RemoveImagesMessage { ImageUrl = entity.AccessUrl }
);

采用异步消息机制删除图片文件,避免阻塞主流程。

动态查询构建

使用 MongoDB Builders 模式构建灵活的查询条件:

var filters = new List<FilterDefinition<PictureRecord>>();
// 根据条件动态添加过滤器
var filterResult = filters.Count > 0 
    ? Builders<PictureRecord>.Filter.And(filters) 
    : filterEmpty;

4. 特殊逻辑

Banner 标签特殊处理

  • 任何涉及 "banner" 标签的操作都会清除首页容器缓存(CacheKey.HomeContainerKey
  • 确保首页轮播图实时更新

技术栈

  • MongoDB:数据存储
  • FusionCache:多级缓存
  • AutoMapper:对象映射
  • 消息队列:异步图片删除
  • AOP:缓存切面(通过特性实现)
评论加载中...