1
This commit is contained in:
89
Yes.Application/Blogs/GetRssQueryHandler.cs
Normal file
89
Yes.Application/Blogs/GetRssQueryHandler.cs
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
|
||||||
|
namespace Yes.Application.Blogs
|
||||||
|
{
|
||||||
|
|
||||||
|
public record GetRssQuery() : IRequest<GetRssQueryResponse>;
|
||||||
|
|
||||||
|
public record GetRssQueryResponse(string Xml);
|
||||||
|
|
||||||
|
public class GetRssQueryHandler(
|
||||||
|
IOptionsMonitor<BlogSettings> options,
|
||||||
|
BlogDbContext db,
|
||||||
|
IMapper mapper) : IRequestHandler<GetRssQuery, GetRssQueryResponse>
|
||||||
|
{
|
||||||
|
private readonly BlogSettings _settings = options.CurrentValue;
|
||||||
|
private readonly BlogDbContext _db = db;
|
||||||
|
public async Task<GetRssQueryResponse> Handle(GetRssQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var queryArticle = _db.Articles.Where(x => x.Type == ArticleTypeEnum.Article && x.Status == ArticleStatusEnum.Normal).AsQueryable();
|
||||||
|
var pubDate = queryArticle.Any() ? _db.Articles.Min(x => x.PublishDate) : DateTime.MinValue;
|
||||||
|
|
||||||
|
var channel = new Channel
|
||||||
|
{
|
||||||
|
Title = _settings.Name,
|
||||||
|
Link = _settings.Url,
|
||||||
|
Description = _settings.Description,
|
||||||
|
Language = "zh-CN",
|
||||||
|
FeedUrl = $"{_settings.Url}/feed/",
|
||||||
|
PubDate = pubDate
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加扩展元素(示例)
|
||||||
|
channel.Extensions.Add(new XElement("generator", _settings.Name));
|
||||||
|
channel.Extensions.Add(new XElement("docs", "http://www.rssboard.org/rss-specification"));
|
||||||
|
|
||||||
|
var query = from article in queryArticle
|
||||||
|
join categoryTemp in _db.Categories.AsNoTracking() on article.CategoryId equals categoryTemp.Id into tempCategory
|
||||||
|
from category in tempCategory.DefaultIfEmpty()
|
||||||
|
join userTemp in _db.Users.AsNoTracking() on article.UserId equals userTemp.Id into tempUser
|
||||||
|
from user in tempUser.DefaultIfEmpty()
|
||||||
|
orderby article.Id descending
|
||||||
|
select new ArticleModel
|
||||||
|
{
|
||||||
|
Id = article.Id,
|
||||||
|
CanComment = article.CanComment,
|
||||||
|
CommentCount = article.CommentCount,
|
||||||
|
CategoryId = article.CategoryId,
|
||||||
|
ModifyDate = article.ModifyDate.ToSafeTime(),
|
||||||
|
PublishDate = article.PublishDate.ToSafeTime(),
|
||||||
|
CreateDate = article.CreateDate.ToSafeTime(),
|
||||||
|
Content = article.Content,
|
||||||
|
CoverUrl = article.CoverUrl,
|
||||||
|
Slug = article.Slug,
|
||||||
|
ReadCount = article.ReadCount,
|
||||||
|
Title = article.Title,
|
||||||
|
Summary = article.Summary,
|
||||||
|
UserId = article.UserId,
|
||||||
|
Category = new CategoryModel
|
||||||
|
{
|
||||||
|
CoverUrl = category.CoverUrl,
|
||||||
|
Description = category.Description,
|
||||||
|
Id = category.Id,
|
||||||
|
Name = category.Name,
|
||||||
|
Slug = category.Slug
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var items = query.Select(x => new Item
|
||||||
|
{
|
||||||
|
Content = x.Content,
|
||||||
|
Title = x.Title,
|
||||||
|
Url = _settings.ArticleRoute.ToRoute(x.Category, x),
|
||||||
|
Description = x.Summary,
|
||||||
|
CommentsCount = x.CommentCount,
|
||||||
|
Creator = x.UserId.ToString(),
|
||||||
|
PublishDate = x.PublishDate,
|
||||||
|
CommentsUrl = $"{_settings.ArticleRoute.ToRoute(x.Category, x)}#comments",
|
||||||
|
CommentRssUrl = $"{_settings.Url}/feed/"
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
var generator = new RssGenerator();
|
||||||
|
string xml = generator.GenerateRss(channel, items);
|
||||||
|
|
||||||
|
return new GetRssQueryResponse(xml);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -37,4 +37,6 @@ global using System.Security.Claims;
|
|||||||
global using System.IdentityModel.Tokens.Jwt;
|
global using System.IdentityModel.Tokens.Jwt;
|
||||||
global using Microsoft.IdentityModel.Tokens;
|
global using Microsoft.IdentityModel.Tokens;
|
||||||
global using System.Text;
|
global using System.Text;
|
||||||
global using Yes.Infrastructure.Authorizations.Identity.Authroizations;
|
global using Yes.Infrastructure.Authorizations.Identity.Authroizations;
|
||||||
|
global using System.Xml.Linq;
|
||||||
|
global using Yes.Infrastructure.Rss;
|
||||||
|
|||||||
19
Yes.Blog/Endpoints/Blogs/RssEndpoint.cs
Normal file
19
Yes.Blog/Endpoints/Blogs/RssEndpoint.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
|
||||||
|
namespace Yes.Blog.Endpoints.Blogs
|
||||||
|
{
|
||||||
|
public class RssEndpoint : BlogEndpointScheme, IEndpoint
|
||||||
|
{
|
||||||
|
public void Map(IEndpointRouteBuilder app) => app.MapGet("feed", Handle).AllowAnonymous();
|
||||||
|
|
||||||
|
private async Task<Microsoft.AspNetCore.Http.IResult> Handle(
|
||||||
|
IMediator mediator,
|
||||||
|
IMapper mapper,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var response = await mediator.Send(new GetRssQuery(), cancellationToken);
|
||||||
|
|
||||||
|
return Results.Text(response.Xml, "application/rss+xml; charset=utf-8");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -51,4 +51,5 @@ global using Yes.Infrastructure.Migrator.Migrations;
|
|||||||
global using System.Security;
|
global using System.Security;
|
||||||
global using System.Security.Cryptography;
|
global using System.Security.Cryptography;
|
||||||
global using Yes.Infrastructure.Helpers;
|
global using Yes.Infrastructure.Helpers;
|
||||||
global using Microsoft.AspNetCore.Authentication.JwtBearer;
|
global using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
|
global using System.Xml.Linq;
|
||||||
21
Yes.Infrastructure/Rss/Channel.cs
Normal file
21
Yes.Infrastructure/Rss/Channel.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
namespace Yes.Infrastructure.Rss
|
||||||
|
{
|
||||||
|
public class Channel
|
||||||
|
{
|
||||||
|
public string Title { get; set; }
|
||||||
|
public string Link { get; set; }
|
||||||
|
public string Description { get; set; }
|
||||||
|
public string Language { get; set; } = "zh-CN";
|
||||||
|
|
||||||
|
// 时间信息
|
||||||
|
public DateTime LastBuildDate { get; set; } = DateTime.Now;
|
||||||
|
public DateTime PubDate { get; set; } = DateTime.Now;
|
||||||
|
|
||||||
|
// Atom 链接
|
||||||
|
public string FeedUrl { get; set; }
|
||||||
|
|
||||||
|
// 扩展能力
|
||||||
|
public List<XElement> Extensions { get; } = new List<XElement>();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
15
Yes.Infrastructure/Rss/Item.cs
Normal file
15
Yes.Infrastructure/Rss/Item.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
namespace Yes.Infrastructure.Rss
|
||||||
|
{
|
||||||
|
public class Item
|
||||||
|
{
|
||||||
|
public string Title { get; set; }
|
||||||
|
public string Url { get; set; }
|
||||||
|
public DateTime PublishDate { get; set; }
|
||||||
|
public string Creator { get; set; }
|
||||||
|
public string Description { get; set; } // 简短描述
|
||||||
|
public string Content { get; set; } // 完整HTML内容
|
||||||
|
public int CommentsCount { get; set; }
|
||||||
|
public string CommentsUrl { get; set; }
|
||||||
|
public string CommentRssUrl { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
80
Yes.Infrastructure/Rss/RssGenerator.cs
Normal file
80
Yes.Infrastructure/Rss/RssGenerator.cs
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
|
||||||
|
namespace Yes.Infrastructure.Rss
|
||||||
|
{
|
||||||
|
public class RssGenerator
|
||||||
|
{
|
||||||
|
|
||||||
|
private readonly XNamespace _contentNs = XNamespace.Get("http://purl.org/rss/1.0/modules/content/");
|
||||||
|
private readonly XNamespace _dcNs = XNamespace.Get("http://purl.org/dc/elements/1.1/");
|
||||||
|
private readonly XNamespace _slashNs = XNamespace.Get("http://purl.org/rss/1.0/modules/slash/");
|
||||||
|
private readonly XNamespace _wfwNs = XNamespace.Get("http://wellformedweb.org/CommentAPI/");
|
||||||
|
|
||||||
|
public string GenerateRss(Channel channel, IEnumerable<Item> items)
|
||||||
|
{
|
||||||
|
var doc = new XDocument(
|
||||||
|
new XDeclaration("1.0", "UTF-8", null),
|
||||||
|
new XElement("rss",
|
||||||
|
new XAttribute("version", "2.0"),
|
||||||
|
GetNamespaceAttributes(),
|
||||||
|
new XElement("channel",
|
||||||
|
RequiredChannelElements(channel),
|
||||||
|
GenerateItems(items)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return doc.ToStringWithDeclaration();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private IEnumerable<XAttribute> GetNamespaceAttributes()
|
||||||
|
{
|
||||||
|
yield return new XAttribute(XNamespace.Xmlns + "content", "http://purl.org/rss/1.0/modules/content/");
|
||||||
|
yield return new XAttribute(XNamespace.Xmlns + "dc", "http://purl.org/dc/elements/1.1/");
|
||||||
|
yield return new XAttribute(XNamespace.Xmlns + "atom", "http://www.w3.org/2005/Atom");
|
||||||
|
yield return new XAttribute(XNamespace.Xmlns + "wfw", "http://wellformedweb.org/CommentAPI/");
|
||||||
|
yield return new XAttribute(XNamespace.Xmlns + "slash", "http://purl.org/rss/1.0/modules/slash/");
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<XElement> RequiredChannelElements(Channel channel)
|
||||||
|
{
|
||||||
|
yield return new XElement("title", channel.Title);
|
||||||
|
yield return new XElement("link", channel.Link);
|
||||||
|
yield return new XElement("description", channel.Description);
|
||||||
|
yield return new XElement("language", channel.Language);
|
||||||
|
yield return new XElement("lastBuildDate", FormatRfc1123(channel.LastBuildDate));
|
||||||
|
yield return new XElement("pubDate", FormatRfc1123(channel.PubDate));
|
||||||
|
yield return new XElement(XNamespace.Get("http://www.w3.org/2005/Atom") + "link",
|
||||||
|
new XAttribute("href", channel.FeedUrl),
|
||||||
|
new XAttribute("rel", "self"),
|
||||||
|
new XAttribute("type", "application/rss+xml"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatRfc1123(DateTimeOffset date)
|
||||||
|
{
|
||||||
|
return date.ToString("r", System.Globalization.CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<XElement> GenerateItems(IEnumerable<Item> items)
|
||||||
|
{
|
||||||
|
foreach (var item in items)
|
||||||
|
{
|
||||||
|
yield return new XElement("item",
|
||||||
|
new XElement("title", item.Title),
|
||||||
|
new XElement("link", item.Url),
|
||||||
|
new XElement("guid", new XAttribute("isPermaLink", "true"), item.Url),
|
||||||
|
new XElement("pubDate", item.PublishDate.ToString("ddd, dd MMM yyyy HH:mm:ss zzz")),
|
||||||
|
new XElement(_dcNs + "creator", item.Creator),
|
||||||
|
new XElement("description",
|
||||||
|
new XCData(item.Description)),
|
||||||
|
new XElement(_contentNs + "encoded",
|
||||||
|
new XAttribute(XNamespace.Xml + "lang", "zh-CN"),
|
||||||
|
new XCData(item.Content)),
|
||||||
|
new XElement(_slashNs + "comments", item.CommentsCount),
|
||||||
|
new XElement("comments", item.CommentsUrl),
|
||||||
|
new XElement(_wfwNs + "commentRss", item.CommentRssUrl)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
Yes.Infrastructure/Rss/XDocumentExtensions.cs
Normal file
15
Yes.Infrastructure/Rss/XDocumentExtensions.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
namespace Yes.Infrastructure.Rss
|
||||||
|
{
|
||||||
|
public static class XDocumentExtensions
|
||||||
|
{
|
||||||
|
public static string ToStringWithDeclaration(this XDocument doc)
|
||||||
|
{
|
||||||
|
if (doc.Declaration == null)
|
||||||
|
return doc.ToString();
|
||||||
|
|
||||||
|
using var writer = new StringWriter();
|
||||||
|
doc.Save(writer);
|
||||||
|
return writer.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user