using AngleSharp;
using CacheManager.Core;
using EFCoreSecondLevelCacheInterceptor;
using Hangfire;
using JiebaNet.Segmenter;
using Masuit.LuceneEFCore.SearchEngine.Interfaces;
using Masuit.MyBlogs.Core.Common;
using Masuit.MyBlogs.Core.Configs;
using Masuit.MyBlogs.Core.Extensions;
using Masuit.MyBlogs.Core.Extensions.Firewall;
using Masuit.MyBlogs.Core.Extensions.Hangfire;
using Masuit.MyBlogs.Core.Infrastructure;
using Masuit.MyBlogs.Core.Infrastructure.Repository;
using Masuit.MyBlogs.Core.Infrastructure.Services.Interface;
using Masuit.MyBlogs.Core.Models.Command;
using Masuit.MyBlogs.Core.Models.DTO;
using Masuit.MyBlogs.Core.Models.Entity;
using Masuit.MyBlogs.Core.Models.Enum;
using Masuit.MyBlogs.Core.Models.ViewModel;
using Masuit.MyBlogs.Core.Views.Post;
using Masuit.Tools;
using Masuit.Tools.Core.Net;
using Masuit.Tools.Html;
using Masuit.Tools.Linq;
using Masuit.Tools.Logging;
using Masuit.Tools.Security;
using Masuit.Tools.Strings;
using Masuit.Tools.Systems;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Net.Http.Headers;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Linq.Dynamic.Core;
using System.Linq.Expressions;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using SameSiteMode = Microsoft.AspNetCore.Http.SameSiteMode;
namespace Masuit.MyBlogs.Core.Controllers
{
///
/// 文章管理
///
public class PostController : BaseController
{
public IPostService PostService { get; set; }
public ICategoryService CategoryService { get; set; }
public ISeminarService SeminarService { get; set; }
public IPostHistoryVersionService PostHistoryVersionService { get; set; }
public IWebHostEnvironment HostEnvironment { get; set; }
public ISearchEngine SearchEngine { get; set; }
public ImagebedClient ImagebedClient { get; set; }
///
/// 文章详情页
///
///
///
///
[Route("{id:int}"), Route("{id:int}/comments/{cid:int}"), ResponseCache(Duration = 600, VaryByHeader = "Cookie")]
public async Task Details(int id, string kw)
{
var post = await PostService.GetAsync(p => p.Id == id && (p.Status == Status.Published || CurrentUser.IsAdmin)) ?? throw new NotFoundException("文章未找到");
CheckPermission(post);
ViewBag.Keyword = post.Keyword + "," + post.Label;
ViewBag.Desc = await post.Content.GetSummary(200);
var modifyDate = post.ModifyDate;
ViewBag.Next = await PostService.GetFromCacheAsync(p => p.ModifyDate > modifyDate && (p.LimitMode ?? 0) == RegionLimitMode.All && (p.Status == Status.Published || CurrentUser.IsAdmin), p => p.ModifyDate);
ViewBag.Prev = await PostService.GetFromCacheAsync(p => p.ModifyDate < modifyDate && (p.LimitMode ?? 0) == RegionLimitMode.All && (p.Status == Status.Published || CurrentUser.IsAdmin), p => p.ModifyDate, false);
if (!string.IsNullOrEmpty(kw))
{
ViewData["keywords"] = post.Content.Contains(kw) ? $"['{kw}']" : SearchEngine.LuceneIndexSearcher.CutKeywords(kw).ToJsonString();
}
ViewBag.Ads = AdsService.GetByWeightedPrice(AdvertiseType.InPage, Request.Location(), post.CategoryId);
var regex = SearchEngine.LuceneIndexSearcher.CutKeywords(string.IsNullOrWhiteSpace(post.Keyword + post.Label) ? post.Title : post.Keyword + post.Label).Join("|");
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);
ViewBag.Related = related;
post.ModifyDate = post.ModifyDate.ToTimeZone(HttpContext.Session.Get(SessionKey.TimeZone));
post.PostDate = post.PostDate.ToTimeZone(HttpContext.Session.Get(SessionKey.TimeZone));
post.Content = ReplaceVariables(post.Content);
post.ProtectContent = ReplaceVariables(post.ProtectContent);
if (CurrentUser.IsAdmin)
{
return View("Details_Admin", post);
}
if (!HttpContext.Request.IsRobot() && string.IsNullOrEmpty(HttpContext.Session.Get("post" + id)))
{
HangfireHelper.CreateJob(typeof(IHangfireBackJob), nameof(HangfireBackJob.RecordPostVisit), args: id);
HttpContext.Session.Set("post" + id, id.ToString());
}
return View(post);
}
private void CheckPermission(Post post)
{
var location = Request.Location() + "|" + Request.Headers[HeaderNames.UserAgent];
switch (post.LimitMode)
{
case RegionLimitMode.AllowRegion:
if (!location.Contains(post.Regions.Split(',', StringSplitOptions.RemoveEmptyEntries)) && !CurrentUser.IsAdmin && !VisitorTokenValid && !Request.IsRobot())
{
Disallow(post);
}
break;
case RegionLimitMode.ForbidRegion:
if (location.Contains(post.Regions.Split(',', StringSplitOptions.RemoveEmptyEntries)) && !CurrentUser.IsAdmin && !VisitorTokenValid && !Request.IsRobot())
{
Disallow(post);
}
break;
case RegionLimitMode.AllowRegionExceptForbidRegion:
if (location.Contains(post.ExceptRegions.Split(',', StringSplitOptions.RemoveEmptyEntries)) && !CurrentUser.IsAdmin && !VisitorTokenValid)
{
Disallow(post);
}
goto case RegionLimitMode.AllowRegion;
case RegionLimitMode.ForbidRegionExceptAllowRegion:
if (location.Contains(post.ExceptRegions.Split(',', StringSplitOptions.RemoveEmptyEntries)) && !CurrentUser.IsAdmin && !VisitorTokenValid)
{
break;
}
goto case RegionLimitMode.ForbidRegion;
}
}
private void Disallow(Post post)
{
RedisHelper.IncrBy("interceptCount");
RedisHelper.LPush("intercept", new IpIntercepter()
{
IP = ClientIP,
RequestUrl = $"//{Request.Host}/{post.Id}",
Referer = Request.Headers[HeaderNames.Referer],
Time = DateTime.Now,
UserAgent = Request.Headers[HeaderNames.UserAgent],
Remark = "无权限查看该文章",
Address = Request.Location()
});
throw new NotFoundException("文章未找到");
}
///
/// 文章历史版本
///
///
///
///
///
[Route("{id:int}/history"), ResponseCache(Duration = 600, VaryByQueryKeys = new[] { "id", "page", "size" }, VaryByHeader = "Cookie")]
public async Task History(int id, [Range(1, int.MaxValue, ErrorMessage = "页码必须大于0")] int page = 1, [Range(1, 50, ErrorMessage = "页大小必须在0到50之间")] int size = 20)
{
var post = await PostService.GetAsync(p => p.Id == id && (p.Status == Status.Published || CurrentUser.IsAdmin)) ?? throw new NotFoundException("文章未找到");
CheckPermission(post);
ViewBag.Primary = post;
var list = await PostHistoryVersionService.GetPagesAsync(page, size, v => v.PostId == id, v => v.ModifyDate, false);
foreach (var item in list.Data)
{
item.ModifyDate = item.ModifyDate.ToTimeZone(HttpContext.Session.Get(SessionKey.TimeZone));
}
ViewBag.Ads = AdsService.GetByWeightedPrice(AdvertiseType.InPage, Request.Location(), post.CategoryId);
return View(list);
}
///
/// 文章历史版本
///
///
///
///
[Route("{id:int}/history/{hid:int}"), ResponseCache(Duration = 600, VaryByQueryKeys = new[] { "id", "hid" }, VaryByHeader = "Cookie")]
public async Task HistoryVersion(int id, int hid)
{
var post = await PostHistoryVersionService.GetAsync(v => v.Id == hid && (v.Post.Status == Status.Published || CurrentUser.IsAdmin)) ?? throw new NotFoundException("文章未找到");
CheckPermission(post.Post);
post.Content = ReplaceVariables(post.Content);
post.ProtectContent = ReplaceVariables(post.ProtectContent);
var next = await PostHistoryVersionService.GetAsync(p => p.PostId == id && p.ModifyDate > post.ModifyDate, p => p.ModifyDate);
var prev = await PostHistoryVersionService.GetAsync(p => p.PostId == id && p.ModifyDate < post.ModifyDate, p => p.ModifyDate, false);
ViewBag.Next = next;
ViewBag.Prev = prev;
ViewBag.Ads = AdsService.GetByWeightedPrice(AdvertiseType.InPage, Request.Location(), post.CategoryId);
return CurrentUser.IsAdmin ? View("HistoryVersion_Admin", post) : View(post);
}
///
/// 版本对比
///
///
///
///
///
[Route("{id:int}/history/{v1:int}-{v2:int}"), ResponseCache(Duration = 600, VaryByQueryKeys = new[] { "id", "v1", "v2" }, VaryByHeader = "Cookie")]
public async Task CompareVersion(int id, int v1, int v2)
{
var post = await PostService.GetAsync(p => p.Id == id && (p.Status == Status.Published || CurrentUser.IsAdmin));
var main = post.Mapper() ?? throw new NotFoundException("文章未找到");
CheckPermission(post);
var left = v1 <= 0 ? main : await PostHistoryVersionService.GetAsync(v => v.Id == v1) ?? throw new NotFoundException("文章未找到");
var right = v2 <= 0 ? main : await PostHistoryVersionService.GetAsync(v => v.Id == v2) ?? throw new NotFoundException("文章未找到");
main.Id = id;
var diff = new HtmlDiff.HtmlDiff(right.Content, left.Content);
var diffOutput = diff.Build();
right.Content = ReplaceVariables(Regex.Replace(Regex.Replace(diffOutput, "", string.Empty), @"<\w+>\w+>", string.Empty));
left.Content = ReplaceVariables(Regex.Replace(Regex.Replace(diffOutput, "", string.Empty), @"<\w+>\w+>", string.Empty));
ViewBag.Ads = AdsService.GetsByWeightedPrice(2, AdvertiseType.InPage, Request.Location(), main.CategoryId);
ViewBag.DisableCopy = post.DisableCopy;
return View(new[] { main, left, right });
}
///
/// 反对
///
///
///
public async Task VoteDown(int id)
{
if (HttpContext.Session.Get("post-vote" + id) != null)
{
return ResultData(null, false, "您刚才已经投过票了,感谢您的参与!");
}
var b = await PostService.GetQuery(p => p.Id == id).UpdateFromQueryAsync(p => new Post()
{
VoteDownCount = p.VoteDownCount + 1
}) > 0;
if (b)
{
HttpContext.Session.Set("post-vote" + id, id.GetBytes());
}
return ResultData(null, b, b ? "投票成功!" : "投票失败!");
}
///
/// 支持
///
///
///
public async Task VoteUp(int id)
{
if (HttpContext.Session.Get("post-vote" + id) != null)
{
return ResultData(null, false, "您刚才已经投过票了,感谢您的参与!");
}
var b = await PostService.GetQuery(p => p.Id == id).UpdateFromQueryAsync(p => new Post()
{
VoteUpCount = p.VoteUpCount + 1
}) > 0;
if (b)
{
HttpContext.Session.Set("post-vote" + id, id.GetBytes());
}
return ResultData(null, b, b ? "投票成功!" : "投票失败!");
}
///
/// 投稿页
///
///
public async Task Publish()
{
var list = await CategoryService.GetQueryFromCacheAsync(c => c.Status == Status.Available);
return View(list);
}
///
/// 发布投稿
///
///
///
///
///
[HttpPost, ValidateAntiForgeryToken]
public async Task Publish(PostCommand post, [Required(ErrorMessage = "验证码不能为空")] string code, CancellationToken cancellationToken)
{
if (await RedisHelper.GetAsync("code:" + post.Email) != code)
{
return ResultData(null, false, "验证码错误!");
}
if (PostService.Any(p => p.Status == Status.Forbidden && p.Email == post.Email))
{
return ResultData(null, false, "由于您曾经恶意投稿,该邮箱已经被标记为黑名单,无法进行投稿,如有疑问,请联系网站管理员进行处理。");
}
var match = Regex.Match(post.Title + post.Author + post.Content, CommonHelper.BanRegex);
if (match.Success)
{
LogManager.Info($"提交内容:{post.Title}/{post.Author}/{post.Content},敏感词:{match.Value}");
return ResultData(null, false, "您提交的内容包含敏感词,被禁止发表,请检查您的内容后尝试重新提交!");
}
if (!CategoryService.Any(c => c.Id == post.CategoryId))
{
return ResultData(null, message: "请选择一个分类");
}
post.Label = string.IsNullOrEmpty(post.Label?.Trim()) ? null : post.Label.Replace(",", ",");
post.Status = Status.Pending;
post.Content = await ImagebedClient.ReplaceImgSrc(await post.Content.HtmlSantinizerStandard().ClearImgAttributes(), cancellationToken);
Post p = post.Mapper();
p.IP = ClientIP;
p.Modifier = p.Author;
p.ModifierEmail = p.Email;
p.DisableCopy = true;
p = PostService.AddEntitySaved(p);
if (p == null)
{
return ResultData(null, false, "文章发表失败!");
}
await RedisHelper.ExpireAsync("code:" + p.Email, 1);
var content = new Template(await System.IO.File.ReadAllTextAsync(HostEnvironment.WebRootPath + "/template/publish.html"))
.Set("link", Url.Action("Details", "Post", new { id = p.Id }, Request.Scheme))
.Set("time", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"))
.Set("title", p.Title).Render();
BackgroundJob.Enqueue(() => CommonHelper.SendMail(CommonHelper.SystemSettings["Title"] + "有访客投稿:", content, CommonHelper.SystemSettings["ReceiveEmail"], ClientIP));
return ResultData(p.Mapper(), message: "文章发表成功,待站长审核通过以后将显示到列表中!");
}
///
/// 获取标签
///
///
[ResponseCache(Duration = 600, VaryByHeader = "Cookie")]
public ActionResult GetTag()
{
return ResultData(PostService.GetTags().Where(p => p.Value > 1).Select(x => x.Key).OrderBy(s => s));
}
///
/// 标签云
///
///
[Route("all"), ResponseCache(Duration = 600, VaryByHeader = "Cookie")]
public async Task All()
{
ViewBag.tags = new Dictionary(PostService.GetTags().Where(x => x.Value > 1).OrderBy(x => x.Key));
ViewBag.cats = await CategoryService.GetAll(c => c.Post.Count, false).ToDictionaryAsync(c => c.Id, c => c.Name); //category
ViewBag.seminars = await SeminarService.GetAll(c => c.Post.Count, false).ToDictionaryAsync(c => c.Id, c => c.Title); //seminars
return View();
}
///
/// 检查访问密码
///
///
///
///
[HttpPost, ValidateAntiForgeryToken, AllowAccessFirewall]
public ActionResult CheckViewToken(string email, string token)
{
if (string.IsNullOrEmpty(token))
{
return ResultData(null, false, "请输入访问密码!");
}
var s = RedisHelper.Get("token:" + email);
if (token.Equals(s))
{
HttpContext.Session.Set("AccessViewToken", token);
Response.Cookies.Append("Email", email, new CookieOptions
{
Expires = DateTime.Now.AddYears(1),
SameSite = SameSiteMode.Lax
});
Response.Cookies.Append("PostAccessToken", email.MDString3(AppConfig.BaiduAK), new CookieOptions
{
Expires = DateTime.Now.AddYears(1),
SameSite = SameSiteMode.Lax
});
return ResultData(null);
}
return ResultData(null, false, "访问密码不正确!");
}
///
/// 检查授权邮箱
///
///
///
[HttpPost, ValidateAntiForgeryToken, AllowAccessFirewall]
public ActionResult GetViewToken(string email)
{
if (string.IsNullOrEmpty(email) || !email.MatchEmail().isMatch)
{
return ResultData(null, false, "请输入正确的邮箱!");
}
if (RedisHelper.Exists("get:" + email))
{
RedisHelper.Expire("get:" + email, 120);
return ResultData(null, false, "发送频率限制,请在2分钟后重新尝试发送邮件!请检查你的邮件,若未收到,请检查你的邮箱地址或邮件垃圾箱!");
}
if (!UserInfoService.Any(b => b.Email.Equals(email)))
{
return ResultData(null, false, "您目前没有权限访问这个链接,请联系站长开通访问权限!");
}
var token = SnowFlake.GetInstance().GetUniqueShortId(6);
RedisHelper.Set("token:" + email, token, 86400);
BackgroundJob.Enqueue(() => CommonHelper.SendMail(Request.Host + "博客访问验证码", $"{Request.Host}本次验证码是:{token},有效期为24h,请按时使用!", email, ClientIP));
RedisHelper.Set("get:" + email, token, 120);
return ResultData(null);
}
///
/// 文章合并
///
///
///
[HttpGet("{id}/merge")]
public async Task PushMerge(int id)
{
var post = await PostService.GetByIdAsync(id) ?? throw new NotFoundException("文章未找到");
CheckPermission(post);
return View(post);
}
///
/// 文章合并
///
///
///
///
[HttpGet("{id}/merge/{mid}")]
public async Task RepushMerge(int id, int mid)
{
var post = await PostService.GetByIdAsync(id) ?? throw new NotFoundException("文章未找到");
CheckPermission(post);
var merge = post.PostMergeRequests.FirstOrDefault(p => p.Id == mid && p.MergeState != MergeStatus.Merged) ?? throw new NotFoundException("待合并文章未找到");
return View(merge);
}
///
/// 文章合并
///
///
///
///
///
[HttpPost("{id}/pushmerge")]
public async Task PushMerge([FromServices] IInternalMessageService messageService, [FromServices] IPostMergeRequestService postMergeRequestService, PostMergeRequestCommand dto)
{
if (await RedisHelper.GetAsync("code:" + dto.ModifierEmail) != dto.Code)
{
return ResultData(null, false, "验证码错误!");
}
var post = await PostService.GetByIdAsync(dto.PostId) ?? throw new NotFoundException("文章未找到");
var htmlDiff = new HtmlDiff.HtmlDiff(post.Content.RemoveHtmlTag(), dto.Content.RemoveHtmlTag());
var diff = htmlDiff.Build();
if (post.Title.Equals(dto.Title) && !diff.Contains(new[] { "diffmod", "diffdel", "diffins" }))
{
return ResultData(null, false, "内容未被修改!");
}
#region 合并验证
if (postMergeRequestService.Any(p => p.ModifierEmail == dto.ModifierEmail && p.MergeState == MergeStatus.Block))
{
return ResultData(null, false, "由于您曾经多次恶意修改文章,已经被标记为黑名单,无法修改任何文章,如有疑问,请联系网站管理员进行处理。");
}
if (post.PostMergeRequests.Any(p => p.ModifierEmail == dto.ModifierEmail && p.MergeState == MergeStatus.Pending))
{
return ResultData(null, false, "您已经提交过一次修改请求正在待处理,暂不能继续提交修改请求!");
}
#endregion
#region 直接合并
if (post.Email.Equals(dto.ModifierEmail))
{
var history = post.Mapper();
Mapper.Map(dto, post);
post.PostHistoryVersion.Add(history);
post.ModifyDate = DateTime.Now;
return await PostService.SaveChangesAsync() > 0 ? ResultData(null, true, "你是文章原作者,无需审核,文章已自动更新并在首页展示!") : ResultData(null, false, "操作失败!");
}
#endregion
var merge = post.PostMergeRequests.FirstOrDefault(r => r.Id == dto.Id && r.MergeState != MergeStatus.Merged);
if (merge != null)
{
Mapper.Map(dto, merge);
merge.SubmitTime = DateTime.Now;
merge.MergeState = MergeStatus.Pending;
}
else
{
merge = Mapper.Map(dto);
merge.SubmitTime = DateTime.Now;
post.PostMergeRequests.Add(merge);
}
var b = await PostService.SaveChangesAsync() > 0;
if (!b)
{
return ResultData(null, false, "操作失败!");
}
await RedisHelper.ExpireAsync("code:" + dto.ModifierEmail, 1);
await messageService.AddEntitySavedAsync(new InternalMessage()
{
Title = $"来自【{dto.Modifier}】对文章《{post.Title}》的修改请求",
Content = dto.Title,
Link = "#/merge/compare?id=" + merge.Id
});
var content = new Template(await System.IO.File.ReadAllTextAsync(HostEnvironment.WebRootPath + "/template/merge-request.html"))
.Set("title", post.Title)
.Set("link", Url.Action("Index", "Dashboard", new { }, Request.Scheme) + "#/merge/compare?id=" + merge.Id)
.Set("diff", diff)
.Set("host", "//" + Request.Host)
.Set("id", merge.Id.ToString())
.Render();
BackgroundJob.Enqueue(() => CommonHelper.SendMail("博客文章修改请求:", content, CommonHelper.SystemSettings["ReceiveEmail"], ClientIP));
return ResultData(null, true, "您的修改请求已提交,已进入审核状态,感谢您的参与!");
}
#region 后端管理
///
/// 固顶
///
///
///
[MyAuthorize]
public async Task Fixtop(int id)
{
Post post = await PostService.GetByIdAsync(id) ?? throw new NotFoundException("文章未找到");
post.IsFixedTop = !post.IsFixedTop;
bool b = await PostService.SaveChangesAsync() > 0;
return b ? ResultData(null, true, post.IsFixedTop ? "置顶成功!" : "取消置顶成功!") : ResultData(null, false, "操作失败!");
}
///
/// 审核
///
///
///
[MyAuthorize]
public async Task Pass(int id)
{
var post = await PostService.GetByIdAsync(id) ?? throw new NotFoundException("文章未找到");
post.Status = Status.Published;
post.ModifyDate = DateTime.Now;
post.PostDate = DateTime.Now;
var b = await PostService.SaveChangesAsync() > 0;
if (!b)
{
return ResultData(null, false, "审核失败!");
}
var js = new JiebaSegmenter();
(post.Keyword + "," + post.Label).Split(',', StringSplitOptions.RemoveEmptyEntries).ForEach(s => js.AddWord(s));
SearchEngine.LuceneIndexer.Add(post);
return ResultData(null, true, "审核通过!");
}
///
/// 删除
///
///
///
[MyAuthorize]
public async Task Delete(int id)
{
var post = await PostService.GetByIdAsync(id) ?? throw new NotFoundException("文章未找到");
post.Status = Status.Deleted;
bool b = await PostService.SaveChangesAsync(true) > 0;
SearchEngine.LuceneIndexer.Delete(post);
return ResultData(null, b, b ? "删除成功!" : "删除失败!");
}
///
/// 还原版本
///
///
///
[MyAuthorize]
public async Task Restore(int id)
{
var post = await PostService.GetByIdAsync(id) ?? throw new NotFoundException("文章未找到");
post.Status = Status.Published;
bool b = await PostService.SaveChangesAsync() > 0;
SearchEngine.LuceneIndexer.Add(post);
return ResultData(null, b, b ? "恢复成功!" : "恢复失败!");
}
///
/// 彻底删除文章
///
///
///
[MyAuthorize]
public ActionResult Truncate(int id)
{
bool b = PostService - id;
return ResultData(null, b, b ? "删除成功!" : "删除失败!");
}
///
/// 获取文章
///
///
///
[MyAuthorize]
public ActionResult Get(int id)
{
Post post = PostService[id] ?? throw new NotFoundException("文章未找到");
PostDto model = post.Mapper();
model.Seminars = post.Seminar.Select(s => s.Title).Join(",");
return ResultData(model);
}
///
/// 获取文章分页
///
///
[MyAuthorize]
public ActionResult GetPageData([FromServices] ICacheManager> 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)
{
Expression> where = p => true;
if (cid.HasValue)
{
where = where.And(p => p.CategoryId == cid.Value);
}
if (!string.IsNullOrEmpty(kw))
{
kw = Regex.Escape(kw);
where = where.And(p => Regex.IsMatch(p.Title + p.Author + p.Email + p.Content, kw));
}
var list = PostService.GetQuery(where).OrderBy($"{nameof(Post.Status)} desc,{nameof(Post.IsFixedTop)} desc,{orderby.GetDisplay()} desc").ToPagedList(page, size, MapperConfig);
foreach (var item in list.Data)
{
item.ModifyDate = item.ModifyDate.ToTimeZone(HttpContext.Session.Get(SessionKey.TimeZone));
item.PostDate = item.PostDate.ToTimeZone(HttpContext.Session.Get(SessionKey.TimeZone));
item.Online = cacheManager.Get(nameof(PostOnline) + ":" + item.Id)?.Count ?? 0;
}
return Ok(list);
}
///
/// 获取未审核文章
///
///
///
///
///
[MyAuthorize]
public async Task GetPending([Range(1, int.MaxValue, ErrorMessage = "页码必须大于0")] int page = 1, [Range(1, 50, ErrorMessage = "页大小必须在0到50之间")] int size = 15, string search = "")
{
Expression> where = p => p.Status == Status.Pending;
if (!string.IsNullOrEmpty(search))
{
where = where.And(p => p.Title.Contains(search) || p.Author.Contains(search) || p.Email.Contains(search) || p.Label.Contains(search));
}
var pages = await PostService.GetQuery(where).OrderByDescending(p => p.IsFixedTop).ThenByDescending(p => p.ModifyDate).ToCachedPagedListAsync(page, size, MapperConfig);
foreach (var item in pages.Data)
{
item.ModifyDate = item.ModifyDate.ToTimeZone(HttpContext.Session.Get(SessionKey.TimeZone));
item.PostDate = item.PostDate.ToTimeZone(HttpContext.Session.Get(SessionKey.TimeZone));
}
return Ok(pages);
}
///
/// 编辑
///
///
/// 是否保留历史版本
///
///
[HttpPost, MyAuthorize]
public async Task Edit(PostCommand post, bool reserve = true, CancellationToken cancellationToken = default)
{
post.Content = await ImagebedClient.ReplaceImgSrc(await post.Content.Trim().ClearImgAttributes(), cancellationToken);
if (!ValidatePost(post, out var resultData))
{
return resultData;
}
Post p = await PostService.GetByIdAsync(post.Id);
if (reserve && p.Status == Status.Published)
{
var context = BrowsingContext.New(Configuration.Default);
var doc1 = await context.OpenAsync(req => req.Content(p.Content), cancellationToken);
var doc2 = await context.OpenAsync(req => req.Content(post.Content), cancellationToken);
if (doc1.Body.TextContent != doc2.Body.TextContent)
{
var history = p.Mapper();
p.PostHistoryVersion.Add(history);
}
p.ModifyDate = DateTime.Now;
var user = HttpContext.Session.Get(SessionKey.UserInfo);
post.Modifier = string.IsNullOrEmpty(post.Modifier) ? user.NickName : post.Modifier;
post.ModifierEmail = string.IsNullOrEmpty(post.ModifierEmail) ? user.Email : post.ModifierEmail;
}
Mapper.Map(post, p);
p.IP = ClientIP;
if (!string.IsNullOrEmpty(post.Seminars))
{
var tmp = post.Seminars.Split(',').Distinct();
p.Seminar.Clear();
foreach (var s in tmp)
{
var seminar = await SeminarService.GetAsync(e => e.Title.Equals(s));
if (seminar != null)
{
p.Seminar.Add(seminar);
}
}
}
var js = new JiebaSegmenter();
(p.Keyword + "," + p.Label).Split(',', StringSplitOptions.RemoveEmptyEntries).ForEach(s => js.AddWord(s));
bool b = await SearchEngine.SaveChangesAsync() > 0;
if (!b)
{
return ResultData(null, false, "文章修改失败!");
}
return ResultData(p.Mapper(), message: "文章修改成功!");
}
///
/// 发布
///
///
///
///
///
[MyAuthorize, HttpPost]
public async Task Write(PostCommand post, DateTime? timespan, bool schedule = false, CancellationToken cancellationToken = default)
{
post.Content = await ImagebedClient.ReplaceImgSrc(await post.Content.Trim().ClearImgAttributes(), cancellationToken);
if (!ValidatePost(post, out var resultData))
{
return resultData;
}
post.Status = Status.Published;
Post p = post.Mapper();
p.Rss = true;
p.Modifier = p.Author;
p.ModifierEmail = p.Email;
p.IP = ClientIP;
if (!string.IsNullOrEmpty(post.Seminars))
{
var tmp = post.Seminars.Split(',').Distinct();
foreach (var s in tmp)
{
var id = s.ToInt32();
Seminar seminar = await SeminarService.GetByIdAsync(id);
p.Seminar.Add(seminar);
}
}
if (schedule)
{
if (!timespan.HasValue || timespan.Value <= DateTime.Now)
{
return ResultData(null, false, "如果要定时发布,请选择正确的一个将来时间点!");
}
p.Status = Status.Schedule;
p.PostDate = timespan.Value.ToUniversalTime();
p.ModifyDate = timespan.Value.ToUniversalTime();
HangfireHelper.CreateJob(typeof(IHangfireBackJob), nameof(HangfireBackJob.PublishPost), args: p);
return ResultData(p.Mapper(), message: $"文章于{timespan.Value:yyyy-MM-dd HH:mm:ss}将会自动发表!");
}
PostService.AddEntity(p);
var js = new JiebaSegmenter();
(p.Keyword + "," + p.Label).Split(',', StringSplitOptions.RemoveEmptyEntries).ForEach(s => js.AddWord(s));
bool b = await SearchEngine.SaveChangesAsync() > 0;
if (!b)
{
return ResultData(null, false, "文章发表失败!");
}
return ResultData(null, true, "文章发表成功!");
}
private bool ValidatePost(PostCommand post, out ActionResult resultData)
{
if (!CategoryService.Any(c => c.Id == post.CategoryId && c.Status == Status.Available))
{
resultData = ResultData(null, false, "请选择一个分类");
return false;
}
switch (post.LimitMode)
{
case RegionLimitMode.AllowRegion:
case RegionLimitMode.ForbidRegion:
if (string.IsNullOrEmpty(post.Regions))
{
resultData = ResultData(null, false, "请输入限制的地区");
return false;
}
break;
case RegionLimitMode.AllowRegionExceptForbidRegion:
case RegionLimitMode.ForbidRegionExceptAllowRegion:
if (string.IsNullOrEmpty(post.ExceptRegions))
{
resultData = ResultData(null, false, "请输入排除的地区");
return false;
}
goto case RegionLimitMode.AllowRegion;
}
if (string.IsNullOrEmpty(post.Label?.Trim()) || post.Label.Equals("null"))
{
post.Label = null;
}
else if (post.Label.Trim().Length > 50)
{
post.Label = post.Label.Replace(",", ",");
post.Label = post.Label.Trim().Substring(0, 50);
}
else
{
post.Label = post.Label.Replace(",", ",");
}
if (string.IsNullOrEmpty(post.ProtectContent?.RemoveHtmlTag()) || post.ProtectContent.Equals("null"))
{
post.ProtectContent = null;
}
resultData = null;
return true;
}
///
/// 添加专题
///
///
///
///
[MyAuthorize]
public async Task AddSeminar(int id, int sid)
{
var post = await PostService.GetByIdAsync(id) ?? throw new NotFoundException("文章未找到");
Seminar seminar = await SeminarService.GetByIdAsync(sid) ?? throw new NotFoundException("专题未找到");
post.Seminar.Add(seminar);
bool b = await PostService.SaveChangesAsync() > 0;
return ResultData(null, b, b ? $"已将文章【{post.Title}】添加到专题【{seminar.Title}】" : "添加失败");
}
///
/// 移除专题
///
///
///
///
[MyAuthorize]
public async Task RemoveSeminar(int id, int sid)
{
var post = await PostService.GetByIdAsync(id) ?? throw new NotFoundException("文章未找到");
Seminar seminar = await SeminarService.GetByIdAsync(sid) ?? throw new NotFoundException("专题未找到");
post.Seminar.Remove(seminar);
bool b = await PostService.SaveChangesAsync() > 0;
return ResultData(null, b, b ? $"已将文章【{post.Title}】从【{seminar.Title}】专题移除" : "添加失败");
}
///
/// 删除历史版本
///
///
///
[MyAuthorize]
public async Task DeleteHistory(int id)
{
bool b = await PostHistoryVersionService.DeleteByIdAsync(id) > 0;
return ResultData(null, b, b ? "历史版本文章删除成功!" : "历史版本文章删除失败!");
}
///
/// 还原版本
///
///
///
[MyAuthorize]
public async Task Revert(int id)
{
var history = await PostHistoryVersionService.GetByIdAsync(id) ?? throw new NotFoundException("版本不存在");
history.Post.Category = history.Category;
history.Post.CategoryId = history.CategoryId;
history.Post.Content = history.Content;
history.Post.Title = history.Title;
history.Post.Label = history.Label;
history.Post.ModifyDate = history.ModifyDate;
history.Post.Seminar.Clear();
foreach (var s in history.Seminar)
{
history.Post.Seminar.Add(s);
}
bool b = await SearchEngine.SaveChangesAsync() > 0;
await PostHistoryVersionService.DeleteByIdAsync(id);
return ResultData(null, b, b ? "回滚成功" : "回滚失败");
}
///
/// 禁用或开启文章评论
///
/// 文章id
///
[MyAuthorize]
public async Task DisableComment(int id)
{
var post = await PostService.GetByIdAsync(id) ?? throw new NotFoundException("文章未找到");
post.DisableComment = !post.DisableComment;
return ResultData(null, await PostService.SaveChangesAsync() > 0, post.DisableComment ? $"已禁用【{post.Title}】这篇文章的评论功能!" : $"已启用【{post.Title}】这篇文章的评论功能!");
}
///
/// 禁用或开启文章评论
///
/// 文章id
///
[MyAuthorize]
public async Task DisableCopy(int id)
{
var post = await PostService.GetByIdAsync(id) ?? throw new NotFoundException("文章未找到");
post.DisableCopy = !post.DisableCopy;
return ResultData(null, await PostService.SaveChangesAsync() > 0, post.DisableCopy ? $"已开启【{post.Title}】这篇文章的防复制功能!" : $"已关闭【{post.Title}】这篇文章的防复制功能!");
}
///
/// 刷新文章
///
/// 文章id
///
[MyAuthorize]
public async Task Refresh(int id)
{
await PostService.GetQuery(p => p.Id == id).UpdateFromQueryAsync(p => new Post()
{
ModifyDate = DateTime.Now
});
return RedirectToAction("Details", new { id });
}
///
/// 标记为恶意修改
///
///
///
[MyAuthorize]
[HttpPost("post/block/{id}")]
public async Task Block(int id)
{
var b = await PostService.GetQuery(p => p.Id == id).UpdateFromQueryAsync(p => new Post()
{
Status = Status.Forbidden
}) > 0;
return b ? ResultData(null, true, "操作成功!") : ResultData(null, false, "操作失败!");
}
///
/// 切换允许rss订阅
///
///
///
[MyAuthorize]
[HttpPost("post/{id}/rss-switch")]
public async Task RssSwitch(int id)
{
await PostService.GetQuery(p => p.Id == id).UpdateFromQueryAsync(p => new Post()
{
Rss = !p.Rss
});
return ResultData(null, message: "操作成功");
}
///
/// 文章统计
///
///
[MyAuthorize]
public async Task Statistic()
{
var keys = await RedisHelper.KeysAsync(nameof(PostOnline) + ":*");
var sets = await keys.SelectAsync(async s => (Id: s.Split(':')[1].ToInt32(), Clients: await RedisHelper.HGetAsync>(s, "value")));
var ids = sets.Where(t => t.Clients?.Count > 0).OrderByDescending(t => t.Clients.Count).Take(10).Select(t => t.Id).ToArray();
var mostHots = await PostService.GetQuery(p => ids.Contains(p.Id)).ToListAsync().ContinueWith(t =>
{
foreach (var item in t.Result)
{
item.ViewCount = sets.FirstOrDefault(x => x.Id == item.Id).Clients.Count;
}
return t.Result.OrderByDescending(p => p.ViewCount);
});
var postsQuery = PostService.GetQuery(p => p.Status == Status.Published);
var mostView = await postsQuery.OrderByDescending(p => p.TotalViewCount).Take(10).Select(p => new PostModelBase()
{
Id = p.Id,
Title = p.Title,
ViewCount = p.TotalViewCount
}).Cacheable().ToListAsync();
var mostAverage = await postsQuery.OrderByDescending(p => p.AverageViewCount).Take(10).Select(p => new PostModelBase()
{
Id = p.Id,
Title = p.Title,
ViewCount = (int)p.AverageViewCount
}).Cacheable().ToListAsync();
return ResultData(new
{
mostHots,
mostView,
mostAverage
});
}
#endregion
}
}