feat: 后端架构重构 — Endpoint→Service→Repository分层 + AI确认机制 + 异步任务持久化

- 核心业务拆分为 Endpoint → Application Service → Repository 三层
- AI写入操作必须用户确认后才写库(确认卡片机制)
- 报告/饮食/用药分析改为持久化任务队列(原子领取/重试/重启恢复)
- 运动计划修复: 连续真实日期替代周模板
- 用药提醒去重 + 通知Outbox预留
- 认证收拢到AuthService, 管理员收拢到AdminService
- AI会话加用户归属校验防串号
- 提示词调整为患者视角
- 开发假数据已关闭
- 21/21测试通过, 0警告0错误
This commit is contained in:
MingNian
2026-06-20 20:41:42 +08:00
parent c610417e29
commit 4d213b5a44
132 changed files with 6733 additions and 2856 deletions

View File

@@ -1,13 +1,40 @@
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 文件(开发环境)
@@ -28,6 +55,18 @@ if (File.Exists(envPath))
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 =>
{
@@ -68,6 +107,44 @@ builder.Services.AddAuthorization();
builder.Services.AddSingleton<JwtProvider>();
builder.Services.AddSingleton<SmsService>();
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<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<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>();
builder.Services.AddScoped<DatabaseSchemaMigrator>();
// ---- AI 客户端(使用 IHttpClientFactory 区分 LLM 和 VLM----
builder.Services.AddHttpClient<DeepSeekClient>(client =>
@@ -85,7 +162,11 @@ builder.Services.AddHttpClient<VisionClient>(client =>
// ---- 后台服务 ----
builder.Services.AddHostedService<MedicationReminderService>();
builder.Services.AddHostedService<ExerciseReminderService>();
builder.Services.AddHostedService<MedicationReminderWorker>();
builder.Services.AddHostedService<CleanupService>();
builder.Services.AddHostedService<ReportAnalysisWorker>();
builder.Services.AddHostedService<DietImageAnalysisWorker>();
// ---- OpenAPI ----
builder.Services.AddOpenApi();
@@ -110,6 +191,14 @@ app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
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();
@@ -118,6 +207,7 @@ using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await db.Database.EnsureCreatedAsync();
await scope.ServiceProvider.GetRequiredService<DatabaseSchemaMigrator>().ApplyAsync();
await DataSeeder.SeedAsync(db);
await DevDataSeeder.SeedIfEnabled(db, app.Configuration);
}
@@ -134,6 +224,7 @@ app.MapUserEndpoints();
app.MapAiChatEndpoints();
app.MapFileEndpoints();
app.MapCalendarEndpoints();
app.MapNotificationEndpoints();
app.MapDoctorEndpoints();
app.MapAdminEndpoints();
app.MapFollowUpEndpoints();