生产环境
This commit is contained in:
+64
-18
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user