PostController.cs 47 KB

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