- 后端新增 Apple 登录端点 /api/auth/apple-login - 新增 AppleTokenValidator 验证 IdentityToken - User 实体添加 AppleUserId 字段,Phone 改为可空 - 前端添加 sign_in_with_apple 依赖和 Apple 登录按钮 - 统一 Bundle ID 为 com.datalumina.YYA,显示名称为小脉健康 - 配置 DEVELOPMENT_TEAM 和 Sign in with Apple Entitlements - 补充 NSCamera/NSPhotoLibrary 权限描述 - 生产 API 地址改为 https://erpapi.datalumina.cn/xiaomai - Flutter 升级至 3.44.6 / Dart 3.12.2 Co-Authored-By: Claude <noreply@anthropic.com>
251 lines
11 KiB
C#
251 lines
11 KiB
C#
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<AppDbContext>(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<JwtProvider>();
|
||
builder.Services.AddSingleton<SmsService>();
|
||
builder.Services.AddSingleton<AppleTokenValidator>(); // Apple Sign-In JWT 验证
|
||
builder.Services.AddSingleton<PromptManager>();
|
||
builder.Services.AddScoped<IAiWriteConfirmationStore, EfAiWriteConfirmationStore>();
|
||
builder.Services.AddScoped<IAiToolExecutionService, AiToolExecutionService>();
|
||
builder.Services.AddScoped<IAdminService, AdminService>();
|
||
builder.Services.AddScoped<IAuthService, AuthService>();
|
||
builder.Services.AddScoped<IAiConversationService, AiConversationService>();
|
||
builder.Services.AddScoped<IAiConversationRepository, EfAiConversationRepository>();
|
||
builder.Services.AddScoped<IPatientContextService, PatientContextService>();
|
||
builder.Services.AddScoped<ICalendarService, CalendarService>();
|
||
builder.Services.AddScoped<ICalendarRepository, EfCalendarRepository>();
|
||
builder.Services.AddScoped<IDietService, DietService>();
|
||
builder.Services.AddScoped<IDietRepository, EfDietRepository>();
|
||
builder.Services.AddScoped<IDietImageAnalysisCoordinator, DietImageAnalysisCoordinator>();
|
||
builder.Services.AddScoped<IDietImageAnalyzer, DietImageAnalyzer>();
|
||
builder.Services.AddScoped<IDietImageAnalysisQueue, DietImageAnalysisQueue>();
|
||
builder.Services.AddScoped<IAttachmentContextBuilder, AttachmentContextBuilder>();
|
||
builder.Services.AddScoped<IExerciseService, ExerciseService>();
|
||
builder.Services.AddScoped<IExerciseRepository, EfExerciseRepository>();
|
||
builder.Services.AddScoped<IExerciseReminderProducer, ExerciseReminderProducer>();
|
||
builder.Services.AddScoped<IHealthArchiveService, HealthArchiveService>();
|
||
builder.Services.AddScoped<IHealthArchiveRepository, EfHealthArchiveRepository>();
|
||
builder.Services.AddScoped<IHealthRecordService, HealthRecordService>();
|
||
builder.Services.AddScoped<IHealthRecordRepository, EfHealthRecordRepository>();
|
||
builder.Services.AddScoped<IMedicationService, MedicationService>();
|
||
builder.Services.AddScoped<IMedicationRepository, EfMedicationRepository>();
|
||
builder.Services.AddScoped<IMedicationReminderScanner, MedicationReminderScanner>();
|
||
builder.Services.AddScoped<IMedicationReminderDispatcher, OutboxMedicationReminderDispatcher>();
|
||
builder.Services.AddScoped<IMedicationReminderQueue, MedicationReminderQueue>();
|
||
builder.Services.AddScoped<IMaintenanceService, MaintenanceService>();
|
||
builder.Services.AddScoped<IMaintenanceRepository, EfMaintenanceRepository>();
|
||
builder.Services.AddScoped<IInAppNotificationService, InAppNotificationService>();
|
||
builder.Services.AddScoped<IInAppNotificationRepository, EfInAppNotificationRepository>();
|
||
builder.Services.AddScoped<IUserNotificationProducer, EfUserNotificationProducer>();
|
||
builder.Services.AddScoped<INotificationOutboxProcessor, EfNotificationOutboxProcessor>();
|
||
builder.Services.AddScoped<IReportService, ReportService>();
|
||
builder.Services.AddScoped<IReportRepository, EfReportRepository>();
|
||
builder.Services.AddScoped<IReportFileStorage, LocalReportFileStorage>();
|
||
builder.Services.AddScoped<IReportAnalysisService, ReportAnalysisService>();
|
||
builder.Services.AddScoped<IReportAnalysisQueue, EfReportAnalysisQueue>();
|
||
builder.Services.AddScoped<IUserService, UserService>();
|
||
builder.Services.AddScoped<IUserRepository, EfUserRepository>();
|
||
|
||
// ---- AI 客户端(使用 IHttpClientFactory 区分 LLM 和 VLM)----
|
||
builder.Services.AddHttpClient<DeepSeekClient>(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<VisionClient>(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<FastGptKnowledgeClient>(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<MedicationReminderService>();
|
||
builder.Services.AddHostedService<ExerciseReminderService>();
|
||
builder.Services.AddHostedService<HealthRecordReminderService>();
|
||
builder.Services.AddHostedService<MedicationReminderWorker>();
|
||
builder.Services.AddHostedService<CleanupService>();
|
||
builder.Services.AddHostedService<ReportAnalysisWorker>();
|
||
builder.Services.AddHostedService<DietImageAnalysisWorker>();
|
||
builder.Services.AddHostedService<NotificationOutboxWorker>();
|
||
|
||
// ---- 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<ExceptionMiddleware>();
|
||
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<AppDbContext>();
|
||
if (app.Configuration.GetValue<bool>("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<Health.WebApi.Hubs.ConsultationHub>("/hubs/consultation");
|
||
|
||
app.Run();
|