PostService.cs 11 KB

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