feat: 重構複習系統為後端驅動架構
- 新增StudyCard和TestResult實體支持詞卡內測驗追蹤 - 擴展StudySession實體添加會話狀態和進度管理 - 修改前端邏輯確保完成所有測驗才標記詞卡完成 - 創建完整重構計劃文檔規劃後續開發 - 改進進度條UI為雙層顯示測驗和詞卡進度 - 添加任務清單彈出模態框提升用戶體驗 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
c21e9de8e5
commit
50cf813400
|
|
@ -19,6 +19,8 @@ public class DramaLingDbContext : DbContext
|
|||
public DbSet<FlashcardTag> FlashcardTags { get; set; }
|
||||
public DbSet<StudySession> StudySessions { get; set; }
|
||||
public DbSet<StudyRecord> StudyRecords { get; set; }
|
||||
public DbSet<StudyCard> StudyCards { get; set; }
|
||||
public DbSet<TestResult> TestResults { get; set; }
|
||||
public DbSet<ErrorReport> ErrorReports { get; set; }
|
||||
public DbSet<DailyStats> DailyStats { get; set; }
|
||||
public DbSet<SentenceAnalysisCache> SentenceAnalysisCache { get; set; }
|
||||
|
|
@ -43,6 +45,8 @@ public class DramaLingDbContext : DbContext
|
|||
modelBuilder.Entity<FlashcardTag>().ToTable("flashcard_tags");
|
||||
modelBuilder.Entity<StudySession>().ToTable("study_sessions");
|
||||
modelBuilder.Entity<StudyRecord>().ToTable("study_records");
|
||||
modelBuilder.Entity<StudyCard>().ToTable("study_cards");
|
||||
modelBuilder.Entity<TestResult>().ToTable("test_results");
|
||||
modelBuilder.Entity<ErrorReport>().ToTable("error_reports");
|
||||
modelBuilder.Entity<DailyStats>().ToTable("daily_stats");
|
||||
modelBuilder.Entity<AudioCache>().ToTable("audio_cache");
|
||||
|
|
|
|||
1565
backend/DramaLing.Api/Migrations/20250926053105_AddStudyCardAndTestResult.Designer.cs
generated
Normal file
1565
backend/DramaLing.Api/Migrations/20250926053105_AddStudyCardAndTestResult.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,162 @@
|
|||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DramaLing.Api.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddStudyCardAndTestResult : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "CompletedCards",
|
||||
table: "study_sessions",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "CompletedTests",
|
||||
table: "study_sessions",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "CurrentCardIndex",
|
||||
table: "study_sessions",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "CurrentTestType",
|
||||
table: "study_sessions",
|
||||
type: "TEXT",
|
||||
maxLength: 50,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "Status",
|
||||
table: "study_sessions",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "TotalTests",
|
||||
table: "study_sessions",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "study_cards",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
StudySessionId = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
FlashcardId = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
Word = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
|
||||
PlannedTestsJson = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Order = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
IsCompleted = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
StartedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
CompletedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
PlannedTests = table.Column<string>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_study_cards", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_study_cards_flashcards_FlashcardId",
|
||||
column: x => x.FlashcardId,
|
||||
principalTable: "flashcards",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_study_cards_study_sessions_StudySessionId",
|
||||
column: x => x.StudySessionId,
|
||||
principalTable: "study_sessions",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "test_results",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
StudyCardId = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
TestType = table.Column<string>(type: "TEXT", maxLength: 50, nullable: false),
|
||||
IsCorrect = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
UserAnswer = table.Column<string>(type: "TEXT", nullable: true),
|
||||
ConfidenceLevel = table.Column<int>(type: "INTEGER", nullable: true),
|
||||
ResponseTimeMs = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
CompletedAt = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_test_results", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_test_results_study_cards_StudyCardId",
|
||||
column: x => x.StudyCardId,
|
||||
principalTable: "study_cards",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_study_cards_FlashcardId",
|
||||
table: "study_cards",
|
||||
column: "FlashcardId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_study_cards_StudySessionId",
|
||||
table: "study_cards",
|
||||
column: "StudySessionId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_test_results_StudyCardId",
|
||||
table: "test_results",
|
||||
column: "StudyCardId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "test_results");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "study_cards");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CompletedCards",
|
||||
table: "study_sessions");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CompletedTests",
|
||||
table: "study_sessions");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CurrentCardIndex",
|
||||
table: "study_sessions");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CurrentTestType",
|
||||
table: "study_sessions");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Status",
|
||||
table: "study_sessions");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "TotalTests",
|
||||
table: "study_sessions");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -756,6 +756,52 @@ namespace DramaLing.Api.Migrations
|
|||
b.ToTable("SentenceAnalysisCache");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DramaLing.Api.Models.Entities.StudyCard", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("CompletedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("FlashcardId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsCompleted")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Order")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("PlannedTests")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PlannedTestsJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("StartedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("StudySessionId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("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<Guid>("Id")
|
||||
|
|
@ -842,10 +888,23 @@ namespace DramaLing.Api.Migrations
|
|||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("average_response_time_ms");
|
||||
|
||||
b.Property<int>("CompletedCards")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("CompletedTests")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("CorrectCount")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("correct_count");
|
||||
|
||||
b.Property<int>("CurrentCardIndex")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("CurrentTestType")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("DurationSeconds")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("duration_seconds");
|
||||
|
|
@ -864,10 +923,16 @@ namespace DramaLing.Api.Migrations
|
|||
.HasColumnType("TEXT")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("TotalCards")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("total_cards");
|
||||
|
||||
b.Property<int>("TotalTests")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("user_id");
|
||||
|
|
@ -914,6 +979,42 @@ namespace DramaLing.Api.Migrations
|
|||
b.ToTable("tags", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DramaLing.Api.Models.Entities.TestResult", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CompletedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("ConfidenceLevel")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsCorrect")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ResponseTimeMs")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid>("StudyCardId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("TestType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("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<Guid>("Id")
|
||||
|
|
@ -1291,6 +1392,25 @@ namespace DramaLing.Api.Migrations
|
|||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DramaLing.Api.Models.Entities.StudyCard", b =>
|
||||
{
|
||||
b.HasOne("DramaLing.Api.Models.Entities.Flashcard", "Flashcard")
|
||||
.WithMany()
|
||||
.HasForeignKey("FlashcardId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("DramaLing.Api.Models.Entities.StudySession", "StudySession")
|
||||
.WithMany("StudyCards")
|
||||
.HasForeignKey("StudySessionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Flashcard");
|
||||
|
||||
b.Navigation("StudySession");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DramaLing.Api.Models.Entities.StudyRecord", b =>
|
||||
{
|
||||
b.HasOne("DramaLing.Api.Models.Entities.Flashcard", "Flashcard")
|
||||
|
|
@ -1340,6 +1460,17 @@ namespace DramaLing.Api.Migrations
|
|||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DramaLing.Api.Models.Entities.TestResult", b =>
|
||||
{
|
||||
b.HasOne("DramaLing.Api.Models.Entities.StudyCard", "StudyCard")
|
||||
.WithMany("TestResults")
|
||||
.HasForeignKey("StudyCardId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("StudyCard");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DramaLing.Api.Models.Entities.UserAudioPreferences", b =>
|
||||
{
|
||||
b.HasOne("DramaLing.Api.Models.Entities.User", "User")
|
||||
|
|
@ -1394,8 +1525,15 @@ namespace DramaLing.Api.Migrations
|
|||
b.Navigation("StudyRecords");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DramaLing.Api.Models.Entities.StudyCard", b =>
|
||||
{
|
||||
b.Navigation("TestResults");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DramaLing.Api.Models.Entities.StudySession", b =>
|
||||
{
|
||||
b.Navigation("StudyCards");
|
||||
|
||||
b.Navigation("StudyRecords");
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,95 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace DramaLing.Api.Models.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// 學習會話中的詞卡進度追蹤
|
||||
/// </summary>
|
||||
public class StudyCard
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
|
||||
public Guid StudySessionId { get; set; }
|
||||
|
||||
public Guid FlashcardId { get; set; }
|
||||
|
||||
[Required]
|
||||
[MaxLength(100)]
|
||||
public string Word { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 該詞卡預定的測驗類型列表 (JSON序列化)
|
||||
/// 例如: ["flip-memory", "vocab-choice", "sentence-fill"]
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string PlannedTestsJson { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 詞卡在會話中的順序
|
||||
/// </summary>
|
||||
public int Order { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否已完成所有測驗
|
||||
/// </summary>
|
||||
public bool IsCompleted { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 詞卡學習開始時間
|
||||
/// </summary>
|
||||
public DateTime StartedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
/// <summary>
|
||||
/// 詞卡學習完成時間
|
||||
/// </summary>
|
||||
public DateTime? CompletedAt { get; set; }
|
||||
|
||||
// Navigation Properties
|
||||
public virtual StudySession StudySession { get; set; } = null!;
|
||||
public virtual Flashcard Flashcard { get; set; } = null!;
|
||||
public virtual ICollection<TestResult> TestResults { get; set; } = new List<TestResult>();
|
||||
|
||||
// Helper Properties (不映射到資料庫)
|
||||
public List<string> PlannedTests
|
||||
{
|
||||
get => string.IsNullOrEmpty(PlannedTestsJson)
|
||||
? new List<string>()
|
||||
: System.Text.Json.JsonSerializer.Deserialize<List<string>>(PlannedTestsJson) ?? new List<string>();
|
||||
set => PlannedTestsJson = System.Text.Json.JsonSerializer.Serialize(value);
|
||||
}
|
||||
|
||||
public int CompletedTestsCount => TestResults?.Count ?? 0;
|
||||
public int PlannedTestsCount => PlannedTests.Count;
|
||||
public bool AllTestsCompleted => CompletedTestsCount >= PlannedTestsCount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 詞卡內的測驗結果記錄
|
||||
/// </summary>
|
||||
public class TestResult
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
|
||||
public Guid StudyCardId { get; set; }
|
||||
|
||||
[Required]
|
||||
[MaxLength(50)]
|
||||
public string TestType { get; set; } = string.Empty; // flip-memory, vocab-choice, etc.
|
||||
|
||||
public bool IsCorrect { get; set; }
|
||||
|
||||
public string? UserAnswer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 信心等級 (1-5, 主要用於翻卡記憶測驗)
|
||||
/// </summary>
|
||||
[Range(1, 5)]
|
||||
public int? ConfidenceLevel { get; set; }
|
||||
|
||||
public int ResponseTimeMs { get; set; }
|
||||
|
||||
public DateTime CompletedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
// Navigation Properties
|
||||
public virtual StudyCard StudyCard { get; set; } = null!;
|
||||
}
|
||||
|
|
@ -2,6 +2,20 @@ using System.ComponentModel.DataAnnotations;
|
|||
|
||||
namespace DramaLing.Api.Models.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// 會話狀態枚舉
|
||||
/// </summary>
|
||||
public enum SessionStatus
|
||||
{
|
||||
Active, // 進行中
|
||||
Completed, // 已完成
|
||||
Paused, // 暫停
|
||||
Abandoned // 放棄
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 學習會話實體 (擴展版本)
|
||||
/// </summary>
|
||||
public class StudySession
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
|
|
@ -24,9 +38,41 @@ public class StudySession
|
|||
|
||||
public int AverageResponseTimeMs { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 會話狀態
|
||||
/// </summary>
|
||||
public SessionStatus Status { get; set; } = SessionStatus.Active;
|
||||
|
||||
/// <summary>
|
||||
/// 當前詞卡索引 (從0開始)
|
||||
/// </summary>
|
||||
public int CurrentCardIndex { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 當前測驗類型
|
||||
/// </summary>
|
||||
[MaxLength(50)]
|
||||
public string? CurrentTestType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 總測驗數量 (所有詞卡的測驗總和)
|
||||
/// </summary>
|
||||
public int TotalTests { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 已完成測驗數量
|
||||
/// </summary>
|
||||
public int CompletedTests { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 已完成詞卡數量
|
||||
/// </summary>
|
||||
public int CompletedCards { get; set; } = 0;
|
||||
|
||||
// Navigation Properties
|
||||
public virtual User User { get; set; } = null!;
|
||||
public virtual ICollection<StudyRecord> StudyRecords { get; set; } = new List<StudyRecord>();
|
||||
public virtual ICollection<StudyCard> StudyCards { get; set; } = new List<StudyCard>();
|
||||
}
|
||||
|
||||
public class StudyRecord
|
||||
|
|
|
|||
Loading…
Reference in New Issue