1
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
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 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<DatabaseOptions>() ?? new DatabaseOptions();
|
||||
|
||||
if (databaseOptions.Provider.Equals("SQLite", StringComparison.OrdinalIgnoreCase) &&
|
||||
!builder.Environment.IsDevelopment())
|
||||
{
|
||||
throw new InvalidOperationException("SQLite 仅允许在 Development 环境使用。生产环境请配置 MySql。");
|
||||
}
|
||||
|
||||
builder.Services.AddDbContext<AppDbContext>(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<ApplicationUser>(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<ApplicationRole>()
|
||||
.AddEntityFrameworkStores<AppDbContext>();
|
||||
|
||||
var jwtOptions = builder.Configuration.GetSection(JwtOptions.SectionName).Get<JwtOptions>()
|
||||
?? throw new InvalidOperationException("缺少 Jwt 配置。");
|
||||
if (Encoding.UTF8.GetByteCount(jwtOptions.Key) < 32)
|
||||
{
|
||||
throw new InvalidOperationException("Jwt:Key 至少需要 32 字节。");
|
||||
}
|
||||
|
||||
builder.Services.Configure<JwtOptions>(
|
||||
builder.Configuration.GetSection(JwtOptions.SectionName));
|
||||
builder.Services.AddScoped<ITokenService, TokenService>();
|
||||
builder.Services.AddScoped<DatabaseInitializer>();
|
||||
|
||||
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<string[]>()
|
||||
?? ["http://localhost:5173"];
|
||||
policy.WithOrigins(origins)
|
||||
.AllowAnyHeader()
|
||||
.AllowAnyMethod();
|
||||
});
|
||||
});
|
||||
|
||||
builder.Services.AddProblemDetails();
|
||||
builder.Services.AddExceptionHandler(options =>
|
||||
{
|
||||
options.ExceptionHandler = async context =>
|
||||
{
|
||||
var exception = context.Features
|
||||
.Get<Microsoft.AspNetCore.Diagnostics.IExceptionHandlerFeature>()?.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.UseCors("Web");
|
||||
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();
|
||||
|
||||
using (var scope = app.Services.CreateScope())
|
||||
{
|
||||
await scope.ServiceProvider.GetRequiredService<DatabaseInitializer>()
|
||||
.InitializeAsync();
|
||||
}
|
||||
|
||||
app.Run();
|
||||
|
||||
public partial class Program;
|
||||
Reference in New Issue
Block a user