dramaling-vocab-learning/backend/DramaLing.Api/Data/DramaLingDbContext.cs

581 lines
32 KiB
C#

using Microsoft.EntityFrameworkCore;
using DramaLing.Api.Models.Entities;
using System.Text.Json;
namespace DramaLing.Api.Data;
public class DramaLingDbContext : DbContext
{
public DramaLingDbContext(DbContextOptions<DramaLingDbContext> options) : base(options)
{
}
// DbSets
public DbSet<User> Users { get; set; }
public DbSet<UserSettings> UserSettings { get; set; }
public DbSet<Flashcard> Flashcards { get; set; }
public DbSet<Tag> Tags { get; set; }
public DbSet<FlashcardTag> FlashcardTags { get; set; }
// StudyRecord removed - study system cleaned
public DbSet<ErrorReport> ErrorReports { get; set; }
public DbSet<DailyStats> DailyStats { get; set; }
public DbSet<SentenceAnalysisCache> SentenceAnalysisCache { get; set; }
public DbSet<WordQueryUsageStats> WordQueryUsageStats { get; set; }
public DbSet<AudioCache> AudioCaches { get; set; }
public DbSet<PronunciationAssessment> PronunciationAssessments { get; set; }
public DbSet<UserAudioPreferences> UserAudioPreferences { get; set; }
public DbSet<ExampleImage> ExampleImages { get; set; }
public DbSet<FlashcardExampleImage> FlashcardExampleImages { get; set; }
public DbSet<ImageGenerationRequest> ImageGenerationRequests { get; set; }
public DbSet<OptionsVocabulary> OptionsVocabularies { get; set; }
public DbSet<FlashcardReview> FlashcardReviews { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// 設定表名稱 (與 Supabase 一致)
modelBuilder.Entity<User>().ToTable("user_profiles");
modelBuilder.Entity<UserSettings>().ToTable("user_settings");
modelBuilder.Entity<Flashcard>().ToTable("flashcards");
modelBuilder.Entity<Tag>().ToTable("tags");
modelBuilder.Entity<FlashcardTag>().ToTable("flashcard_tags");
// StudyRecord table removed
modelBuilder.Entity<ErrorReport>().ToTable("error_reports");
modelBuilder.Entity<DailyStats>().ToTable("daily_stats");
modelBuilder.Entity<AudioCache>().ToTable("audio_cache");
modelBuilder.Entity<PronunciationAssessment>().ToTable("pronunciation_assessments");
modelBuilder.Entity<UserAudioPreferences>().ToTable("user_audio_preferences");
modelBuilder.Entity<ExampleImage>().ToTable("example_images");
modelBuilder.Entity<FlashcardExampleImage>().ToTable("flashcard_example_images");
modelBuilder.Entity<ImageGenerationRequest>().ToTable("image_generation_requests");
modelBuilder.Entity<OptionsVocabulary>().ToTable("options_vocabularies");
modelBuilder.Entity<FlashcardReview>().ToTable("flashcard_reviews");
modelBuilder.Entity<SentenceAnalysisCache>().ToTable("sentence_analysis_cache");
modelBuilder.Entity<WordQueryUsageStats>().ToTable("word_query_usage_stats");
// 配置屬性名稱 (snake_case)
ConfigureUserEntity(modelBuilder);
ConfigureUserSettingsEntity(modelBuilder);
ConfigureFlashcardEntity(modelBuilder);
// ConfigureStudyEntities 已移除 - StudyRecord 實體已清理
ConfigureTagEntities(modelBuilder);
ConfigureErrorReportEntity(modelBuilder);
ConfigureDailyStatsEntity(modelBuilder);
ConfigureSentenceAnalysisCacheEntity(modelBuilder);
ConfigureWordQueryUsageStatsEntity(modelBuilder);
ConfigureAudioEntities(modelBuilder);
ConfigureImageGenerationEntities(modelBuilder);
ConfigureOptionsVocabularyEntity(modelBuilder);
ConfigureFlashcardReviewEntity(modelBuilder);
// 複合主鍵
modelBuilder.Entity<FlashcardTag>()
.HasKey(ft => new { ft.FlashcardId, ft.TagId });
modelBuilder.Entity<FlashcardExampleImage>()
.HasKey(fei => new { fei.FlashcardId, fei.ExampleImageId });
modelBuilder.Entity<DailyStats>()
.HasIndex(ds => new { ds.UserId, ds.Date })
.IsUnique();
// 外鍵關係
ConfigureRelationships(modelBuilder);
}
private void ConfigureUserEntity(ModelBuilder modelBuilder)
{
var userEntity = modelBuilder.Entity<User>();
userEntity.Property(u => u.Id).HasColumnName("id");
userEntity.Property(u => u.Username).HasColumnName("username");
userEntity.Property(u => u.Email).HasColumnName("email");
userEntity.Property(u => u.PasswordHash).HasColumnName("password_hash");
userEntity.Property(u => u.DisplayName).HasColumnName("display_name");
userEntity.Property(u => u.AvatarUrl).HasColumnName("avatar_url");
userEntity.Property(u => u.SubscriptionType).HasColumnName("subscription_type");
userEntity.Property(u => u.Preferences)
.HasColumnName("preferences")
.HasConversion(
v => System.Text.Json.JsonSerializer.Serialize(v, (System.Text.Json.JsonSerializerOptions)null),
v => System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, object>>(v, (System.Text.Json.JsonSerializerOptions)null) ?? new Dictionary<string, object>());
// 新增個人化欄位映射
userEntity.Property(u => u.EnglishLevel).HasColumnName("english_level");
userEntity.Property(u => u.EnglishLevelNumeric)
.HasColumnName("english_level_numeric")
.HasDefaultValue(2); // 預設 A2
userEntity.Property(u => u.LevelUpdatedAt).HasColumnName("level_updated_at");
userEntity.Property(u => u.IsLevelVerified).HasColumnName("is_level_verified");
userEntity.Property(u => u.LevelNotes).HasColumnName("level_notes");
userEntity.Property(u => u.CreatedAt).HasColumnName("created_at");
userEntity.Property(u => u.UpdatedAt).HasColumnName("updated_at");
// Add unique indexes
userEntity.HasIndex(u => u.Email).IsUnique();
userEntity.HasIndex(u => u.Username).IsUnique();
}
private void ConfigureFlashcardEntity(ModelBuilder modelBuilder)
{
var flashcardEntity = modelBuilder.Entity<Flashcard>();
flashcardEntity.Property(f => f.Id).HasColumnName("id");
flashcardEntity.Property(f => f.UserId).HasColumnName("user_id");
flashcardEntity.Property(f => f.Word).HasColumnName("word");
flashcardEntity.Property(f => f.Translation).HasColumnName("translation");
flashcardEntity.Property(f => f.Definition).HasColumnName("definition");
flashcardEntity.Property(f => f.PartOfSpeech).HasColumnName("part_of_speech");
flashcardEntity.Property(f => f.Pronunciation).HasColumnName("pronunciation");
flashcardEntity.Property(f => f.Example).HasColumnName("example");
flashcardEntity.Property(f => f.ExampleTranslation).HasColumnName("example_translation");
flashcardEntity.Property(f => f.Synonyms).HasColumnName("synonyms");
// 已刪除的復習相關屬性配置
// EasinessFactor, IntervalDays, NextReviewDate, MasteryLevel,
// TimesReviewed, TimesCorrect, LastReviewedAt 已移除
flashcardEntity.Property(f => f.IsFavorite).HasColumnName("is_favorite");
flashcardEntity.Property(f => f.IsArchived).HasColumnName("is_archived");
// 難度等級映射 - 使用數字格式
flashcardEntity.Property(f => f.DifficultyLevelNumeric).HasColumnName("difficulty_level_numeric");
flashcardEntity.Property(f => f.CreatedAt).HasColumnName("created_at");
flashcardEntity.Property(f => f.UpdatedAt).HasColumnName("updated_at");
}
// ConfigureStudyEntities 方法已移除 - StudyRecord 實體已清理
private void ConfigureTagEntities(ModelBuilder modelBuilder)
{
var tagEntity = modelBuilder.Entity<Tag>();
tagEntity.Property(t => t.Id).HasColumnName("id");
tagEntity.Property(t => t.UserId).HasColumnName("user_id");
tagEntity.Property(t => t.Name).HasColumnName("name");
tagEntity.Property(t => t.Color).HasColumnName("color");
tagEntity.Property(t => t.UsageCount).HasColumnName("usage_count");
tagEntity.Property(t => t.CreatedAt).HasColumnName("created_at");
var flashcardTagEntity = modelBuilder.Entity<FlashcardTag>();
flashcardTagEntity.Property(ft => ft.FlashcardId).HasColumnName("flashcard_id");
flashcardTagEntity.Property(ft => ft.TagId).HasColumnName("tag_id");
}
private void ConfigureErrorReportEntity(ModelBuilder modelBuilder)
{
var errorEntity = modelBuilder.Entity<ErrorReport>();
errorEntity.Property(e => e.Id).HasColumnName("id");
errorEntity.Property(e => e.UserId).HasColumnName("user_id");
errorEntity.Property(e => e.FlashcardId).HasColumnName("flashcard_id");
errorEntity.Property(e => e.ReportType).HasColumnName("report_type");
errorEntity.Property(e => e.Description).HasColumnName("description");
errorEntity.Property(e => e.StudyMode).HasColumnName("study_mode");
errorEntity.Property(e => e.Status).HasColumnName("status");
errorEntity.Property(e => e.AdminNotes).HasColumnName("admin_notes");
errorEntity.Property(e => e.ResolvedAt).HasColumnName("resolved_at");
errorEntity.Property(e => e.ResolvedBy).HasColumnName("resolved_by");
errorEntity.Property(e => e.CreatedAt).HasColumnName("created_at");
}
private void ConfigureDailyStatsEntity(ModelBuilder modelBuilder)
{
var statsEntity = modelBuilder.Entity<DailyStats>();
statsEntity.Property(d => d.Id).HasColumnName("id");
statsEntity.Property(d => d.UserId).HasColumnName("user_id");
statsEntity.Property(d => d.Date).HasColumnName("date");
statsEntity.Property(d => d.WordsStudied).HasColumnName("words_studied");
statsEntity.Property(d => d.WordsCorrect).HasColumnName("words_correct");
statsEntity.Property(d => d.StudyTimeSeconds).HasColumnName("study_time_seconds");
statsEntity.Property(d => d.SessionCount).HasColumnName("session_count");
statsEntity.Property(d => d.CardsGenerated).HasColumnName("cards_generated");
statsEntity.Property(d => d.AiApiCalls).HasColumnName("ai_api_calls");
statsEntity.Property(d => d.CreatedAt).HasColumnName("created_at");
}
private void ConfigureUserSettingsEntity(ModelBuilder modelBuilder)
{
var settingsEntity = modelBuilder.Entity<UserSettings>();
settingsEntity.Property(us => us.Id).HasColumnName("id");
settingsEntity.Property(us => us.UserId).HasColumnName("user_id");
settingsEntity.Property(us => us.DailyGoal).HasColumnName("daily_goal");
settingsEntity.Property(us => us.ReminderTime).HasColumnName("reminder_time");
settingsEntity.Property(us => us.ReminderEnabled).HasColumnName("reminder_enabled");
settingsEntity.Property(us => us.DifficultyPreference).HasColumnName("difficulty_preference");
settingsEntity.Property(us => us.AutoPlayAudio).HasColumnName("auto_play_audio");
settingsEntity.Property(us => us.ShowPronunciation).HasColumnName("show_pronunciation");
settingsEntity.Property(us => us.CreatedAt).HasColumnName("created_at");
settingsEntity.Property(us => us.UpdatedAt).HasColumnName("updated_at");
}
private void ConfigureSentenceAnalysisCacheEntity(ModelBuilder modelBuilder)
{
var cacheEntity = modelBuilder.Entity<SentenceAnalysisCache>();
cacheEntity.Property(sac => sac.Id).HasColumnName("id");
cacheEntity.Property(sac => sac.InputTextHash).HasColumnName("input_text_hash");
cacheEntity.Property(sac => sac.InputText).HasColumnName("input_text");
cacheEntity.Property(sac => sac.CorrectedText).HasColumnName("corrected_text");
cacheEntity.Property(sac => sac.HasGrammarErrors).HasColumnName("has_grammar_errors");
cacheEntity.Property(sac => sac.GrammarCorrections).HasColumnName("grammar_corrections");
cacheEntity.Property(sac => sac.AnalysisResult).HasColumnName("analysis_result");
cacheEntity.Property(sac => sac.HighValueWords).HasColumnName("high_value_words");
cacheEntity.Property(sac => sac.IdiomsDetected).HasColumnName("idioms_detected");
cacheEntity.Property(sac => sac.CreatedAt).HasColumnName("created_at");
cacheEntity.Property(sac => sac.ExpiresAt).HasColumnName("expires_at");
cacheEntity.Property(sac => sac.AccessCount).HasColumnName("access_count");
cacheEntity.Property(sac => sac.LastAccessedAt).HasColumnName("last_accessed_at");
}
private void ConfigureWordQueryUsageStatsEntity(ModelBuilder modelBuilder)
{
var statsEntity = modelBuilder.Entity<WordQueryUsageStats>();
statsEntity.Property(wq => wq.Id).HasColumnName("id");
statsEntity.Property(wq => wq.UserId).HasColumnName("user_id");
statsEntity.Property(wq => wq.Date).HasColumnName("date");
statsEntity.Property(wq => wq.SentenceAnalysisCount).HasColumnName("sentence_analysis_count");
statsEntity.Property(wq => wq.HighValueWordClicks).HasColumnName("high_value_word_clicks");
statsEntity.Property(wq => wq.LowValueWordClicks).HasColumnName("low_value_word_clicks");
statsEntity.Property(wq => wq.TotalApiCalls).HasColumnName("total_api_calls");
statsEntity.Property(wq => wq.UniqueWordsQueried).HasColumnName("unique_words_queried");
statsEntity.Property(wq => wq.CreatedAt).HasColumnName("created_at");
statsEntity.Property(wq => wq.UpdatedAt).HasColumnName("updated_at");
}
private void ConfigureRelationships(ModelBuilder modelBuilder)
{
// User relationships
modelBuilder.Entity<Flashcard>()
.HasOne(f => f.User)
.WithMany(u => u.Flashcards)
.HasForeignKey(f => f.UserId)
.OnDelete(DeleteBehavior.Cascade);
// Study relationships 已移除 - StudyRecord 實體已清理
// FlashcardReview relationships
modelBuilder.Entity<FlashcardReview>()
.HasOne(fr => fr.Flashcard)
.WithMany()
.HasForeignKey(fr => fr.FlashcardId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<FlashcardReview>()
.HasOne(fr => fr.User)
.WithMany()
.HasForeignKey(fr => fr.UserId)
.OnDelete(DeleteBehavior.Cascade);
// 複習記錄唯一性約束 (每個用戶每張卡片只能有一條記錄)
modelBuilder.Entity<FlashcardReview>()
.HasIndex(fr => new { fr.FlashcardId, fr.UserId })
.IsUnique();
// Tag relationships
modelBuilder.Entity<FlashcardTag>()
.HasOne(ft => ft.Flashcard)
.WithMany(f => f.FlashcardTags)
.HasForeignKey(ft => ft.FlashcardId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<FlashcardTag>()
.HasOne(ft => ft.Tag)
.WithMany(t => t.FlashcardTags)
.HasForeignKey(ft => ft.TagId)
.OnDelete(DeleteBehavior.Cascade);
// Error report relationships
modelBuilder.Entity<ErrorReport>()
.HasOne(er => er.User)
.WithMany(u => u.ErrorReports)
.HasForeignKey(er => er.UserId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<ErrorReport>()
.HasOne(er => er.Flashcard)
.WithMany(f => f.ErrorReports)
.HasForeignKey(er => er.FlashcardId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<ErrorReport>()
.HasOne(er => er.ResolvedByUser)
.WithMany()
.HasForeignKey(er => er.ResolvedBy)
.OnDelete(DeleteBehavior.SetNull);
// User settings relationship
modelBuilder.Entity<UserSettings>()
.HasOne(us => us.User)
.WithOne(u => u.Settings)
.HasForeignKey<UserSettings>(us => us.UserId)
.OnDelete(DeleteBehavior.Cascade);
// Daily stats relationship
modelBuilder.Entity<DailyStats>()
.HasOne(ds => ds.User)
.WithMany(u => u.DailyStats)
.HasForeignKey(ds => ds.UserId)
.OnDelete(DeleteBehavior.Cascade);
// Sentence analysis cache configuration
modelBuilder.Entity<SentenceAnalysisCache>()
.HasIndex(sac => sac.InputTextHash)
.HasDatabaseName("IX_SentenceAnalysisCache_Hash");
modelBuilder.Entity<SentenceAnalysisCache>()
.HasIndex(sac => sac.ExpiresAt)
.HasDatabaseName("IX_SentenceAnalysisCache_Expires");
modelBuilder.Entity<SentenceAnalysisCache>()
.HasIndex(sac => new { sac.InputTextHash, sac.ExpiresAt })
.HasDatabaseName("IX_SentenceAnalysisCache_Hash_Expires");
// Word query usage stats configuration
modelBuilder.Entity<WordQueryUsageStats>()
.HasOne(wq => wq.User)
.WithMany()
.HasForeignKey(wq => wq.UserId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<WordQueryUsageStats>()
.HasIndex(wq => new { wq.UserId, wq.Date })
.IsUnique()
.HasDatabaseName("IX_WordQueryUsageStats_UserDate");
modelBuilder.Entity<WordQueryUsageStats>()
.HasIndex(wq => wq.CreatedAt)
.HasDatabaseName("IX_WordQueryUsageStats_CreatedAt");
// Audio entities relationships
ConfigureAudioRelationships(modelBuilder);
}
private void ConfigureAudioEntities(ModelBuilder modelBuilder)
{
// AudioCache configuration
var audioCacheEntity = modelBuilder.Entity<AudioCache>();
audioCacheEntity.Property(ac => ac.Id).HasColumnName("id");
audioCacheEntity.Property(ac => ac.TextHash).HasColumnName("text_hash");
audioCacheEntity.Property(ac => ac.TextContent).HasColumnName("text_content");
audioCacheEntity.Property(ac => ac.Accent).HasColumnName("accent");
audioCacheEntity.Property(ac => ac.VoiceId).HasColumnName("voice_id");
audioCacheEntity.Property(ac => ac.AudioUrl).HasColumnName("audio_url");
audioCacheEntity.Property(ac => ac.FileSize).HasColumnName("file_size");
audioCacheEntity.Property(ac => ac.DurationMs).HasColumnName("duration_ms");
audioCacheEntity.Property(ac => ac.CreatedAt).HasColumnName("created_at");
audioCacheEntity.Property(ac => ac.LastAccessed).HasColumnName("last_accessed");
audioCacheEntity.Property(ac => ac.AccessCount).HasColumnName("access_count");
audioCacheEntity.HasIndex(ac => ac.TextHash)
.IsUnique()
.HasDatabaseName("IX_AudioCache_TextHash");
audioCacheEntity.HasIndex(ac => ac.LastAccessed)
.HasDatabaseName("IX_AudioCache_LastAccessed");
// PronunciationAssessment configuration
var pronunciationEntity = modelBuilder.Entity<PronunciationAssessment>();
pronunciationEntity.Property(pa => pa.Id).HasColumnName("id");
pronunciationEntity.Property(pa => pa.UserId).HasColumnName("user_id");
pronunciationEntity.Property(pa => pa.FlashcardId).HasColumnName("flashcard_id");
pronunciationEntity.Property(pa => pa.ReferenceText).HasColumnName("reference_text");
pronunciationEntity.Property(pa => pa.TranscribedText).HasColumnName("transcribed_text");
pronunciationEntity.Property(pa => pa.OverallScore).HasColumnName("overall_score");
pronunciationEntity.Property(pa => pa.AccuracyScore).HasColumnName("accuracy_score");
pronunciationEntity.Property(pa => pa.FluencyScore).HasColumnName("fluency_score");
pronunciationEntity.Property(pa => pa.CompletenessScore).HasColumnName("completeness_score");
pronunciationEntity.Property(pa => pa.ProsodyScore).HasColumnName("prosody_score");
pronunciationEntity.Property(pa => pa.AudioDuration).HasColumnName("audio_duration");
pronunciationEntity.Property(pa => pa.ProcessingTime).HasColumnName("processing_time");
pronunciationEntity.Property(pa => pa.AzureRequestId).HasColumnName("azure_request_id");
pronunciationEntity.Property(pa => pa.WordLevelResults).HasColumnName("word_level_results");
pronunciationEntity.Property(pa => pa.Feedback).HasColumnName("feedback");
// StudySessionId removed
pronunciationEntity.Property(pa => pa.PracticeMode).HasColumnName("practice_mode");
pronunciationEntity.Property(pa => pa.CreatedAt).HasColumnName("created_at");
pronunciationEntity.HasIndex(pa => new { pa.UserId, pa.FlashcardId })
.HasDatabaseName("IX_PronunciationAssessment_UserFlashcard");
// StudySessionId index removed
// UserAudioPreferences configuration
var audioPrefsEntity = modelBuilder.Entity<UserAudioPreferences>();
audioPrefsEntity.Property(uap => uap.UserId).HasColumnName("user_id");
audioPrefsEntity.Property(uap => uap.PreferredAccent).HasColumnName("preferred_accent");
audioPrefsEntity.Property(uap => uap.PreferredVoiceMale).HasColumnName("preferred_voice_male");
audioPrefsEntity.Property(uap => uap.PreferredVoiceFemale).HasColumnName("preferred_voice_female");
audioPrefsEntity.Property(uap => uap.DefaultSpeed).HasColumnName("default_speed");
audioPrefsEntity.Property(uap => uap.AutoPlayEnabled).HasColumnName("auto_play_enabled");
audioPrefsEntity.Property(uap => uap.PronunciationDifficulty).HasColumnName("pronunciation_difficulty");
audioPrefsEntity.Property(uap => uap.TargetScoreThreshold).HasColumnName("target_score_threshold");
audioPrefsEntity.Property(uap => uap.EnableDetailedFeedback).HasColumnName("enable_detailed_feedback");
audioPrefsEntity.Property(uap => uap.UpdatedAt).HasColumnName("updated_at");
}
private void ConfigureAudioRelationships(ModelBuilder modelBuilder)
{
// PronunciationAssessment relationships
modelBuilder.Entity<PronunciationAssessment>()
.HasOne(pa => pa.User)
.WithMany()
.HasForeignKey(pa => pa.UserId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<PronunciationAssessment>()
.HasOne(pa => pa.Flashcard)
.WithMany()
.HasForeignKey(pa => pa.FlashcardId)
.OnDelete(DeleteBehavior.SetNull);
// StudySession relationship removed
// UserAudioPreferences relationship
modelBuilder.Entity<UserAudioPreferences>()
.HasOne(uap => uap.User)
.WithOne()
.HasForeignKey<UserAudioPreferences>(uap => uap.UserId)
.OnDelete(DeleteBehavior.Cascade);
}
private void ConfigureImageGenerationEntities(ModelBuilder modelBuilder)
{
// ExampleImage configuration
var exampleImageEntity = modelBuilder.Entity<ExampleImage>();
exampleImageEntity.Property(ei => ei.Id).HasColumnName("id");
exampleImageEntity.Property(ei => ei.RelativePath).HasColumnName("relative_path");
exampleImageEntity.Property(ei => ei.AltText).HasColumnName("alt_text");
exampleImageEntity.Property(ei => ei.GeminiPrompt).HasColumnName("gemini_prompt");
exampleImageEntity.Property(ei => ei.GeminiDescription).HasColumnName("gemini_description");
exampleImageEntity.Property(ei => ei.ReplicatePrompt).HasColumnName("replicate_prompt");
exampleImageEntity.Property(ei => ei.ReplicateModel).HasColumnName("replicate_model");
exampleImageEntity.Property(ei => ei.ReplicateVersion).HasColumnName("replicate_version");
exampleImageEntity.Property(ei => ei.GeminiCost).HasColumnName("gemini_cost");
exampleImageEntity.Property(ei => ei.ReplicateCost).HasColumnName("replicate_cost");
exampleImageEntity.Property(ei => ei.TotalGenerationCost).HasColumnName("total_generation_cost");
exampleImageEntity.Property(ei => ei.FileSize).HasColumnName("file_size");
exampleImageEntity.Property(ei => ei.ImageWidth).HasColumnName("image_width");
exampleImageEntity.Property(ei => ei.ImageHeight).HasColumnName("image_height");
exampleImageEntity.Property(ei => ei.ContentHash).HasColumnName("content_hash");
exampleImageEntity.Property(ei => ei.QualityScore).HasColumnName("quality_score");
exampleImageEntity.Property(ei => ei.ModerationStatus).HasColumnName("moderation_status");
exampleImageEntity.Property(ei => ei.ModerationNotes).HasColumnName("moderation_notes");
exampleImageEntity.Property(ei => ei.AccessCount).HasColumnName("access_count");
exampleImageEntity.Property(ei => ei.CreatedAt).HasColumnName("created_at");
exampleImageEntity.Property(ei => ei.UpdatedAt).HasColumnName("updated_at");
exampleImageEntity.HasIndex(ei => ei.ContentHash).IsUnique();
exampleImageEntity.HasIndex(ei => ei.AccessCount);
// FlashcardExampleImage configuration
var flashcardImageEntity = modelBuilder.Entity<FlashcardExampleImage>();
flashcardImageEntity.Property(fei => fei.FlashcardId).HasColumnName("flashcard_id");
flashcardImageEntity.Property(fei => fei.ExampleImageId).HasColumnName("example_image_id");
flashcardImageEntity.Property(fei => fei.DisplayOrder).HasColumnName("display_order");
flashcardImageEntity.Property(fei => fei.IsPrimary).HasColumnName("is_primary");
flashcardImageEntity.Property(fei => fei.ContextRelevance).HasColumnName("context_relevance");
flashcardImageEntity.Property(fei => fei.CreatedAt).HasColumnName("created_at");
// ImageGenerationRequest configuration
var generationRequestEntity = modelBuilder.Entity<ImageGenerationRequest>();
generationRequestEntity.Property(igr => igr.Id).HasColumnName("id");
generationRequestEntity.Property(igr => igr.UserId).HasColumnName("user_id");
generationRequestEntity.Property(igr => igr.FlashcardId).HasColumnName("flashcard_id");
generationRequestEntity.Property(igr => igr.OverallStatus).HasColumnName("overall_status");
generationRequestEntity.Property(igr => igr.GeminiStatus).HasColumnName("gemini_status");
generationRequestEntity.Property(igr => igr.ReplicateStatus).HasColumnName("replicate_status");
generationRequestEntity.Property(igr => igr.OriginalRequest).HasColumnName("original_request");
generationRequestEntity.Property(igr => igr.GeminiPrompt).HasColumnName("gemini_prompt");
generationRequestEntity.Property(igr => igr.GeneratedDescription).HasColumnName("generated_description");
generationRequestEntity.Property(igr => igr.FinalReplicatePrompt).HasColumnName("final_replicate_prompt");
generationRequestEntity.Property(igr => igr.GeneratedImageId).HasColumnName("generated_image_id");
generationRequestEntity.Property(igr => igr.GeminiErrorMessage).HasColumnName("gemini_error_message");
generationRequestEntity.Property(igr => igr.ReplicateErrorMessage).HasColumnName("replicate_error_message");
generationRequestEntity.Property(igr => igr.GeminiProcessingTimeMs).HasColumnName("gemini_processing_time_ms");
generationRequestEntity.Property(igr => igr.ReplicateProcessingTimeMs).HasColumnName("replicate_processing_time_ms");
generationRequestEntity.Property(igr => igr.TotalProcessingTimeMs).HasColumnName("total_processing_time_ms");
generationRequestEntity.Property(igr => igr.GeminiCost).HasColumnName("gemini_cost");
generationRequestEntity.Property(igr => igr.ReplicateCost).HasColumnName("replicate_cost");
generationRequestEntity.Property(igr => igr.TotalCost).HasColumnName("total_cost");
generationRequestEntity.Property(igr => igr.CreatedAt).HasColumnName("created_at");
generationRequestEntity.Property(igr => igr.GeminiStartedAt).HasColumnName("gemini_started_at");
generationRequestEntity.Property(igr => igr.GeminiCompletedAt).HasColumnName("gemini_completed_at");
generationRequestEntity.Property(igr => igr.ReplicateStartedAt).HasColumnName("replicate_started_at");
generationRequestEntity.Property(igr => igr.ReplicateCompletedAt).HasColumnName("replicate_completed_at");
generationRequestEntity.Property(igr => igr.CompletedAt).HasColumnName("completed_at");
// 關聯關係
flashcardImageEntity
.HasOne(fei => fei.Flashcard)
.WithMany(f => f.FlashcardExampleImages) // 指定反向導航屬性
.HasForeignKey(fei => fei.FlashcardId)
.OnDelete(DeleteBehavior.Cascade);
flashcardImageEntity
.HasOne(fei => fei.ExampleImage)
.WithMany(ei => ei.FlashcardExampleImages)
.HasForeignKey(fei => fei.ExampleImageId)
.OnDelete(DeleteBehavior.Cascade);
generationRequestEntity
.HasOne(igr => igr.User)
.WithMany()
.HasForeignKey(igr => igr.UserId)
.OnDelete(DeleteBehavior.Cascade);
generationRequestEntity
.HasOne(igr => igr.Flashcard)
.WithMany()
.HasForeignKey(igr => igr.FlashcardId)
.OnDelete(DeleteBehavior.Cascade);
generationRequestEntity
.HasOne(igr => igr.GeneratedImage)
.WithMany()
.HasForeignKey(igr => igr.GeneratedImageId)
.OnDelete(DeleteBehavior.SetNull);
}
private void ConfigureOptionsVocabularyEntity(ModelBuilder modelBuilder)
{
var optionsVocabEntity = modelBuilder.Entity<OptionsVocabulary>();
// Configure column names (snake_case)
optionsVocabEntity.Property(ov => ov.Id).HasColumnName("id");
optionsVocabEntity.Property(ov => ov.Word).HasColumnName("word");
optionsVocabEntity.Property(ov => ov.CEFRLevel).HasColumnName("cefr_level");
optionsVocabEntity.Property(ov => ov.PartOfSpeech).HasColumnName("part_of_speech");
optionsVocabEntity.Property(ov => ov.WordLength).HasColumnName("word_length");
optionsVocabEntity.Property(ov => ov.IsActive).HasColumnName("is_active");
optionsVocabEntity.Property(ov => ov.CreatedAt).HasColumnName("created_at");
optionsVocabEntity.Property(ov => ov.UpdatedAt).HasColumnName("updated_at");
// Configure default values
optionsVocabEntity.Property(ov => ov.IsActive).HasDefaultValue(true);
optionsVocabEntity.Property(ov => ov.CreatedAt).HasDefaultValueSql("CURRENT_TIMESTAMP");
optionsVocabEntity.Property(ov => ov.UpdatedAt).HasDefaultValueSql("CURRENT_TIMESTAMP");
}
private void ConfigureFlashcardReviewEntity(ModelBuilder modelBuilder)
{
var reviewEntity = modelBuilder.Entity<FlashcardReview>();
// Configure column names (snake_case)
reviewEntity.Property(fr => fr.Id).HasColumnName("id");
reviewEntity.Property(fr => fr.FlashcardId).HasColumnName("flashcard_id");
reviewEntity.Property(fr => fr.UserId).HasColumnName("user_id");
reviewEntity.Property(fr => fr.SuccessCount).HasColumnName("success_count");
reviewEntity.Property(fr => fr.NextReviewDate).HasColumnName("next_review_date");
reviewEntity.Property(fr => fr.LastReviewDate).HasColumnName("last_review_date");
reviewEntity.Property(fr => fr.LastSuccessDate).HasColumnName("last_success_date");
reviewEntity.Property(fr => fr.TotalSkipCount).HasColumnName("total_skip_count");
reviewEntity.Property(fr => fr.TotalWrongCount).HasColumnName("total_wrong_count");
reviewEntity.Property(fr => fr.TotalCorrectCount).HasColumnName("total_correct_count");
reviewEntity.Property(fr => fr.CreatedAt).HasColumnName("created_at");
reviewEntity.Property(fr => fr.UpdatedAt).HasColumnName("updated_at");
// Configure indexes for performance
reviewEntity.HasIndex(fr => fr.NextReviewDate)
.HasDatabaseName("IX_FlashcardReviews_NextReviewDate");
reviewEntity.HasIndex(fr => new { fr.UserId, fr.NextReviewDate })
.HasDatabaseName("IX_FlashcardReviews_UserId_NextReviewDate");
}
}