using System.Text.RegularExpressions;

namespace Dpz.Core.Service.RepositoryServiceImpl;

public class BookmarkService(IRepository<Bookmark> repository, IMapper mapper) : IBookmarkService
{
    [Cache(ExpirationSeconds = 30 * ExpirationTime.Day)]
    public async Task<List<VmBookmark>> GetBookmarksAsync(string? title, string[]? categories)
    {
        var empty = Builders<Bookmark>.Filter.Empty;
        var titleFilter = string.IsNullOrEmpty(title)
            ? empty
            : Builders<Bookmark>.Filter.Regex(x => x.Title, new BsonRegularExpression(title, "i"));
        var categoryFilter =
            categories?.Any() == true
                ? Builders<Bookmark>.Filter.All(x => x.Categories, categories)
                : empty;
        var source = await repository
            .SearchFor(Builders<Bookmark>.Filter.And(titleFilter, categoryFilter))
            .ToListAsync();
        return mapper.Map<List<VmBookmark>>(source);
    }

    [Cache(ExpirationSeconds = 30 * ExpirationTime.Day)]
    public async Task<List<string>> SearchAsync(
        string? title = null,
        IReadOnlyCollection<string>? categories = null
    )
    {
        if (string.IsNullOrEmpty(title))
        {
            return [];
        }

        var empty = Builders<Bookmark>.Filter.Empty;
        var regexTitle = Regex.Escape(title);
        var titleFilter = Builders<Bookmark>.Filter.Regex(
            x => x.Title,
            new BsonRegularExpression(regexTitle, "i")
        );
        var categoryFilter = categories is { Count: > 0 }
            ? Builders<Bookmark>.Filter.All(x => x.Categories, categories)
            : empty;

        return await repository
            .SearchFor(Builders<Bookmark>.Filter.And(titleFilter, categoryFilter))
            .Limit(10)
            .Project(x => x.Title)
            .ToListAsync();
    }

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

    [Cache(ExpirationSeconds = 30 * ExpirationTime.Day)]
    public async Task<List<string>> GetCategoriesAsync()
    {
        return await repository
            .MongodbQueryable.SelectMany(x => x.Categories)
            .GroupBy(x => x)
            .Select(x => x.Key)
            .OrderByDescending(x => x)
            .ToListAsync();
    }

    [InvalidateCache(
        Methods = [
            nameof(GetBookmarksAsync),
            nameof(SearchAsync),
            nameof(GetBookmarkAsync),
            nameof(GetCategoriesAsync),
        ]
    )]
    public async Task CreateBookmarkAsync(VmCreateBookmark bookmark)
    {
        var entity = mapper.Map<Bookmark>(bookmark);
        if (bookmark.UploadIcon != null && !string.IsNullOrEmpty(entity.Icon))
        {
            var iconUrl = await bookmark.UploadIcon(entity.Icon);
            if (!string.IsNullOrEmpty(iconUrl))
            {
                entity.Icon = iconUrl;
            }
        }
        if (bookmark.UploadImage != null && !string.IsNullOrEmpty(entity.Image))
        {
            var imageUrl = await bookmark.UploadImage(entity.Image);
            if (!string.IsNullOrEmpty(imageUrl))
            {
                entity.Image = imageUrl;
            }
        }
        await repository.InsertAsync(entity);
    }

    [InvalidateCache(
        Methods = [
            nameof(GetBookmarksAsync),
            nameof(SearchAsync),
            nameof(GetBookmarkAsync),
            nameof(GetCategoriesAsync),
        ]
    )]
    public async Task UpdateAsync(VmUpdateBookmark bookmark)
    {
        var entity = await repository.TryGetAsync(bookmark.Id);
        if (entity == null)
        {
            return;
        }

        if (bookmark.Icon != entity.Icon)
        {
            if (!string.IsNullOrEmpty(entity.Icon) && bookmark.DeleteIcon != null)
            {
                await bookmark.DeleteIcon(entity.Icon);
                entity.Icon = null;
            }

            if (!string.IsNullOrEmpty(bookmark.Icon) && bookmark.UploadIcon != null)
            {
                var icon = await bookmark.UploadIcon(bookmark.Icon);
                entity.Icon = icon;
            }
        }

        if (bookmark.Image != entity.Image)
        {
            if (!string.IsNullOrEmpty(entity.Image) && bookmark.DeleteImage != null)
            {
                await bookmark.DeleteImage(entity.Image);
                entity.Image = null;
            }
            if (!string.IsNullOrEmpty(bookmark.Image) && bookmark.UploadImage != null)
            {
                var image = await bookmark.UploadImage(bookmark.Image);
                entity.Image = image;
            }
        }

        entity.Title = bookmark.Title ?? entity.Title;
        entity.Url = bookmark.Url ?? entity.Url;
        entity.Categories = bookmark.Categories;

        await repository.UpdateAsync(entity);
    }

    [InvalidateCache(
        Methods = [
            nameof(GetBookmarksAsync),
            nameof(SearchAsync),
            nameof(GetBookmarkAsync),
            nameof(GetCategoriesAsync),
        ]
    )]
    public async Task DeleteAsync(string id, Func<VmBookmark, Task> deleteSource)
    {
        var entity = await repository.TryGetAsync(id);
        if (entity == null)
        {
            return;
        }
        await deleteSource(mapper.Map<VmBookmark>(entity));
        await repository.DeleteAsync(entity.Id);
    }
}
⚠⚠    以下内容为AI分析的结果,请根据实际情况进行判断。

代码解释

这是一个基于 C# 的**书签服务(BookmarkService)**实现类,使用了 MongoDB 作为数据存储,并集成了缓存和对象映射功能。

整体架构

  • 依赖注入:通过主构造函数注入 IRepository<Bookmark>IMapper
  • 缓存策略:使用 [Cache][InvalidateCache] 特性管理缓存
  • 数据访问:通过 MongoDB 驱动进行查询和操作

核心方法解析

1. GetBookmarksAsync - 获取书签列表

[Cache(ExpirationSeconds = 30 * ExpirationTime.Day)]
  • 功能:根据标题和分类过滤查询书签
  • 缓存:结果缓存 30 天
  • 实现细节
    • 使用 MongoDB Filter.Regex 进行标题模糊搜索(不区分大小写)
    • 使用 Filter.All 检查分类数组是否包含指定分类
    • 返回映射后的 ViewModel 列表

2. SearchAsync - 搜索建议

  • 功能:提供搜索自动完成功能
  • 限制:最多返回 10 条结果
  • 安全性:使用 Regex.Escape 防止正则注入
  • 返回:仅返回书签标题列表

3. GetBookmarkAsync - 获取单个书签

  • 功能:根据 ID 获取书签详情
  • 安全处理:实体不存在时返回 null

4. GetCategoriesAsync - 获取所有分类

  • 功能:提取所有唯一分类
  • 实现
    • SelectMany 展平所有书签的分类数组
    • GroupBy 去重
    • 降序排列

数据修改方法

5. CreateBookmarkAsync - 创建书签

[InvalidateCache(Methods = [...])]
  • 缓存失效:创建后清除相关查询方法的缓存
  • 文件上传处理
    • 支持图标(Icon)和图片(Image)上传
    • 委托方式处理文件上传逻辑(UploadIcon/UploadImage
    • 上传成功后更新实体 URL

6. UpdateAsync - 更新书签

  • 智能更新:仅更新变化的字段
  • 文件管理
    • 检测图标/图片是否变化
    • 删除旧文件(通过委托 DeleteIcon/DeleteImage
    • 上传新文件
  • 部分更新:使用 ?? 运算符保留未修改字段

7. DeleteAsync - 删除书签

  • 回调机制:通过 deleteSource 委托执行额外清理逻辑
  • 安全删除:先检查实体存在性

设计模式与特性

特性说明
仓储模式通过 IRepository 抽象数据访问
对象映射使用 AutoMapper 等工具自动转换实体
缓存层AOP 方式管理缓存(读取缓存/修改失效)
委托注入文件操作通过委托实现解耦
MongoDB 特性使用 Builder 模式构建查询过滤器

优点

✅ 清晰的关注点分离
✅ 自动缓存管理提升性能
✅ 灵活的文件处理机制
✅ 类型安全的查询构建

潜在改进点

⚠️ 错误处理较少(文件上传失败等场景)
⚠️ 缓存过期时间硬编码(建议配置化)
⚠️ UpdateAsync 方法较长,可拆分为私有方法

评论加载中...