17 KiB
17 KiB
複習系統後端規格書
版本: 1.0 對應: 前端規格.md + 技術實作規格.md 技術棧: .NET 8 + Entity Framework + SQLite 架構: RESTful API + Clean Architecture 最後更新: 2025-10-03
🏗️ API端點設計
階段1: 純靜態 (當前MVP)
前端: 使用 api_seeds.json 靜態數據
後端: 完全不需要開發
狀態: 前端React useState管理所有邏輯
目的: 驗證用戶體驗和延遲計數系統
階段2: 基礎持久化 (用戶要求時)
前端: 同階段1,但添加localStorage持久化
後端: 仍然不需要開發
新增: 本地進度保存和恢復
觸發: 用戶反饋希望保存進度
階段3: API集成 (遠期需求)
獲取複習卡片:
GET /api/flashcards/due
Query Parameters:
- limit: number (default: 10)
- userId: string (from auth)
Response:
{
"success": true,
"data": {
"flashcards": [
{
"id": "uuid",
"word": "evidence",
"definition": "facts indicating truth",
"partOfSpeech": "noun",
"pronunciation": "/ˈevɪdəns/",
"example": "There was evidence of...",
"exampleTranslation": "有...的證據",
"cefr": "B2",
"difficultyLevelNumeric": 4,
"isFavorite": true,
"hasExampleImage": false,
"primaryImageUrl": null,
"createdAt": "2025-10-01T12:48:11Z",
"updatedAt": "2025-10-01T13:37:22Z"
}
],
"count": 4
},
"message": null,
"timestamp": "2025-10-03T18:57:25Z"
}
記錄複習結果:
POST /api/flashcards/{flashcardId}/review
Headers:
- Authorization: Bearer {token}
- Content-Type: application/json
Request Body:
{
"confidence": 2, // 1=模糊, 2=一般, 3=熟悉
"isCorrect": true, // 基於 confidence >= 2 判斷
"reviewType": "flip-memory",
"responseTimeMs": 3500, // 回應時間
"wasSkipped": false, // 是否是跳過的題目
"skipCount": 0, // 該卡片的跳過次數 (前端統計)
"wrongCount": 1 // 該卡片的答錯次數 (前端統計)
}
Response:
{
"success": true,
"data": {
"flashcardId": "uuid",
"newSuccessCount": 3,
"nextReviewDate": "2025-10-08T12:00:00Z",
"masteryLevelChange": 0.1
},
"timestamp": "2025-10-03T19:00:00Z"
}
⏰ 核心算法: 間隔重複系統
複習時間計算公式 (您的需求)
// 基礎公式: 下一次複習時間 = 2^成功複習次數
public DateTime CalculateNextReviewDate(int successCount)
{
// 計算間隔天數
var intervalDays = Math.Pow(2, successCount);
// 設定最大間隔限制 (避免間隔過長)
var maxIntervalDays = 180; // 最多半年
var finalInterval = Math.Min(intervalDays, maxIntervalDays);
return DateTime.UtcNow.AddDays(finalInterval);
}
具體計算範例
// 學習進度示例
成功次數 0: 下次複習 = 今天 + 2^0 = 明天 (1天後)
成功次數 1: 下次複習 = 今天 + 2^1 = 後天 (2天後)
成功次數 2: 下次複習 = 今天 + 2^2 = 4天後
成功次數 3: 下次複習 = 今天 + 2^3 = 8天後
成功次數 4: 下次複習 = 今天 + 2^4 = 16天後
成功次數 5: 下次複習 = 今天 + 2^5 = 32天後
成功次數 6: 下次複習 = 今天 + 2^6 = 64天後
成功次數 7: 下次複習 = 今天 + 2^7 = 128天後
成功次數 8+: 下次複習 = 今天 + 180天 (上限)
成功計數更新邏輯
public async Task<ReviewResult> ProcessReviewAttemptAsync(string flashcardId, ReviewAttemptRequest request)
{
var stats = await GetFlashcardStatsAsync(flashcardId, request.UserId);
if (request.IsCorrect)
{
// 答對: 增加成功次數,計算新的複習時間
stats.SuccessCount++;
stats.NextReviewDate = CalculateNextReviewDate(stats.SuccessCount);
stats.LastSuccessDate = DateTime.UtcNow;
}
else
{
// 答錯或跳過: 重置成功次數,明天再複習
stats.SuccessCount = 0;
stats.NextReviewDate = DateTime.UtcNow.AddDays(1);
}
// 記錄延遲統計 (配合前端延遲計數)
if (request.WasSkipped)
{
stats.TotalSkipCount += 1;
}
if (!request.IsCorrect && !request.WasSkipped)
{
stats.TotalWrongCount += 1;
}
await SaveStatsAsync(stats);
return new ReviewResult
{
FlashcardId = flashcardId,
NewSuccessCount = stats.SuccessCount,
NextReviewDate = stats.NextReviewDate,
IntervalDays = CalculateIntervalDays(stats.SuccessCount)
};
}
與前端延遲計數的配合
// 前端延遲計數系統的後端記錄
public class FlashcardSessionState
{
public string FlashcardId { get; set; }
public int SessionSkipCount { get; set; } // 本次會話跳過次數
public int SessionWrongCount { get; set; } // 本次會話答錯次數
public DateTime LastAttemptTime { get; set; }
}
// 處理會話中的延遲行為
public void RecordSessionDelay(string flashcardId, bool wasSkipped, bool wasWrong)
{
var sessionState = GetOrCreateSessionState(flashcardId);
if (wasSkipped) sessionState.SessionSkipCount++;
if (wasWrong) sessionState.SessionWrongCount++;
sessionState.LastAttemptTime = DateTime.UtcNow;
}
🗃️ 數據庫設計
現有表格 (已存在)
-- Flashcards 表 (主要詞卡信息)
CREATE TABLE Flashcards (
Id UNIQUEIDENTIFIER PRIMARY KEY,
Word NVARCHAR(100) NOT NULL,
Definition NVARCHAR(500),
PartOfSpeech NVARCHAR(50),
Pronunciation NVARCHAR(100),
Example NVARCHAR(500),
ExampleTranslation NVARCHAR(500),
Cefr NVARCHAR(10),
DifficultyLevelNumeric INT,
IsFavorite BIT DEFAULT 0,
CreatedAt DATETIME2,
UpdatedAt DATETIME2
)
極簡數據庫設計 (階段3需要時)
-- 只需要一個簡單的複習記錄表
CREATE TABLE FlashcardReviews (
Id UNIQUEIDENTIFIER PRIMARY KEY,
FlashcardId UNIQUEIDENTIFIER NOT NULL,
UserId UNIQUEIDENTIFIER NOT NULL,
-- 核心欄位 (您的算法需要)
SuccessCount INT DEFAULT 0, -- 連續成功次數 (用於2^n計算)
NextReviewDate DATETIME2, -- 下次複習時間
LastReviewDate DATETIME2, -- 最後複習時間
-- 創建和更新時間
CreatedAt DATETIME2 DEFAULT GETUTCDATE(),
UpdatedAt DATETIME2 DEFAULT GETUTCDATE(),
FOREIGN KEY (FlashcardId) REFERENCES Flashcards(Id),
UNIQUE(FlashcardId, UserId)
)
-- 就這樣!不需要複雜的Sessions和Attempts表
-- 前端延遲計數在前端處理,不需要後端記錄
🎯 業務邏輯服務
極簡ReviewService
// 簡化的介面 (只保留核心功能)
public interface IReviewService
{
Task<ApiResponse<FlashcardDto[]>> GetDueFlashcardsAsync(string userId, int limit = 10);
Task<ApiResponse<ReviewResult>> UpdateReviewStatusAsync(string flashcardId, bool isCorrect, string userId);
}
// 極簡實作 (專注核心邏輯)
public class ReviewService : IReviewService
{
public async Task<ApiResponse<ReviewResult>> UpdateReviewStatusAsync(
string flashcardId,
bool isCorrect,
string userId)
{
// 1. 獲取或創建複習記錄
var review = await GetOrCreateReviewAsync(flashcardId, userId);
// 2. 更新成功次數 (您的核心算法)
if (isCorrect)
{
review.SuccessCount++;
}
else
{
review.SuccessCount = 0; // 重置
}
// 3. 計算下次複習時間 (您的公式)
if (isCorrect)
{
var intervalDays = Math.Pow(2, review.SuccessCount);
review.NextReviewDate = DateTime.UtcNow.AddDays(intervalDays);
}
else
{
review.NextReviewDate = DateTime.UtcNow.AddDays(1); // 明天再試
}
review.LastReviewDate = DateTime.UtcNow;
review.UpdatedAt = DateTime.UtcNow;
await _context.SaveChangesAsync();
return new ApiResponse<ReviewResult>
{
Success = true,
Data = new ReviewResult
{
FlashcardId = flashcardId,
SuccessCount = review.SuccessCount,
NextReviewDate = review.NextReviewDate,
IntervalDays = (int)Math.Pow(2, review.SuccessCount)
}
};
}
// 獲取到期詞卡 (簡化邏輯)
public async Task<ApiResponse<FlashcardDto[]>> GetDueFlashcardsAsync(string userId, int limit = 10)
{
var dueCards = await _context.Flashcards
.Where(f => f.UserId == userId)
.LeftJoin(_context.FlashcardReviews,
f => f.Id,
r => r.FlashcardId,
(flashcard, review) => new { flashcard, review })
.Where(x => x.review == null || x.review.NextReviewDate <= DateTime.UtcNow)
.Select(x => x.flashcard)
.Take(limit)
.ToArrayAsync();
return new ApiResponse<FlashcardDto[]>
{
Success = true,
Data = dueCards.Select(MapToDto).ToArray()
};
}
}
📊 階段性實作計劃
階段1: 純前端 (當前)
實作範圍: 無後端開發
前端邏輯: 延遲計數系統、排序邏輯都在React中
數據來源: api_seeds.json 靜態文件
階段2: 本地持久化
實作範圍: 仍無後端開發
前端增強: localStorage 保存學習進度
觸發條件: 用戶要求保存進度
階段3: 基礎API
實作範圍: 極簡後端API
核心功能:
- GET /api/flashcards/due (獲取到期詞卡)
- POST /api/flashcards/{id}/review (更新複習狀態)
- 簡單的NextReviewDate計算
避免功能:
- 詳細的統計分析
- 複雜的會話管理
- 用戶行為追蹤
🔐 安全性規格
身份驗證和授權
// JWT Token 驗證
[Authorize]
[Route("api/flashcards")]
public class FlashcardsController : ControllerBase
{
// 確保用戶只能存取自己的數據
private async Task<bool> ValidateUserAccessAsync(string flashcardId)
{
var userId = User.GetUserId();
return await _flashcardService.BelongsToUserAsync(flashcardId, userId);
}
}
// 資料驗證
public class ReviewAttemptRequest
{
[Range(1, 3)]
public int Confidence { get; set; }
[Range(0, 300000)] // 最多5分鐘
public int ResponseTimeMs { get; set; }
[Range(0, int.MaxValue)]
public int SkipCount { get; set; }
[Range(0, int.MaxValue)]
public int WrongCount { get; set; }
}
資料保護
// 個人資料保護
public class PrivacyService
{
// 匿名化統計數據
public async Task<AnonymizedStats> GetAnonymizedStatsAsync()
{
// 移除個人識別信息的統計
}
// 資料導出 (GDPR)
public async Task<UserDataExport> ExportUserDataAsync(string userId)
{
return new UserDataExport
{
ReviewSessions = await GetUserReviewSessions(userId),
Flashcards = await GetUserFlashcards(userId),
LearningStats = await GetUserStats(userId)
};
}
// 資料刪除
public async Task DeleteUserDataAsync(string userId)
{
// 完全刪除用戶所有學習數據
}
}
⚡ 性能優化規格
資料庫優化
-- 重要索引
CREATE INDEX IX_ReviewAttempts_FlashcardId_UserId
ON ReviewAttempts (FlashcardId, UserId)
CREATE INDEX IX_FlashcardReviewStats_UserId_NextReviewDate
ON FlashcardReviewStats (UserId, NextReviewDate)
CREATE INDEX IX_ReviewSessions_UserId_StartTime
ON ReviewSessions (UserId, StartTime)
API性能目標
// 性能指標
public static class PerformanceTargets
{
public const int GetDueCardsMaxMs = 500; // 獲取卡片 < 500ms
public const int RecordAttemptMaxMs = 200; // 記錄結果 < 200ms
public const int DatabaseQueryMaxMs = 100; // 資料庫查詢 < 100ms
}
// 快取策略
[ResponseCache(Duration = 300)] // 5分鐘快取
public async Task<ActionResult<ApiResponse<DueCardsResult>>> GetDueFlashcards()
{
// 實作快取邏輯
}
📊 階段性實作計劃
階段1: 靜態數據 (已完成)
前端: 使用 api_seeds.json
後端: 無需開發
目的: 驗證前端邏輯
階段2: 基礎API (階段3觸發時)
實作範圍:
✅ GET /api/flashcards/due (基礎版)
✅ POST /api/flashcards/{id}/review (簡化版)
✅ 基礎的錯誤處理
不實作:
❌ 複雜的快取機制
❌ 高級統計分析
❌ 複雜的權限控制
階段3: 完整API (遠期)
實作範圍:
✅ 完整的用戶權限驗證
✅ 詳細的學習統計
✅ 系統監控和分析
✅ 性能優化和快取
🔧 開發環境設置
API開發工具
// Swagger文檔配置
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo
{
Title = "DramaLing Review API",
Version = "v1"
});
// 加入JWT驗證
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
In = ParameterLocation.Header,
Description = "Please enter JWT token",
Name = "Authorization",
Type = SecuritySchemeType.Http,
BearerFormat = "JWT",
Scheme = "bearer"
});
});
測試環境
// 測試資料庫
public class TestDbContext : DbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseInMemoryDatabase("TestReviewDb");
}
}
// API測試
[TestClass]
public class ReviewApiTests
{
[TestMethod]
public async Task GetDueCards_ShouldReturnUserCards()
{
// 準備測試數據
// 調用API
// 驗證結果
}
}
📋 部署和監控規格
部署配置
# Docker配置
services:
review-api:
image: dramaling/review-api:latest
environment:
- ConnectionStrings__DefaultConnection=${DB_CONNECTION}
- JwtSettings__Secret=${JWT_SECRET}
ports:
- "5008:8080"
depends_on:
- database
database:
image: postgres:15
environment:
- POSTGRES_DB=dramaling_review
- POSTGRES_USER=${DB_USER}
- POSTGRES_PASSWORD=${DB_PASSWORD}
監控指標
// 健康檢查
services.AddHealthChecks()
.AddDbContextCheck<ApplicationDbContext>()
.AddCheck<ApiHealthCheck>("api-health");
// 性能監控
public class PerformanceMiddleware
{
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
var stopwatch = Stopwatch.StartNew();
await next(context);
stopwatch.Stop();
// 記錄慢查詢
if (stopwatch.ElapsedMilliseconds > 1000)
{
_logger.LogWarning($"Slow request: {context.Request.Path} took {stopwatch.ElapsedMilliseconds}ms");
}
}
}
🚨 錯誤處理規格
統一錯誤回應
public class ApiResponse<T>
{
public bool Success { get; set; }
public T? Data { get; set; }
public string? Message { get; set; }
public string? Error { get; set; }
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
}
// 錯誤類型
public enum ErrorType
{
Validation, // 資料驗證錯誤
NotFound, // 資源不存在
Unauthorized, // 權限不足
RateLimit, // 請求頻率限制
ServerError // 服務器錯誤
}
錯誤處理中介軟體
public class ErrorHandlingMiddleware
{
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
try
{
await next(context);
}
catch (ValidationException ex)
{
await HandleValidationErrorAsync(context, ex);
}
catch (UnauthorizedAccessException ex)
{
await HandleUnauthorizedErrorAsync(context, ex);
}
catch (Exception ex)
{
await HandleGenericErrorAsync(context, ex);
}
}
}
後端規格維護: 後端開發團隊 實作觸發: 階段3 API集成需求確認時 目標: 支持前端複習功能的後端服務