590 lines
16 KiB
Markdown
590 lines
16 KiB
Markdown
# DramaLing 後端架構詳細說明
|
|
|
|
## 1. 技術棧概覽
|
|
|
|
### 1.1 核心技術
|
|
- **框架**: ASP.NET Core 8.0
|
|
- **語言**: C# .NET 8
|
|
- **ORM**: Entity Framework Core 8.0
|
|
- **資料庫**: SQLite 3.x
|
|
- **認證**: JWT Bearer Token
|
|
- **依賴注入**: Microsoft.Extensions.DependencyInjection
|
|
|
|
### 1.2 專案結構
|
|
|
|
```
|
|
backend/DramaLing.Api/
|
|
├── Controllers/ # API 控制器
|
|
│ ├── FlashcardsController.cs
|
|
│ ├── AIController.cs
|
|
│ └── AuthController.cs
|
|
├── Models/
|
|
│ ├── Entities/ # 資料模型
|
|
│ │ ├── Flashcard.cs
|
|
│ │ ├── User.cs
|
|
│ │ └── CardSet.cs
|
|
│ ├── DTOs/ # 資料傳輸物件
|
|
│ └── Configuration/ # 配置模型
|
|
├── Data/ # 資料存取層
|
|
│ ├── DramaLingDbContext.cs
|
|
│ └── Migrations/
|
|
├── Services/ # 業務邏輯層
|
|
│ ├── AI/ # AI 服務
|
|
│ ├── Caching/ # 快取服務
|
|
│ └── AuthService.cs
|
|
├── Extensions/ # 擴展方法
|
|
│ └── ServiceCollectionExtensions.cs
|
|
└── Program.cs # 應用程式入口
|
|
```
|
|
|
|
## 2. 資料模型架構
|
|
|
|
### 2.1 詞卡實體模型 (Flashcard)
|
|
|
|
```csharp
|
|
public class Flashcard
|
|
{
|
|
// 主鍵和關聯
|
|
public Guid Id { get; set; }
|
|
public Guid UserId { get; set; }
|
|
public Guid? CardSetId { get; set; }
|
|
|
|
// 詞卡內容
|
|
[Required, MaxLength(255)]
|
|
public string Word { get; set; }
|
|
[Required]
|
|
public string Translation { get; set; }
|
|
[Required]
|
|
public string Definition { get; set; }
|
|
[MaxLength(50)]
|
|
public string? PartOfSpeech { get; set; }
|
|
[MaxLength(255)]
|
|
public string? Pronunciation { get; set; }
|
|
public string? Example { get; set; }
|
|
public string? ExampleTranslation { get; set; }
|
|
|
|
// SM-2 學習算法參數
|
|
public float EasinessFactor { get; set; } = 2.5f;
|
|
public int Repetitions { get; set; } = 0;
|
|
public int IntervalDays { get; set; } = 1;
|
|
public DateTime NextReviewDate { get; set; }
|
|
|
|
// 學習統計
|
|
[Range(0, 100)]
|
|
public int MasteryLevel { get; set; } = 0;
|
|
public int TimesReviewed { get; set; } = 0;
|
|
public int TimesCorrect { get; set; } = 0;
|
|
public DateTime? LastReviewedAt { get; set; }
|
|
|
|
// 狀態管理
|
|
public bool IsFavorite { get; set; } = false;
|
|
public bool IsArchived { get; set; } = false;
|
|
[MaxLength(10)]
|
|
public string? DifficultyLevel { get; set; } // A1-C2
|
|
|
|
// 時間戳記
|
|
public DateTime CreatedAt { get; set; }
|
|
public DateTime UpdatedAt { get; set; }
|
|
|
|
// 導航屬性
|
|
public virtual User User { get; set; }
|
|
public virtual CardSet? CardSet { get; set; }
|
|
public virtual ICollection<StudyRecord> StudyRecords { get; set; }
|
|
public virtual ICollection<FlashcardTag> FlashcardTags { get; set; }
|
|
public virtual ICollection<ErrorReport> ErrorReports { get; set; }
|
|
}
|
|
```
|
|
|
|
### 2.2 資料庫關聯設計
|
|
|
|
```
|
|
Users (1) ──────────────── (*) Flashcards
|
|
│ │
|
|
│ │ (*)
|
|
│ │
|
|
└─── (1) CardSets (*) ───────┘
|
|
|
|
StudyRecords (*) ──── (1) Flashcards
|
|
ErrorReports (*) ──── (1) Flashcards
|
|
FlashcardTags (*) ─── (1) Flashcards
|
|
```
|
|
|
|
## 3. API 架構設計
|
|
|
|
### 3.1 控制器架構
|
|
|
|
#### FlashcardsController.cs
|
|
```csharp
|
|
[ApiController]
|
|
[Route("api/flashcards")]
|
|
[AllowAnonymous] // 開發階段暫時移除認證
|
|
public class FlashcardsController : ControllerBase
|
|
{
|
|
private readonly DramaLingDbContext _context;
|
|
private readonly ILogger<FlashcardsController> _logger;
|
|
|
|
// 標準 RESTful API 端點
|
|
[HttpGet] // GET /api/flashcards
|
|
[HttpGet("{id}")] // GET /api/flashcards/{id}
|
|
[HttpPost] // POST /api/flashcards
|
|
[HttpPut("{id}")] // PUT /api/flashcards/{id}
|
|
[HttpDelete("{id}")] // DELETE /api/flashcards/{id}
|
|
[HttpPost("{id}/favorite")] // POST /api/flashcards/{id}/favorite
|
|
}
|
|
```
|
|
|
|
### 3.2 API 回應格式標準化
|
|
|
|
#### 成功回應格式
|
|
```json
|
|
{
|
|
"success": true,
|
|
"data": {
|
|
"flashcards": [...],
|
|
"count": 42
|
|
},
|
|
"message": "操作成功"
|
|
}
|
|
```
|
|
|
|
#### 錯誤回應格式
|
|
```json
|
|
{
|
|
"success": false,
|
|
"error": "錯誤描述",
|
|
"details": "詳細錯誤信息",
|
|
"timestamp": "2025-09-24T10:30:00Z"
|
|
}
|
|
```
|
|
|
|
### 3.3 查詢參數支援
|
|
|
|
#### GET /api/flashcards
|
|
```csharp
|
|
public async Task<ActionResult> GetFlashcards(
|
|
[FromQuery] string? search = null, // 搜尋關鍵字
|
|
[FromQuery] bool favoritesOnly = false // 僅收藏詞卡
|
|
)
|
|
```
|
|
|
|
## 4. 服務層架構
|
|
|
|
### 4.1 依賴注入配置 (ServiceCollectionExtensions.cs)
|
|
|
|
```csharp
|
|
public static class ServiceCollectionExtensions
|
|
{
|
|
// 資料庫服務配置
|
|
public static IServiceCollection AddDatabaseServices(...)
|
|
|
|
// Repository 服務配置
|
|
public static IServiceCollection AddRepositoryServices(...)
|
|
|
|
// 快取服務配置
|
|
public static IServiceCollection AddCachingServices(...)
|
|
|
|
// AI 服務配置
|
|
public static IServiceCollection AddAIServices(...)
|
|
|
|
// 業務服務配置
|
|
public static IServiceCollection AddBusinessServices(...)
|
|
|
|
// 認證服務配置
|
|
public static IServiceCollection AddAuthenticationServices(...)
|
|
|
|
// CORS 政策配置
|
|
public static IServiceCollection AddCorsServices(...)
|
|
}
|
|
```
|
|
|
|
### 4.2 業務服務層
|
|
|
|
#### 已實現的服務
|
|
```csharp
|
|
// 認證服務
|
|
services.AddScoped<IAuthService, AuthService>();
|
|
|
|
// 使用量追蹤
|
|
services.AddScoped<IUsageTrackingService, UsageTrackingService>();
|
|
|
|
// Azure 語音服務
|
|
services.AddScoped<IAzureSpeechService, AzureSpeechService>();
|
|
|
|
// 音頻快取
|
|
services.AddScoped<IAudioCacheService, AudioCacheService>();
|
|
|
|
// AI 提供商管理
|
|
services.AddScoped<IAIProviderManager, AIProviderManager>();
|
|
services.AddScoped<IAIProvider, GeminiAIProvider>();
|
|
```
|
|
|
|
## 5. 資料存取層
|
|
|
|
### 5.1 DbContext 配置
|
|
|
|
```csharp
|
|
public class DramaLingDbContext : DbContext
|
|
{
|
|
public DbSet<User> Users { get; set; }
|
|
public DbSet<Flashcard> Flashcards { get; set; }
|
|
public DbSet<CardSet> CardSets { get; set; }
|
|
public DbSet<StudyRecord> StudyRecords { get; set; }
|
|
public DbSet<ErrorReport> ErrorReports { get; set; }
|
|
|
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
|
{
|
|
// 詞卡配置
|
|
modelBuilder.Entity<Flashcard>(entity =>
|
|
{
|
|
entity.HasKey(e => e.Id);
|
|
entity.Property(e => e.Word).IsRequired().HasMaxLength(255);
|
|
entity.Property(e => e.Translation).IsRequired();
|
|
entity.Property(e => e.Definition).IsRequired();
|
|
|
|
// 關聯配置
|
|
entity.HasOne(f => f.User)
|
|
.WithMany(u => u.Flashcards)
|
|
.HasForeignKey(f => f.UserId);
|
|
|
|
entity.HasOne(f => f.CardSet)
|
|
.WithMany(cs => cs.Flashcards)
|
|
.HasForeignKey(f => f.CardSetId)
|
|
.IsRequired(false); // CardSetId 可為空
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
### 5.2 資料庫連接配置
|
|
|
|
#### 開發環境
|
|
```csharp
|
|
// 環境變數或配置檔案
|
|
var connectionString = Environment.GetEnvironmentVariable("DRAMALING_DB_CONNECTION")
|
|
?? configuration.GetConnectionString("DefaultConnection")
|
|
?? "Data Source=dramaling_test.db";
|
|
|
|
services.AddDbContext<DramaLingDbContext>(options =>
|
|
options.UseSqlite(connectionString));
|
|
```
|
|
|
|
#### 記憶體資料庫 (測試用)
|
|
```csharp
|
|
var useInMemoryDb = Environment.GetEnvironmentVariable("USE_INMEMORY_DB") == "true";
|
|
if (useInMemoryDb)
|
|
{
|
|
services.AddDbContext<DramaLingDbContext>(options =>
|
|
options.UseSqlite("Data Source=:memory:"));
|
|
}
|
|
```
|
|
|
|
## 6. 認證與授權
|
|
|
|
### 6.1 JWT 配置
|
|
|
|
```csharp
|
|
public static IServiceCollection AddAuthenticationServices(...)
|
|
{
|
|
var supabaseUrl = Environment.GetEnvironmentVariable("DRAMALING_SUPABASE_URL")
|
|
?? "https://localhost";
|
|
var jwtSecret = Environment.GetEnvironmentVariable("DRAMALING_SUPABASE_JWT_SECRET")
|
|
?? "dev-secret-minimum-32-characters-long-for-jwt-signing-in-development-mode-only";
|
|
|
|
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
|
.AddJwtBearer(options =>
|
|
{
|
|
options.TokenValidationParameters = new TokenValidationParameters
|
|
{
|
|
ValidateIssuer = true,
|
|
ValidateAudience = true,
|
|
ValidateLifetime = true,
|
|
ValidateIssuerSigningKey = true,
|
|
ValidIssuer = supabaseUrl,
|
|
ValidAudience = "authenticated",
|
|
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSecret))
|
|
};
|
|
});
|
|
}
|
|
```
|
|
|
|
### 6.2 開發階段認證處理
|
|
|
|
```csharp
|
|
// 暫時移除認證要求,使用固定測試用戶
|
|
private Guid GetUserId()
|
|
{
|
|
return Guid.Parse("00000000-0000-0000-0000-000000000001");
|
|
|
|
// 生產環境將啟用:
|
|
// var userIdString = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
|
// if (Guid.TryParse(userIdString, out var userId))
|
|
// return userId;
|
|
// throw new UnauthorizedAccessException("Invalid user ID in token");
|
|
}
|
|
```
|
|
|
|
## 7. CORS 設定
|
|
|
|
### 7.1 跨域政策配置
|
|
|
|
```csharp
|
|
services.AddCors(options =>
|
|
{
|
|
options.AddPolicy("AllowFrontend", policy =>
|
|
{
|
|
policy.WithOrigins("http://localhost:3000", "http://localhost:3001", "http://localhost:3002")
|
|
.AllowAnyHeader()
|
|
.AllowAnyMethod()
|
|
.AllowCredentials()
|
|
.SetPreflightMaxAge(TimeSpan.FromMinutes(5));
|
|
});
|
|
|
|
options.AddPolicy("AllowAll", policy =>
|
|
{
|
|
policy.AllowAnyOrigin()
|
|
.AllowAnyHeader()
|
|
.AllowAnyMethod();
|
|
});
|
|
});
|
|
```
|
|
|
|
## 8. AI 服務整合
|
|
|
|
### 8.1 AI 提供商架構
|
|
|
|
```csharp
|
|
// AI 提供商介面
|
|
public interface IAIProvider
|
|
{
|
|
Task<SentenceAnalysisResult> AnalyzeSentenceAsync(string inputText, AnalysisOptions options);
|
|
}
|
|
|
|
// Gemini AI 實作
|
|
public class GeminiAIProvider : IAIProvider
|
|
{
|
|
private readonly HttpClient _httpClient;
|
|
private readonly GeminiOptions _options;
|
|
|
|
public async Task<SentenceAnalysisResult> AnalyzeSentenceAsync(...)
|
|
{
|
|
// 調用 Google Gemini API
|
|
// 處理回應和錯誤
|
|
// 返回標準化結果
|
|
}
|
|
}
|
|
```
|
|
|
|
### 8.2 AI 服務配置
|
|
|
|
```csharp
|
|
// 強型別配置
|
|
services.Configure<GeminiOptions>(configuration.GetSection(GeminiOptions.SectionName));
|
|
services.AddSingleton<IValidateOptions<GeminiOptions>, GeminiOptionsValidator>();
|
|
|
|
// AI 服務註冊
|
|
services.AddHttpClient<GeminiAIProvider>();
|
|
services.AddScoped<IAIProvider, GeminiAIProvider>();
|
|
services.AddScoped<IAIProviderManager, AIProviderManager>();
|
|
```
|
|
|
|
## 9. 錯誤處理架構
|
|
|
|
### 9.1 全域異常處理
|
|
|
|
```csharp
|
|
app.UseExceptionHandler(errorApp =>
|
|
{
|
|
errorApp.Run(async context =>
|
|
{
|
|
var errorFeature = context.Features.Get<IExceptionHandlerFeature>();
|
|
if (errorFeature != null)
|
|
{
|
|
var logger = context.RequestServices.GetRequiredService<ILogger<Program>>();
|
|
logger.LogError(errorFeature.Error, "Unhandled exception occurred");
|
|
|
|
context.Response.StatusCode = 500;
|
|
context.Response.ContentType = "application/json";
|
|
|
|
var response = new
|
|
{
|
|
success = false,
|
|
error = "Internal server error",
|
|
timestamp = DateTime.UtcNow
|
|
};
|
|
|
|
await context.Response.WriteAsync(JsonSerializer.Serialize(response));
|
|
}
|
|
});
|
|
});
|
|
```
|
|
|
|
### 9.2 控制器級錯誤處理
|
|
|
|
```csharp
|
|
try
|
|
{
|
|
var result = await flashcardsService.CreateFlashcard(data);
|
|
return Ok(new { success = true, data = result });
|
|
}
|
|
catch (ValidationException ex)
|
|
{
|
|
return BadRequest(new { success = false, error = ex.Message });
|
|
}
|
|
catch (DbUpdateException ex)
|
|
{
|
|
_logger.LogError(ex, "Database error during flashcard creation");
|
|
return StatusCode(500, new { success = false, error = "Database operation failed" });
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Unexpected error during flashcard creation");
|
|
return StatusCode(500, new { success = false, error = "Internal server error" });
|
|
}
|
|
```
|
|
|
|
## 10. 開發與部署
|
|
|
|
### 10.1 開發環境設定
|
|
|
|
#### 啟動開發伺服器
|
|
```bash
|
|
cd backend
|
|
dotnet run --project DramaLing.Api
|
|
|
|
# 伺服器運行於: http://localhost:5008
|
|
# Swagger UI: http://localhost:5008/swagger
|
|
```
|
|
|
|
#### 環境變數設定
|
|
```bash
|
|
export DRAMALING_DB_CONNECTION="Data Source=dramaling_test.db"
|
|
export DRAMALING_SUPABASE_URL="https://localhost"
|
|
export DRAMALING_SUPABASE_JWT_SECRET="dev-secret-minimum-32-characters-long-for-jwt-signing-in-development-mode-only"
|
|
export USE_INMEMORY_DB="false"
|
|
```
|
|
|
|
### 10.2 資料庫管理
|
|
|
|
#### Entity Framework 遷移
|
|
```bash
|
|
# 新增遷移
|
|
dotnet ef migrations add MigrationName
|
|
|
|
# 更新資料庫
|
|
dotnet ef database update
|
|
|
|
# 查看遷移狀態
|
|
dotnet ef migrations list
|
|
```
|
|
|
|
#### 測試資料初始化
|
|
```csharp
|
|
// 自動創建測試用戶
|
|
var testUser = await _context.Users.FirstOrDefaultAsync(u => u.Id == userId);
|
|
if (testUser == null)
|
|
{
|
|
testUser = new User
|
|
{
|
|
Id = userId,
|
|
Email = "test@dramaling.com",
|
|
Name = "Test User",
|
|
CreatedAt = DateTime.UtcNow
|
|
};
|
|
_context.Users.Add(testUser);
|
|
await _context.SaveChangesAsync();
|
|
}
|
|
```
|
|
|
|
## 11. 效能優化
|
|
|
|
### 11.1 查詢優化
|
|
```csharp
|
|
// 使用 AsNoTracking 提升查詢效能
|
|
var flashcards = await _context.Flashcards
|
|
.AsNoTracking()
|
|
.Where(f => f.UserId == userId)
|
|
.OrderByDescending(f => f.CreatedAt)
|
|
.ToListAsync();
|
|
|
|
// 避免 N+1 查詢問題
|
|
var flashcardsWithDetails = await _context.Flashcards
|
|
.Include(f => f.StudyRecords)
|
|
.Include(f => f.CardSet)
|
|
.Where(f => f.UserId == userId)
|
|
.ToListAsync();
|
|
```
|
|
|
|
### 11.2 快取策略
|
|
```csharp
|
|
// 記憶體快取服務
|
|
services.AddMemoryCache();
|
|
services.AddScoped<ICacheService, HybridCacheService>();
|
|
|
|
// 快取使用範例
|
|
var cacheKey = $"flashcards:user:{userId}";
|
|
var cachedCards = await _cacheService.GetAsync<List<Flashcard>>(cacheKey);
|
|
if (cachedCards == null)
|
|
{
|
|
cachedCards = await LoadFlashcardsFromDatabase(userId);
|
|
await _cacheService.SetAsync(cacheKey, cachedCards, TimeSpan.FromMinutes(30));
|
|
}
|
|
```
|
|
|
|
## 12. 安全性措施
|
|
|
|
### 12.1 輸入驗證
|
|
```csharp
|
|
// 模型驗證特性
|
|
[Required, MaxLength(255)]
|
|
public string Word { get; set; }
|
|
|
|
// 控制器層驗證
|
|
if (!ModelState.IsValid)
|
|
{
|
|
return BadRequest(ModelState);
|
|
}
|
|
```
|
|
|
|
### 12.2 SQL 注入防護
|
|
```csharp
|
|
// Entity Framework 自動參數化查詢
|
|
var flashcards = _context.Flashcards
|
|
.Where(f => f.Word.Contains(searchTerm)) // 自動參數化
|
|
.ToList();
|
|
```
|
|
|
|
### 12.3 XSS 防護
|
|
```csharp
|
|
// 自動 HTML 編碼
|
|
public string Definition { get; set; } // EF Core 自動處理
|
|
```
|
|
|
|
## 13. 監控與日誌
|
|
|
|
### 13.1 結構化日誌
|
|
```csharp
|
|
_logger.LogInformation("Creating flashcard for user {UserId}, word: {Word}",
|
|
userId, request.Word);
|
|
|
|
_logger.LogError(ex, "Failed to create flashcard for user {UserId}", userId);
|
|
```
|
|
|
|
### 13.2 健康檢查
|
|
```csharp
|
|
services.AddHealthChecks()
|
|
.AddDbContextCheck<DramaLingDbContext>();
|
|
|
|
app.MapHealthChecks("/health");
|
|
```
|
|
|
|
---
|
|
|
|
**文檔版本**: v1.0
|
|
**建立日期**: 2025-09-24
|
|
**維護負責**: 後端開發團隊
|
|
**下次審核**: 架構變更時
|
|
|
|
> 📋 相關文檔:
|
|
> - [系統架構總覽](./system-architecture.md)
|
|
> - [前端架構詳細說明](./frontend-architecture.md)
|
|
> - [詞卡 API 規格](./flashcard-api-specification.md) |