From 22e9aefe1a2bd67d40c39a09e10c00444711b1f0 Mon Sep 17 00:00:00 2001 From: xiang <27800734@qq.com> Date: Thu, 15 May 2025 12:53:47 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9Erss=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UpdateConfigurationCommandHandler.cs | 1 + Yes.Application/Blogs/GetRssQueryHandler.cs | 89 ---------------- .../HangfireJobRegistrationService.cs | 38 +++++++ .../Core/HostedServices/RssGenerationJob.cs | 100 ++++++++++++++++++ Yes.Application/DependencyInjection.cs | 1 + Yes.Application/GlobalUsings.cs | 5 + Yes.Application/Yes.Application.csproj | 5 + .../Admin/Pages/Configuration/Index.cshtml | 5 + Yes.Blog/DependencyInjection.cs | 65 +++++++++++- .../UpdateConfigurationEndpoint.cs | 7 +- Yes.Blog/Endpoints/Blogs/RssEndpoint.cs | 19 ---- Yes.Blog/GlobalUsings.cs | 7 +- Yes.Blog/Program.cs | 6 ++ Yes.Blog/Yes.Blog.csproj | 5 + Yes.Blog/wwwroot/defaulttheme/_Comment.liquid | 63 +++++------ Yes.Blog/wwwroot/defaulttheme/_Footer.liquid | 2 +- Yes.Blog/wwwroot/defaulttheme/_Layout.liquid | 2 + Yes.Blog/wwwroot/defaulttheme/_Rss.liquid | 5 + Yes.Blog/wwwroot/defaulttheme/css/style.css | 9 +- Yes.Blog/wwwroot/defaulttheme/rss.png | Bin 0 -> 324 bytes Yes.Blog/wwwroot/feed.xml | 24 +++++ Yes.Domain/Configurations/BlogSettings.cs | 3 + .../AuthenticationExtensions.cs | 2 +- .../MySqlConnectionStringBuilder.cs | 6 +- 24 files changed, 318 insertions(+), 151 deletions(-) delete mode 100644 Yes.Application/Blogs/GetRssQueryHandler.cs create mode 100644 Yes.Application/Core/HostedServices/HangfireJobRegistrationService.cs create mode 100644 Yes.Application/Core/HostedServices/RssGenerationJob.cs delete mode 100644 Yes.Blog/Endpoints/Blogs/RssEndpoint.cs create mode 100644 Yes.Blog/wwwroot/defaulttheme/_Rss.liquid create mode 100644 Yes.Blog/wwwroot/defaulttheme/rss.png create mode 100644 Yes.Blog/wwwroot/feed.xml diff --git a/Yes.Application/Admins/Configurations/UpdateConfigurationCommandHandler.cs b/Yes.Application/Admins/Configurations/UpdateConfigurationCommandHandler.cs index ae4f50c..63f9e9b 100644 --- a/Yes.Application/Admins/Configurations/UpdateConfigurationCommandHandler.cs +++ b/Yes.Application/Admins/Configurations/UpdateConfigurationCommandHandler.cs @@ -10,6 +10,7 @@ string Script, int PageSizeOfHomepage, int PageSizeOfListpage, + int RssGenerationIntervalHours, string ArticleRoute, string PageRoute, string CategoryRoute, diff --git a/Yes.Application/Blogs/GetRssQueryHandler.cs b/Yes.Application/Blogs/GetRssQueryHandler.cs deleted file mode 100644 index b94668e..0000000 --- a/Yes.Application/Blogs/GetRssQueryHandler.cs +++ /dev/null @@ -1,89 +0,0 @@ - -namespace Yes.Application.Blogs -{ - - public record GetRssQuery() : IRequest; - - public record GetRssQueryResponse(string Xml); - - public class GetRssQueryHandler( - IOptionsMonitor options, - BlogDbContext db, - IMapper mapper) : IRequestHandler - { - private readonly BlogSettings _settings = options.CurrentValue; - private readonly BlogDbContext _db = db; - public async Task 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); - } - - - } -} diff --git a/Yes.Application/Core/HostedServices/HangfireJobRegistrationService.cs b/Yes.Application/Core/HostedServices/HangfireJobRegistrationService.cs new file mode 100644 index 0000000..9e9cd09 --- /dev/null +++ b/Yes.Application/Core/HostedServices/HangfireJobRegistrationService.cs @@ -0,0 +1,38 @@ + + +namespace Yes.Application.Core.HostedServices +{ + public class HangfireJobRegistrationService : BackgroundService + { + private readonly IServiceProvider _serviceProvider; + private readonly BlogSettings _settings; + + public HangfireJobRegistrationService( + IServiceProvider serviceProvider, + IOptionsMonitor options) + { + _serviceProvider = serviceProvider; + _settings = options.CurrentValue; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + using var scope = _serviceProvider.CreateScope(); + var recurringJobManager = scope.ServiceProvider.GetRequiredService(); + + var intervalHours = _settings.RssGenerationIntervalHours; + + //recurringJobManager.AddOrUpdate( + // nameof(RssGenerationJob), + // x => x.Execute(), + // Cron.Hourly(intervalHours) + //); + + recurringJobManager.AddOrUpdate( + nameof(RssGenerationJob), + x => x.Execute(), + Cron.Minutely + ); + } + } +} diff --git a/Yes.Application/Core/HostedServices/RssGenerationJob.cs b/Yes.Application/Core/HostedServices/RssGenerationJob.cs new file mode 100644 index 0000000..3ca445b --- /dev/null +++ b/Yes.Application/Core/HostedServices/RssGenerationJob.cs @@ -0,0 +1,100 @@ +namespace Yes.Application.Core.HostedServices +{ + public class RssGenerationJob + { + private readonly BlogSettings _settings; + private readonly BlogDbContext _db; + private readonly ILogger _logger; + private readonly IWebHostEnvironment _env; + public RssGenerationJob( + IOptionsMonitor options, + BlogDbContext db, + IWebHostEnvironment env, + ILogger logger) + { + _settings = options.CurrentValue; + _db = db; + _env = env; + _logger = logger; + } + + public async Task Execute() + { + try + { + 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.Url}/{_settings.ArticleRoute.ToRoute(x.Category, x)}", + Description = x.Summary, + CommentsCount = x.CommentCount, + Creator = x.UserId.ToString(), + PublishDate = x.PublishDate, + CommentsUrl = $"{_settings.Url}/{_settings.ArticleRoute.ToRoute(x.Category, x)}#comments", + CommentRssUrl = $"{_settings.Url}/feed/" + }); + + + var generator = new RssGenerator(); + string xml = generator.GenerateRss(channel, items); + + + File.WriteAllText(Path.Combine(_env.WebRootPath, "feed.xml"), xml); + } + catch (Exception ex) + { + _logger.LogError(ex, "RSS generation failed"); + } + } + } +} diff --git a/Yes.Application/DependencyInjection.cs b/Yes.Application/DependencyInjection.cs index d77945f..9577e4a 100644 --- a/Yes.Application/DependencyInjection.cs +++ b/Yes.Application/DependencyInjection.cs @@ -39,5 +39,6 @@ } + } } diff --git a/Yes.Application/GlobalUsings.cs b/Yes.Application/GlobalUsings.cs index 9eadd23..085dafc 100644 --- a/Yes.Application/GlobalUsings.cs +++ b/Yes.Application/GlobalUsings.cs @@ -40,3 +40,8 @@ global using System.Text; global using Yes.Infrastructure.Authorizations.Identity.Authroizations; global using System.Xml.Linq; global using Yes.Infrastructure.Rss; + +global using Microsoft.Extensions.Configuration; +global using Microsoft.Extensions.Logging; +global using Hangfire; +global using Microsoft.Extensions.Hosting; \ No newline at end of file diff --git a/Yes.Application/Yes.Application.csproj b/Yes.Application/Yes.Application.csproj index 840a725..e80260d 100644 --- a/Yes.Application/Yes.Application.csproj +++ b/Yes.Application/Yes.Application.csproj @@ -11,6 +11,11 @@ + + + + + diff --git a/Yes.Blog/Areas/Admin/Pages/Configuration/Index.cshtml b/Yes.Blog/Areas/Admin/Pages/Configuration/Index.cshtml index 6e760d9..e4d97d6 100644 --- a/Yes.Blog/Areas/Admin/Pages/Configuration/Index.cshtml +++ b/Yes.Blog/Areas/Admin/Pages/Configuration/Index.cshtml @@ -101,6 +101,11 @@ + +
+ + +
diff --git a/Yes.Blog/DependencyInjection.cs b/Yes.Blog/DependencyInjection.cs index a8aa280..6e637cc 100644 --- a/Yes.Blog/DependencyInjection.cs +++ b/Yes.Blog/DependencyInjection.cs @@ -1,4 +1,5 @@ -namespace Yes.Blog + +namespace Yes.Blog { public static class DependencyInjection { @@ -30,6 +31,8 @@ services.AddEndpointsApiExplorer(); + services.AddHangfire(configuration); + services.AddOpenApi(options => { options.AddDocumentTransformer((document, context, cancellationToken) => @@ -240,7 +243,7 @@ } - + app.MapHangfireDashboard(); return app; } @@ -273,5 +276,63 @@ return app; } + + public static IServiceCollection AddHangfire(this IServiceCollection services, IConfiguration configuration) + { + Enum.TryParse(configuration["DatabaseType"], out DatabaseTypeEnum databaseType); + var connectionString = configuration["ConnectionString"] ?? ""; + var dbName = configuration["DatabaseName"] ?? ""; + + var builder = ConnectionStringBuilderFactory.CreateStringBuilder(databaseType, connectionString); + builder.SetDatabaseName(dbName); + + connectionString = builder.GetConnectionString(); + + // Add Hangfire services + services.AddHangfire(config => + { + switch (databaseType) + { + case DatabaseTypeEnum.SqlServer: + config.UseSqlServerStorage(connectionString); + break; + case DatabaseTypeEnum.MySql: + config.UseStorage( + new MySqlStorage( + connectionString, + new MySqlStorageOptions + { + TransactionIsolationLevel = IsolationLevel.ReadCommitted, + QueuePollInterval = TimeSpan.FromSeconds(15), + JobExpirationCheckInterval = TimeSpan.FromHours(1), + CountersAggregateInterval = TimeSpan.FromMinutes(5), + PrepareSchemaIfNecessary = true, + DashboardJobListLimit = 50000, + TransactionTimeout = TimeSpan.FromMinutes(1), + TablesPrefix = "Hangfire" + })); + break; + case DatabaseTypeEnum.PostgreSQL: + config.UsePostgreSqlStorage(c =>c.UseNpgsqlConnection(connectionString)); + break; + default: + throw new InvalidOperationException($"Unsupported database type: {databaseType}"); + } + + config.SetDataCompatibilityLevel(CompatibilityLevel.Version_180).UseSimpleAssemblyNameTypeSerializer().UseRecommendedSerializerSettings(); + }); + + + // Add the processing server as IHostedService + services.AddHangfireServer(); + + + // var minute = 60; + //int.TryParse(configuration["DatabaseName"], out minute); + // RecurringJob.AddOrUpdate("dynamic-job", x => x.Execute(), Cron.Minutely); + + + return services; + } } } diff --git a/Yes.Blog/Endpoints/Admins/Configurations/UpdateConfigurationEndpoint.cs b/Yes.Blog/Endpoints/Admins/Configurations/UpdateConfigurationEndpoint.cs index 9cda1ff..9eb52e8 100644 --- a/Yes.Blog/Endpoints/Admins/Configurations/UpdateConfigurationEndpoint.cs +++ b/Yes.Blog/Endpoints/Admins/Configurations/UpdateConfigurationEndpoint.cs @@ -14,7 +14,8 @@ string Script, int PageSizeOfHomepage, int PageSizeOfListpage, - string ArticleRoute, + int RssGenerationIntervalHours, + string ArticleRoute, string PageRoute, string CategoryRoute, string SearchRoute, @@ -33,6 +34,10 @@ RuleFor(x => x.CategoryRoute).NotEmpty().WithMessage("分类页路径不能为空!"); RuleFor(x => x.SearchRoute).NotEmpty().WithMessage("搜索页路径不能为空!"); RuleFor(x => x.TagRoute).NotEmpty().WithMessage("标签页路径不能为空!"); + + RuleFor(x => x.PageSizeOfHomepage).GreaterThan(0).WithMessage("主页每页文章数必须大于0!"); + RuleFor(x => x.PageSizeOfListpage).GreaterThan(0).WithMessage("列表页每页文章数必须大于0!"); + RuleFor(x => x.RssGenerationIntervalHours).GreaterThan(0).WithMessage("RSS生成频率必须大于0!"); } } diff --git a/Yes.Blog/Endpoints/Blogs/RssEndpoint.cs b/Yes.Blog/Endpoints/Blogs/RssEndpoint.cs deleted file mode 100644 index 80e5044..0000000 --- a/Yes.Blog/Endpoints/Blogs/RssEndpoint.cs +++ /dev/null @@ -1,19 +0,0 @@ - -namespace Yes.Blog.Endpoints.Blogs -{ - public class RssEndpoint : BlogEndpointScheme, IEndpoint - { - public void Map(IEndpointRouteBuilder app) => app.MapGet("feed", Handle).AllowAnonymous(); - - private async Task 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"); - } - - } -} diff --git a/Yes.Blog/GlobalUsings.cs b/Yes.Blog/GlobalUsings.cs index 5348deb..e5739d3 100644 --- a/Yes.Blog/GlobalUsings.cs +++ b/Yes.Blog/GlobalUsings.cs @@ -46,4 +46,9 @@ global using Microsoft.AspNetCore.Authorization; global using Yes.Application.Installs; global using Scalar.AspNetCore; global using Yes.Blog.Areas.Admin.Pages; -global using Yes.Infrastructure.Authorizations.User; \ No newline at end of file +global using Yes.Infrastructure.Authorizations.User; +global using System.Transactions; +global using Hangfire; +global using Hangfire.MySql; +global using Hangfire.PostgreSql; +global using Yes.Infrastructure.Data.ConnectionStringBuilders; \ No newline at end of file diff --git a/Yes.Blog/Program.cs b/Yes.Blog/Program.cs index c9a1b16..d263f42 100644 --- a/Yes.Blog/Program.cs +++ b/Yes.Blog/Program.cs @@ -1,4 +1,6 @@ +using Hangfire; + namespace Yes.Blog { public class Program @@ -22,6 +24,8 @@ namespace Yes.Blog app.MapOpenApi(); app.MapScalarApiReference(); + + app.UseHangfireDashboard(); } @@ -34,6 +38,8 @@ namespace Yes.Blog app.UseGlobalExceptionHandler(); + + app.UseAuthentication(); app.UseAuthorization(); diff --git a/Yes.Blog/Yes.Blog.csproj b/Yes.Blog/Yes.Blog.csproj index 5ee2ab5..f2932b4 100644 --- a/Yes.Blog/Yes.Blog.csproj +++ b/Yes.Blog/Yes.Blog.csproj @@ -39,6 +39,11 @@ + + + + + diff --git a/Yes.Blog/wwwroot/defaulttheme/_Comment.liquid b/Yes.Blog/wwwroot/defaulttheme/_Comment.liquid index 63e5fa2..7d939fb 100644 --- a/Yes.Blog/wwwroot/defaulttheme/_Comment.liquid +++ b/Yes.Blog/wwwroot/defaulttheme/_Comment.liquid @@ -1,4 +1,4 @@ -
+
@@ -70,14 +70,6 @@ {% endfor %} - {% if Model.Comments.HasPreviousPage %} - 上一页 - {% endif %} - - {% if Model.Comments.HasNextPage %} - 下一页 - {% endif %} -
{% endif %}
@@ -91,31 +83,34 @@ {% section scripts %}