排课计算、课表发布、补考自动编排三类任务统一接入消息系统。
业务任务与 Outbox 消息同一事务落库,避免“数据库成功但消息没发出去”。 RabbitMQ 使用持久化消息、发布确认、手动 ACK、Quorum Queue、死信队列。 增加处理租约、心跳、异常重试、最大重试次数和幂等状态控制。 保留 InMemory 模式,开发环境无需安装 RabbitMQ。 支持多实例竞争消费,后续可以横向扩容。 新增 /health/messaging 消息系统健康检查。
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
using Jiaowu.Api.Domain.Identity;
|
||||
using Jiaowu.Api.Infrastructure.BackgroundJobs;
|
||||
using Jiaowu.Api.Infrastructure.Configuration;
|
||||
using Jiaowu.Api.Infrastructure.Auth;
|
||||
using Jiaowu.Api.Infrastructure.Caching;
|
||||
@@ -70,6 +71,12 @@ var cacheOptions = builder.Configuration
|
||||
var officialDocumentOptions = builder.Configuration
|
||||
.GetSection(OfficialDocumentOptions.SectionName)
|
||||
.Get<OfficialDocumentOptions>() ?? new OfficialDocumentOptions();
|
||||
var backgroundJobOptions = builder.Configuration
|
||||
.GetSection(BackgroundJobOptions.SectionName)
|
||||
.Get<BackgroundJobOptions>() ?? new BackgroundJobOptions();
|
||||
var rabbitMqOptions = builder.Configuration
|
||||
.GetSection(RabbitMqOptions.SectionName)
|
||||
.Get<RabbitMqOptions>() ?? new RabbitMqOptions();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(officialDocumentOptions.InstitutionName) ||
|
||||
string.IsNullOrWhiteSpace(officialDocumentOptions.IssuingOffice) ||
|
||||
@@ -119,9 +126,46 @@ if (cacheOptions.ReferenceExpirationMinutes is < 1 or > 1440 ||
|
||||
"Cache 缓存时间或 MaximumPayloadKilobytes 超出允许范围。");
|
||||
}
|
||||
|
||||
if (!backgroundJobOptions.Transport.Equals(
|
||||
"InMemory",
|
||||
StringComparison.OrdinalIgnoreCase) &&
|
||||
!backgroundJobOptions.UsesRabbitMq)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"BackgroundJobs:Transport 仅支持 InMemory 或 RabbitMq。");
|
||||
}
|
||||
if (backgroundJobOptions.PollIntervalMilliseconds is < 100 or > 30000 ||
|
||||
backgroundJobOptions.LeaseSeconds is < 30 or > 3600 ||
|
||||
backgroundJobOptions.PrefetchCount is < 1 or > 100 ||
|
||||
backgroundJobOptions.ProcessingAttemptLimit is < 1 or > 100 ||
|
||||
string.IsNullOrWhiteSpace(backgroundJobOptions.Exchange) ||
|
||||
string.IsNullOrWhiteSpace(backgroundJobOptions.QueuePrefix))
|
||||
{
|
||||
throw new InvalidOperationException("BackgroundJobs 配置超出允许范围。");
|
||||
}
|
||||
if (backgroundJobOptions.UsesRabbitMq &&
|
||||
(string.IsNullOrWhiteSpace(rabbitMqOptions.HostName) ||
|
||||
rabbitMqOptions.Port is < 1 or > 65535 ||
|
||||
string.IsNullOrWhiteSpace(rabbitMqOptions.UserName) ||
|
||||
string.IsNullOrWhiteSpace(rabbitMqOptions.Password) ||
|
||||
string.IsNullOrWhiteSpace(rabbitMqOptions.VirtualHost)))
|
||||
{
|
||||
throw new InvalidOperationException("RabbitMq 连接配置不完整。");
|
||||
}
|
||||
if (backgroundJobOptions.UsesRabbitMq &&
|
||||
!builder.Environment.IsDevelopment() &&
|
||||
(rabbitMqOptions.UserName.Equals("guest", StringComparison.OrdinalIgnoreCase) ||
|
||||
rabbitMqOptions.Password == "guest"))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"生产环境启用 RabbitMQ 时不能使用默认 guest 凭据。");
|
||||
}
|
||||
|
||||
builder.Services.AddSingleton(databaseOptions);
|
||||
builder.Services.AddSingleton(cacheOptions);
|
||||
builder.Services.AddSingleton(officialDocumentOptions);
|
||||
builder.Services.AddSingleton(backgroundJobOptions);
|
||||
builder.Services.AddSingleton(rabbitMqOptions);
|
||||
builder.Services.Configure<OfficialDocumentOptions>(
|
||||
builder.Configuration.GetSection(OfficialDocumentOptions.SectionName));
|
||||
builder.Services.AddDbContextPool<AppDbContext>(options =>
|
||||
@@ -218,19 +262,29 @@ builder.Services.AddScoped<AutomaticScheduleGenerator>();
|
||||
builder.Services.AddScoped<PersonalCalendarService>();
|
||||
builder.Services.AddScoped<ClassroomReservationAvailabilityService>();
|
||||
builder.Services.AddScoped<AutomaticScheduleJobProcessor>();
|
||||
builder.Services.AddSingleton<AutomaticScheduleJobQueue>();
|
||||
builder.Services.AddHostedService<AutomaticScheduleJobWorker>();
|
||||
builder.Services.AddScoped<SchedulePlanPublisher>();
|
||||
builder.Services.AddScoped<SchedulePublishJobProcessor>();
|
||||
builder.Services.AddSingleton<SchedulePublishJobQueue>();
|
||||
builder.Services.AddHostedService<SchedulePublishJobWorker>();
|
||||
builder.Services.AddHostedService<WarningCheckWorker>();
|
||||
builder.Services.AddScoped<ExamArrangementService>();
|
||||
builder.Services.AddScoped<MakeupExamEligibilityService>();
|
||||
builder.Services.AddScoped<MakeupExamArrangementService>();
|
||||
builder.Services.AddSingleton<MakeupExamAutoJobQueue>();
|
||||
builder.Services.AddHostedService<MakeupExamAutoJobWorker>();
|
||||
builder.Services.AddScoped<MakeupExamAutoJobProcessor>();
|
||||
builder.Services.AddSingleton<BackgroundJobRunner>();
|
||||
if (backgroundJobOptions.UsesRabbitMq)
|
||||
{
|
||||
builder.Services.AddSingleton<
|
||||
IBackgroundJobTransport,
|
||||
RabbitMqBackgroundJobTransport>();
|
||||
builder.Services.AddHostedService<RabbitMqBackgroundJobWorker>();
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Services.AddSingleton<InMemoryBackgroundJobTransport>();
|
||||
builder.Services.AddSingleton<IBackgroundJobTransport>(services =>
|
||||
services.GetRequiredService<InMemoryBackgroundJobTransport>());
|
||||
builder.Services.AddHostedService<InMemoryBackgroundJobWorker>();
|
||||
}
|
||||
builder.Services.AddHostedService<BackgroundJobOutboxPublisher>();
|
||||
builder.Services.AddSingleton<IOfficialDocumentPdfGenerator, OfficialDocumentPdfGenerator>();
|
||||
builder.Services.AddScoped<OfficialDocumentService>();
|
||||
|
||||
@@ -391,6 +445,7 @@ app.MapGet("/health/live", () => Results.Ok(new { Status = "healthy" }))
|
||||
app.MapGet("/health", CheckDatabaseHealthAsync).AllowAnonymous();
|
||||
app.MapGet("/health/ready", CheckDatabaseHealthAsync).AllowAnonymous();
|
||||
app.MapGet("/health/cache", CheckCacheHealthAsync).AllowAnonymous();
|
||||
app.MapGet("/health/messaging", CheckMessagingHealthAsync).AllowAnonymous();
|
||||
app.MapFallback(async context =>
|
||||
{
|
||||
if (context.Request.Path.StartsWithSegments("/api") ||
|
||||
@@ -477,4 +532,17 @@ static async Task<IResult> CheckCacheHealthAsync(
|
||||
}
|
||||
}
|
||||
|
||||
static async Task<IResult> CheckMessagingHealthAsync(
|
||||
IBackgroundJobTransport transport,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var healthy = await transport.CheckHealthAsync(cancellationToken);
|
||||
var backend = transport.IsDurable ? "rabbitmq" : "memory";
|
||||
return healthy
|
||||
? Results.Ok(new { Status = "healthy", Backend = backend })
|
||||
: Results.Json(
|
||||
new { Status = "unhealthy", Backend = backend },
|
||||
statusCode: StatusCodes.Status503ServiceUnavailable);
|
||||
}
|
||||
|
||||
public partial class Program;
|
||||
|
||||
Reference in New Issue
Block a user