diff --git a/backend/DramaLing.Api/Controllers/StudyController.cs b/backend/DramaLing.Api/Controllers/StudyController.cs
index e17dccb..d5b0322 100644
--- a/backend/DramaLing.Api/Controllers/StudyController.cs
+++ b/backend/DramaLing.Api/Controllers/StudyController.cs
@@ -560,6 +560,169 @@ public class StudyController : ControllerBase
});
}
}
+
+ ///
+ /// 獲取已完成的測驗記錄 (支援學習狀態恢復)
+ ///
+ [HttpGet("completed-tests")]
+ public async Task GetCompletedTests([FromQuery] string? cardIds = null)
+ {
+ try
+ {
+ var userId = await _authService.GetUserIdFromTokenAsync(Request.Headers.Authorization);
+ if (userId == null)
+ return Unauthorized(new { Success = false, Error = "Invalid token" });
+
+ var query = _context.StudyRecords.Where(r => r.UserId == userId);
+
+ // 如果提供了詞卡ID列表,則篩選
+ if (!string.IsNullOrEmpty(cardIds))
+ {
+ var cardIdList = cardIds.Split(',')
+ .Where(id => Guid.TryParse(id, out _))
+ .Select(Guid.Parse)
+ .ToList();
+
+ if (cardIdList.Any())
+ {
+ query = query.Where(r => cardIdList.Contains(r.FlashcardId));
+ }
+ }
+
+ var completedTests = await query
+ .Select(r => new
+ {
+ FlashcardId = r.FlashcardId,
+ TestType = r.StudyMode,
+ IsCorrect = r.IsCorrect,
+ CompletedAt = r.StudiedAt,
+ UserAnswer = r.UserAnswer
+ })
+ .ToListAsync();
+
+ _logger.LogInformation("Retrieved {Count} completed tests for user {UserId}",
+ completedTests.Count, userId);
+
+ return Ok(new
+ {
+ Success = true,
+ Data = completedTests
+ });
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error retrieving completed tests for user");
+ return StatusCode(500, new
+ {
+ Success = false,
+ Error = "Failed to retrieve completed tests",
+ Timestamp = DateTime.UtcNow
+ });
+ }
+ }
+
+ ///
+ /// 直接記錄測驗完成狀態 (不觸發SM2更新)
+ ///
+ [HttpPost("record-test")]
+ public async Task RecordTestCompletion([FromBody] RecordTestRequest request)
+ {
+ try
+ {
+ var userId = await _authService.GetUserIdFromTokenAsync(Request.Headers.Authorization);
+ if (userId == null)
+ {
+ _logger.LogWarning("RecordTest failed: Invalid or missing token");
+ return Unauthorized(new { Success = false, Error = "Invalid token" });
+ }
+
+ _logger.LogInformation("Recording test for user {UserId}: FlashcardId={FlashcardId}, TestType={TestType}",
+ userId, request.FlashcardId, request.TestType);
+
+ // 驗證測驗類型
+ var validTestTypes = new[] { "flip-memory", "vocab-choice", "vocab-listening",
+ "sentence-listening", "sentence-fill", "sentence-reorder", "sentence-speaking" };
+ if (!validTestTypes.Contains(request.TestType))
+ {
+ _logger.LogWarning("Invalid test type: {TestType}", request.TestType);
+ return BadRequest(new { Success = false, Error = "Invalid test type" });
+ }
+
+ // 先檢查詞卡是否存在
+ var flashcard = await _context.Flashcards
+ .FirstOrDefaultAsync(f => f.Id == request.FlashcardId);
+
+ if (flashcard == null)
+ {
+ _logger.LogWarning("Flashcard {FlashcardId} does not exist", request.FlashcardId);
+ return NotFound(new { Success = false, Error = "Flashcard does not exist" });
+ }
+
+ // 再檢查詞卡是否屬於用戶
+ if (flashcard.UserId != userId)
+ {
+ _logger.LogWarning("Flashcard {FlashcardId} does not belong to user {UserId}, actual owner: {ActualOwner}",
+ request.FlashcardId, userId, flashcard.UserId);
+ return Forbid();
+ }
+
+ // 檢查是否已經完成過這個測驗
+ var existingRecord = await _context.StudyRecords
+ .FirstOrDefaultAsync(r => r.UserId == userId &&
+ r.FlashcardId == request.FlashcardId &&
+ r.StudyMode == request.TestType);
+
+ if (existingRecord != null)
+ {
+ return Conflict(new { Success = false, Error = "Test already completed",
+ CompletedAt = existingRecord.StudiedAt });
+ }
+
+ // 記錄測驗完成狀態
+ var studyRecord = new StudyRecord
+ {
+ Id = Guid.NewGuid(),
+ UserId = userId.Value,
+ FlashcardId = request.FlashcardId,
+ SessionId = Guid.NewGuid(), // 臨時會話ID
+ StudyMode = request.TestType, // 記錄具體測驗類型
+ QualityRating = request.ConfidenceLevel ?? (request.IsCorrect ? 4 : 2),
+ ResponseTimeMs = request.ResponseTimeMs,
+ UserAnswer = request.UserAnswer,
+ IsCorrect = request.IsCorrect,
+ StudiedAt = DateTime.UtcNow
+ };
+
+ _context.StudyRecords.Add(studyRecord);
+ await _context.SaveChangesAsync();
+
+ _logger.LogInformation("Recorded test completion: {TestType} for {Word}, correct: {IsCorrect}",
+ request.TestType, flashcard.Word, request.IsCorrect);
+
+ return Ok(new
+ {
+ Success = true,
+ Data = new
+ {
+ RecordId = studyRecord.Id,
+ TestType = request.TestType,
+ IsCorrect = request.IsCorrect,
+ CompletedAt = studyRecord.StudiedAt
+ },
+ Message = $"Test {request.TestType} recorded successfully"
+ });
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error recording test completion");
+ return StatusCode(500, new
+ {
+ Success = false,
+ Error = "Failed to record test completion",
+ Timestamp = DateTime.UtcNow
+ });
+ }
+ }
}
// Request DTOs
@@ -581,4 +744,14 @@ public class RecordStudyResultRequest
public class CompleteStudySessionRequest
{
public int DurationSeconds { get; set; }
+}
+
+public class RecordTestRequest
+{
+ public Guid FlashcardId { get; set; }
+ public string TestType { get; set; } = string.Empty; // flip-memory, vocab-choice, etc.
+ public bool IsCorrect { get; set; }
+ public string? UserAnswer { get; set; }
+ public int? ConfidenceLevel { get; set; } // 1-5
+ public int? ResponseTimeMs { get; set; }
}
\ No newline at end of file
diff --git a/backend/DramaLing.Api/Controllers/StudySessionController.cs b/backend/DramaLing.Api/Controllers/StudySessionController.cs
new file mode 100644
index 0000000..6f75a4e
--- /dev/null
+++ b/backend/DramaLing.Api/Controllers/StudySessionController.cs
@@ -0,0 +1,276 @@
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Authorization;
+using DramaLing.Api.Services;
+
+namespace DramaLing.Api.Controllers;
+
+[ApiController]
+[Route("api/study/sessions")]
+[Authorize]
+public class StudySessionController : ControllerBase
+{
+ private readonly IStudySessionService _studySessionService;
+ private readonly IAuthService _authService;
+ private readonly ILogger _logger;
+
+ public StudySessionController(
+ IStudySessionService studySessionService,
+ IAuthService authService,
+ ILogger logger)
+ {
+ _studySessionService = studySessionService;
+ _authService = authService;
+ _logger = logger;
+ }
+
+ ///
+ /// 開始新的學習會話
+ ///
+ [HttpPost("start")]
+ public async Task StartSession()
+ {
+ try
+ {
+ var userId = await _authService.GetUserIdFromTokenAsync(Request.Headers.Authorization);
+ if (userId == null)
+ return Unauthorized(new { Success = false, Error = "Invalid token" });
+
+ var session = await _studySessionService.StartSessionAsync(userId.Value);
+
+ return Ok(new
+ {
+ Success = true,
+ Data = new
+ {
+ SessionId = session.Id,
+ TotalCards = session.TotalCards,
+ TotalTests = session.TotalTests,
+ CurrentCardIndex = session.CurrentCardIndex,
+ CurrentTestType = session.CurrentTestType,
+ StartedAt = session.StartedAt
+ },
+ Message = $"Study session started with {session.TotalCards} cards and {session.TotalTests} tests"
+ });
+ }
+ catch (InvalidOperationException ex)
+ {
+ return BadRequest(new { Success = false, Error = ex.Message });
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error starting study session");
+ return StatusCode(500, new
+ {
+ Success = false,
+ Error = "Failed to start study session",
+ Timestamp = DateTime.UtcNow
+ });
+ }
+ }
+
+ ///
+ /// 獲取當前測驗
+ ///
+ [HttpGet("{sessionId}/current-test")]
+ public async Task GetCurrentTest(Guid sessionId)
+ {
+ try
+ {
+ var userId = await _authService.GetUserIdFromTokenAsync(Request.Headers.Authorization);
+ if (userId == null)
+ return Unauthorized(new { Success = false, Error = "Invalid token" });
+
+ var currentTest = await _studySessionService.GetCurrentTestAsync(sessionId);
+
+ return Ok(new
+ {
+ Success = true,
+ Data = currentTest
+ });
+ }
+ catch (InvalidOperationException ex)
+ {
+ return BadRequest(new { Success = false, Error = ex.Message });
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error getting current test for session {SessionId}", sessionId);
+ return StatusCode(500, new
+ {
+ Success = false,
+ Error = "Failed to get current test",
+ Timestamp = DateTime.UtcNow
+ });
+ }
+ }
+
+ ///
+ /// 提交測驗結果
+ ///
+ [HttpPost("{sessionId}/submit-test")]
+ public async Task SubmitTest(Guid sessionId, [FromBody] SubmitTestRequestDto request)
+ {
+ try
+ {
+ var userId = await _authService.GetUserIdFromTokenAsync(Request.Headers.Authorization);
+ if (userId == null)
+ return Unauthorized(new { Success = false, Error = "Invalid token" });
+
+ // 基本驗證
+ if (string.IsNullOrEmpty(request.TestType))
+ {
+ return BadRequest(new { Success = false, Error = "Test type is required" });
+ }
+
+ if (request.ConfidenceLevel.HasValue && (request.ConfidenceLevel < 1 || request.ConfidenceLevel > 5))
+ {
+ return BadRequest(new { Success = false, Error = "Confidence level must be between 1 and 5" });
+ }
+
+ var response = await _studySessionService.SubmitTestAsync(sessionId, request);
+
+ return Ok(new
+ {
+ Success = response.Success,
+ Data = new
+ {
+ IsCardCompleted = response.IsCardCompleted,
+ Progress = response.Progress
+ },
+ Message = response.IsCardCompleted ? "Card completed!" : "Test recorded successfully"
+ });
+ }
+ catch (InvalidOperationException ex)
+ {
+ return BadRequest(new { Success = false, Error = ex.Message });
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error submitting test for session {SessionId}", sessionId);
+ return StatusCode(500, new
+ {
+ Success = false,
+ Error = "Failed to submit test result",
+ Timestamp = DateTime.UtcNow
+ });
+ }
+ }
+
+ ///
+ /// 獲取下一個測驗
+ ///
+ [HttpGet("{sessionId}/next-test")]
+ public async Task GetNextTest(Guid sessionId)
+ {
+ try
+ {
+ var userId = await _authService.GetUserIdFromTokenAsync(Request.Headers.Authorization);
+ if (userId == null)
+ return Unauthorized(new { Success = false, Error = "Invalid token" });
+
+ var nextTest = await _studySessionService.GetNextTestAsync(sessionId);
+
+ return Ok(new
+ {
+ Success = true,
+ Data = nextTest
+ });
+ }
+ catch (InvalidOperationException ex)
+ {
+ return BadRequest(new { Success = false, Error = ex.Message });
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error getting next test for session {SessionId}", sessionId);
+ return StatusCode(500, new
+ {
+ Success = false,
+ Error = "Failed to get next test",
+ Timestamp = DateTime.UtcNow
+ });
+ }
+ }
+
+ ///
+ /// 獲取詳細進度
+ ///
+ [HttpGet("{sessionId}/progress")]
+ public async Task GetProgress(Guid sessionId)
+ {
+ try
+ {
+ var userId = await _authService.GetUserIdFromTokenAsync(Request.Headers.Authorization);
+ if (userId == null)
+ return Unauthorized(new { Success = false, Error = "Invalid token" });
+
+ var progress = await _studySessionService.GetProgressAsync(sessionId);
+
+ return Ok(new
+ {
+ Success = true,
+ Data = progress
+ });
+ }
+ catch (InvalidOperationException ex)
+ {
+ return BadRequest(new { Success = false, Error = ex.Message });
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error getting progress for session {SessionId}", sessionId);
+ return StatusCode(500, new
+ {
+ Success = false,
+ Error = "Failed to get progress",
+ Timestamp = DateTime.UtcNow
+ });
+ }
+ }
+
+ ///
+ /// 完成學習會話
+ ///
+ [HttpPut("{sessionId}/complete")]
+ public async Task CompleteSession(Guid sessionId)
+ {
+ try
+ {
+ var userId = await _authService.GetUserIdFromTokenAsync(Request.Headers.Authorization);
+ if (userId == null)
+ return Unauthorized(new { Success = false, Error = "Invalid token" });
+
+ var session = await _studySessionService.CompleteSessionAsync(sessionId);
+
+ return Ok(new
+ {
+ Success = true,
+ Data = new
+ {
+ SessionId = session.Id,
+ CompletedAt = session.EndedAt,
+ TotalCards = session.TotalCards,
+ CompletedCards = session.CompletedCards,
+ TotalTests = session.TotalTests,
+ CompletedTests = session.CompletedTests,
+ DurationSeconds = session.DurationSeconds
+ },
+ Message = "Study session completed successfully"
+ });
+ }
+ catch (InvalidOperationException ex)
+ {
+ return BadRequest(new { Success = false, Error = ex.Message });
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error completing session {SessionId}", sessionId);
+ return StatusCode(500, new
+ {
+ Success = false,
+ Error = "Failed to complete session",
+ Timestamp = DateTime.UtcNow
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/backend/DramaLing.Api/Data/DramaLingDbContext.cs b/backend/DramaLing.Api/Data/DramaLingDbContext.cs
index a38721e..1554346 100644
--- a/backend/DramaLing.Api/Data/DramaLingDbContext.cs
+++ b/backend/DramaLing.Api/Data/DramaLingDbContext.cs
@@ -153,6 +153,11 @@ public class DramaLingDbContext : DbContext
recordEntity.Property(r => r.UserAnswer).HasColumnName("user_answer");
recordEntity.Property(r => r.IsCorrect).HasColumnName("is_correct");
recordEntity.Property(r => r.StudiedAt).HasColumnName("studied_at");
+
+ // 添加複合唯一索引:防止同一用戶同一詞卡同一測驗類型重複記錄
+ recordEntity.HasIndex(r => new { r.UserId, r.FlashcardId, r.StudyMode })
+ .IsUnique()
+ .HasDatabaseName("IX_StudyRecord_UserCard_TestType_Unique");
}
private void ConfigureTagEntities(ModelBuilder modelBuilder)
diff --git a/backend/DramaLing.Api/Migrations/20250926061341_AddStudyRecordUniqueIndex.Designer.cs b/backend/DramaLing.Api/Migrations/20250926061341_AddStudyRecordUniqueIndex.Designer.cs
new file mode 100644
index 0000000..a107638
--- /dev/null
+++ b/backend/DramaLing.Api/Migrations/20250926061341_AddStudyRecordUniqueIndex.Designer.cs
@@ -0,0 +1,1567 @@
+//
+using System;
+using DramaLing.Api.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace DramaLing.Api.Migrations
+{
+ [DbContext(typeof(DramaLingDbContext))]
+ [Migration("20250926061341_AddStudyRecordUniqueIndex")]
+ partial class AddStudyRecordUniqueIndex
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "8.0.10");
+
+ modelBuilder.Entity("DramaLing.Api.Models.Entities.AudioCache", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("Accent")
+ .IsRequired()
+ .HasMaxLength(2)
+ .HasColumnType("TEXT");
+
+ b.Property("AccessCount")
+ .HasColumnType("INTEGER")
+ .HasColumnName("access_count");
+
+ b.Property("AudioUrl")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasColumnName("audio_url");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT")
+ .HasColumnName("created_at");
+
+ b.Property("DurationMs")
+ .HasColumnType("INTEGER")
+ .HasColumnName("duration_ms");
+
+ b.Property("FileSize")
+ .HasColumnType("INTEGER")
+ .HasColumnName("file_size");
+
+ b.Property("LastAccessed")
+ .HasColumnType("TEXT")
+ .HasColumnName("last_accessed");
+
+ b.Property("TextContent")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasColumnName("text_content");
+
+ b.Property("TextHash")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT")
+ .HasColumnName("text_hash");
+
+ b.Property("VoiceId")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("TEXT")
+ .HasColumnName("voice_id");
+
+ b.HasKey("Id");
+
+ b.HasIndex("LastAccessed")
+ .HasDatabaseName("IX_AudioCache_LastAccessed");
+
+ b.HasIndex("TextHash")
+ .IsUnique()
+ .HasDatabaseName("IX_AudioCache_TextHash");
+
+ b.ToTable("audio_cache", (string)null);
+ });
+
+ modelBuilder.Entity("DramaLing.Api.Models.Entities.CardSet", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("TEXT");
+
+ b.Property("CardCount")
+ .HasColumnType("INTEGER");
+
+ b.Property("Color")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("Description")
+ .HasColumnType("TEXT");
+
+ b.Property("IsDefault")
+ .HasColumnType("INTEGER");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("card_sets", (string)null);
+ });
+
+ modelBuilder.Entity("DramaLing.Api.Models.Entities.DailyStats", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("AiApiCalls")
+ .HasColumnType("INTEGER")
+ .HasColumnName("ai_api_calls");
+
+ b.Property("CardsGenerated")
+ .HasColumnType("INTEGER")
+ .HasColumnName("cards_generated");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT")
+ .HasColumnName("created_at");
+
+ b.Property("Date")
+ .HasColumnType("TEXT");
+
+ b.Property("SessionCount")
+ .HasColumnType("INTEGER")
+ .HasColumnName("session_count");
+
+ b.Property("StudyTimeSeconds")
+ .HasColumnType("INTEGER")
+ .HasColumnName("study_time_seconds");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT")
+ .HasColumnName("user_id");
+
+ b.Property("WordsCorrect")
+ .HasColumnType("INTEGER")
+ .HasColumnName("words_correct");
+
+ b.Property("WordsStudied")
+ .HasColumnType("INTEGER")
+ .HasColumnName("words_studied");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "Date")
+ .IsUnique();
+
+ b.ToTable("daily_stats", (string)null);
+ });
+
+ modelBuilder.Entity("DramaLing.Api.Models.Entities.ErrorReport", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("AdminNotes")
+ .HasColumnType("TEXT")
+ .HasColumnName("admin_notes");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT")
+ .HasColumnName("created_at");
+
+ b.Property("Description")
+ .HasColumnType("TEXT");
+
+ b.Property("FlashcardId")
+ .HasColumnType("TEXT")
+ .HasColumnName("flashcard_id");
+
+ b.Property("ReportType")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("TEXT")
+ .HasColumnName("report_type");
+
+ b.Property("ResolvedAt")
+ .HasColumnType("TEXT")
+ .HasColumnName("resolved_at");
+
+ b.Property("ResolvedBy")
+ .HasColumnType("TEXT")
+ .HasColumnName("resolved_by");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("TEXT");
+
+ b.Property("StudyMode")
+ .HasMaxLength(50)
+ .HasColumnType("TEXT")
+ .HasColumnName("study_mode");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id");
+
+ b.HasIndex("FlashcardId");
+
+ b.HasIndex("ResolvedBy");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("error_reports", (string)null);
+ });
+
+ modelBuilder.Entity("DramaLing.Api.Models.Entities.ExampleImage", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("AccessCount")
+ .HasColumnType("INTEGER")
+ .HasColumnName("access_count");
+
+ b.Property("AltText")
+ .HasMaxLength(200)
+ .HasColumnType("TEXT")
+ .HasColumnName("alt_text");
+
+ b.Property("ContentHash")
+ .HasMaxLength(64)
+ .HasColumnType("TEXT")
+ .HasColumnName("content_hash");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT")
+ .HasColumnName("created_at");
+
+ b.Property("FileSize")
+ .HasColumnType("INTEGER")
+ .HasColumnName("file_size");
+
+ b.Property("GeminiCost")
+ .HasColumnType("TEXT")
+ .HasColumnName("gemini_cost");
+
+ b.Property("GeminiDescription")
+ .HasColumnType("TEXT")
+ .HasColumnName("gemini_description");
+
+ b.Property("GeminiPrompt")
+ .HasColumnType("TEXT")
+ .HasColumnName("gemini_prompt");
+
+ b.Property("ImageHeight")
+ .HasColumnType("INTEGER")
+ .HasColumnName("image_height");
+
+ b.Property("ImageWidth")
+ .HasColumnType("INTEGER")
+ .HasColumnName("image_width");
+
+ b.Property("ModerationNotes")
+ .HasColumnType("TEXT")
+ .HasColumnName("moderation_notes");
+
+ b.Property("ModerationStatus")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("TEXT")
+ .HasColumnName("moderation_status");
+
+ b.Property("QualityScore")
+ .HasColumnType("TEXT")
+ .HasColumnName("quality_score");
+
+ b.Property("RelativePath")
+ .IsRequired()
+ .HasMaxLength(500)
+ .HasColumnType("TEXT")
+ .HasColumnName("relative_path");
+
+ b.Property("ReplicateCost")
+ .HasColumnType("TEXT")
+ .HasColumnName("replicate_cost");
+
+ b.Property("ReplicateModel")
+ .HasMaxLength(100)
+ .HasColumnType("TEXT")
+ .HasColumnName("replicate_model");
+
+ b.Property("ReplicatePrompt")
+ .HasColumnType("TEXT")
+ .HasColumnName("replicate_prompt");
+
+ b.Property("ReplicateVersion")
+ .HasMaxLength(100)
+ .HasColumnType("TEXT")
+ .HasColumnName("replicate_version");
+
+ b.Property("TotalGenerationCost")
+ .HasColumnType("TEXT")
+ .HasColumnName("total_generation_cost");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT")
+ .HasColumnName("updated_at");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AccessCount");
+
+ b.HasIndex("ContentHash")
+ .IsUnique();
+
+ b.ToTable("example_images", (string)null);
+ });
+
+ modelBuilder.Entity("DramaLing.Api.Models.Entities.Flashcard", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("CardSetId")
+ .HasColumnType("TEXT")
+ .HasColumnName("card_set_id");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT")
+ .HasColumnName("created_at");
+
+ b.Property("Definition")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("DifficultyLevel")
+ .HasMaxLength(10)
+ .HasColumnType("TEXT")
+ .HasColumnName("difficulty_level");
+
+ b.Property("EasinessFactor")
+ .HasColumnType("REAL")
+ .HasColumnName("easiness_factor");
+
+ b.Property("Example")
+ .HasColumnType("TEXT");
+
+ b.Property("ExampleTranslation")
+ .HasColumnType("TEXT")
+ .HasColumnName("example_translation");
+
+ b.Property("IntervalDays")
+ .HasColumnType("INTEGER")
+ .HasColumnName("interval_days");
+
+ b.Property("IsArchived")
+ .HasColumnType("INTEGER")
+ .HasColumnName("is_archived");
+
+ b.Property("IsFavorite")
+ .HasColumnType("INTEGER")
+ .HasColumnName("is_favorite");
+
+ b.Property("LastQuestionType")
+ .HasMaxLength(50)
+ .HasColumnType("TEXT");
+
+ b.Property("LastReviewedAt")
+ .HasColumnType("TEXT")
+ .HasColumnName("last_reviewed_at");
+
+ b.Property("MasteryLevel")
+ .HasColumnType("INTEGER")
+ .HasColumnName("mastery_level");
+
+ b.Property("NextReviewDate")
+ .HasColumnType("TEXT")
+ .HasColumnName("next_review_date");
+
+ b.Property("PartOfSpeech")
+ .HasMaxLength(50)
+ .HasColumnType("TEXT")
+ .HasColumnName("part_of_speech");
+
+ b.Property("Pronunciation")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property("Repetitions")
+ .HasColumnType("INTEGER");
+
+ b.Property("ReviewHistory")
+ .HasColumnType("TEXT");
+
+ b.Property("TimesCorrect")
+ .HasColumnType("INTEGER")
+ .HasColumnName("times_correct");
+
+ b.Property("TimesReviewed")
+ .HasColumnType("INTEGER")
+ .HasColumnName("times_reviewed");
+
+ b.Property("Translation")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT")
+ .HasColumnName("updated_at");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT")
+ .HasColumnName("user_id");
+
+ b.Property("Word")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CardSetId");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("flashcards", (string)null);
+ });
+
+ modelBuilder.Entity("DramaLing.Api.Models.Entities.FlashcardExampleImage", b =>
+ {
+ b.Property("FlashcardId")
+ .HasColumnType("TEXT")
+ .HasColumnName("flashcard_id");
+
+ b.Property("ExampleImageId")
+ .HasColumnType("TEXT")
+ .HasColumnName("example_image_id");
+
+ b.Property("ContextRelevance")
+ .HasColumnType("TEXT")
+ .HasColumnName("context_relevance");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT")
+ .HasColumnName("created_at");
+
+ b.Property("DisplayOrder")
+ .HasColumnType("INTEGER")
+ .HasColumnName("display_order");
+
+ b.Property("IsPrimary")
+ .HasColumnType("INTEGER")
+ .HasColumnName("is_primary");
+
+ b.HasKey("FlashcardId", "ExampleImageId");
+
+ b.HasIndex("ExampleImageId");
+
+ b.ToTable("flashcard_example_images", (string)null);
+ });
+
+ modelBuilder.Entity("DramaLing.Api.Models.Entities.FlashcardTag", b =>
+ {
+ b.Property("FlashcardId")
+ .HasColumnType("TEXT")
+ .HasColumnName("flashcard_id");
+
+ b.Property("TagId")
+ .HasColumnType("TEXT")
+ .HasColumnName("tag_id");
+
+ b.HasKey("FlashcardId", "TagId");
+
+ b.HasIndex("TagId");
+
+ b.ToTable("flashcard_tags", (string)null);
+ });
+
+ modelBuilder.Entity("DramaLing.Api.Models.Entities.ImageGenerationRequest", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("CompletedAt")
+ .HasColumnType("TEXT")
+ .HasColumnName("completed_at");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT")
+ .HasColumnName("created_at");
+
+ b.Property("FinalReplicatePrompt")
+ .HasColumnType("TEXT")
+ .HasColumnName("final_replicate_prompt");
+
+ b.Property("FlashcardId")
+ .HasColumnType("TEXT")
+ .HasColumnName("flashcard_id");
+
+ b.Property("GeminiCompletedAt")
+ .HasColumnType("TEXT")
+ .HasColumnName("gemini_completed_at");
+
+ b.Property("GeminiCost")
+ .HasColumnType("TEXT")
+ .HasColumnName("gemini_cost");
+
+ b.Property("GeminiErrorMessage")
+ .HasColumnType("TEXT")
+ .HasColumnName("gemini_error_message");
+
+ b.Property("GeminiProcessingTimeMs")
+ .HasColumnType("INTEGER")
+ .HasColumnName("gemini_processing_time_ms");
+
+ b.Property("GeminiPrompt")
+ .HasColumnType("TEXT")
+ .HasColumnName("gemini_prompt");
+
+ b.Property("GeminiStartedAt")
+ .HasColumnType("TEXT")
+ .HasColumnName("gemini_started_at");
+
+ b.Property("GeminiStatus")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("TEXT")
+ .HasColumnName("gemini_status");
+
+ b.Property("GeneratedDescription")
+ .HasColumnType("TEXT")
+ .HasColumnName("generated_description");
+
+ b.Property("GeneratedImageId")
+ .HasColumnType("TEXT")
+ .HasColumnName("generated_image_id");
+
+ b.Property("OriginalRequest")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasColumnName("original_request");
+
+ b.Property("OverallStatus")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("TEXT")
+ .HasColumnName("overall_status");
+
+ b.Property("ReplicateCompletedAt")
+ .HasColumnType("TEXT")
+ .HasColumnName("replicate_completed_at");
+
+ b.Property("ReplicateCost")
+ .HasColumnType("TEXT")
+ .HasColumnName("replicate_cost");
+
+ b.Property("ReplicateErrorMessage")
+ .HasColumnType("TEXT")
+ .HasColumnName("replicate_error_message");
+
+ b.Property("ReplicateProcessingTimeMs")
+ .HasColumnType("INTEGER")
+ .HasColumnName("replicate_processing_time_ms");
+
+ b.Property("ReplicateStartedAt")
+ .HasColumnType("TEXT")
+ .HasColumnName("replicate_started_at");
+
+ b.Property("ReplicateStatus")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("TEXT")
+ .HasColumnName("replicate_status");
+
+ b.Property("TotalCost")
+ .HasColumnType("TEXT")
+ .HasColumnName("total_cost");
+
+ b.Property("TotalProcessingTimeMs")
+ .HasColumnType("INTEGER")
+ .HasColumnName("total_processing_time_ms");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id");
+
+ b.HasIndex("FlashcardId");
+
+ b.HasIndex("GeneratedImageId");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("image_generation_requests", (string)null);
+ });
+
+ modelBuilder.Entity("DramaLing.Api.Models.Entities.PronunciationAssessment", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("AccuracyScore")
+ .HasColumnType("TEXT")
+ .HasColumnName("accuracy_score");
+
+ b.Property("AudioUrl")
+ .HasColumnType("TEXT")
+ .HasColumnName("audio_url");
+
+ b.Property("CompletenessScore")
+ .HasColumnType("TEXT")
+ .HasColumnName("completeness_score");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT")
+ .HasColumnName("created_at");
+
+ b.Property("FlashcardId")
+ .HasColumnType("TEXT")
+ .HasColumnName("flashcard_id");
+
+ b.Property("FluencyScore")
+ .HasColumnType("TEXT")
+ .HasColumnName("fluency_score");
+
+ b.Property("OverallScore")
+ .HasColumnType("INTEGER")
+ .HasColumnName("overall_score");
+
+ b.Property("PhonemeScores")
+ .HasColumnType("TEXT")
+ .HasColumnName("phoneme_scores");
+
+ b.Property("PracticeMode")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("TEXT")
+ .HasColumnName("practice_mode");
+
+ b.Property("ProsodyScore")
+ .HasColumnType("TEXT")
+ .HasColumnName("prosody_score");
+
+ b.Property("StudySessionId")
+ .HasColumnType("TEXT")
+ .HasColumnName("study_session_id");
+
+ b.Property("Suggestions")
+ .HasColumnType("TEXT")
+ .HasColumnName("suggestions");
+
+ b.Property("TargetText")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasColumnName("target_text");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id");
+
+ b.HasIndex("FlashcardId");
+
+ b.HasIndex("StudySessionId")
+ .HasDatabaseName("IX_PronunciationAssessment_Session");
+
+ b.HasIndex("UserId", "FlashcardId")
+ .HasDatabaseName("IX_PronunciationAssessment_UserFlashcard");
+
+ b.ToTable("pronunciation_assessments", (string)null);
+ });
+
+ modelBuilder.Entity("DramaLing.Api.Models.Entities.SentenceAnalysisCache", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("AccessCount")
+ .HasColumnType("INTEGER");
+
+ b.Property("AnalysisResult")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("CorrectedText")
+ .HasMaxLength(1000)
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("ExpiresAt")
+ .HasColumnType("TEXT");
+
+ b.Property("GrammarCorrections")
+ .HasColumnType("TEXT");
+
+ b.Property("HasGrammarErrors")
+ .HasColumnType("INTEGER");
+
+ b.Property("HighValueWords")
+ .HasColumnType("TEXT");
+
+ b.Property("IdiomsDetected")
+ .HasColumnType("TEXT");
+
+ b.Property("InputText")
+ .IsRequired()
+ .HasMaxLength(1000)
+ .HasColumnType("TEXT");
+
+ b.Property("InputTextHash")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property("LastAccessedAt")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ExpiresAt")
+ .HasDatabaseName("IX_SentenceAnalysisCache_Expires");
+
+ b.HasIndex("InputTextHash")
+ .HasDatabaseName("IX_SentenceAnalysisCache_Hash");
+
+ b.HasIndex("InputTextHash", "ExpiresAt")
+ .HasDatabaseName("IX_SentenceAnalysisCache_Hash_Expires");
+
+ b.ToTable("SentenceAnalysisCache");
+ });
+
+ modelBuilder.Entity("DramaLing.Api.Models.Entities.StudyCard", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("CompletedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("FlashcardId")
+ .HasColumnType("TEXT");
+
+ b.Property("IsCompleted")
+ .HasColumnType("INTEGER");
+
+ b.Property("Order")
+ .HasColumnType("INTEGER");
+
+ b.Property("PlannedTests")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("PlannedTestsJson")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("StartedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("StudySessionId")
+ .HasColumnType("TEXT");
+
+ b.Property("Word")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("FlashcardId");
+
+ b.HasIndex("StudySessionId");
+
+ b.ToTable("study_cards", (string)null);
+ });
+
+ modelBuilder.Entity("DramaLing.Api.Models.Entities.StudyRecord", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("FlashcardId")
+ .HasColumnType("TEXT")
+ .HasColumnName("flashcard_id");
+
+ b.Property("IsCorrect")
+ .HasColumnType("INTEGER")
+ .HasColumnName("is_correct");
+
+ b.Property("NewEasinessFactor")
+ .HasColumnType("REAL");
+
+ b.Property("NewIntervalDays")
+ .HasColumnType("INTEGER");
+
+ b.Property("NewRepetitions")
+ .HasColumnType("INTEGER");
+
+ b.Property("NextReviewDate")
+ .HasColumnType("TEXT");
+
+ b.Property("PreviousEasinessFactor")
+ .HasColumnType("REAL");
+
+ b.Property("PreviousIntervalDays")
+ .HasColumnType("INTEGER");
+
+ b.Property("PreviousRepetitions")
+ .HasColumnType("INTEGER");
+
+ b.Property("QualityRating")
+ .HasColumnType("INTEGER")
+ .HasColumnName("quality_rating");
+
+ b.Property("ResponseTimeMs")
+ .HasColumnType("INTEGER")
+ .HasColumnName("response_time_ms");
+
+ b.Property("SessionId")
+ .HasColumnType("TEXT")
+ .HasColumnName("session_id");
+
+ b.Property("StudiedAt")
+ .HasColumnType("TEXT")
+ .HasColumnName("studied_at");
+
+ b.Property("StudyMode")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("TEXT")
+ .HasColumnName("study_mode");
+
+ b.Property("UserAnswer")
+ .HasColumnType("TEXT")
+ .HasColumnName("user_answer");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id");
+
+ b.HasIndex("FlashcardId");
+
+ b.HasIndex("SessionId");
+
+ b.HasIndex("UserId", "FlashcardId", "StudyMode")
+ .IsUnique()
+ .HasDatabaseName("IX_StudyRecord_UserCard_TestType_Unique");
+
+ b.ToTable("study_records", (string)null);
+ });
+
+ modelBuilder.Entity("DramaLing.Api.Models.Entities.StudySession", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("AverageResponseTimeMs")
+ .HasColumnType("INTEGER")
+ .HasColumnName("average_response_time_ms");
+
+ b.Property("CompletedCards")
+ .HasColumnType("INTEGER");
+
+ b.Property("CompletedTests")
+ .HasColumnType("INTEGER");
+
+ b.Property("CorrectCount")
+ .HasColumnType("INTEGER")
+ .HasColumnName("correct_count");
+
+ b.Property("CurrentCardIndex")
+ .HasColumnType("INTEGER");
+
+ b.Property("CurrentTestType")
+ .HasMaxLength(50)
+ .HasColumnType("TEXT");
+
+ b.Property("DurationSeconds")
+ .HasColumnType("INTEGER")
+ .HasColumnName("duration_seconds");
+
+ b.Property("EndedAt")
+ .HasColumnType("TEXT")
+ .HasColumnName("ended_at");
+
+ b.Property("SessionType")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("TEXT")
+ .HasColumnName("session_type");
+
+ b.Property("StartedAt")
+ .HasColumnType("TEXT")
+ .HasColumnName("started_at");
+
+ b.Property("Status")
+ .HasColumnType("INTEGER");
+
+ b.Property("TotalCards")
+ .HasColumnType("INTEGER")
+ .HasColumnName("total_cards");
+
+ b.Property("TotalTests")
+ .HasColumnType("INTEGER");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("study_sessions", (string)null);
+ });
+
+ modelBuilder.Entity("DramaLing.Api.Models.Entities.Tag", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("Color")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT")
+ .HasColumnName("created_at");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("TEXT");
+
+ b.Property("UsageCount")
+ .HasColumnType("INTEGER")
+ .HasColumnName("usage_count");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("tags", (string)null);
+ });
+
+ modelBuilder.Entity("DramaLing.Api.Models.Entities.TestResult", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("CompletedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("ConfidenceLevel")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsCorrect")
+ .HasColumnType("INTEGER");
+
+ b.Property("ResponseTimeMs")
+ .HasColumnType("INTEGER");
+
+ b.Property("StudyCardId")
+ .HasColumnType("TEXT");
+
+ b.Property("TestType")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("TEXT");
+
+ b.Property("UserAnswer")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("StudyCardId");
+
+ b.ToTable("test_results", (string)null);
+ });
+
+ modelBuilder.Entity("DramaLing.Api.Models.Entities.User", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("AvatarUrl")
+ .HasColumnType("TEXT")
+ .HasColumnName("avatar_url");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT")
+ .HasColumnName("created_at");
+
+ b.Property("DisplayName")
+ .HasMaxLength(100)
+ .HasColumnType("TEXT")
+ .HasColumnName("display_name");
+
+ b.Property("Email")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT")
+ .HasColumnName("email");
+
+ b.Property("EnglishLevel")
+ .IsRequired()
+ .HasMaxLength(10)
+ .HasColumnType("TEXT")
+ .HasColumnName("english_level");
+
+ b.Property("IsLevelVerified")
+ .HasColumnType("INTEGER")
+ .HasColumnName("is_level_verified");
+
+ b.Property("LevelNotes")
+ .HasMaxLength(500)
+ .HasColumnType("TEXT")
+ .HasColumnName("level_notes");
+
+ b.Property("LevelUpdatedAt")
+ .HasColumnType("TEXT")
+ .HasColumnName("level_updated_at");
+
+ b.Property("PasswordHash")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT")
+ .HasColumnName("password_hash");
+
+ b.Property("Preferences")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasColumnName("preferences");
+
+ b.Property("SubscriptionType")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("TEXT")
+ .HasColumnName("subscription_type");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT")
+ .HasColumnName("updated_at");
+
+ b.Property("Username")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("TEXT")
+ .HasColumnName("username");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Email")
+ .IsUnique();
+
+ b.HasIndex("Username")
+ .IsUnique();
+
+ b.ToTable("user_profiles", (string)null);
+ });
+
+ modelBuilder.Entity("DramaLing.Api.Models.Entities.UserAudioPreferences", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property("AutoPlayEnabled")
+ .HasColumnType("INTEGER")
+ .HasColumnName("auto_play_enabled");
+
+ b.Property("DefaultSpeed")
+ .HasColumnType("TEXT")
+ .HasColumnName("default_speed");
+
+ b.Property("EnableDetailedFeedback")
+ .HasColumnType("INTEGER")
+ .HasColumnName("enable_detailed_feedback");
+
+ b.Property("PreferredAccent")
+ .IsRequired()
+ .HasMaxLength(2)
+ .HasColumnType("TEXT")
+ .HasColumnName("preferred_accent");
+
+ b.Property("PreferredVoiceFemale")
+ .HasMaxLength(50)
+ .HasColumnType("TEXT")
+ .HasColumnName("preferred_voice_female");
+
+ b.Property("PreferredVoiceMale")
+ .HasMaxLength(50)
+ .HasColumnType("TEXT")
+ .HasColumnName("preferred_voice_male");
+
+ b.Property("PronunciationDifficulty")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("TEXT")
+ .HasColumnName("pronunciation_difficulty");
+
+ b.Property("TargetScoreThreshold")
+ .HasColumnType("INTEGER")
+ .HasColumnName("target_score_threshold");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT")
+ .HasColumnName("updated_at");
+
+ b.HasKey("UserId");
+
+ b.ToTable("user_audio_preferences", (string)null);
+ });
+
+ modelBuilder.Entity("DramaLing.Api.Models.Entities.UserSettings", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("AutoPlayAudio")
+ .HasColumnType("INTEGER");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("DailyGoal")
+ .HasColumnType("INTEGER");
+
+ b.Property("DifficultyPreference")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("TEXT");
+
+ b.Property("ReminderEnabled")
+ .HasColumnType("INTEGER");
+
+ b.Property("ReminderTime")
+ .HasColumnType("TEXT");
+
+ b.Property("ShowPronunciation")
+ .HasColumnType("INTEGER");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("user_settings", (string)null);
+ });
+
+ modelBuilder.Entity("DramaLing.Api.Models.Entities.WordQueryUsageStats", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("Date")
+ .HasColumnType("TEXT");
+
+ b.Property("HighValueWordClicks")
+ .HasColumnType("INTEGER");
+
+ b.Property("LowValueWordClicks")
+ .HasColumnType("INTEGER");
+
+ b.Property("SentenceAnalysisCount")
+ .HasColumnType("INTEGER");
+
+ b.Property