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<FlashcardTag> FlashcardTags { get; set; }
|
||||||
public DbSet<StudySession> StudySessions { get; set; }
|
public DbSet<StudySession> StudySessions { get; set; }
|
||||||
public DbSet<StudyRecord> StudyRecords { 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<ErrorReport> ErrorReports { get; set; }
|
||||||
public DbSet<DailyStats> DailyStats { get; set; }
|
public DbSet<DailyStats> DailyStats { get; set; }
|
||||||
public DbSet<SentenceAnalysisCache> SentenceAnalysisCache { get; set; }
|
public DbSet<SentenceAnalysisCache> SentenceAnalysisCache { get; set; }
|
||||||
|
|
@ -43,6 +45,8 @@ public class DramaLingDbContext : DbContext
|
||||||
modelBuilder.Entity<FlashcardTag>().ToTable("flashcard_tags");
|
modelBuilder.Entity<FlashcardTag>().ToTable("flashcard_tags");
|
||||||
modelBuilder.Entity<StudySession>().ToTable("study_sessions");
|
modelBuilder.Entity<StudySession>().ToTable("study_sessions");
|
||||||
modelBuilder.Entity<StudyRecord>().ToTable("study_records");
|
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<ErrorReport>().ToTable("error_reports");
|
||||||
modelBuilder.Entity<DailyStats>().ToTable("daily_stats");
|
modelBuilder.Entity<DailyStats>().ToTable("daily_stats");
|
||||||
modelBuilder.Entity<AudioCache>().ToTable("audio_cache");
|
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");
|
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 =>
|
modelBuilder.Entity("DramaLing.Api.Models.Entities.StudyRecord", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
|
|
@ -842,10 +888,23 @@ namespace DramaLing.Api.Migrations
|
||||||
.HasColumnType("INTEGER")
|
.HasColumnType("INTEGER")
|
||||||
.HasColumnName("average_response_time_ms");
|
.HasColumnName("average_response_time_ms");
|
||||||
|
|
||||||
|
b.Property<int>("CompletedCards")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("CompletedTests")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<int>("CorrectCount")
|
b.Property<int>("CorrectCount")
|
||||||
.HasColumnType("INTEGER")
|
.HasColumnType("INTEGER")
|
||||||
.HasColumnName("correct_count");
|
.HasColumnName("correct_count");
|
||||||
|
|
||||||
|
b.Property<int>("CurrentCardIndex")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("CurrentTestType")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<int>("DurationSeconds")
|
b.Property<int>("DurationSeconds")
|
||||||
.HasColumnType("INTEGER")
|
.HasColumnType("INTEGER")
|
||||||
.HasColumnName("duration_seconds");
|
.HasColumnName("duration_seconds");
|
||||||
|
|
@ -864,10 +923,16 @@ namespace DramaLing.Api.Migrations
|
||||||
.HasColumnType("TEXT")
|
.HasColumnType("TEXT")
|
||||||
.HasColumnName("started_at");
|
.HasColumnName("started_at");
|
||||||
|
|
||||||
|
b.Property<int>("Status")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<int>("TotalCards")
|
b.Property<int>("TotalCards")
|
||||||
.HasColumnType("INTEGER")
|
.HasColumnType("INTEGER")
|
||||||
.HasColumnName("total_cards");
|
.HasColumnName("total_cards");
|
||||||
|
|
||||||
|
b.Property<int>("TotalTests")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<Guid>("UserId")
|
b.Property<Guid>("UserId")
|
||||||
.HasColumnType("TEXT")
|
.HasColumnType("TEXT")
|
||||||
.HasColumnName("user_id");
|
.HasColumnName("user_id");
|
||||||
|
|
@ -914,6 +979,42 @@ namespace DramaLing.Api.Migrations
|
||||||
b.ToTable("tags", (string)null);
|
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 =>
|
modelBuilder.Entity("DramaLing.Api.Models.Entities.User", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
|
|
@ -1291,6 +1392,25 @@ namespace DramaLing.Api.Migrations
|
||||||
b.Navigation("User");
|
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 =>
|
modelBuilder.Entity("DramaLing.Api.Models.Entities.StudyRecord", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("DramaLing.Api.Models.Entities.Flashcard", "Flashcard")
|
b.HasOne("DramaLing.Api.Models.Entities.Flashcard", "Flashcard")
|
||||||
|
|
@ -1340,6 +1460,17 @@ namespace DramaLing.Api.Migrations
|
||||||
b.Navigation("User");
|
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 =>
|
modelBuilder.Entity("DramaLing.Api.Models.Entities.UserAudioPreferences", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("DramaLing.Api.Models.Entities.User", "User")
|
b.HasOne("DramaLing.Api.Models.Entities.User", "User")
|
||||||
|
|
@ -1394,8 +1525,15 @@ namespace DramaLing.Api.Migrations
|
||||||
b.Navigation("StudyRecords");
|
b.Navigation("StudyRecords");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DramaLing.Api.Models.Entities.StudyCard", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("TestResults");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("DramaLing.Api.Models.Entities.StudySession", b =>
|
modelBuilder.Entity("DramaLing.Api.Models.Entities.StudySession", b =>
|
||||||
{
|
{
|
||||||
|
b.Navigation("StudyCards");
|
||||||
|
|
||||||
b.Navigation("StudyRecords");
|
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;
|
namespace DramaLing.Api.Models.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 會話狀態枚舉
|
||||||
|
/// </summary>
|
||||||
|
public enum SessionStatus
|
||||||
|
{
|
||||||
|
Active, // 進行中
|
||||||
|
Completed, // 已完成
|
||||||
|
Paused, // 暫停
|
||||||
|
Abandoned // 放棄
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 學習會話實體 (擴展版本)
|
||||||
|
/// </summary>
|
||||||
public class StudySession
|
public class StudySession
|
||||||
{
|
{
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
|
|
@ -24,9 +38,41 @@ public class StudySession
|
||||||
|
|
||||||
public int AverageResponseTimeMs { get; set; } = 0;
|
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
|
// Navigation Properties
|
||||||
public virtual User User { get; set; } = null!;
|
public virtual User User { get; set; } = null!;
|
||||||
public virtual ICollection<StudyRecord> StudyRecords { get; set; } = new List<StudyRecord>();
|
public virtual ICollection<StudyRecord> StudyRecords { get; set; } = new List<StudyRecord>();
|
||||||
|
public virtual ICollection<StudyCard> StudyCards { get; set; } = new List<StudyCard>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public class StudyRecord
|
public class StudyRecord
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue