(p);
history.PostId = p.Id;
PostHistoryVersionService.AddEntity(history);
}
if (p.Title.HammingDistance(post.Title) > 10 && CommentService.Any(c => c.PostId == p.Id && c.ParentId == null))
{
CommentService.AddEntity(new Comment
{
Status = Status.Published,
NickName = "系统自动评论",
Email = p.Email,
Content = $"温馨提示:由于文章发生了重大更新,本条评论之前的所有评论仅作为原文《{p.Title}》的历史评论保留,不作为本文的最新评论参考,请知悉!了解更多信息,请查阅本文的历史修改记录。
",
PostId = p.Id,
CommentDate = DateTime.Now,
IsMaster = true,
IsAuthor = true,
IP = "127.0.0.1",
Location = "内网",
GroupTag = SnowFlake.NewId,
Path = SnowFlake.NewId,
});
}
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.ToString();
p.Seminar.Clear();
if (!string.IsNullOrEmpty(post.Seminars))
{
var tmp = post.Seminars.Split(',', StringSplitOptions.RemoveEmptyEntries).Distinct().Select(int.Parse).ToArray();
var seminars = SeminarService.GetQuery(s => tmp.Contains(s.Id)).ToPooledListScope();
p.Seminar.AddRange(seminars);
}
(p.Keyword + "," + p.Label).Split(',', StringSplitOptions.RemoveEmptyEntries).ForEach(KeywordsManager.AddWords);
PostTagService.AddOrUpdate(t => t.Name, p.Label.AsNotNull().Split(',', StringSplitOptions.RemoveEmptyEntries).Select(s => new PostTag()
{
Name = s,
Count = PostService.Count(t => t.Label.Contains(s))
}));
bool b = await SearchEngine.SaveChangesAsync() > 0;
if (!b)
{
return ResultData(null, false, "文章修改失败!");
}
if (p.LimitMode == RegionLimitMode.OnlyForSearchEngine)
{
SearchEngine.LuceneIndexer.Delete(p);
}
return ResultData(Mapper.Map(p), message: "文章修改成功!");
}
///
/// 发布
///
///
///
///
///
///
[MyAuthorize, HttpPost]
public async Task Write([FromBodyOrDefault] PostCommand post, [FromBodyOrDefault] DateTime? timespan, [FromBodyOrDefault] 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 = Mapper.Map(post);
p.Modifier = p.Author;
p.ModifierEmail = p.Email;
p.IP = ClientIP.ToString();
p.Rss = p.LimitMode is null or RegionLimitMode.All;
if (!string.IsNullOrEmpty(post.Seminars))
{
var tmp = post.Seminars.Split(',').Distinct().Select(int.Parse).ToArray();
p.Seminar.AddRange(SeminarService[s => tmp.Contains(s.Id)]);
}
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();
BackgroundJob.Enqueue(job => job.PublishPost(p));
return ResultData(Mapper.Map(p), message: $"文章于{timespan.Value:yyyy-MM-dd HH:mm:ss}将会自动发表!");
}
PostService.AddEntity(p);
(p.Keyword + "," + p.Label).Split(',', StringSplitOptions.RemoveEmptyEntries).ForEach(KeywordsManager.AddWords);
PostTagService.AddOrUpdate(t => t.Name, p.Label.AsNotNull().Split(',', StringSplitOptions.RemoveEmptyEntries).Select(s => new PostTag()
{
Name = s,
Count = PostService.Count(t => t.Label.Contains(s))
}));
bool b = await SearchEngine.SaveChangesAsync() > 0;
if (!b)
{
return ResultData(null, false, "文章发表失败!");
}
if (p.LimitMode == RegionLimitMode.OnlyForSearchEngine)
{
SearchEngine.LuceneIndexer.Delete(p);
}
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;
}
post.Regions = post.Regions.Replace(",", "|").Replace(",", "|");
break;
case RegionLimitMode.AllowRegionExceptForbidRegion:
case RegionLimitMode.ForbidRegionExceptAllowRegion:
if (string.IsNullOrEmpty(post.ExceptRegions))
{
resultData = ResultData(null, false, "请输入排除的地区");
return false;
}
post.ExceptRegions = post.ExceptRegions.Replace(",", "|").Replace(",", "|");
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]
[HttpPost("post/{id}/DisableComment")]
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]
[HttpPost("post/{id}/DisableCopy")]
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}】这篇文章的防复制功能!");
}
///
/// 禁用或开启NSFW
///
/// 文章id
///
[MyAuthorize]
[HttpPost("post/{id}/nsfw")]
public async Task Nsfw(int id)
{
var post = await PostService.GetByIdAsync(id) ?? throw new NotFoundException("文章未找到");
post.IsNsfw = !post.IsNsfw;
return ResultData(null, await PostService.SaveChangesAsync() > 0, post.IsNsfw ? $"已将文章【{post.Title}】标记为不安全内容!" : $"已将文章【{post.Title}】取消标记为不安全内容!");
}
///
/// 修改分类
///
///
///
///
[HttpPost("post/{id}/ChangeCategory/{cid}")]
public async Task ChangeCategory(int id, int cid)
{
await PostService.GetQuery(p => p.Id == id).ExecuteUpdateAsync(s => s.SetProperty(p => p.CategoryId, cid));
return Ok();
}
///
/// 修改专题
///
///
///
///
[HttpPost("post/{id}/ChangeSeminar")]
public async Task ChangeSeminar(int id, string sids)
{
var post = PostService.GetQuery(e => e.Id == id).Include(e => e.Seminar).FirstOrDefault() ?? throw new NotFoundException("文章不存在");
post.Seminar.Clear();
if (!string.IsNullOrEmpty(sids))
{
var ids = sids.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToArray();
post.Seminar.AddRange(SeminarService[s => ids.Contains(s.Id)]);
}
await PostService.SaveChangesAsync();
return Ok();
}
///
/// 刷新文章
///
/// 文章id
///
///
[MyAuthorize]
public async Task Refresh(int id, CancellationToken cancellationToken = default)
{
await PostService.GetQuery(p => p.Id == id).ExecuteUpdateAsync(s => s.SetProperty(m => m.ModifyDate, DateTime.Now), cancellationToken: cancellationToken);
return RedirectToAction("Details", new { id });
}
///
/// 标记为恶意修改
///
///
///
///
[MyAuthorize]
[HttpPost("post/block/{id}")]
public async Task Block(int id, CancellationToken cancellationToken = default)
{
var b = await PostService.GetQuery(p => p.Id == id).ExecuteUpdateAsync(s => s.SetProperty(m => m.Status, Status.Forbidden), cancellationToken: cancellationToken) > 0;
return b ? ResultData(null, true, "操作成功!") : ResultData(null, false, "操作失败!");
}
///
/// 切换允许rss订阅
///
///
///
///
[MyAuthorize]
[HttpPost("post/{id}/rss-switch")]
public async Task RssSwitch(int id, CancellationToken cancellationToken = default)
{
await PostService.GetQuery(p => p.Id == id).ExecuteUpdateAsync(s => s.SetProperty(m => m.Rss, p => !p.Rss), cancellationToken: cancellationToken);
return ResultData(null, message: "操作成功");
}
///
/// 切换锁定编辑
///
///
///
///
[MyAuthorize]
[HttpPost("post/{id}/locked-switch")]
public async Task LockedSwitch(int id, CancellationToken cancellationToken = default)
{
await PostService.GetQuery(p => p.Id == id).ExecuteUpdateAsync(s => s.SetProperty(m => m.Locked, p => !p.Locked), cancellationToken: cancellationToken);
return ResultData(null, message: "操作成功");
}
///
/// 文章统计
///
///
[MyAuthorize]
public async Task Statistic(CancellationToken cancellationToken = default)
{
var keys = RedisHelper.Keys(nameof(PostOnline) + ":*");
var sets = keys.Select(s => (Id: s.Split(':')[1].ToInt32(), Clients: RedisHelper.HGet>(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
}).ToListAsync(cancellationToken);
var mostAverage = await postsQuery.OrderByDescending(p => p.AverageViewCount).Take(10).Select(p => new PostModelBase()
{
Id = p.Id,
Title = p.Title,
ViewCount = (int)p.AverageViewCount
}).ToListAsync(cancellationToken);
var yesterday = DateTime.Now.AddDays(-1);
var trending = await postsQuery.Select(p => new PostModelBase()
{
Id = p.Id,
Title = p.Title,
ViewCount = p.PostVisitRecords.Count(t => t.Time >= yesterday)
}).OrderByDescending(p => p.ViewCount).Take(10).ToListAsync(cancellationToken);
var readCount = PostVisitRecordService.Count(e => e.Time >= yesterday);
return ResultData(new
{
mostHots,
mostView,
mostAverage,
trending,
readCount
});
}
///
/// 文章访问记录
///
///
///
///
///
[HttpGet("/{id}/records"), MyAuthorize]
[ProducesResponseType(typeof(PagedList), (int)HttpStatusCode.OK)]
public async Task PostVisitRecords(int id, int page = 1, int size = 15, string kw = "")
{
Expression> where = e => e.PostId == id;
if (!string.IsNullOrEmpty(kw))
{
kw = Regex.Escape(kw);
where = where.And(e => Regex.IsMatch(e.IP + e.Location + e.Referer + e.RequestUrl, kw, RegexOptions.IgnoreCase));
}
var pages = await PostVisitRecordService.GetPagesAsync(page, size, where, e => e.Time, false);
return Ok(pages);
}
///
/// 导出文章访问记录
///
///
///
[HttpGet("/{id}/records-export"), MyAuthorize]
[ProducesResponseType(typeof(PagedList), (int)HttpStatusCode.OK)]
public IActionResult ExportPostVisitRecords(int id)
{
var list = PostVisitRecordService.GetQuery(e => e.PostId == id, e => e.Time, false).ToPooledListScope();
using var ms = list.ToExcel();
var post = PostService[id];
return this.ResumeFile(ms.ToArray(), ContentType.Xlsx, post.Title + "访问记录.xlsx");
}
///
/// 文章访问记录图表
///
///
[HttpGet("/{id}/records-chart"), MyAuthorize]
[ProducesResponseType((int)HttpStatusCode.OK)]
public async Task PostVisitRecordChart([FromServices] IPostVisitRecordStatsService statsService, int id, bool compare, uint period, CancellationToken cancellationToken)
{
if (compare)
{
var start1 = DateTime.Today.AddDays(-period);
var list1 = await statsService.GetQuery(e => e.PostId == id && e.Date >= start1).GroupBy(t => t.Date).Select(g => new
{
Date = g.Key,
Count = g.Sum(t => t.Count),
UV = g.Sum(t => t.UV)
}).OrderBy(a => a.Date).ToListAsync(cancellationToken);
if (list1.Count == 0)
{
return Ok(Array.Empty());
}
var start2 = start1.AddDays(-period - 1);
var list2 = await statsService.GetQuery(e => e.PostId == id && e.Date >= start2 && e.Date < start1).GroupBy(t => t.Date).Select(g => new
{
Date = g.Key,
Count = g.Sum(t => t.Count),
UV = g.Sum(t => t.UV)
}).OrderBy(a => a.Date).ToListAsync(cancellationToken);
// 将数据填充成连续的数据
for (var i = start1; i <= DateTime.Today; i = i.AddDays(1))
{
if (list1.All(a => a.Date != i))
{
list1.Add(new { Date = i, Count = 0, UV = 0 });
}
}
for (var i = start2; i < start1; i = i.AddDays(1))
{
if (list2.All(a => a.Date != i))
{
list2.Add(new { Date = i, Count = 0, UV = 0 });
}
}
return Ok(new[] { list1.OrderBy(a => a.Date), list2.OrderBy(a => a.Date) });
}
var list = await statsService.GetQuery(e => e.PostId == id).GroupBy(t => t.Date).Select(g => new
{
Date = g.Key,
Count = g.Sum(t => t.Count),
UV = g.Sum(t => t.UV)
}).OrderBy(a => a.Date).ToListAsync(cancellationToken);
var min = list.Min(a => a.Date);
var max = list.Max(a => a.Date);
for (var i = min; i < max; i = i.AddDays(1))
{
if (list.All(a => a.Date != i))
{
list.Add(new { Date = i, Count = 0, UV = 0 });
}
}
return Ok(new[] { list.OrderBy(a => a.Date) });
}
///
/// 文章访问记录图表
///
///
[HttpGet("/post/records-chart"), MyAuthorize]
[ProducesResponseType((int)HttpStatusCode.OK)]
public async Task PostVisitRecordChart(bool compare, uint period, CancellationToken cancellationToken)
{
if (compare)
{
var start1 = DateTime.Today.AddDays(-period);
var list1 = await PostVisitRecordService.GetQuery(e => e.Time >= start1).Select(e => new { e.Time.Date, e.IP }).GroupBy(t => t.Date).Select(g => new
{
Date = g.Key,
Count = g.Count(),
UV = g.Select(e => e.IP).Distinct().Count()
}).OrderBy(a => a.Date).ToListAsync(cancellationToken);
if (list1.Count == 0)
{
return Ok(Array.Empty());
}
var start2 = start1.AddDays(-period - 1);
var list2 = await PostVisitRecordService.GetQuery(e => e.Time >= start2 && e.Time < start1).Select(e => new { e.Time.Date, e.IP }).GroupBy(t => t.Date).Select(g => new
{
Date = g.Key,
Count = g.Count(),
UV = g.Select(e => e.IP).Distinct().Count()
}).OrderBy(a => a.Date).ToListAsync(cancellationToken);
// 将数据填充成连续的数据
for (var i = start1; i <= DateTime.Today; i = i.AddDays(1))
{
if (list1.All(a => a.Date != i))
{
list1.Add(new { Date = i, Count = 0, UV = 0 });
}
}
for (var i = start2; i < start1; i = i.AddDays(1))
{
if (list2.All(a => a.Date != i))
{
list2.Add(new { Date = i, Count = 0, UV = 0 });
}
}
return Ok(new[] { list1.OrderBy(a => a.Date), list2.OrderBy(a => a.Date) });
}
var list = await PostVisitRecordService.GetAll().Select(e => new { e.Time.Date, e.IP }).GroupBy(t => t.Date).Select(g => new
{
Date = g.Key,
Count = g.Count(),
UV = g.Select(e => e.IP).Distinct().Count()
}).OrderBy(a => a.Date).ToListAsync(cancellationToken);
var min = list.Min(a => a.Date);
var max = list.Max(a => a.Date);
for (var i = min; i < max; i = i.AddDays(1))
{
if (list.All(a => a.Date != i))
{
list.Add(new { Date = i, Count = 0, UV = 0 });
}
}
return Ok(new[] { list.OrderBy(a => a.Date) });
}
///
/// 文章访问记录分析
///
///
///
[HttpGet("/{id}/insight"), MyAuthorize]
[ProducesResponseType(typeof(PagedList), (int)HttpStatusCode.OK)]
public IActionResult PostVisitRecordInsight(int id)
{
return View(PostService[id]);
}
///
/// 获取地区集
///
///
///
[MyAuthorize]
[ProducesResponseType(typeof(List), (int)HttpStatusCode.OK)]
public async Task GetRegions(string name)
{
return ResultData(await PostService.GetAll().Select(p => EF.Property(p, name)).Distinct().ToListAsync());
}
#endregion 后端管理
}