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

namespace Dpz.Core.Service.V4.Implements;

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

    public async Task<ICollection<VmArticleMini>> GetTopArticlesAsync(
        int days = -7,
        uint count = 15
    )
    {
        if (days > 0)
            throw new ArgumentException("days > 0", nameof(days));
        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();
        return mapper.Map<List<VmArticleMini>>(source);
    }

    public async Task<ICollection<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 source = await repository
            .SearchFor(x => x.Tags.Contains("cnBeta") || x.Tags.Contains("ItHome"))
            .Sample(sample)
            .ToListAsync();
        return mapper.Map<List<VmArticleMini>>(source);
    }

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

    public async Task<IPagedList<VmArticleMini>> GetPagesAsync(
        int pageIndex = 1,
        int pageSize = 20,
        string? title = "",
        string? account = "",
        params string?[]? tags
    )
    {
        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()
            ?? new List<string>();
        if (clearTags.Any())
        {
            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;

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

    public async Task<ICollection<string>> GetAllTagsAsync()
    {
        return await repository
            .MongodbQueryable.SelectMany(x => x.Tags)
            .GroupBy(x => x)
            .Select(x => x.Key)
            .OrderBy(x => x)
            .ToListAsync();
    }

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

    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<ICollection<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 source = await repository
            //.SearchFor(x => true)
            .MongodbQueryable.OrderByDescending(x => x.CreateTime)
            .Take(range)
            .ToListAsync();
        return mapper.Map<List<VmArticleMini>>(source);
    }

    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);
        }
    }

    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,
        };

        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);
            }
        }
    }

    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 ValueTask ClearCacheAsync()
    {
        logger.LogInformation("article cache cleared");
        return ValueTask.CompletedTask;
    }

    public async Task<List<ArticleSearchResultResponse>> SearchAsync(string keyword)
    {
        var keywords = keyword
            .Split(' ')
            .Where(x => !string.IsNullOrEmpty(x) && x.Length > 1)
            .Distinct()
            .Select(Regex.Escape)
            .ToList();
        var pattern = string.Join('|', keywords);

        var titleFilter = Builders<Article>.Filter.Regex(
            x => x.Title,
            new BsonRegularExpression(pattern, "i")
        );
        var contentFilter = Builders<Article>.Filter.Regex(
            x => x.Markdown,
            new BsonRegularExpression(pattern, "i")
        );
        var filters = Builders<Article>.Filter.Or(titleFilter, contentFilter);
        var result = repository.SearchForAsync(filters).OrderByDescending(x => x.Id);

        var articleSearchRequest = new ArticleSearchRequest
        {
            Articles = result,
            Pattern = pattern,
        };
        return await mediator.Send(articleSearchRequest);
    }
}
loading