dramaling-vocab-learning/note/複習系統/後端規格.md

657 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 複習系統後端規格書
**版本**: 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集成 (遠期需求)**
**獲取複習卡片**:
```http
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"
}
```
**記錄複習結果**:
```http
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"
}
```
---
## ⏰ **核心算法: 間隔重複系統**
### **複習時間計算公式** (您的需求)
```csharp
// 基礎公式: 下一次複習時間 = 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);
}
```
### **具體計算範例**
```csharp
// 學習進度示例
成功次數 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 (上限)
```
### **成功計數更新邏輯**
```csharp
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)
};
}
```
### **與前端延遲計數的配合**
```csharp
// 前端延遲計數系統的後端記錄
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;
}
```
---
## 🗃️ **數據庫設計**
### **現有表格** (已存在)
```sql
-- 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需要時)
```sql
-- 只需要一個簡單的複習記錄表
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**
```csharp
// 簡化的介面 (只保留核心功能)
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計算
避免功能:
- 詳細的統計分析
- 複雜的會話管理
- 用戶行為追蹤
```
---
## 🔐 **安全性規格**
### **身份驗證和授權**
```csharp
// 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; }
}
```
### **資料保護**
```csharp
// 個人資料保護
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)
{
// 完全刪除用戶所有學習數據
}
}
```
---
## ⚡ **性能優化規格**
### **資料庫優化**
```sql
-- 重要索引
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性能目標**
```csharp
// 性能指標
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開發工具**
```csharp
// 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"
});
});
```
### **測試環境**
```csharp
// 測試資料庫
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
// 驗證結果
}
}
```
---
## 📋 **部署和監控規格**
### **部署配置**
```yaml
# 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}
```
### **監控指標**
```csharp
// 健康檢查
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");
}
}
}
```
---
## 🚨 **錯誤處理規格**
### **統一錯誤回應**
```csharp
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 // 服務器錯誤
}
```
### **錯誤處理中介軟體**
```csharp
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集成需求確認時*
*目標: 支持前端複習功能的後端服務*