网站首页 网站源码
website
站点相关全部源代码,隐藏了一些关于服务器的信息
using Dpz.Core.Public.ViewModel.RequestEvent;
using Medallion.Threading;

namespace Dpz.Core.Service.V4.Implements;

public class ArticleService(
    IRepository<Article> repository,
    IMediator mediator,
    IMapper mapper,
    ILogger<ArticleService> logger,
    IDistributedLockProvider distributedLockProvider,
    IFusionCache fusionCache
) : AbstractCacheService(fusionCache), IArticleService
{
#pragma warning disable MALinq2001

    protected override string CachePrefixKey => "Dpz.Core.Service.V4.Implements.ArticleService";
    protected override TimeSpan CacheDefaultExpiration => TimeSpan.FromHours(3);

    private readonly IFusionCache _fusionCache = fusionCache;

    public async Task<List<VmArticleMini>> GetTopArticlesAsync(int days = -7, uint count = 15)
    {
        if (days > 0)
        {
            throw new ArgumentException("days > 0", nameof(days));
        }
        var cache = await GetOrSetCacheAsync<List<VmArticleMini>>(
            nameof(GetTopArticlesAsync),
            async (_, cancellationToken) =>
            {
                var date = DateTime.Now.AddDays(days);
                var length = (int)count;
                var source = await repository
                    .SearchFor(x => x.CreateTime > date)
                    .OrderByDescending(x => x.ViewCount)
                    .Take(length)
                    .ToListAsync(cancellationToken);
                return mapper.Map<List<VmArticleMini>>(source);
            },
            new { days, count }
        );
        return cache;
    }

    public async Task<List<VmArticleMini>> GetRandomArticlesAsync(int sample = 8)
    {
        if (sample <= 0)
        {
            throw new ArgumentException("sample <= 0", nameof(sample));
        }
        if (sample > 30)
        {
            throw new ArgumentException("sample > 30", nameof(sample));
        }
        var cache = await GetOrSetCacheAsync<List<VmArticleMini>>(
            nameof(GetRandomArticlesAsync),
            async (_, cancellationToken) =>
            {
                var source = await repository
                    .SearchFor(x => x.Tags.Contains("cnBeta") || x.Tags.Contains("ItHome"))
                    .Sample(sample)
                    .ToListAsync(cancellationToken);
                return mapper.Map<List<VmArticleMini>>(source);
            },
            new { sample }
        );
        return cache;
    }

    public async Task<List<VmArticleMini>> GetPublishArticlesAsync()
    {
        var cache = await _fusionCache.GetOrSetAsync<List<VmArticleMini>>(
            nameof(GetPublishArticlesAsync),
            async (_, cancellationToken) =>
            {
                var source = await repository
                    .SearchFor(x =>
                        (!x.Tags.Contains("cnBeta") && !x.Tags.Contains("ItHome"))
                        || x.CommentCount > 0
                    )
                    .OrderByDescending(x => x.CreateTime)
                    .ToListAsync(cancellationToken);
                return mapper.Map<List<VmArticleMini>>(source);
            }
        );
        return cache;
    }

    public async Task<IPagedList<VmArticleMini>> GetPagesAsync(
        int pageIndex = 1,
        int pageSize = 20,
        string? title = "",
        string? account = "",
        params string?[]? tags
    )
    {
        return await GetOrSetPagedListAsync<VmArticleMini>(
            nameof(GetPagesAsync),
            async _ =>
            {
                var filterEmpty = Builders<Article>.Filter.Empty;
                var filters = new List<FilterDefinition<Article>>();
                if (!string.IsNullOrEmpty(account))
                {
                    var filter = Builders<Article>.Filter.Eq(x => x.Author.Id, account);
                    filters.Add(filter);
                }

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

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

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

                var result = await repository
                    .SearchFor(filterResult)
                    .SortByDescending(x => x.CreateTime)
                    .ToPagedListAsync<Article, VmArticleMini>(pageIndex, pageSize);

                return result;
            },
            new
            {
                pageIndex,
                pageSize,
                title,
                account,
                tags,
            }
        );
    }

    public async Task<List<string>> GetAllTagsAsync()
    {
        var cache = await GetOrSetCacheAsync<List<string>>(
            nameof(GetAllTagsAsync),
            async (_, cancellationToken) =>
            {
                return await repository
                    .MongodbQueryable.SelectMany(x => x.Tags)
                    .GroupBy(x => x)
                    .Select(x => x.Key)
                    .OrderBy(x => x)
                    .ToListAsync(cancellationToken);
            }
        );
        return cache;
    }

    public async Task<VmArticle?> GetArticleAsync(string id)
    {
        var cache = await GetOrSetCacheAsync<VmArticle?>(
            nameof(GetArticleAsync),
            async (_, _) =>
            {
                var article = await repository.TryGetAsync(id);
                return article == null ? null : mapper.Map<VmArticle>(article);
            },
            new { id }
        );
        return cache;
    }

    public async Task ViewAsync(string id)
    {
        if (ObjectId.TryParse(id, out var oid))
        {
            var update = Builders<Article>.Update.Inc(x => x.ViewCount, 1);
            await repository.UpdateAsync(x => x.Id == oid, update);
        }
    }

    public async Task<List<VmArticleMini>> GetLatestAsync(int range = 5)
    {
        if (range <= 0)
        {
            throw new ArgumentException("range <= 0", nameof(range));
        }

        if (range > 200)
        {
            throw new ArgumentException("range > 200", nameof(range));
        }

        var cache = await GetOrSetCacheAsync<List<VmArticleMini>>(
            nameof(GetLatestAsync),
            async (_, cancellationToken) =>
            {
                var source = await repository
                    //.SearchFor(x => true)
                    .MongodbQueryable.OrderByDescending(x => x.CreateTime)
                    .Take(range)
                    .ToListAsync(cancellationToken);
                return mapper.Map<List<VmArticleMini>>(source);
            },
            new { range }
        );
        return cache;
    }

    public async Task<bool> IsExistsAsync(string title)
    {
        var blog = await repository.SearchFor(x => x.Title == title).FirstOrDefaultAsync();
        return blog != null;
    }

    public async Task<IReadOnlyCollection<string>> NoExistsByFromAsync(
        IReadOnlyCollection<string> feeds
    )
    {
        var filter = Builders<Article>.Filter.In(x => x.From, feeds);
        var exists = await repository.SearchFor(filter).Project(x => x.From).ToListAsync();
        return feeds.Except(exists).ToArray();
    }

    public async Task DeleteAsync(string id)
    {
        if (ObjectId.TryParse(id, out var oid))
        {
            var article = await repository.FindAsync(oid);

            await mediator.Send(new RemoveImagesRequest { Images = article?.ImagesAddress });
            await repository.DeleteAsync(x => x.Id == oid);

            var cacheKey = BuildCacheKey(nameof(GetArticleAsync), new { id });
            await _fusionCache.RemoveAsync(cacheKey);
        }
    }

    public async Task DeleteOldCnBetaAsync(int month, int limit)
    {
        if (month <= 0)
        {
            throw new ArgumentException("month no can't <= 0", nameof(month));
        }

        var date = DateTime.Now.AddMonths(-month);
        var list = await repository
            .SearchFor(x =>
                (x.Tags.Contains("cnBeta") || x.Tags.Contains("ItHome"))
                && x.CreateTime < date
                && x.CommentCount == 0
            )
            .OrderBy(x => x.CreateTime)
            .Take(limit)
            .ToListAsync();
        if (!list.Any())
        {
            return;
        }

        await mediator.Send(
            new RemoveImagesRequest
            {
                Images = list.SelectMany(x => x.ImagesAddress ?? []).ToList(),
            }
        );
        var ids = list.Select(x => x.Id);
        await repository.DeleteAsync(x => ids.Contains(x.Id));
        logger.LogInformation("旧数据删除完毕,共删除{Count}篇文章", list.Count);
    }

    public async Task<VmArticle> CreateArticleAsync(VmCreateArticleV4 article, VmUserInfo creator)
    {
        var author = mapper.Map<UserInfo>(creator);

        var htmlContent = article.Markdown.MarkdownToHtml(false);
        var htmlParse = new HtmlParser();
        var document = await htmlParse.ParseDocumentAsync(htmlContent);
        var imageElements = document.GetElementsByTagName("img");

        var entity = new Article
        {
            Author = author,
            HtmlContent = article.Content,
            Title = article.Title,
            Tags = article.Tags,
            Markdown = article.Markdown,
            CommentCount = 0,
            CreateTime = article.PublishTime ?? DateTime.Now,
            From = article.From,
            ImagesAddress = imageElements.GetElementsImageUrls(),
            Introduction = article.Introduction,
            MainImage = imageElements.FirstOrDefault()?.GetAttribute("src"),
            ViewCount = 0,
            LastUpdateTime = DateTime.Now,
            Categories = article.Categories,
            AdWeight = article.AdWeight,
        };

        await using (
            await distributedLockProvider.AcquireLockAsync($"Article.Create.Lock:{entity.Title}")
        )
        {
            if (await IsExistsAsync(entity.Title))
            {
                throw new ExistsException($"《{entity.Title}》已存在");
            }
            await repository.InsertAsync(entity);
        }
        return mapper.Map<VmArticle>(entity);
    }

    public async Task EditArticleAsync(VmEditArticleV4 article, VmUserInfo editor)
    {
        if (ObjectId.TryParse(article.Id, out var id))
        {
            var entity = await repository.FindAsync(id);
            if (entity != null)
            {
                var author = mapper.Map<UserInfo>(editor);
                var request = new EditMarkdownRequest
                {
                    Markdown = article.Markdown,
                    OriginalMarkdown = entity.Markdown,
                };

                var images = await mediator.Send(request);

                var update = Builders<Article>
                    .Update.Set(x => x.Title, article.Title)
                    .Set(x => x.Markdown, article.Markdown)
                    .Set(x => x.HtmlContent, article.Content)
                    .Set(x => x.Tags, article.Tags)
                    .Set(x => x.Introduction, article.Introduction)
                    .Set(x => x.MainImage, images.FirstOrDefault())
                    .Set(x => x.ImagesAddress, images)
                    .Set(x => x.Author, author)
                    .Set(x => x.LastUpdateTime, DateTime.Now);
                await repository.UpdateAsync(x => x.Id == id, update);

                var cacheKey = BuildCacheKey(nameof(GetArticleAsync), new { id });
                await _fusionCache.RemoveAsync(cacheKey);
            }
        }
    }

    public async Task<int> GetTotalCountAsync()
    {
        checked
        {
            return (int)
                await repository.Collection.CountDocumentsAsync(FilterDefinition<Article>.Empty);
        }
    }

    public async Task<int> GetTodayCountAsync()
    {
        checked
        {
            var filter = Builders<Article>.Filter.Gte(x => x.CreateTime, DateTime.Now.Date);
            return (int)await repository.Collection.CountDocumentsAsync(filter);
        }
    }

    public async ValueTask ClearCacheAsync()
    {
        var removeTags = new[]
        {
            CachePrefixKey + nameof(GetTopArticlesAsync),
            CachePrefixKey + nameof(GetPagesAsync),
            CachePrefixKey + nameof(GetAllTagsAsync),
            CachePrefixKey + nameof(GetLatestAsync),
        };

        await _fusionCache.RemoveByTagAsync(removeTags);
    }
}
⚠⚠    以下内容为AI分析的结果,请根据实际情况进行判断。

上述代码是一个 C# 类 ArticleService 的实现,主要用于处理与文章相关的操作。这个类实现了 IArticleService 接口,提供了一系列方法来管理和操作文章数据。以下是对代码中各个部分的详细解释:

1. 类的构造函数

ArticleService 的构造函数接受多个依赖项,包括:

  • IRepository<Article>:用于与文章数据进行交互的仓储接口。
  • IMediator:用于处理请求和发送消息的中介者。
  • IMapper:用于对象映射的工具。
  • ILogger<ArticleService>:用于记录日志的接口。
  • IDistributedLockProvider:用于分布式锁的提供者,确保在创建文章时的线程安全。
  • IHybridCachingProvider:用于缓存的提供者。

2. 主要功能方法

以下是类中定义的一些主要方法及其功能:

  • GetTopArticlesAsync:获取最近 days 天内的最受欢迎的文章,按查看次数排序,返回指定数量的文章。

  • GetRandomArticlesAsync:获取随机的文章,文章标签包含 "cnBeta" 或 "ItHome",返回指定数量的文章。

  • GetPublishArticlesAsync:获取已发布的文章,过滤掉标签为 "cnBeta" 和 "ItHome" 的文章,或评论数大于零的文章。

  • GetPagesAsync:获取分页的文章列表,支持根据标题、作者和标签进行过滤。

  • GetAllTagsAsync:获取所有文章的标签,按字母顺序排序。

  • GetArticleAsync:根据文章 ID 获取单篇文章的详细信息。

  • ViewAsync:增加文章的查看次数。

  • GetLatestAsync:获取最新的文章,返回指定数量的文章。

  • IsExistsAsync:检查是否存在具有特定标题的文章。

  • NoExistsByFromAsync:检查给定的来源是否存在于文章中,返回不存在的来源。

  • DeleteAsync:根据文章 ID 删除文章,并删除与文章相关的图片。

  • DeleteOldCnBetaAsync:删除旧的 "cnBeta" 或 "ItHome" 标签的文章,条件是创建时间早于指定月份且评论数为零。

  • CreateArticleAsync:创建新文章,解析 Markdown 内容并提取图片地址,确保文章标题唯一。

  • EditArticleAsync:编辑现有文章,更新文章的各个字段,并处理图片的更新。

  • GetTotalCountAsync:获取文章的总数。

  • GetTodayCountAsync:获取今天创建的文章数量。

  • ClearCacheAsync:清除文章相关的缓存。

  • SearchAsync:根据关键词搜索文章,支持在标题和内容中进行正则匹配。

3. 错误处理

在多个方法中,使用了 ArgumentException 来处理无效的参数输入,例如负数或超出范围的值。

4. 日志记录

使用 ILogger 记录操作的日志,例如删除旧数据的数量。

5. 数据库操作

大部分方法使用了 MongoDB 的查询构建器(Builders<Article>)来构建查询条件,并通过 repository 进行异步数据库操作。

总结

ArticleService 类提供了一整套用于管理文章的功能,包括创建、编辑、删除、查询和搜索文章等操作,适用于一个内容管理系统或博客平台。通过依赖注入的方式,类的设计遵循了良好的软件工程原则,便于测试和维护。

loading