PostController.cs 46 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064
  1. using AngleSharp;
  2. using CacheManager.Core;
  3. using EFCoreSecondLevelCacheInterceptor;
  4. using Hangfire;
  5. using JiebaNet.Segmenter;
  6. using Masuit.LuceneEFCore.SearchEngine.Interfaces;
  7. using Masuit.MyBlogs.Core.Common;
  8. using Masuit.MyBlogs.Core.Configs;
  9. using Masuit.MyBlogs.Core.Extensions;
  10. using Masuit.MyBlogs.Core.Extensions.Firewall;
  11. using Masuit.MyBlogs.Core.Extensions.Hangfire;
  12. using Masuit.MyBlogs.Core.Infrastructure;
  13. using Masuit.MyBlogs.Core.Infrastructure.Repository;
  14. using Masuit.MyBlogs.Core.Infrastructure.Services.Interface;
  15. using Masuit.MyBlogs.Core.Models.Command;
  16. using Masuit.MyBlogs.Core.Models.DTO;
  17. using Masuit.MyBlogs.Core.Models.Entity;
  18. using Masuit.MyBlogs.Core.Models.Enum;
  19. using Masuit.MyBlogs.Core.Models.ViewModel;
  20. using Masuit.MyBlogs.Core.Views.Post;
  21. using Masuit.Tools;
  22. using Masuit.Tools.Core.Net;
  23. using Masuit.Tools.Html;
  24. using Masuit.Tools.Linq;
  25. using Masuit.Tools.Logging;
  26. using Masuit.Tools.Security;
  27. using Masuit.Tools.Strings;
  28. using Masuit.Tools.Systems;
  29. using Microsoft.AspNetCore.Hosting;
  30. using Microsoft.AspNetCore.Http;
  31. using Microsoft.AspNetCore.Mvc;
  32. using Microsoft.EntityFrameworkCore;
  33. using Microsoft.Net.Http.Headers;
  34. using System;
  35. using System.Collections.Generic;
  36. using System.ComponentModel.DataAnnotations;
  37. using System.Linq;
  38. using System.Linq.Dynamic.Core;
  39. using System.Linq.Expressions;
  40. using System.Text.RegularExpressions;
  41. using System.Threading;
  42. using System.Threading.Tasks;
  43. using SameSiteMode = Microsoft.AspNetCore.Http.SameSiteMode;
  44. namespace Masuit.MyBlogs.Core.Controllers
  45. {
  46. /// <summary>
  47. /// 文章管理
  48. /// </summary>
  49. public class PostController : BaseController
  50. {
  51. public IPostService PostService { get; set; }
  52. public ICategoryService CategoryService { get; set; }
  53. public ISeminarService SeminarService { get; set; }
  54. public IPostHistoryVersionService PostHistoryVersionService { get; set; }
  55. public IWebHostEnvironment HostEnvironment { get; set; }
  56. public ISearchEngine<DataContext> SearchEngine { get; set; }
  57. public ImagebedClient ImagebedClient { get; set; }
  58. /// <summary>
  59. /// 文章详情页
  60. /// </summary>
  61. /// <param name="id"></param>
  62. /// <param name="kw"></param>
  63. /// <returns></returns>
  64. [Route("{id:int}"), ResponseCache(Duration = 600, VaryByQueryKeys = new[] { "id" }, VaryByHeader = "Cookie")]
  65. public async Task<ActionResult> Details(int id, string kw)
  66. {
  67. var post = await PostService.GetAsync(p => p.Id == id && (p.Status == Status.Published || CurrentUser.IsAdmin)) ?? throw new NotFoundException("文章未找到");
  68. CheckPermission(post);
  69. ViewBag.Keyword = post.Keyword + "," + post.Label;
  70. ViewBag.Desc = await post.Content.GetSummary(200);
  71. var modifyDate = post.ModifyDate;
  72. ViewBag.Next = await PostService.GetFromCacheAsync<DateTime, PostModelBase>(p => p.ModifyDate > modifyDate && (p.Status == Status.Published || CurrentUser.IsAdmin), p => p.ModifyDate);
  73. ViewBag.Prev = await PostService.GetFromCacheAsync<DateTime, PostModelBase>(p => p.ModifyDate < modifyDate && (p.Status == Status.Published || CurrentUser.IsAdmin), p => p.ModifyDate, false);
  74. if (!string.IsNullOrEmpty(kw))
  75. {
  76. ViewData["keywords"] = post.Content.Contains(kw) ? $"['{kw}']" : SearchEngine.LuceneIndexSearcher.CutKeywords(kw).ToJsonString();
  77. }
  78. ViewBag.Ads = AdsService.GetByWeightedPrice(AdvertiseType.InPage, Request.Location(), post.CategoryId);
  79. var related = PostService.ScoreSearch(1, 11, string.IsNullOrWhiteSpace(post.Keyword + post.Label) ? post.Title : post.Keyword + post.Label);
  80. related.RemoveAll(p => p.Id == id);
  81. if (related.Count <= 1)
  82. {
  83. related = (await PostService.GetPagesFromCacheAsync(1, 10, p => p.Id != id && p.CategoryId == post.CategoryId, p => p.TotalViewCount, false)).Data;
  84. }
  85. ViewBag.Related = related;
  86. post.ModifyDate = post.ModifyDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
  87. post.PostDate = post.PostDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
  88. post.Content = ReplaceVariables(post.Content);
  89. post.ProtectContent = ReplaceVariables(post.ProtectContent);
  90. if (CurrentUser.IsAdmin)
  91. {
  92. return View("Details_Admin", post);
  93. }
  94. if (!HttpContext.Request.IsRobot() && string.IsNullOrEmpty(HttpContext.Session.Get<string>("post" + id)))
  95. {
  96. HangfireHelper.CreateJob(typeof(IHangfireBackJob), nameof(HangfireBackJob.RecordPostVisit), args: id);
  97. HttpContext.Session.Set("post" + id, id.ToString());
  98. }
  99. return View(post);
  100. }
  101. private void CheckPermission(Post post)
  102. {
  103. var location = Request.Location() + "|" + Request.Headers[HeaderNames.UserAgent];
  104. switch (post.LimitMode)
  105. {
  106. case RegionLimitMode.AllowRegion:
  107. if (!location.Contains(post.Regions.Split(',', StringSplitOptions.RemoveEmptyEntries)) && !CurrentUser.IsAdmin && !VisitorTokenValid && !Request.IsRobot())
  108. {
  109. Disallow(post);
  110. }
  111. break;
  112. case RegionLimitMode.ForbidRegion:
  113. if (location.Contains(post.Regions.Split(',', StringSplitOptions.RemoveEmptyEntries)) && !CurrentUser.IsAdmin && !VisitorTokenValid && !Request.IsRobot())
  114. {
  115. Disallow(post);
  116. }
  117. break;
  118. case RegionLimitMode.AllowRegionExceptForbidRegion:
  119. if (location.Contains(post.ExceptRegions.Split(',', StringSplitOptions.RemoveEmptyEntries)) && !CurrentUser.IsAdmin && !VisitorTokenValid)
  120. {
  121. Disallow(post);
  122. }
  123. goto case RegionLimitMode.AllowRegion;
  124. case RegionLimitMode.ForbidRegionExceptAllowRegion:
  125. if (location.Contains(post.ExceptRegions.Split(',', StringSplitOptions.RemoveEmptyEntries)) && !CurrentUser.IsAdmin && !VisitorTokenValid)
  126. {
  127. break;
  128. }
  129. goto case RegionLimitMode.ForbidRegion;
  130. }
  131. }
  132. private void Disallow(Post post)
  133. {
  134. RedisHelper.IncrBy("interceptCount");
  135. RedisHelper.LPush("intercept", new IpIntercepter()
  136. {
  137. IP = ClientIP,
  138. RequestUrl = $"//{Request.Host}/{post.Id}",
  139. Time = DateTime.Now,
  140. UserAgent = Request.Headers[HeaderNames.UserAgent],
  141. Remark = "无权限查看该文章",
  142. Address = Request.Location()
  143. });
  144. throw new NotFoundException("文章未找到");
  145. }
  146. /// <summary>
  147. /// 文章历史版本
  148. /// </summary>
  149. /// <param name="id"></param>
  150. /// <param name="page"></param>
  151. /// <param name="size"></param>
  152. /// <returns></returns>
  153. [Route("{id:int}/history"), ResponseCache(Duration = 600, VaryByQueryKeys = new[] { "id", "page", "size" }, VaryByHeader = "Cookie")]
  154. public async Task<ActionResult> History(int id, [Range(1, int.MaxValue, ErrorMessage = "页码必须大于0")] int page = 1, [Range(1, 50, ErrorMessage = "页大小必须在0到50之间")] int size = 20)
  155. {
  156. var post = await PostService.GetAsync(p => p.Id == id && (p.Status == Status.Published || CurrentUser.IsAdmin)) ?? throw new NotFoundException("文章未找到");
  157. CheckPermission(post);
  158. ViewBag.Primary = post;
  159. var list = await PostHistoryVersionService.GetPagesAsync(page, size, v => v.PostId == id, v => v.ModifyDate, false);
  160. foreach (var item in list.Data)
  161. {
  162. item.ModifyDate = item.ModifyDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
  163. }
  164. ViewBag.Ads = AdsService.GetByWeightedPrice(AdvertiseType.InPage, Request.Location(), post.CategoryId);
  165. return View(list);
  166. }
  167. /// <summary>
  168. /// 文章历史版本
  169. /// </summary>
  170. /// <param name="id"></param>
  171. /// <param name="hid"></param>
  172. /// <returns></returns>
  173. [Route("{id:int}/history/{hid:int}"), ResponseCache(Duration = 600, VaryByQueryKeys = new[] { "id", "hid" }, VaryByHeader = "Cookie")]
  174. public async Task<ActionResult> HistoryVersion(int id, int hid)
  175. {
  176. var post = await PostHistoryVersionService.GetAsync(v => v.Id == hid && (v.Post.Status == Status.Published || CurrentUser.IsAdmin)) ?? throw new NotFoundException("文章未找到");
  177. CheckPermission(post.Post);
  178. post.Content = ReplaceVariables(post.Content);
  179. post.ProtectContent = ReplaceVariables(post.ProtectContent);
  180. var next = await PostHistoryVersionService.GetAsync(p => p.PostId == id && p.ModifyDate > post.ModifyDate, p => p.ModifyDate);
  181. var prev = await PostHistoryVersionService.GetAsync(p => p.PostId == id && p.ModifyDate < post.ModifyDate, p => p.ModifyDate, false);
  182. ViewBag.Next = next;
  183. ViewBag.Prev = prev;
  184. ViewBag.Ads = AdsService.GetByWeightedPrice(AdvertiseType.InPage, Request.Location(), post.CategoryId);
  185. return CurrentUser.IsAdmin ? View("HistoryVersion_Admin", post) : View(post);
  186. }
  187. /// <summary>
  188. /// 版本对比
  189. /// </summary>
  190. /// <param name="id"></param>
  191. /// <param name="v1"></param>
  192. /// <param name="v2"></param>
  193. /// <returns></returns>
  194. [Route("{id:int}/history/{v1:int}-{v2:int}"), ResponseCache(Duration = 600, VaryByQueryKeys = new[] { "id", "v1", "v2" }, VaryByHeader = "Cookie")]
  195. public async Task<ActionResult> CompareVersion(int id, int v1, int v2)
  196. {
  197. var post = await PostService.GetAsync(p => p.Id == id && (p.Status == Status.Published || CurrentUser.IsAdmin));
  198. var main = post.Mapper<PostHistoryVersion>() ?? throw new NotFoundException("文章未找到");
  199. CheckPermission(post);
  200. var left = v1 <= 0 ? main : await PostHistoryVersionService.GetAsync(v => v.Id == v1) ?? throw new NotFoundException("文章未找到");
  201. var right = v2 <= 0 ? main : await PostHistoryVersionService.GetAsync(v => v.Id == v2) ?? throw new NotFoundException("文章未找到");
  202. main.Id = id;
  203. var diff = new HtmlDiff.HtmlDiff(right.Content, left.Content);
  204. var diffOutput = diff.Build();
  205. right.Content = ReplaceVariables(Regex.Replace(Regex.Replace(diffOutput, "<ins.+?</ins>", string.Empty), @"<\w+></\w+>", string.Empty));
  206. left.Content = ReplaceVariables(Regex.Replace(Regex.Replace(diffOutput, "<del.+?</del>", string.Empty), @"<\w+></\w+>", string.Empty));
  207. ViewBag.Ads = AdsService.GetsByWeightedPrice(2, AdvertiseType.InPage, Request.Location(), main.CategoryId);
  208. ViewBag.DisableCopy = post.DisableCopy;
  209. return View(new[] { main, left, right });
  210. }
  211. /// <summary>
  212. /// 反对
  213. /// </summary>
  214. /// <param name="id"></param>
  215. /// <returns></returns>
  216. public async Task<ActionResult> VoteDown(int id)
  217. {
  218. if (HttpContext.Session.Get("post-vote" + id) != null)
  219. {
  220. return ResultData(null, false, "您刚才已经投过票了,感谢您的参与!");
  221. }
  222. var b = await PostService.GetQuery(p => p.Id == id).UpdateFromQueryAsync(p => new Post()
  223. {
  224. VoteDownCount = p.VoteDownCount + 1
  225. }) > 0;
  226. if (b)
  227. {
  228. HttpContext.Session.Set("post-vote" + id, id.GetBytes());
  229. }
  230. return ResultData(null, b, b ? "投票成功!" : "投票失败!");
  231. }
  232. /// <summary>
  233. /// 支持
  234. /// </summary>
  235. /// <param name="id"></param>
  236. /// <returns></returns>
  237. public async Task<ActionResult> VoteUp(int id)
  238. {
  239. if (HttpContext.Session.Get("post-vote" + id) != null)
  240. {
  241. return ResultData(null, false, "您刚才已经投过票了,感谢您的参与!");
  242. }
  243. var b = await PostService.GetQuery(p => p.Id == id).UpdateFromQueryAsync(p => new Post()
  244. {
  245. VoteUpCount = p.VoteUpCount + 1
  246. }) > 0;
  247. if (b)
  248. {
  249. HttpContext.Session.Set("post-vote" + id, id.GetBytes());
  250. }
  251. return ResultData(null, b, b ? "投票成功!" : "投票失败!");
  252. }
  253. /// <summary>
  254. /// 投稿页
  255. /// </summary>
  256. /// <returns></returns>
  257. public async Task<ActionResult> Publish()
  258. {
  259. var list = await CategoryService.GetQueryFromCacheAsync(c => c.Status == Status.Available);
  260. return View(list);
  261. }
  262. /// <summary>
  263. /// 发布投稿
  264. /// </summary>
  265. /// <param name="post"></param>
  266. /// <param name="code"></param>
  267. /// <param name="cancellationToken"></param>
  268. /// <returns></returns>
  269. [HttpPost, ValidateAntiForgeryToken]
  270. public async Task<ActionResult> Publish(PostCommand post, [Required(ErrorMessage = "验证码不能为空")] string code, CancellationToken cancellationToken)
  271. {
  272. if (await RedisHelper.GetAsync("code:" + post.Email) != code)
  273. {
  274. return ResultData(null, false, "验证码错误!");
  275. }
  276. if (PostService.Any(p => p.Status == Status.Forbidden && p.Email == post.Email))
  277. {
  278. return ResultData(null, false, "由于您曾经恶意投稿,该邮箱已经被标记为黑名单,无法进行投稿,如有疑问,请联系网站管理员进行处理。");
  279. }
  280. var match = Regex.Match(post.Title + post.Author + post.Content, CommonHelper.BanRegex);
  281. if (match.Success)
  282. {
  283. LogManager.Info($"提交内容:{post.Title}/{post.Author}/{post.Content},敏感词:{match.Value}");
  284. return ResultData(null, false, "您提交的内容包含敏感词,被禁止发表,请检查您的内容后尝试重新提交!");
  285. }
  286. if (!CategoryService.Any(c => c.Id == post.CategoryId))
  287. {
  288. return ResultData(null, message: "请选择一个分类");
  289. }
  290. post.Label = string.IsNullOrEmpty(post.Label?.Trim()) ? null : post.Label.Replace(",", ",");
  291. post.Status = Status.Pending;
  292. post.Content = await ImagebedClient.ReplaceImgSrc(await post.Content.HtmlSantinizerStandard().ClearImgAttributes(), cancellationToken);
  293. Post p = post.Mapper<Post>();
  294. p.IP = ClientIP;
  295. p.Modifier = p.Author;
  296. p.ModifierEmail = p.Email;
  297. p.DisableCopy = true;
  298. p = PostService.AddEntitySaved(p);
  299. if (p == null)
  300. {
  301. return ResultData(null, false, "文章发表失败!");
  302. }
  303. await RedisHelper.ExpireAsync("code:" + p.Email, 1);
  304. var content = new Template(await System.IO.File.ReadAllTextAsync(HostEnvironment.WebRootPath + "/template/publish.html"))
  305. .Set("link", Url.Action("Details", "Post", new { id = p.Id }, Request.Scheme))
  306. .Set("time", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"))
  307. .Set("title", p.Title).Render();
  308. BackgroundJob.Enqueue(() => CommonHelper.SendMail(CommonHelper.SystemSettings["Title"] + "有访客投稿:", content, CommonHelper.SystemSettings["ReceiveEmail"], ClientIP));
  309. return ResultData(p.Mapper<PostDto>(), message: "文章发表成功,待站长审核通过以后将显示到列表中!");
  310. }
  311. /// <summary>
  312. /// 获取标签
  313. /// </summary>
  314. /// <returns></returns>
  315. [ResponseCache(Duration = 600, VaryByHeader = "Cookie")]
  316. public ActionResult GetTag()
  317. {
  318. var list = PostService.GetQuery(p => !string.IsNullOrEmpty(p.Label)).Select(p => p.Label).Distinct().ToList().SelectMany(s => s.Split(',', ',')).GroupBy(s => s).Where(g => g.Count() > 1).OrderBy(s => s.Key).Select(g => g.Key).ToHashSet();
  319. return ResultData(list);
  320. }
  321. /// <summary>
  322. /// 标签云
  323. /// </summary>
  324. /// <returns></returns>
  325. [Route("all"), ResponseCache(Duration = 600, VaryByHeader = "Cookie")]
  326. public async Task<ActionResult> All()
  327. {
  328. var tags = PostService.GetQuery(p => !string.IsNullOrEmpty(p.Label)).Select(p => p.Label).Distinct().ToList().SelectMany(s => s.Split(',', ',')).GroupBy(t => t).Where(g => g.Count() > 1).OrderByDescending(g => g.Count()).ThenBy(g => g.Key).ToList(); //tag
  329. ViewBag.tags = tags;
  330. ViewBag.cats = await CategoryService.GetAll(c => c.Post.Count, false).Select(c => new TagCloudViewModel
  331. {
  332. Id = c.Id,
  333. Name = c.Name,
  334. Count = c.Post.Count(p => p.Status == Status.Published || CurrentUser.IsAdmin)
  335. }).ToListAsync(); //category
  336. ViewBag.seminars = await SeminarService.GetAll(c => c.Post.Count, false).Select(c => new TagCloudViewModel
  337. {
  338. Id = c.Id,
  339. Name = c.Title,
  340. Count = c.Post.Count(p => p.Status == Status.Published || CurrentUser.IsAdmin)
  341. }).ToListAsync(); //seminars
  342. return View();
  343. }
  344. /// <summary>
  345. /// 检查访问密码
  346. /// </summary>
  347. /// <param name="email"></param>
  348. /// <param name="token"></param>
  349. /// <returns></returns>
  350. [HttpPost, ValidateAntiForgeryToken, AllowAccessFirewall]
  351. public ActionResult CheckViewToken(string email, string token)
  352. {
  353. if (string.IsNullOrEmpty(token))
  354. {
  355. return ResultData(null, false, "请输入访问密码!");
  356. }
  357. var s = RedisHelper.Get("token:" + email);
  358. if (token.Equals(s))
  359. {
  360. HttpContext.Session.Set("AccessViewToken", token);
  361. Response.Cookies.Append("Email", email, new CookieOptions
  362. {
  363. Expires = DateTime.Now.AddYears(1),
  364. SameSite = SameSiteMode.Lax
  365. });
  366. Response.Cookies.Append("PostAccessToken", email.MDString3(AppConfig.BaiduAK), new CookieOptions
  367. {
  368. Expires = DateTime.Now.AddYears(1),
  369. SameSite = SameSiteMode.Lax
  370. });
  371. return ResultData(null);
  372. }
  373. return ResultData(null, false, "访问密码不正确!");
  374. }
  375. /// <summary>
  376. /// 检查授权邮箱
  377. /// </summary>
  378. /// <param name="email"></param>
  379. /// <returns></returns>
  380. [HttpPost, ValidateAntiForgeryToken, AllowAccessFirewall]
  381. public ActionResult GetViewToken(string email)
  382. {
  383. if (string.IsNullOrEmpty(email) || !email.MatchEmail().isMatch)
  384. {
  385. return ResultData(null, false, "请输入正确的邮箱!");
  386. }
  387. if (RedisHelper.Exists("get:" + email))
  388. {
  389. RedisHelper.Expire("get:" + email, 120);
  390. return ResultData(null, false, "发送频率限制,请在2分钟后重新尝试发送邮件!请检查你的邮件,若未收到,请检查你的邮箱地址或邮件垃圾箱!");
  391. }
  392. if (!UserInfoService.Any(b => b.Email.Equals(email)))
  393. {
  394. return ResultData(null, false, "您目前没有权限访问这个链接,请联系站长开通访问权限!");
  395. }
  396. var token = SnowFlake.GetInstance().GetUniqueShortId(6);
  397. RedisHelper.Set("token:" + email, token, 86400);
  398. BackgroundJob.Enqueue(() => CommonHelper.SendMail(Request.Host + "博客访问验证码", $"{Request.Host}本次验证码是:<span style='color:red'>{token}</span>,有效期为24h,请按时使用!", email, ClientIP));
  399. RedisHelper.Set("get:" + email, token, 120);
  400. return ResultData(null);
  401. }
  402. /// <summary>
  403. /// 文章合并
  404. /// </summary>
  405. /// <param name="id"></param>
  406. /// <returns></returns>
  407. [HttpGet("{id}/merge")]
  408. public async Task<ActionResult> PushMerge(int id)
  409. {
  410. var post = await PostService.GetByIdAsync(id) ?? throw new NotFoundException("文章未找到");
  411. return View(post);
  412. }
  413. /// <summary>
  414. /// 文章合并
  415. /// </summary>
  416. /// <param name="id"></param>
  417. /// <param name="mid"></param>
  418. /// <returns></returns>
  419. [HttpGet("{id}/merge/{mid}")]
  420. public async Task<ActionResult> RepushMerge(int id, int mid)
  421. {
  422. var post = await PostService.GetByIdAsync(id) ?? throw new NotFoundException("文章未找到");
  423. var merge = post.PostMergeRequests.FirstOrDefault(p => p.Id == mid && p.MergeState != MergeStatus.Merged) ?? throw new NotFoundException("待合并文章未找到");
  424. return View(merge);
  425. }
  426. /// <summary>
  427. /// 文章合并
  428. /// </summary>
  429. /// <param name="messageService"></param>
  430. /// <param name="postMergeRequestService"></param>
  431. /// <param name="dto"></param>
  432. /// <returns></returns>
  433. [HttpPost("{id}/pushmerge")]
  434. public async Task<ActionResult> PushMerge([FromServices] IInternalMessageService messageService, [FromServices] IPostMergeRequestService postMergeRequestService, PostMergeRequestCommand dto)
  435. {
  436. if (await RedisHelper.GetAsync("code:" + dto.ModifierEmail) != dto.Code)
  437. {
  438. return ResultData(null, false, "验证码错误!");
  439. }
  440. var post = await PostService.GetByIdAsync(dto.PostId) ?? throw new NotFoundException("文章未找到");
  441. var htmlDiff = new HtmlDiff.HtmlDiff(post.Content.RemoveHtmlTag(), dto.Content.RemoveHtmlTag());
  442. var diff = htmlDiff.Build();
  443. if (post.Title.Equals(dto.Title) && !diff.Contains(new[] { "diffmod", "diffdel", "diffins" }))
  444. {
  445. return ResultData(null, false, "内容未被修改!");
  446. }
  447. #region 合并验证
  448. if (postMergeRequestService.Any(p => p.ModifierEmail == dto.ModifierEmail && p.MergeState == MergeStatus.Block))
  449. {
  450. return ResultData(null, false, "由于您曾经多次恶意修改文章,已经被标记为黑名单,无法修改任何文章,如有疑问,请联系网站管理员进行处理。");
  451. }
  452. if (post.PostMergeRequests.Any(p => p.ModifierEmail == dto.ModifierEmail && p.MergeState == MergeStatus.Pending))
  453. {
  454. return ResultData(null, false, "您已经提交过一次修改请求正在待处理,暂不能继续提交修改请求!");
  455. }
  456. #endregion
  457. #region 直接合并
  458. if (post.Email.Equals(dto.ModifierEmail))
  459. {
  460. var history = post.Mapper<PostHistoryVersion>();
  461. Mapper.Map(dto, post);
  462. post.PostHistoryVersion.Add(history);
  463. post.ModifyDate = DateTime.Now;
  464. return await PostService.SaveChangesAsync() > 0 ? ResultData(null, true, "你是文章原作者,无需审核,文章已自动更新并在首页展示!") : ResultData(null, false, "操作失败!");
  465. }
  466. #endregion
  467. var merge = post.PostMergeRequests.FirstOrDefault(r => r.Id == dto.Id && r.MergeState != MergeStatus.Merged);
  468. if (merge != null)
  469. {
  470. Mapper.Map(dto, merge);
  471. merge.SubmitTime = DateTime.Now;
  472. merge.MergeState = MergeStatus.Pending;
  473. }
  474. else
  475. {
  476. merge = Mapper.Map<PostMergeRequest>(dto);
  477. merge.SubmitTime = DateTime.Now;
  478. post.PostMergeRequests.Add(merge);
  479. }
  480. var b = await PostService.SaveChangesAsync() > 0;
  481. if (!b)
  482. {
  483. return ResultData(null, false, "操作失败!");
  484. }
  485. await RedisHelper.ExpireAsync("code:" + dto.ModifierEmail, 1);
  486. await messageService.AddEntitySavedAsync(new InternalMessage()
  487. {
  488. Title = $"来自【{dto.Modifier}】对文章《{post.Title}》的修改请求",
  489. Content = dto.Title,
  490. Link = "#/merge/compare?id=" + merge.Id
  491. });
  492. var content = new Template(await System.IO.File.ReadAllTextAsync(HostEnvironment.WebRootPath + "/template/merge-request.html"))
  493. .Set("title", post.Title)
  494. .Set("link", Url.Action("Index", "Dashboard", new { }, Request.Scheme) + "#/merge/compare?id=" + merge.Id)
  495. .Set("diff", diff)
  496. .Set("host", "//" + Request.Host)
  497. .Set("id", merge.Id.ToString())
  498. .Render();
  499. BackgroundJob.Enqueue(() => CommonHelper.SendMail("博客文章修改请求:", content, CommonHelper.SystemSettings["ReceiveEmail"], ClientIP));
  500. return ResultData(null, true, "您的修改请求已提交,已进入审核状态,感谢您的参与!");
  501. }
  502. #region 后端管理
  503. /// <summary>
  504. /// 固顶
  505. /// </summary>
  506. /// <param name="id"></param>
  507. /// <returns></returns>
  508. [MyAuthorize]
  509. public async Task<ActionResult> Fixtop(int id)
  510. {
  511. Post post = await PostService.GetByIdAsync(id) ?? throw new NotFoundException("文章未找到");
  512. post.IsFixedTop = !post.IsFixedTop;
  513. bool b = await PostService.SaveChangesAsync() > 0;
  514. return b ? ResultData(null, true, post.IsFixedTop ? "置顶成功!" : "取消置顶成功!") : ResultData(null, false, "操作失败!");
  515. }
  516. /// <summary>
  517. /// 审核
  518. /// </summary>
  519. /// <param name="id"></param>
  520. /// <returns></returns>
  521. [MyAuthorize]
  522. public async Task<ActionResult> Pass(int id)
  523. {
  524. var post = await PostService.GetByIdAsync(id) ?? throw new NotFoundException("文章未找到");
  525. post.Status = Status.Published;
  526. post.ModifyDate = DateTime.Now;
  527. post.PostDate = DateTime.Now;
  528. var b = await PostService.SaveChangesAsync() > 0;
  529. if (!b)
  530. {
  531. return ResultData(null, false, "审核失败!");
  532. }
  533. var js = new JiebaSegmenter();
  534. (post.Keyword + "," + post.Label).Split(',', StringSplitOptions.RemoveEmptyEntries).ForEach(s => js.AddWord(s));
  535. SearchEngine.LuceneIndexer.Add(post);
  536. return ResultData(null, true, "审核通过!");
  537. }
  538. /// <summary>
  539. /// 删除
  540. /// </summary>
  541. /// <param name="id"></param>
  542. /// <returns></returns>
  543. [MyAuthorize]
  544. public async Task<ActionResult> Delete(int id)
  545. {
  546. var post = await PostService.GetByIdAsync(id) ?? throw new NotFoundException("文章未找到");
  547. post.Status = Status.Deleted;
  548. bool b = await PostService.SaveChangesAsync(true) > 0;
  549. SearchEngine.LuceneIndexer.Delete(post);
  550. return ResultData(null, b, b ? "删除成功!" : "删除失败!");
  551. }
  552. /// <summary>
  553. /// 还原版本
  554. /// </summary>
  555. /// <param name="id"></param>
  556. /// <returns></returns>
  557. [MyAuthorize]
  558. public async Task<ActionResult> Restore(int id)
  559. {
  560. var post = await PostService.GetByIdAsync(id) ?? throw new NotFoundException("文章未找到");
  561. post.Status = Status.Published;
  562. bool b = await PostService.SaveChangesAsync() > 0;
  563. SearchEngine.LuceneIndexer.Add(post);
  564. return ResultData(null, b, b ? "恢复成功!" : "恢复失败!");
  565. }
  566. /// <summary>
  567. /// 彻底删除文章
  568. /// </summary>
  569. /// <param name="id"></param>
  570. /// <returns></returns>
  571. [MyAuthorize]
  572. public ActionResult Truncate(int id)
  573. {
  574. bool b = PostService - id;
  575. return ResultData(null, b, b ? "删除成功!" : "删除失败!");
  576. }
  577. /// <summary>
  578. /// 获取文章
  579. /// </summary>
  580. /// <param name="id"></param>
  581. /// <returns></returns>
  582. [MyAuthorize]
  583. public ActionResult Get(int id)
  584. {
  585. Post post = PostService[id] ?? throw new NotFoundException("文章未找到");
  586. PostDto model = post.Mapper<PostDto>();
  587. model.Seminars = post.Seminar.Select(s => s.Title).Join(",");
  588. return ResultData(model);
  589. }
  590. /// <summary>
  591. /// 获取文章分页
  592. /// </summary>
  593. /// <returns></returns>
  594. [MyAuthorize]
  595. public ActionResult GetPageData([FromServices] ICacheManager<HashSet<string>> cacheManager, [Range(1, int.MaxValue, ErrorMessage = "页数必须大于0")] int page = 1, [Range(1, int.MaxValue, ErrorMessage = "页大小必须大于0")] int size = 10, OrderBy orderby = OrderBy.ModifyDate, string kw = "", int? cid = null)
  596. {
  597. Expression<Func<Post, bool>> where = p => true;
  598. if (cid.HasValue)
  599. {
  600. where = where.And(p => p.CategoryId == cid.Value);
  601. }
  602. if (!string.IsNullOrEmpty(kw))
  603. {
  604. where = where.And(p => p.Title.Contains(kw) || p.Author.Contains(kw) || p.Email.Contains(kw) || p.Label.Contains(kw) || p.Content.Contains(kw));
  605. }
  606. var list = PostService.GetQuery(where).OrderBy($"{nameof(Post.Status)} desc,{nameof(Post.IsFixedTop)} desc,{orderby.GetDisplay()} desc").ToPagedList<Post, PostDataModel>(page, size, MapperConfig);
  607. foreach (var item in list.Data)
  608. {
  609. item.ModifyDate = item.ModifyDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
  610. item.PostDate = item.PostDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
  611. item.Online = cacheManager.Get(nameof(PostOnline) + ":" + item.Id)?.Count ?? 0;
  612. }
  613. return Ok(list);
  614. }
  615. /// <summary>
  616. /// 获取未审核文章
  617. /// </summary>
  618. /// <param name="page"></param>
  619. /// <param name="size"></param>
  620. /// <param name="search"></param>
  621. /// <returns></returns>
  622. [MyAuthorize]
  623. public async Task<ActionResult> GetPending([Range(1, int.MaxValue, ErrorMessage = "页码必须大于0")] int page = 1, [Range(1, 50, ErrorMessage = "页大小必须在0到50之间")] int size = 15, string search = "")
  624. {
  625. Expression<Func<Post, bool>> where = p => p.Status == Status.Pending;
  626. if (!string.IsNullOrEmpty(search))
  627. {
  628. where = where.And(p => p.Title.Contains(search) || p.Author.Contains(search) || p.Email.Contains(search) || p.Label.Contains(search));
  629. }
  630. var pages = await PostService.GetQuery(where).OrderByDescending(p => p.IsFixedTop).ThenByDescending(p => p.ModifyDate).ToCachedPagedListAsync<Post, PostDataModel>(page, size, MapperConfig);
  631. foreach (var item in pages.Data)
  632. {
  633. item.ModifyDate = item.ModifyDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
  634. item.PostDate = item.PostDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
  635. }
  636. return Ok(pages);
  637. }
  638. /// <summary>
  639. /// 编辑
  640. /// </summary>
  641. /// <param name="post"></param>
  642. /// <param name="reserve">是否保留历史版本</param>
  643. /// <param name="cancellationToken"></param>
  644. /// <returns></returns>
  645. [HttpPost, MyAuthorize]
  646. public async Task<ActionResult> Edit(PostCommand post, bool reserve = true, CancellationToken cancellationToken = default)
  647. {
  648. post.Content = await ImagebedClient.ReplaceImgSrc(await post.Content.Trim().ClearImgAttributes(), cancellationToken);
  649. if (!ValidatePost(post, out var resultData))
  650. {
  651. return resultData;
  652. }
  653. Post p = await PostService.GetByIdAsync(post.Id);
  654. if (reserve && p.Status == Status.Published)
  655. {
  656. var context = BrowsingContext.New(Configuration.Default);
  657. var doc1 = await context.OpenAsync(req => req.Content(p.Content), cancellationToken);
  658. var doc2 = await context.OpenAsync(req => req.Content(post.Content), cancellationToken);
  659. if (doc1.Body.TextContent != doc2.Body.TextContent)
  660. {
  661. var history = p.Mapper<PostHistoryVersion>();
  662. p.PostHistoryVersion.Add(history);
  663. }
  664. p.ModifyDate = DateTime.Now;
  665. var user = HttpContext.Session.Get<UserInfoDto>(SessionKey.UserInfo);
  666. p.Modifier = user.NickName;
  667. p.ModifierEmail = user.Email;
  668. }
  669. p.IP = ClientIP;
  670. Mapper.Map(post, p);
  671. if (!string.IsNullOrEmpty(post.Seminars))
  672. {
  673. var tmp = post.Seminars.Split(',').Distinct();
  674. p.Seminar.Clear();
  675. foreach (var s in tmp)
  676. {
  677. var seminar = await SeminarService.GetAsync(e => e.Title.Equals(s));
  678. if (seminar != null)
  679. {
  680. p.Seminar.Add(seminar);
  681. }
  682. }
  683. }
  684. var js = new JiebaSegmenter();
  685. (p.Keyword + "," + p.Label).Split(',', StringSplitOptions.RemoveEmptyEntries).ForEach(s => js.AddWord(s));
  686. bool b = await SearchEngine.SaveChangesAsync() > 0;
  687. if (!b)
  688. {
  689. return ResultData(null, false, "文章修改失败!");
  690. }
  691. return ResultData(p.Mapper<PostDto>(), message: "文章修改成功!");
  692. }
  693. /// <summary>
  694. /// 发布
  695. /// </summary>
  696. /// <param name="post"></param>
  697. /// <param name="timespan"></param>
  698. /// <param name="schedule"></param>
  699. /// <returns></returns>
  700. [MyAuthorize, HttpPost]
  701. public async Task<ActionResult> Write(PostCommand post, DateTime? timespan, bool schedule = false, CancellationToken cancellationToken = default)
  702. {
  703. post.Content = await ImagebedClient.ReplaceImgSrc(await post.Content.Trim().ClearImgAttributes(), cancellationToken);
  704. if (!ValidatePost(post, out var resultData))
  705. {
  706. return resultData;
  707. }
  708. post.Status = Status.Published;
  709. Post p = post.Mapper<Post>();
  710. p.Rss = true;
  711. p.Modifier = p.Author;
  712. p.ModifierEmail = p.Email;
  713. p.IP = ClientIP;
  714. if (!string.IsNullOrEmpty(post.Seminars))
  715. {
  716. var tmp = post.Seminars.Split(',').Distinct();
  717. foreach (var s in tmp)
  718. {
  719. var id = s.ToInt32();
  720. Seminar seminar = await SeminarService.GetByIdAsync(id);
  721. p.Seminar.Add(seminar);
  722. }
  723. }
  724. if (schedule)
  725. {
  726. if (!timespan.HasValue || timespan.Value <= DateTime.Now)
  727. {
  728. return ResultData(null, false, "如果要定时发布,请选择正确的一个将来时间点!");
  729. }
  730. p.Status = Status.Schedule;
  731. p.PostDate = timespan.Value.ToUniversalTime();
  732. p.ModifyDate = timespan.Value.ToUniversalTime();
  733. HangfireHelper.CreateJob(typeof(IHangfireBackJob), nameof(HangfireBackJob.PublishPost), args: p);
  734. return ResultData(p.Mapper<PostDto>(), message: $"文章于{timespan.Value:yyyy-MM-dd HH:mm:ss}将会自动发表!");
  735. }
  736. PostService.AddEntity(p);
  737. var js = new JiebaSegmenter();
  738. (p.Keyword + "," + p.Label).Split(',', StringSplitOptions.RemoveEmptyEntries).ForEach(s => js.AddWord(s));
  739. bool b = await SearchEngine.SaveChangesAsync() > 0;
  740. if (!b)
  741. {
  742. return ResultData(null, false, "文章发表失败!");
  743. }
  744. return ResultData(null, true, "文章发表成功!");
  745. }
  746. private bool ValidatePost(PostCommand post, out ActionResult resultData)
  747. {
  748. if (!CategoryService.Any(c => c.Id == post.CategoryId && c.Status == Status.Available))
  749. {
  750. resultData = ResultData(null, false, "请选择一个分类");
  751. return false;
  752. }
  753. switch (post.LimitMode)
  754. {
  755. case RegionLimitMode.AllowRegion:
  756. case RegionLimitMode.ForbidRegion:
  757. if (string.IsNullOrEmpty(post.Regions))
  758. {
  759. resultData = ResultData(null, false, "请输入限制的地区");
  760. return false;
  761. }
  762. break;
  763. case RegionLimitMode.AllowRegionExceptForbidRegion:
  764. case RegionLimitMode.ForbidRegionExceptAllowRegion:
  765. if (string.IsNullOrEmpty(post.ExceptRegions))
  766. {
  767. resultData = ResultData(null, false, "请输入排除的地区");
  768. return false;
  769. }
  770. goto case RegionLimitMode.AllowRegion;
  771. }
  772. if (string.IsNullOrEmpty(post.Label?.Trim()) || post.Label.Equals("null"))
  773. {
  774. post.Label = null;
  775. }
  776. else if (post.Label.Trim().Length > 50)
  777. {
  778. post.Label = post.Label.Replace(",", ",");
  779. post.Label = post.Label.Trim().Substring(0, 50);
  780. }
  781. else
  782. {
  783. post.Label = post.Label.Replace(",", ",");
  784. }
  785. if (string.IsNullOrEmpty(post.ProtectContent?.RemoveHtmlTag()) || post.ProtectContent.Equals("null"))
  786. {
  787. post.ProtectContent = null;
  788. }
  789. resultData = null;
  790. return true;
  791. }
  792. /// <summary>
  793. /// 添加专题
  794. /// </summary>
  795. /// <param name="id"></param>
  796. /// <param name="sid"></param>
  797. /// <returns></returns>
  798. [MyAuthorize]
  799. public async Task<ActionResult> AddSeminar(int id, int sid)
  800. {
  801. var post = await PostService.GetByIdAsync(id) ?? throw new NotFoundException("文章未找到");
  802. Seminar seminar = await SeminarService.GetByIdAsync(sid) ?? throw new NotFoundException("专题未找到");
  803. post.Seminar.Add(seminar);
  804. bool b = await PostService.SaveChangesAsync() > 0;
  805. return ResultData(null, b, b ? $"已将文章【{post.Title}】添加到专题【{seminar.Title}】" : "添加失败");
  806. }
  807. /// <summary>
  808. /// 移除专题
  809. /// </summary>
  810. /// <param name="id"></param>
  811. /// <param name="sid"></param>
  812. /// <returns></returns>
  813. [MyAuthorize]
  814. public async Task<ActionResult> RemoveSeminar(int id, int sid)
  815. {
  816. var post = await PostService.GetByIdAsync(id) ?? throw new NotFoundException("文章未找到");
  817. Seminar seminar = await SeminarService.GetByIdAsync(sid) ?? throw new NotFoundException("专题未找到");
  818. post.Seminar.Remove(seminar);
  819. bool b = await PostService.SaveChangesAsync() > 0;
  820. return ResultData(null, b, b ? $"已将文章【{post.Title}】从【{seminar.Title}】专题移除" : "添加失败");
  821. }
  822. /// <summary>
  823. /// 删除历史版本
  824. /// </summary>
  825. /// <param name="id"></param>
  826. /// <returns></returns>
  827. [MyAuthorize]
  828. public async Task<ActionResult> DeleteHistory(int id)
  829. {
  830. bool b = await PostHistoryVersionService.DeleteByIdAsync(id) > 0;
  831. return ResultData(null, b, b ? "历史版本文章删除成功!" : "历史版本文章删除失败!");
  832. }
  833. /// <summary>
  834. /// 还原版本
  835. /// </summary>
  836. /// <param name="id"></param>
  837. /// <returns></returns>
  838. [MyAuthorize]
  839. public async Task<ActionResult> Revert(int id)
  840. {
  841. var history = await PostHistoryVersionService.GetByIdAsync(id) ?? throw new NotFoundException("版本不存在");
  842. history.Post.Category = history.Category;
  843. history.Post.CategoryId = history.CategoryId;
  844. history.Post.Content = history.Content;
  845. history.Post.Title = history.Title;
  846. history.Post.Label = history.Label;
  847. history.Post.ModifyDate = history.ModifyDate;
  848. history.Post.Seminar.Clear();
  849. foreach (var s in history.Seminar)
  850. {
  851. history.Post.Seminar.Add(s);
  852. }
  853. bool b = await SearchEngine.SaveChangesAsync() > 0;
  854. await PostHistoryVersionService.DeleteByIdAsync(id);
  855. return ResultData(null, b, b ? "回滚成功" : "回滚失败");
  856. }
  857. /// <summary>
  858. /// 禁用或开启文章评论
  859. /// </summary>
  860. /// <param name="id">文章id</param>
  861. /// <returns></returns>
  862. [MyAuthorize]
  863. public async Task<ActionResult> DisableComment(int id)
  864. {
  865. var post = await PostService.GetByIdAsync(id) ?? throw new NotFoundException("文章未找到");
  866. post.DisableComment = !post.DisableComment;
  867. return ResultData(null, await PostService.SaveChangesAsync() > 0, post.DisableComment ? $"已禁用【{post.Title}】这篇文章的评论功能!" : $"已启用【{post.Title}】这篇文章的评论功能!");
  868. }
  869. /// <summary>
  870. /// 禁用或开启文章评论
  871. /// </summary>
  872. /// <param name="id">文章id</param>
  873. /// <returns></returns>
  874. [MyAuthorize]
  875. public async Task<ActionResult> DisableCopy(int id)
  876. {
  877. var post = await PostService.GetByIdAsync(id) ?? throw new NotFoundException("文章未找到");
  878. post.DisableCopy = !post.DisableCopy;
  879. return ResultData(null, await PostService.SaveChangesAsync() > 0, post.DisableCopy ? $"已开启【{post.Title}】这篇文章的防复制功能!" : $"已关闭【{post.Title}】这篇文章的防复制功能!");
  880. }
  881. /// <summary>
  882. /// 刷新文章
  883. /// </summary>
  884. /// <param name="id">文章id</param>
  885. /// <returns></returns>
  886. [MyAuthorize]
  887. public async Task<ActionResult> Refresh(int id)
  888. {
  889. await PostService.GetQuery(p => p.Id == id).UpdateFromQueryAsync(p => new Post()
  890. {
  891. ModifyDate = DateTime.Now
  892. });
  893. return RedirectToAction("Details", new { id });
  894. }
  895. /// <summary>
  896. /// 标记为恶意修改
  897. /// </summary>
  898. /// <param name="id"></param>
  899. /// <returns></returns>
  900. [MyAuthorize]
  901. [HttpPost("post/block/{id}")]
  902. public async Task<ActionResult> Block(int id)
  903. {
  904. var b = await PostService.GetQuery(p => p.Id == id).UpdateFromQueryAsync(p => new Post()
  905. {
  906. Status = Status.Forbidden
  907. }) > 0;
  908. return b ? ResultData(null, true, "操作成功!") : ResultData(null, false, "操作失败!");
  909. }
  910. /// <summary>
  911. /// 切换允许rss订阅
  912. /// </summary>
  913. /// <param name="id"></param>
  914. /// <returns></returns>
  915. [MyAuthorize]
  916. [HttpPost("post/{id}/rss-switch")]
  917. public async Task<ActionResult> RssSwitch(int id)
  918. {
  919. await PostService.GetQuery(p => p.Id == id).UpdateFromQueryAsync(p => new Post()
  920. {
  921. Rss = !p.Rss
  922. });
  923. return ResultData(null, message: "操作成功");
  924. }
  925. /// <summary>
  926. /// 文章统计
  927. /// </summary>
  928. /// <returns></returns>
  929. [MyAuthorize]
  930. public async Task<IActionResult> Statistic()
  931. {
  932. var keys = await RedisHelper.KeysAsync(nameof(PostOnline) + ":*");
  933. var sets = await keys.SelectAsync(async s => (Id: s.Split(':')[1].ToInt32(), Clients: await RedisHelper.HGetAsync<HashSet<string>>(s, "value")));
  934. var ids = sets.Where(t => t.Clients.Count > 0).OrderByDescending(t => t.Clients.Count).Take(10).Select(t => t.Id).ToArray();
  935. var mostHots = await PostService.GetQuery<PostModelBase>(p => ids.Contains(p.Id)).Cacheable().ToListAsync().ContinueWith(t =>
  936. {
  937. foreach (var item in t.Result)
  938. {
  939. item.ViewCount = sets.FirstOrDefault(t => t.Id == item.Id).Clients.Count;
  940. }
  941. return t.Result.OrderByDescending(p => p.ViewCount);
  942. });
  943. var postsQuery = PostService.GetQuery(p => p.Status == Status.Published);
  944. var mostView = await postsQuery.OrderByDescending(p => p.TotalViewCount).Take(10).Select(p => new PostModelBase()
  945. {
  946. Id = p.Id,
  947. Title = p.Title,
  948. ViewCount = p.TotalViewCount
  949. }).Cacheable().ToListAsync();
  950. var mostAverage = await postsQuery.OrderByDescending(p => p.AverageViewCount).Take(10).Select(p => new PostModelBase()
  951. {
  952. Id = p.Id,
  953. Title = p.Title,
  954. ViewCount = (int)p.AverageViewCount
  955. }).Cacheable().ToListAsync();
  956. return ResultData(new
  957. {
  958. mostHots,
  959. mostView,
  960. mostAverage
  961. });
  962. }
  963. #endregion
  964. }
  965. }