PostService.cs 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. using AngleSharp;
  2. using AngleSharp.Dom;
  3. using AngleSharp.Html.Parser;
  4. using AutoMapper;
  5. using CacheManager.Core;
  6. using Masuit.LuceneEFCore.SearchEngine;
  7. using Masuit.LuceneEFCore.SearchEngine.Interfaces;
  8. using Masuit.MyBlogs.Core.Infrastructure.Repository.Interface;
  9. using Masuit.MyBlogs.Core.Infrastructure.Services.Interface;
  10. using Masuit.MyBlogs.Core.Models.DTO;
  11. using Masuit.MyBlogs.Core.Models.Entity;
  12. using Masuit.MyBlogs.Core.Models.Enum;
  13. using Masuit.MyBlogs.Core.Models.ViewModel;
  14. using Masuit.Tools;
  15. using Masuit.Tools.Html;
  16. using Microsoft.EntityFrameworkCore;
  17. using PanGu;
  18. using PanGu.HighLight;
  19. using System.Linq.Expressions;
  20. using System.Reflection;
  21. using System.Text.RegularExpressions;
  22. using Z.EntityFramework.Plus;
  23. namespace Masuit.MyBlogs.Core.Infrastructure.Services;
  24. public sealed class PostService : BaseService<Post>, IPostService
  25. {
  26. private readonly ICacheManager<SearchResult<PostDto>> _cacheManager;
  27. private readonly ICategoryRepository _categoryRepository;
  28. private readonly IMapper _mapper;
  29. private readonly IPostTagsRepository _postTagsRepository;
  30. public PostService(IPostRepository repository, ISearchEngine<DataContext> searchEngine, ILuceneIndexSearcher searcher, ICacheManager<SearchResult<PostDto>> cacheManager, ICategoryRepository categoryRepository, IMapper mapper, IPostTagsRepository postTagsRepository) : base(repository, searchEngine, searcher)
  31. {
  32. _cacheManager = cacheManager;
  33. _categoryRepository = categoryRepository;
  34. _mapper = mapper;
  35. _postTagsRepository = postTagsRepository;
  36. }
  37. /// <summary>
  38. /// 文章高亮关键词处理
  39. /// </summary>
  40. /// <param name="p"></param>
  41. /// <param name="keyword"></param>
  42. public async Task Highlight(Post p, string keyword)
  43. {
  44. try
  45. {
  46. var simpleHtmlFormatter = new SimpleHTMLFormatter("<span style='color:red;background-color:yellow;font-size: 1.1em;font-weight:700;'>", "</span>");
  47. var highlighter = new Highlighter(simpleHtmlFormatter, new Segment()) { FragmentSize = int.MaxValue };
  48. keyword = Regex.Replace(keyword, @"<|>|\(|\)|\{|\}|\[|\]", " ");
  49. var keywords = Searcher.CutKeywords(keyword);
  50. var context = BrowsingContext.New(Configuration.Default);
  51. var document = await context.OpenAsync(req => req.Content(p.Content));
  52. var elements = document.DocumentElement.GetElementsByTagName("p");
  53. foreach (var e in elements)
  54. {
  55. for (var index = 0; index < e.ChildNodes.Length; index++)
  56. {
  57. var node = e.ChildNodes[index];
  58. bool handled = false;
  59. foreach (var s in keywords)
  60. {
  61. string frag;
  62. if (handled == false && node.TextContent.Contains(s, StringComparison.CurrentCultureIgnoreCase) && !string.IsNullOrEmpty(frag = highlighter.GetBestFragment(s, node.TextContent)))
  63. {
  64. switch (node)
  65. {
  66. case IElement el:
  67. el.InnerHtml = frag;
  68. handled = true;
  69. break;
  70. case IText t:
  71. var parser = new HtmlParser();
  72. var parseDoc = parser.ParseDocument(frag).Body;
  73. e.ReplaceChild(parseDoc, t);
  74. handled = true;
  75. break;
  76. }
  77. }
  78. }
  79. }
  80. }
  81. p.Content = document.Body.InnerHtml;
  82. }
  83. catch
  84. {
  85. // ignored
  86. }
  87. }
  88. public SearchResult<PostDto> SearchPage(int page, int size, string keyword)
  89. {
  90. var cacheKey = $"search:{keyword}:{page}:{size}";
  91. var result = _cacheManager.GetOrAdd(cacheKey, _ =>
  92. {
  93. var searchResult = SearchEngine.ScoredSearch<Post>(BuildSearchOptions(page, size, keyword));
  94. var entities = searchResult.Results.Where(s => s.Entity.Status == Status.Published).DistinctBy(s => s.Entity.Id).ToList();
  95. var ids = entities.Select(s => s.Entity.Id).ToArray();
  96. var dic = GetQuery<PostDto>(p => ids.Contains(p.Id) && p.LimitMode != RegionLimitMode.OnlyForSearchEngine).ToDictionary(p => p.Id);
  97. var posts = entities.Where(s => dic.ContainsKey(s.Entity.Id)).Select(s => dic[s.Entity.Id]).ToList();
  98. var simpleHtmlFormatter = new SimpleHTMLFormatter("<span style='color:red;background-color:yellow;font-size: 1.1em;font-weight:700;'>", "</span>");
  99. var highlighter = new Highlighter(simpleHtmlFormatter, new Segment()) { FragmentSize = 200 };
  100. var keywords = Searcher.CutKeywords(keyword);
  101. HighlightSegment(posts, keywords, highlighter);
  102. SolvePostsCategory(posts);
  103. return new SearchResult<PostDto>()
  104. {
  105. Results = posts,
  106. Elapsed = searchResult.Elapsed,
  107. Total = searchResult.TotalHits
  108. };
  109. });
  110. return result;
  111. }
  112. public void SolvePostsCategory(IList<PostDto> posts)
  113. {
  114. var cids = posts.Select(p => p.CategoryId).Distinct().ToArray();
  115. var categories = _categoryRepository.GetQuery(c => cids.Contains(c.Id)).Include(c => c.Parent).ToDictionary(c => c.Id);
  116. posts.ForEach(p => p.Category = _mapper.Map<CategoryDto_P>(categories[p.CategoryId]));
  117. }
  118. /// <summary>
  119. /// 高亮截取处理
  120. /// </summary>
  121. /// <param name="posts"></param>
  122. /// <param name="keywords"></param>
  123. /// <param name="highlighter"></param>
  124. private static void HighlightSegment(IList<PostDto> posts, List<string> keywords, Highlighter highlighter)
  125. {
  126. foreach (var p in posts)
  127. {
  128. p.Content = p.Content.RemoveHtmlTag();
  129. foreach (var s in keywords)
  130. {
  131. string frag;
  132. if (p.Title.Contains(s) && !string.IsNullOrEmpty(frag = highlighter.GetBestFragment(s, p.Title)))
  133. {
  134. p.Title = frag;
  135. break;
  136. }
  137. }
  138. bool handled = false;
  139. foreach (var s in keywords)
  140. {
  141. string frag;
  142. if (p.Content.Contains(s) && !string.IsNullOrEmpty(frag = highlighter.GetBestFragment(s, p.Content)))
  143. {
  144. p.Content = frag;
  145. handled = true;
  146. break;
  147. }
  148. }
  149. if (p.Content.Length > 200 && !handled)
  150. {
  151. p.Content = p.Content[..200];
  152. }
  153. }
  154. }
  155. private static SearchOptions BuildSearchOptions(int page, int size, string keyword)
  156. {
  157. keyword = Regex.Replace(keyword, @":\s+", ":");
  158. var fields = new List<string>();
  159. var newkeywords = new List<string>();
  160. foreach (var item in keyword.Split(' ', ' ').Where(s => s.Contains(new[] { ":", ":" })))
  161. {
  162. var part = item.Split(':', ':');
  163. var field = typeof(Post).GetProperty(part[0], BindingFlags.IgnoreCase)?.Name;
  164. if (!string.IsNullOrEmpty(field))
  165. {
  166. fields.Add(field);
  167. }
  168. newkeywords.Add(part[1]);
  169. }
  170. var searchOptions = fields.Any() ? new SearchOptions(newkeywords.Join(" "), page, size, fields.Join(",")) : new SearchOptions(keyword, page, size, typeof(Post));
  171. if (keyword.Contains(new[] { " ", ",", ";" }))
  172. {
  173. searchOptions.Score = 0.3f;
  174. }
  175. return searchOptions;
  176. }
  177. /// <summary>
  178. /// 文章所有tag
  179. /// </summary>
  180. /// <returns></returns>
  181. public Dictionary<string, int> GetTags()
  182. {
  183. return _postTagsRepository.GetAll(t => t.Count, false).FromCache().ToDictionary(g => g.Name, g => g.Count);
  184. }
  185. /// <summary>
  186. /// 添加实体并保存
  187. /// </summary>
  188. /// <param name="t">需要添加的实体</param>
  189. /// <returns>添加成功</returns>
  190. public override Post AddEntitySaved(Post t)
  191. {
  192. t = base.AddEntity(t);
  193. SearchEngine.SaveChanges(t.Status == Status.Published);
  194. return t;
  195. }
  196. /// <summary>
  197. /// 添加实体并保存(异步)
  198. /// </summary>
  199. /// <param name="t">需要添加的实体</param>
  200. /// <returns>添加成功</returns>
  201. public override Task<int> AddEntitySavedAsync(Post t)
  202. {
  203. base.AddEntity(t);
  204. return SearchEngine.SaveChangesAsync(t.Status == Status.Published);
  205. }
  206. /// <summary>
  207. /// 根据ID删除实体并保存
  208. /// </summary>
  209. /// <param name="id">实体id</param>
  210. /// <returns>删除成功</returns>
  211. public override bool DeleteById(int id)
  212. {
  213. DeleteEntity(GetById(id));
  214. return SearchEngine.SaveChanges() > 0;
  215. }
  216. /// <summary>
  217. /// 根据ID删除实体并保存(异步)
  218. /// </summary>
  219. /// <param name="id">实体id</param>
  220. /// <returns>删除成功</returns>
  221. public override Task<int> DeleteByIdAsync(int id)
  222. {
  223. base.DeleteById(id);
  224. return SearchEngine.SaveChangesAsync();
  225. }
  226. /// <summary>
  227. /// 删除多个实体并保存(异步)
  228. /// </summary>
  229. /// <param name="list">实体集合</param>
  230. /// <returns>删除成功</returns>
  231. public override Task<int> DeleteEntitiesSavedAsync(IEnumerable<Post> list)
  232. {
  233. base.DeleteEntities(list);
  234. return SearchEngine.SaveChangesAsync();
  235. }
  236. /// <summary>
  237. /// 根据条件删除实体
  238. /// </summary>
  239. /// <param name="where">查询条件</param>
  240. /// <returns>删除成功</returns>
  241. public override int DeleteEntitySaved(Expression<Func<Post, bool>> where)
  242. {
  243. base.DeleteEntity(where);
  244. return SearchEngine.SaveChanges();
  245. }
  246. /// <summary>
  247. /// 删除实体并保存
  248. /// </summary>
  249. /// <param name="t">需要删除的实体</param>
  250. /// <returns>删除成功</returns>
  251. public override bool DeleteEntitySaved(Post t)
  252. {
  253. base.DeleteEntity(t);
  254. return SearchEngine.SaveChanges() > 0;
  255. }
  256. /// <summary>
  257. /// 根据条件删除实体
  258. /// </summary>
  259. /// <param name="where">查询条件</param>
  260. /// <returns>删除成功</returns>
  261. public override Task<int> DeleteEntitySavedAsync(Expression<Func<Post, bool>> where)
  262. {
  263. base.DeleteEntity(where);
  264. return SearchEngine.SaveChangesAsync();
  265. }
  266. /// <summary>
  267. /// 统一保存的方法
  268. /// </summary>
  269. /// <returns>受影响的行数</returns>
  270. public int SaveChanges(bool flushIndex)
  271. {
  272. return flushIndex ? SearchEngine.SaveChanges() : base.SaveChanges();
  273. }
  274. /// <summary>
  275. /// 统一保存数据
  276. /// </summary>
  277. /// <returns>受影响的行数</returns>
  278. public async Task<int> SaveChangesAsync(bool flushIndex)
  279. {
  280. return flushIndex ? await SearchEngine.SaveChangesAsync() : await base.SaveChangesAsync();
  281. }
  282. }