PostController.cs 48 KB

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