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

207 lines
8.1 KiB
C#

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Health.Infrastructure.Data.Records;
namespace Health.Infrastructure.Data;
/// <summary>
/// 应用程序数据库上下文
/// </summary>
public sealed class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
{
// 核心业务表
public DbSet<User> Users => Set<User>();
public DbSet<HealthRecord> HealthRecords => Set<HealthRecord>();
public DbSet<Medication> Medications => Set<Medication>();
public DbSet<MedicationLog> MedicationLogs => Set<MedicationLog>();
public DbSet<DietRecord> DietRecords => Set<DietRecord>();
public DbSet<DietFoodItem> DietFoodItems => Set<DietFoodItem>();
public DbSet<ExercisePlan> ExercisePlans => Set<ExercisePlan>();
public DbSet<ExercisePlanItem> ExercisePlanItems => Set<ExercisePlanItem>();
public DbSet<Report> Reports => Set<Report>();
public DbSet<Conversation> Conversations => Set<Conversation>();
public DbSet<ConversationMessage> ConversationMessages => Set<ConversationMessage>();
public DbSet<Consultation> Consultations => Set<Consultation>();
public DbSet<ConsultationMessage> ConsultationMessages => Set<ConsultationMessage>();
public DbSet<Doctor> Doctors => Set<Doctor>();
public DbSet<DoctorProfile> DoctorProfiles => Set<DoctorProfile>();
public DbSet<FollowUp> FollowUps => Set<FollowUp>();
public DbSet<HealthArchive> HealthArchives => Set<HealthArchive>();
// 支撑表
public DbSet<RefreshToken> RefreshTokens => Set<RefreshToken>();
public DbSet<VerificationCode> VerificationCodes => Set<VerificationCode>();
public DbSet<NotificationPreference> NotificationPreferences => Set<NotificationPreference>();
public DbSet<DeviceToken> DeviceTokens => Set<DeviceToken>();
public DbSet<AiWriteCommandRecord> AiWriteCommands => Set<AiWriteCommandRecord>();
public DbSet<ReportAnalysisTaskRecord> ReportAnalysisTasks => Set<ReportAnalysisTaskRecord>();
public DbSet<DietImageAnalysisTaskRecord> DietImageAnalysisTasks => Set<DietImageAnalysisTaskRecord>();
public DbSet<MedicationReminderTaskRecord> MedicationReminderTasks => Set<MedicationReminderTaskRecord>();
public DbSet<NotificationOutboxRecord> NotificationOutbox => Set<NotificationOutboxRecord>();
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
// ---- User ----
builder.Entity<User>(e =>
{
e.HasIndex(u => u.Phone).IsUnique();
e.Property(u => u.Role).HasMaxLength(32).HasDefaultValue("User");
e.HasOne(u => u.Doctor).WithMany().HasForeignKey(u => u.DoctorId).IsRequired(false).OnDelete(DeleteBehavior.SetNull);
});
// ---- Doctor ----
builder.Entity<Doctor>(e =>
{
e.Property(d => d.Phone).HasMaxLength(20);
e.Property(d => d.ProfessionalDirection).HasMaxLength(256);
});
// ---- DoctorProfile ----
builder.Entity<DoctorProfile>(e =>
{
e.HasIndex(d => d.UserId).IsUnique();
e.HasOne(d => d.User).WithOne(u => u.DoctorProfile).HasForeignKey<DoctorProfile>(d => d.UserId);
e.HasOne(d => d.Doctor).WithMany().HasForeignKey(d => d.DoctorId).IsRequired(false).OnDelete(DeleteBehavior.SetNull);
});
// ---- HealthRecord ----
builder.Entity<HealthRecord>(e =>
{
e.HasIndex(r => new { r.UserId, r.RecordedAt }).IsDescending(false, true);
e.HasIndex(r => new { r.UserId, r.MetricType });
e.Property(r => r.MetricType).HasConversion<string>();
e.Property(r => r.Source).HasConversion<string>();
});
// ---- Medication ----
builder.Entity<Medication>(e =>
{
e.HasIndex(m => new { m.UserId, m.IsActive });
e.Property(m => m.Frequency).HasConversion<string>();
e.Property(m => m.Source).HasConversion<string>();
e.Property(m => m.TimeOfDay).HasColumnType("time[]");
});
builder.Entity<MedicationLog>(e =>
{
e.HasIndex(l => new { l.MedicationId, l.CreatedAt }).IsDescending(false, true);
e.Property(l => l.Status).HasConversion<string>();
});
// ---- Diet ----
builder.Entity<DietRecord>(e =>
{
e.HasIndex(d => new { d.UserId, d.RecordedAt }).IsDescending(false, true);
e.Property(d => d.MealType).HasConversion<string>();
});
// ---- ExercisePlan ----
builder.Entity<ExercisePlan>(e =>
{
e.HasIndex(p => new { p.UserId, p.StartDate, p.EndDate });
});
builder.Entity<ExercisePlanItem>(e =>
{
e.HasIndex(i => new { i.PlanId, i.ScheduledDate }).IsUnique();
});
// ---- Report ----
builder.Entity<Report>(e =>
{
e.Property(r => r.FileType).HasConversion<string>();
e.Property(r => r.Category).HasConversion<string>();
e.Property(r => r.Status).HasConversion<string>();
});
// ---- Conversation ----
builder.Entity<Conversation>(e =>
{
e.HasIndex(c => new { c.UserId, c.UpdatedAt }).IsDescending(false, true);
e.Property(c => c.AgentType).HasConversion<string>();
});
builder.Entity<ConversationMessage>(e =>
{
e.HasIndex(m => new { m.ConversationId, m.CreatedAt });
e.Property(m => m.Role).HasConversion<string>();
});
// ---- Consultation ----
builder.Entity<Consultation>(e =>
{
e.Property(c => c.Status).HasConversion<string>();
});
builder.Entity<ConsultationMessage>(e =>
{
e.HasIndex(m => new { m.ConsultationId, m.CreatedAt }).IsDescending(false, true);
e.Property(m => m.SenderType).HasConversion<string>();
});
// ---- VerificationCode ----
builder.Entity<VerificationCode>(e =>
{
e.HasIndex(v => new { v.Phone, v.CreatedAt }).IsDescending(false, true);
});
// ---- NotificationPreference ----
builder.Entity<NotificationPreference>(e =>
{
e.HasIndex(n => n.UserId).IsUnique();
});
// ---- HealthArchive ----
builder.Entity<HealthArchive>(e =>
{
e.HasIndex(a => a.UserId).IsUnique();
});
builder.Entity<AiWriteCommandRecord>(e =>
{
e.ToTable("AiWriteCommands");
e.HasIndex(x => new { x.UserId, x.Status, x.ExpiresAt });
e.Property(x => x.ToolName).HasMaxLength(64);
e.Property(x => x.Status).HasMaxLength(32);
e.Property(x => x.Arguments).HasColumnType("jsonb");
});
builder.Entity<ReportAnalysisTaskRecord>(e =>
{
e.ToTable("ReportAnalysisTasks");
e.HasIndex(x => new { x.Status, x.AvailableAt });
e.HasIndex(x => x.ReportId);
e.Property(x => x.Status).HasMaxLength(32);
});
builder.Entity<DietImageAnalysisTaskRecord>(e =>
{
e.ToTable("DietImageAnalysisTasks");
e.HasIndex(x => new { x.Status, x.AvailableAt });
e.Property(x => x.FilePaths).HasColumnType("jsonb");
e.Property(x => x.Status).HasMaxLength(32);
});
builder.Entity<MedicationReminderTaskRecord>(e =>
{
e.ToTable("MedicationReminderTasks");
e.HasIndex(x => new { x.Status, x.AvailableAt });
e.HasIndex(x => new { x.MedicationId, x.ScheduledDate, x.ScheduledTime }).IsUnique();
e.Property(x => x.Status).HasMaxLength(32);
});
builder.Entity<NotificationOutboxRecord>(e =>
{
e.ToTable("NotificationOutbox");
e.HasIndex(x => x.SourceTaskId).IsUnique();
e.HasIndex(x => new { x.Status, x.AvailableAt });
e.Property(x => x.Type).HasMaxLength(64);
e.Property(x => x.Status).HasMaxLength(32);
e.Property(x => x.Payload).HasColumnType("jsonb");
});
}
}