- 核心业务拆分为 Endpoint → Application Service → Repository 三层 - AI写入操作必须用户确认后才写库(确认卡片机制) - 报告/饮食/用药分析改为持久化任务队列(原子领取/重试/重启恢复) - 运动计划修复: 连续真实日期替代周模板 - 用药提醒去重 + 通知Outbox预留 - 认证收拢到AuthService, 管理员收拢到AdminService - AI会话加用户归属校验防串号 - 提示词调整为患者视角 - 开发假数据已关闭 - 21/21测试通过, 0警告0错误
207 lines
8.1 KiB
C#
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");
|
|
});
|
|
}
|
|
}
|