XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

EARSystem

【ASP】耳间,网页端的情绪智能推荐系统

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Security.Claims;
using System.Text.Json.Serialization;
using System.Threading.RateLimiting;
using EARSystem.Core;
using EARSystem.Infrastructure;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

var seedOnly = args.Contains("--seed-demo", StringComparer.Ordinal);
var builder = WebApplication.CreateBuilder(args.Where(x => x != "--seed-demo").ToArray());
if (seedOnly && !builder.Environment.IsDevelopment())
    throw new InvalidOperationException("--seed-demo 只能用于 Development 环境。");
builder.Services.AddDbContext<AppDbContext>(o => o.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
builder.Services.AddIdentity<ApplicationUser, IdentityRole>(o => {
    o.Password.RequiredLength = 10;
    o.User.RequireUniqueEmail = true;
    o.Lockout.MaxFailedAccessAttempts = 5;
    o.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(15);
}).AddEntityFrameworkStores<AppDbContext>().AddDefaultTokenProviders();
builder.Services.ConfigureApplicationCookie(o => {
    o.LoginPath = "/Account/Login"; o.AccessDeniedPath = "/Account/Denied";
    o.Cookie.HttpOnly = true; o.Cookie.SameSite = SameSiteMode.Lax;
    o.Cookie.SecurePolicy = builder.Environment.IsProduction() ? CookieSecurePolicy.Always : CookieSecurePolicy.SameAsRequest;
    o.ExpireTimeSpan = TimeSpan.FromDays(7); o.SlidingExpiration = true;
    o.Events.OnRedirectToLogin = context => {
        if (context.Request.Path.StartsWithSegments("/api")) context.Response.StatusCode = 401;
        else context.Response.Redirect(context.RedirectUri);
        return Task.CompletedTask;
    };
    o.Events.OnRedirectToAccessDenied = context => {
        if (context.Request.Path.StartsWithSegments("/api")) context.Response.StatusCode = 403;
        else context.Response.Redirect(context.RedirectUri);
        return Task.CompletedTask;
    };
});
builder.Services.AddAntiforgery(o => o.HeaderName = "X-CSRF-TOKEN");
builder.Services.AddControllersWithViews(o => o.Filters.Add(new AutoValidateAntiforgeryTokenAttribute()))
    .AddJsonOptions(o => o.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()));
builder.Services.AddDataProtection().PersistKeysToFileSystem(new DirectoryInfo(Path.Combine(builder.Environment.ContentRootPath, "App_Data", "keys")))
    .SetApplicationName("EARSystem");
builder.Services.AddSingleton(TimeProvider.System);
builder.Services.AddSingleton<EmotionTagAnalyzer>();
builder.Services.AddScoped<IEmotionProfileService, EmotionProfileService>();
builder.Services.AddScoped<IRecommendationService, RecommendationService>();
builder.Services.AddScoped<IExperienceService, ExperienceService>();
builder.Services.AddScoped<IPlaceProvider, PlaceProvider>();
builder.Services.AddScoped<CatalogPlaceProvider>();
builder.Services.AddScoped<DemoSeeder>();
builder.Services.Configure<AMapOptions>(builder.Configuration.GetSection("AMap"));
builder.Services.AddHttpClient<AMapClient>(http => http.Timeout = TimeSpan.FromSeconds(8)).RemoveAllLoggers();
builder.Services.AddHttpClient("MapProxy", http => http.Timeout = TimeSpan.FromSeconds(8)).RemoveAllLoggers();
builder.Services.AddRateLimiter(o => {
    o.RejectionStatusCode = 429;
    o.AddPolicy("maps", context => RateLimitPartition.GetFixedWindowLimiter(
        context.User.FindFirstValue(ClaimTypes.NameIdentifier) ?? context.Connection.RemoteIpAddress?.ToString() ?? "anonymous",
        _ => new FixedWindowRateLimiterOptions { PermitLimit = 30, Window = TimeSpan.FromMinutes(1), QueueLimit = 0 }));
});
builder.Logging.AddFilter("Microsoft.EntityFrameworkCore.Database.Command", LogLevel.Warning);
var app = builder.Build();
app.UseExceptionHandler(errorApp => errorApp.Run(async context => {
    var error = context.Features.Get<IExceptionHandlerFeature>()?.Error;
    var (status, message) = error switch {
        DbUpdateConcurrencyException => (409, "记录已在其他设备修改。请保留当前输入,重新加载最新记录后再保存。"),
        MapServiceException ex => (503, ex.Message),
        OperationCanceledException => (503, "请求超时,请重试;地图服务不可用时可以切换演示模式。"),
        KeyNotFoundException => (404, "记录不存在。"),
        ArgumentException ex => (400, ex.Message),
        DbUpdateException => (409, "记录冲突,请刷新后重试。"),
        _ => (500, "服务暂时发生错误,请稍后重试。")
    };
    context.Response.StatusCode = status;
    context.Response.ContentType = "application/problem+json";
    app.Logger.LogWarning("Request failed: {ErrorType}, status {Status}", error?.GetType().Name, status);
    await context.Response.WriteAsJsonAsync(new ProblemDetails { Status = status, Title = message });
}));
if (app.Environment.IsProduction()) { app.UseHsts(); app.UseHttpsRedirection(); }
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseRateLimiter();
app.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}");
await using (var scope = app.Services.CreateAsyncScope()) {
    var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
    if (app.Environment.IsEnvironment("Testing")) await db.Database.EnsureCreatedAsync();
    else if (app.Environment.IsDevelopment()) await db.Database.MigrateAsync();
    var demo = !app.Environment.IsProduction() && (seedOnly || builder.Configuration.GetValue("Demo:Enabled", false));
    await scope.ServiceProvider.GetRequiredService<DemoSeeder>().InitializeAsync(demo,
        app.Environment.IsDevelopment() ? Path.Combine(app.Environment.ContentRootPath, "App_Data", "demo-credentials.txt") : null,
        builder.Configuration["BootstrapAdmin:Email"], builder.Configuration["BootstrapAdmin:Password"],
        builder.Configuration.GetValue("Demo:Longitude", 120.155), builder.Configuration.GetValue("Demo:Latitude", 30.274));
}
if (seedOnly) {
    Console.WriteLine("演示数据初始化完成。已存在的数据保持不变,账号凭据保存在本地 App_Data/demo-credentials.txt。");
    return;
}
app.Run();
public partial class Program { }