PostController.cs 42 KB

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