PostController.cs 48 KB

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