生产环境

This commit is contained in:
2026-07-25 20:21:42 +08:00 Unverified
parent 1da6543a38
commit a576cc815d
30 changed files with 3325 additions and 4129 deletions
+64 -18
View File
@@ -27,7 +27,22 @@ if (databaseOptions.Provider.Equals("SQLite", StringComparison.OrdinalIgnoreCase
throw new InvalidOperationException("SQLite 仅允许在 Development 环境使用。生产环境请配置 MySql。");
}
builder.Services.AddDbContext<AppDbContext>(options =>
var allowedHosts = builder.Configuration["AllowedHosts"];
if (!builder.Environment.IsDevelopment() &&
(string.IsNullOrWhiteSpace(allowedHosts) || allowedHosts == "*"))
{
throw new InvalidOperationException(
"生产环境必须通过 AllowedHosts 配置明确的访问域名,不能使用通配符。");
}
if (databaseOptions.CommandTimeoutSeconds is < 5 or > 300)
{
throw new InvalidOperationException(
"Database:CommandTimeoutSeconds 必须在 5 到 300 秒之间。");
}
builder.Services.AddSingleton(databaseOptions);
builder.Services.AddDbContextPool<AppDbContext>(options =>
{
if (databaseOptions.Provider.Equals("SQLite", StringComparison.OrdinalIgnoreCase))
{
@@ -59,7 +74,14 @@ builder.Services.AddDbContext<AppDbContext>(options =>
throw new InvalidOperationException(
"缺少 MySQL 连接串。请通过 ConnectionStrings__MySql 环境变量配置。");
}
options.UseMySQL(connectionString);
options.UseMySQL(connectionString, mySqlOptions =>
{
mySqlOptions.CommandTimeout(databaseOptions.CommandTimeoutSeconds);
mySqlOptions.EnableRetryOnFailure(
maxRetryCount: 5,
maxRetryDelay: TimeSpan.FromSeconds(10),
errorNumbersToAdd: null);
});
});
builder.Services
@@ -79,9 +101,11 @@ builder.Services
var jwtOptions = builder.Configuration.GetSection(JwtOptions.SectionName).Get<JwtOptions>()
?? throw new InvalidOperationException("缺少 Jwt 配置。");
if (Encoding.UTF8.GetByteCount(jwtOptions.Key) < 32)
if (Encoding.UTF8.GetByteCount(jwtOptions.Key) < 32 ||
jwtOptions.Key.Contains("REPLACE", StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException("Jwt:Key 至少需要 32 字节。");
throw new InvalidOperationException(
"Jwt:Key 必须配置为至少 32 字节的随机生产密钥,不能使用示例值。");
}
builder.Services.Configure<JwtOptions>(
@@ -92,7 +116,6 @@ builder.Services.AddScoped<ICurrentUserDataScope, CurrentUserDataScope>();
builder.Services.AddScoped<DatabaseInitializer>();
builder.Services.AddScoped<DevelopmentSqliteMigrator>();
builder.Services.AddScoped<TimetableDataService>();
builder.Services.AddScoped<DevelopmentDemoDataSeeder>();
builder.Services.AddScoped<AutomaticScheduleGenerator>();
builder.Services.AddScoped<AutomaticScheduleJobProcessor>();
builder.Services.AddSingleton<AutomaticScheduleJobQueue>();
@@ -147,13 +170,17 @@ builder.Services.AddCors(options =>
options.AddPolicy("Web", policy =>
{
var origins = builder.Configuration.GetSection("Cors:Origins").Get<string[]>()
?? ["http://localhost:5173"];
policy.WithOrigins(origins)
.AllowAnyHeader()
.AllowAnyMethod();
?? [];
if (origins.Length > 0)
{
policy.WithOrigins(origins)
.AllowAnyHeader()
.AllowAnyMethod();
}
});
});
builder.Services.AddResponseCompression(options => options.EnableForHttps = true);
builder.Services.AddProblemDetails();
builder.Services.AddExceptionHandler(options =>
{
@@ -215,6 +242,7 @@ builder.Services.AddSwaggerGen(options =>
var app = builder.Build();
app.UseExceptionHandler();
app.UseResponseCompression();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
@@ -246,13 +274,10 @@ app.UseAuthentication();
app.UseAuthorization();
app.UseMiddleware<AuditMiddleware>();
app.MapControllers();
app.MapGet("/health", () => Results.Ok(new
{
Status = "healthy",
Database = databaseOptions.Provider,
Environment = app.Environment.EnvironmentName,
Time = DateTimeOffset.UtcNow
})).AllowAnonymous();
app.MapGet("/health/live", () => Results.Ok(new { Status = "healthy" }))
.AllowAnonymous();
app.MapGet("/health", CheckDatabaseHealthAsync).AllowAnonymous();
app.MapGet("/health/ready", CheckDatabaseHealthAsync).AllowAnonymous();
app.MapFallback(async context =>
{
if (context.Request.Path.StartsWithSegments("/api") ||
@@ -280,14 +305,35 @@ app.MapFallback(async context =>
using (var scope = app.Services.CreateScope())
{
await scope.ServiceProvider.GetRequiredService<DatabaseInitializer>()
.InitializeAsync();
.InitializeAsync(
args.Contains("--migrate-only", StringComparer.OrdinalIgnoreCase));
}
if (args.Contains("--seed-only", StringComparer.OrdinalIgnoreCase))
if (args.Contains("--migrate-only", StringComparer.OrdinalIgnoreCase))
{
return;
}
app.Run();
static async Task<IResult> CheckDatabaseHealthAsync(
AppDbContext db,
CancellationToken cancellationToken)
{
try
{
return await db.Database.CanConnectAsync(cancellationToken)
? Results.Ok(new { Status = "healthy" })
: Results.Json(
new { Status = "unhealthy" },
statusCode: StatusCodes.Status503ServiceUnavailable);
}
catch
{
return Results.Json(
new { Status = "unhealthy" },
statusCode: StatusCodes.Status503ServiceUnavailable);
}
}
public partial class Program;