using System.Text; using System.Text.Json.Serialization; using Jiaowu.Api.Domain.Identity; using Jiaowu.Api.Infrastructure.Auth; using Jiaowu.Api.Infrastructure.Middleware; using Jiaowu.Api.Infrastructure.Persistence; using Jiaowu.Api.Infrastructure.Scheduling; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; using Microsoft.OpenApi.Models; var builder = WebApplication.CreateBuilder(args); var databaseOptions = builder.Configuration .GetSection(DatabaseOptions.SectionName) .Get() ?? new DatabaseOptions(); if (databaseOptions.Provider.Equals("SQLite", StringComparison.OrdinalIgnoreCase) && !builder.Environment.IsDevelopment()) { throw new InvalidOperationException("SQLite 仅允许在 Development 环境使用。生产环境请配置 MySql。"); } builder.Services.AddDbContext(options => { if (databaseOptions.Provider.Equals("SQLite", StringComparison.OrdinalIgnoreCase)) { var sqliteConnectionString = builder.Configuration.GetConnectionString("SQLite") ?? throw new InvalidOperationException("缺少 ConnectionStrings:SQLite。"); var sqliteBuilder = new SqliteConnectionStringBuilder(sqliteConnectionString); if (!Path.IsPathRooted(sqliteBuilder.DataSource)) { sqliteBuilder.DataSource = Path.GetFullPath( sqliteBuilder.DataSource, builder.Environment.ContentRootPath); } Directory.CreateDirectory( Path.GetDirectoryName(sqliteBuilder.DataSource) ?? builder.Environment.ContentRootPath); options.UseSqlite(sqliteBuilder.ConnectionString); return; } if (!databaseOptions.Provider.Equals("MySql", StringComparison.OrdinalIgnoreCase)) { throw new InvalidOperationException( $"不支持数据库 Provider '{databaseOptions.Provider}',可选值为 SQLite、MySql。"); } var connectionString = builder.Configuration.GetConnectionString("MySql"); if (string.IsNullOrWhiteSpace(connectionString)) { throw new InvalidOperationException( "缺少 MySQL 连接串。请通过 ConnectionStrings__MySql 环境变量配置。"); } options.UseMySQL(connectionString); }); builder.Services .AddIdentityCore(options => { options.Password.RequiredLength = 8; options.Password.RequireDigit = true; options.Password.RequireLowercase = true; options.Password.RequireUppercase = true; options.Password.RequireNonAlphanumeric = true; options.User.RequireUniqueEmail = false; options.Lockout.MaxFailedAccessAttempts = 5; options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(15); }) .AddRoles() .AddEntityFrameworkStores(); var jwtOptions = builder.Configuration.GetSection(JwtOptions.SectionName).Get() ?? throw new InvalidOperationException("缺少 Jwt 配置。"); if (Encoding.UTF8.GetByteCount(jwtOptions.Key) < 32) { throw new InvalidOperationException("Jwt:Key 至少需要 32 字节。"); } builder.Services.Configure( builder.Configuration.GetSection(JwtOptions.SectionName)); builder.Services.AddHttpContextAccessor(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddSingleton(); builder.Services.AddHostedService(); builder.Services .AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = jwtOptions.Issuer, ValidAudience = jwtOptions.Audience, IssuerSigningKey = new SymmetricSecurityKey( Encoding.UTF8.GetBytes(jwtOptions.Key)), ClockSkew = TimeSpan.FromMinutes(1) }; }); builder.Services.AddAuthorization(); builder.Services.AddCors(options => { options.AddPolicy("Web", policy => { var origins = builder.Configuration.GetSection("Cors:Origins").Get() ?? ["http://localhost:5173"]; policy.WithOrigins(origins) .AllowAnyHeader() .AllowAnyMethod(); }); }); builder.Services.AddProblemDetails(); builder.Services.AddExceptionHandler(options => { options.ExceptionHandler = async context => { var exception = context.Features .Get()?.Error; var isConstraintConflict = exception is DbUpdateException; var statusCode = isConstraintConflict ? StatusCodes.Status409Conflict : StatusCodes.Status500InternalServerError; context.Response.StatusCode = statusCode; await Results.Problem( title: isConstraintConflict ? "数据约束冲突" : "服务器处理请求时发生错误", detail: isConstraintConflict ? "编码可能已存在,或该数据正在被其他业务引用。" : builder.Environment.IsDevelopment() ? exception?.Message : "请稍后重试,并联系系统管理员查看日志。", statusCode: statusCode) .ExecuteAsync(context); }; }); builder.Services.AddControllers() .AddJsonOptions(options => options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter())); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(options => { options.SwaggerDoc("v1", new OpenApiInfo { Title = "大学教务管理系统 API", Version = "v1" }); options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme { Name = "Authorization", Type = SecuritySchemeType.Http, Scheme = "bearer", BearerFormat = "JWT", In = ParameterLocation.Header }); options.AddSecurityRequirement(new OpenApiSecurityRequirement { [ new OpenApiSecurityScheme { Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" } } ] = [] }); }); var app = builder.Build(); app.UseExceptionHandler(); if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseDefaultFiles(); app.UseStaticFiles(new StaticFileOptions { OnPrepareResponse = context => { if (context.Context.Request.Path.StartsWithSegments("/assets")) { context.Context.Response.Headers.CacheControl = "public,max-age=31536000,immutable"; } else if (string.Equals( Path.GetExtension(context.File.Name), ".html", StringComparison.OrdinalIgnoreCase)) { context.Context.Response.Headers.CacheControl = "no-cache,no-store"; } } }); app.UseCors("Web"); app.UseAuthentication(); app.UseAuthorization(); app.UseMiddleware(); app.MapControllers(); app.MapGet("/health", () => Results.Ok(new { Status = "healthy", Database = databaseOptions.Provider, Environment = app.Environment.EnvironmentName, Time = DateTimeOffset.UtcNow })).AllowAnonymous(); app.MapFallback(async context => { if (context.Request.Path.StartsWithSegments("/api") || context.Request.Path.StartsWithSegments("/health") || context.Request.Path.StartsWithSegments("/swagger")) { context.Response.StatusCode = StatusCodes.Status404NotFound; return; } var webRoot = app.Environment.WebRootPath ?? Path.Combine(app.Environment.ContentRootPath, "wwwroot"); var indexPath = Path.Combine(webRoot, "index.html"); if (!File.Exists(indexPath)) { context.Response.StatusCode = StatusCodes.Status404NotFound; return; } context.Response.Headers.CacheControl = "no-cache,no-store"; context.Response.ContentType = "text/html; charset=utf-8"; await context.Response.SendFileAsync(indexPath); }); using (var scope = app.Services.CreateScope()) { await scope.ServiceProvider.GetRequiredService() .InitializeAsync(); } if (args.Contains("--seed-only", StringComparer.OrdinalIgnoreCase)) { return; } app.Run(); public partial class Program;