dramaling-vocab-learning/docs/04_technical/backend-architecture.md

16 KiB

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)

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

[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 回應格式標準化

成功回應格式

{
  "success": true,
  "data": {
    "flashcards": [...],
    "count": 42
  },
  "message": "操作成功"
}

錯誤回應格式

{
  "success": false,
  "error": "錯誤描述",
  "details": "詳細錯誤信息",
  "timestamp": "2025-09-24T10:30:00Z"
}

3.3 查詢參數支援

GET /api/flashcards

public async Task<ActionResult> GetFlashcards(
    [FromQuery] string? search = null,      // 搜尋關鍵字
    [FromQuery] bool favoritesOnly = false  // 僅收藏詞卡
)

4. 服務層架構

4.1 依賴注入配置 (ServiceCollectionExtensions.cs)

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 業務服務層

已實現的服務

// 認證服務
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 配置

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 資料庫連接配置

開發環境

// 環境變數或配置檔案
var connectionString = Environment.GetEnvironmentVariable("DRAMALING_DB_CONNECTION")
    ?? configuration.GetConnectionString("DefaultConnection")
    ?? "Data Source=dramaling_test.db";

services.AddDbContext<DramaLingDbContext>(options =>
    options.UseSqlite(connectionString));

記憶體資料庫 (測試用)

var useInMemoryDb = Environment.GetEnvironmentVariable("USE_INMEMORY_DB") == "true";
if (useInMemoryDb)
{
    services.AddDbContext<DramaLingDbContext>(options =>
        options.UseSqlite("Data Source=:memory:"));
}

6. 認證與授權

6.1 JWT 配置

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 開發階段認證處理

// 暫時移除認證要求,使用固定測試用戶
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 跨域政策配置

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 提供商架構

// 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 服務配置

// 強型別配置
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 全域異常處理

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 控制器級錯誤處理

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 開發環境設定

啟動開發伺服器

cd backend
dotnet run --project DramaLing.Api

# 伺服器運行於: http://localhost:5008
# Swagger UI: http://localhost:5008/swagger

環境變數設定

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 遷移

# 新增遷移
dotnet ef migrations add MigrationName

# 更新資料庫
dotnet ef database update

# 查看遷移狀態
dotnet ef migrations list

測試資料初始化

// 自動創建測試用戶
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 查詢優化

// 使用 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 快取策略

// 記憶體快取服務
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 輸入驗證

// 模型驗證特性
[Required, MaxLength(255)]
public string Word { get; set; }

// 控制器層驗證
if (!ModelState.IsValid)
{
    return BadRequest(ModelState);
}

12.2 SQL 注入防護

// Entity Framework 自動參數化查詢
var flashcards = _context.Flashcards
    .Where(f => f.Word.Contains(searchTerm)) // 自動參數化
    .ToList();

12.3 XSS 防護

// 自動 HTML 編碼
public string Definition { get; set; } // EF Core 自動處理

13. 監控與日誌

13.1 結構化日誌

_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 健康檢查

services.AddHealthChecks()
    .AddDbContextCheck<DramaLingDbContext>();

app.MapHealthChecks("/health");

文檔版本: v1.0 建立日期: 2025-09-24 維護負責: 後端開發團隊 下次審核: 架構變更時

📋 相關文檔: