using System.Text; using Health.Application.AI; using Health.Application.Admin; using Health.Application.Auth; using Health.Application.Calendars; using Health.Application.Diets; using Health.Application.Exercises; using Health.Application.HealthArchives; using Health.Application.HealthRecords; using Health.Application.Medications; using Health.Application.Maintenance; using Health.Application.Notifications; using Health.Application.Reports; using Health.Application.Users; using Health.Infrastructure.AI; using Health.Infrastructure.Admin; using Health.Infrastructure.Auth; using Health.Infrastructure.Calendars; using Health.Infrastructure.Data; using Health.Infrastructure.Diets; using Health.Infrastructure.Exercises; using Health.Infrastructure.HealthArchives; using Health.Infrastructure.HealthRecords; using Health.Infrastructure.Medications; using Health.Infrastructure.Maintenance; using Health.Infrastructure.Notifications; using Health.Infrastructure.Reports; using Health.Infrastructure.Services; using Health.Infrastructure.Users; using Health.WebApi.BackgroundServices; using Health.WebApi.Converters; using Health.WebApi.Endpoints; using Health.WebApi.Middleware; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.FileProviders; using Microsoft.IdentityModel.Tokens; // 加载 .env 文件(开发环境) var envPath = Path.Combine(Directory.GetCurrentDirectory(), "..", "..", ".env"); if (File.Exists(envPath)) { foreach (var line in File.ReadAllLines(envPath)) { var trimmed = line.Trim(); if (string.IsNullOrEmpty(trimmed) || trimmed.StartsWith('#')) continue; var eqIdx = trimmed.IndexOf('='); if (eqIdx <= 0) continue; var key = trimmed[..eqIdx].Trim(); var value = trimmed[(eqIdx + 1)..].Trim(); Environment.SetEnvironmentVariable(key, value); } } var builder = WebApplication.CreateBuilder(args); if (builder.Environment.IsDevelopment()) { builder.Logging.ClearProviders(); builder.Logging.AddConsole(); builder.Logging.AddDebug(); var dataProtectionPath = Path.Combine(Directory.GetCurrentDirectory(), "data-protection-keys"); Directory.CreateDirectory(dataProtectionPath); builder.Services.AddDataProtection() .PersistKeysToFileSystem(new DirectoryInfo(dataProtectionPath)) .SetApplicationName("HealthManager.Development"); } // ---- JSON 配置(枚举字符串、camelCase、UTC时间统一带Z)---- builder.Services.ConfigureHttpJsonOptions(options => { options.SerializerOptions.ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.IgnoreCycles; options.SerializerOptions.Converters.Add(new System.Text.Json.Serialization.JsonStringEnumConverter()); options.SerializerOptions.Converters.Add(new UtcDateTimeConverter()); options.SerializerOptions.PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase; options.SerializerOptions.PropertyNameCaseInsensitive = true; }); // ---- 数据库 ---- builder.Services.AddDbContext(options => options.UseNpgsql( builder.Configuration.GetConnectionString("Default") ?? builder.Configuration["DB_CONNECTION"] ?? "Host=localhost;Database=health_manager;Username=postgres;Password=postgres", npgsql => npgsql.MigrationsAssembly(typeof(AppDbContext).Assembly.FullName))); // ---- JWT 认证 ---- var jwtSecret = builder.Configuration["JWT_SECRET"]; if (string.IsNullOrEmpty(jwtSecret) && !builder.Environment.IsDevelopment()) throw new InvalidOperationException("JWT_SECRET 环境变量未配置,生产环境必须设置"); jwtSecret ??= "dev-secret-key-change-in-production-min-32-chars!!"; builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = builder.Configuration["JWT_ISSUER"] ?? "health-manager", ValidAudience = builder.Configuration["JWT_AUDIENCE"] ?? "health-manager-app", IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSecret)), ClockSkew = TimeSpan.Zero }; }); builder.Services.AddAuthorization(); // ---- 业务服务 ---- builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); // Apple Sign-In JWT 验证 builder.Services.AddSingleton(); 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.AddScoped(); 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.AddScoped(); 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.AddScoped(); 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.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); // ---- AI 客户端(使用 IHttpClientFactory 区分 LLM 和 VLM)---- builder.Services.AddHttpClient(client => { client.BaseAddress = new Uri((builder.Configuration["DEEPSEEK_BASE_URL"] ?? "https://api.deepseek.com/v1").TrimEnd('/') + "/"); client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", builder.Configuration["DEEPSEEK_API_KEY"] ?? ""); client.Timeout = TimeSpan.FromSeconds(60); }); builder.Services.AddHttpClient(client => { client.BaseAddress = new Uri((builder.Configuration["VLM_BASE_URL"] ?? "https://dashscope.aliyuncs.com/compatible-mode/v1").TrimEnd('/') + "/"); client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", builder.Configuration["VLM_API_KEY"] ?? ""); client.Timeout = TimeSpan.FromSeconds(120); }); builder.Services.AddHttpClient(client => { client.BaseAddress = new Uri((builder.Configuration["FASTGPT_BASE_URL"] ?? "https://cloud.fastgpt.cn/api").TrimEnd('/') + "/"); client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", builder.Configuration["FASTGPT_API_KEY"] ?? ""); client.Timeout = TimeSpan.FromSeconds(15); }); // ---- 后台服务 ---- builder.Services.AddHostedService(); builder.Services.AddHostedService(); builder.Services.AddHostedService(); builder.Services.AddHostedService(); builder.Services.AddHostedService(); builder.Services.AddHostedService(); builder.Services.AddHostedService(); builder.Services.AddHostedService(); // ---- OpenAPI ---- builder.Services.AddOpenApi(); // ---- SignalR ---- builder.Services.AddSignalR(); // ---- CORS(SignalR 需要明确 origins + AllowCredentials)---- builder.Services.AddCors(options => { options.AddDefaultPolicy(policy => { policy.SetIsOriginAllowed(_ => true).AllowAnyMethod().AllowAnyHeader().AllowCredentials(); }); }); var app = builder.Build(); // ---- 中间件管道(ExceptionMiddleware 放最前面)---- app.UseMiddleware(); app.UseCors(); app.UseAuthentication(); app.UseAuthorization(); // 默认 wwwroot 静态文件(法律文档 H5 页面等) app.UseDefaultFiles(); app.UseStaticFiles(); var uploadsPath = Path.Combine(Directory.GetCurrentDirectory(), "uploads"); Directory.CreateDirectory(uploadsPath); app.UseStaticFiles(new StaticFileOptions { FileProvider = new PhysicalFileProvider(uploadsPath), RequestPath = "/uploads" }); if (app.Environment.IsDevelopment()) app.MapOpenApi(); // ---- 数据库迁移(EF Core Migrations,可用 AUTO_MIGRATE=false 禁用自动迁移)---- using (var scope = app.Services.CreateScope()) { var db = scope.ServiceProvider.GetRequiredService(); if (app.Configuration.GetValue("AUTO_MIGRATE", true)) await db.Database.MigrateAsync(); } // ---- 注册 API 端点 ---- app.MapAuthEndpoints(); app.MapHealthEndpoints(); app.MapDietEndpoints(); app.MapMedicationEndpoints(); app.MapReportEndpoints(); app.MapConsultationEndpoints(); app.MapExerciseEndpoints(); app.MapUserEndpoints(); app.MapAiChatEndpoints(); app.MapFileEndpoints(); app.MapCalendarEndpoints(); app.MapNotificationEndpoints(); app.MapDoctorEndpoints(); app.MapAdminEndpoints(); app.MapFollowUpEndpoints(); // SignalR Hub app.MapHub("/hubs/consultation"); app.Run();