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