14 KiB
14 KiB
DramaLing API 開發指南
版本: 1.0 最後更新: 2025-09-30 適用對象: 後端開發者、新團隊成員
🚀 快速開始
開發環境要求
- .NET 8 SDK (最新 LTS 版本)
- Visual Studio Code 或 Visual Studio 2022
- Git 版本控制
- SQLite (開發環境) / SQL Server (生產環境)
環境變數配置
建立 .env 檔案或設定系統環境變數:
# Gemini AI 配置
DRAMALING_GEMINI_API_KEY=your-gemini-api-key
# Supabase 認證配置
DRAMALING_SUPABASE_URL=your-supabase-url
DRAMALING_SUPABASE_JWT_SECRET=your-jwt-secret
# Replicate AI 配置
DRAMALING_REPLICATE_API_TOKEN=your-replicate-token
# Azure Speech 配置
DRAMALING_AZURE_SPEECH_KEY=your-azure-speech-key
DRAMALING_AZURE_SPEECH_REGION=your-region
# 資料庫連接 (可選,預設使用 SQLite)
DRAMALING_DB_CONNECTION=your-connection-string
# 測試環境
USE_INMEMORY_DB=true # 測試時使用記憶體資料庫
🏗️ 開發工作流程
1. 專案設定
# Clone 專案
git clone <repository-url>
cd dramaling-vocab-learning/backend/DramaLing.Api
# 安裝相依套件
dotnet restore
# 執行資料庫遷移
dotnet ef database update
# 啟動開發伺服器
dotnet run
# 訪問 Swagger UI
open https://localhost:7001/swagger
2. 開發分支策略
# 主要分支
main # 生產環境代碼
develop # 開發整合分支
# 功能分支命名規則
feature/user-auth # 新功能開發
bugfix/cache-issue # Bug 修復
hotfix/security-patch # 緊急修復
refactor/clean-arch # 重構改善
📝 編碼規範
1. C# 編碼標準
命名規則:
// 類別和介面 - PascalCase
public class FlashcardService { }
public interface IFlashcardRepository { }
// 方法和屬性 - PascalCase
public async Task<Flashcard> GetByIdAsync(Guid id) { }
public string UserName { get; set; }
// 私有欄位 - camelCase with underscore
private readonly ILogger<FlashcardService> _logger;
// 參數和區域變數 - camelCase
public void ProcessData(string inputData)
{
var processedResult = Transform(inputData);
}
// 常數 - PascalCase
public const int MaxRetryCount = 3;
異步方法規範:
// ✅ 正確:異步方法使用 Async 後綴
public async Task<User> GetUserAsync(Guid userId) { }
// ✅ 正確:使用 ConfigureAwait(false) 在類別庫中
var result = await httpClient.GetAsync(url).ConfigureAwait(false);
// ❌ 錯誤:同步調用異步方法
var user = GetUserAsync(id).Result; // 可能導致死鎖
2. 資料夾和檔案組織
Services/Domain/Feature/
├── IFeatureService.cs # 介面定義
├── FeatureService.cs # 實現
├── FeatureModels.cs # 相關模型 (如果簡單)
└── README.md # 服務說明 (複雜功能)
Tests/Unit/Services/Domain/
└── FeatureServiceTests.cs # 對應測試
3. 文檔註解標準
/// <summary>
/// 根據使用者 ID 獲取單字卡列表
/// </summary>
/// <param name="userId">使用者唯一識別碼</param>
/// <param name="search">搜尋關鍵字,可為空</param>
/// <param name="favoritesOnly">是否只顯示收藏的單字卡</param>
/// <returns>符合條件的單字卡列表</returns>
/// <exception cref="ArgumentNullException">當 userId 為空時拋出</exception>
public async Task<IEnumerable<Flashcard>> GetByUserIdAsync(
Guid userId,
string? search = null,
bool favoritesOnly = false)
{
// 實作邏輯...
}
🧩 功能開發指南
1. 新增 API 端點
步驟 1: 定義 DTO
// Models/DTOs/Feature/
public class CreateFeatureRequest
{
[Required]
public string Name { get; set; } = string.Empty;
[StringLength(500)]
public string? Description { get; set; }
}
public class FeatureResponse
{
public Guid Id { get; set; }
public string Name { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; }
}
步驟 2: 建立 Repository (如需要)
// Repositories/IFeatureRepository.cs
public interface IFeatureRepository : IRepository<Feature>
{
Task<IEnumerable<Feature>> GetByUserIdAsync(Guid userId);
Task<Feature?> GetByNameAsync(string name);
}
// Repositories/FeatureRepository.cs
public class FeatureRepository : BaseRepository<Feature>, IFeatureRepository
{
public FeatureRepository(DramaLingDbContext context, ILogger<BaseRepository<Feature>> logger)
: base(context, logger) { }
public async Task<IEnumerable<Feature>> GetByUserIdAsync(Guid userId)
{
return await DbSet.Where(f => f.UserId == userId).ToListAsync();
}
}
步驟 3: 實作 Service
// Services/Domain/IFeatureService.cs
public interface IFeatureService
{
Task<FeatureResponse> CreateAsync(CreateFeatureRequest request);
Task<IEnumerable<FeatureResponse>> GetByUserIdAsync(Guid userId);
}
// Services/Domain/FeatureService.cs
public class FeatureService : IFeatureService
{
private readonly IFeatureRepository _repository;
private readonly ILogger<FeatureService> _logger;
public FeatureService(IFeatureRepository repository, ILogger<FeatureService> logger)
{
_repository = repository;
_logger = logger;
}
public async Task<FeatureResponse> CreateAsync(CreateFeatureRequest request)
{
var feature = new Feature
{
Name = request.Name,
Description = request.Description,
CreatedAt = DateTime.UtcNow
};
await _repository.AddAsync(feature);
return new FeatureResponse
{
Id = feature.Id,
Name = feature.Name,
CreatedAt = feature.CreatedAt
};
}
}
步驟 4: 建立 Controller
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class FeatureController : ControllerBase
{
private readonly IFeatureService _featureService;
public FeatureController(IFeatureService featureService)
{
_featureService = featureService;
}
/// <summary>
/// 建立新功能
/// </summary>
[HttpPost]
public async Task<ActionResult<FeatureResponse>> CreateFeature(CreateFeatureRequest request)
{
var result = await _featureService.CreateAsync(request);
return CreatedAtAction(nameof(GetFeature), new { id = result.Id }, result);
}
}
步驟 5: 註冊服務
// Extensions/ServiceCollectionExtensions.cs
public static IServiceCollection AddBusinessServices(this IServiceCollection services)
{
// ... 其他服務
services.AddScoped<IFeatureRepository, FeatureRepository>();
services.AddScoped<IFeatureService, FeatureService>();
return services;
}
2. 撰寫單元測試
// Tests/Unit/Services/FeatureServiceTests.cs
public class FeatureServiceTests : TestBase
{
private readonly IFeatureService _service;
private readonly Mock<IFeatureRepository> _mockRepository;
public FeatureServiceTests()
{
_mockRepository = new Mock<IFeatureRepository>();
_service = new FeatureService(_mockRepository.Object, Mock.Of<ILogger<FeatureService>>());
}
[Fact]
public async Task CreateAsync_ValidRequest_ShouldReturnFeatureResponse()
{
// Arrange
var request = new CreateFeatureRequest { Name = "Test Feature" };
var expectedFeature = new Feature { Id = Guid.NewGuid(), Name = "Test Feature" };
_mockRepository.Setup(r => r.AddAsync(It.IsAny<Feature>()))
.Returns(Task.CompletedTask)
.Callback<Feature>(f => f.Id = expectedFeature.Id);
// Act
var result = await _service.CreateAsync(request);
// Assert
result.Should().NotBeNull();
result.Name.Should().Be("Test Feature");
_mockRepository.Verify(r => r.AddAsync(It.IsAny<Feature>()), Times.Once);
}
}
🧪 測試策略
1. 測試分類
單元測試 - 隔離測試個別類別
[Fact]
[Trait("Category", "Unit")]
public async Task ServiceMethod_ValidInput_ReturnsExpectedResult()
{
// AAA 模式測試
}
整合測試 - 測試多個組件協作
[Fact]
[Trait("Category", "Integration")]
public async Task ApiEndpoint_ValidRequest_ReturnsCorrectResponse()
{
// 使用 TestServer 測試整個請求流程
}
端到端測試 - 完整使用者場景
[Fact]
[Trait("Category", "E2E")]
public async Task UserWorkflow_CompleteScenario_WorksCorrectly()
{
// 模擬真實使用者操作流程
}
2. 測試執行
# 執行所有測試
dotnet test
# 執行特定分類測試
dotnet test --filter "Category=Unit"
dotnet test --filter "Category=Integration"
# 產生覆蓋率報告
dotnet test --collect:"XPlat Code Coverage"
# 執行特定測試類別
dotnet test --filter "ClassName=FeatureServiceTests"
🐛 除錯與診斷
1. 日誌記錄最佳實務
public class FeatureService : IFeatureService
{
private readonly ILogger<FeatureService> _logger;
public async Task<Feature> ProcessFeatureAsync(Guid featureId)
{
_logger.LogInformation("開始處理功能 {FeatureId}", featureId);
try
{
var feature = await _repository.GetByIdAsync(featureId);
if (feature == null)
{
_logger.LogWarning("功能不存在 {FeatureId}", featureId);
throw new NotFoundException($"Feature {featureId} not found");
}
// 處理邏輯...
_logger.LogInformation("功能處理完成 {FeatureId}", featureId);
return feature;
}
catch (Exception ex)
{
_logger.LogError(ex, "處理功能時發生錯誤 {FeatureId}", featureId);
throw;
}
}
}
2. 效能監控
// 使用 Stopwatch 監控關鍵操作
using var activity = Activity.StartActivity("ProcessFeature");
var stopwatch = Stopwatch.StartNew();
try
{
// 業務邏輯執行
var result = await ProcessComplexOperation();
stopwatch.Stop();
_logger.LogInformation("操作完成,耗時 {ElapsedMs}ms", stopwatch.ElapsedMilliseconds);
return result;
}
catch (Exception ex)
{
stopwatch.Stop();
_logger.LogError(ex, "操作失敗,耗時 {ElapsedMs}ms", stopwatch.ElapsedMilliseconds);
throw;
}
3. 常見問題診斷
問題: 資料庫連接失敗
# 檢查連接字串
dotnet user-secrets list
# 測試資料庫連接
dotnet ef database update --dry-run
問題: JWT 驗證失敗
// 在 Startup/Program.cs 中啟用詳細日誌
builder.Logging.AddFilter("Microsoft.AspNetCore.Authentication", LogLevel.Debug);
問題: 快取不工作
// 檢查快取配置和依賴注入
services.AddMemoryCache();
services.AddScoped<ICacheService, HybridCacheService>();
🔧 工具和擴展
1. 推薦 VS Code 擴展
// .vscode/extensions.json
{
"recommendations": [
"ms-dotnettools.csharp",
"ms-dotnettools.vscode-dotnet-runtime",
"bradlc.vscode-tailwindcss",
"esbenp.prettier-vscode",
"ms-vscode.vscode-json",
"humao.rest-client"
]
}
2. EditorConfig 設定
# .editorconfig
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.cs]
indent_style = space
indent_size = 4
[*.{json,yml,yaml}]
indent_style = space
indent_size = 2
3. Git 鉤子設定
# .githooks/pre-commit
#!/bin/sh
# 執行程式碼格式化
dotnet format --verify-no-changes
# 執行測試
dotnet test --no-build --verbosity quiet
# 執行靜態分析
dotnet build --verbosity quiet
📚 學習資源
1. 核心概念學習
- Clean Architecture: Robert C. Martin 的 Clean Architecture 書籍
- Domain-Driven Design: Eric Evans 的 DDD 經典著作
- ASP.NET Core: Microsoft 官方文檔
- Entity Framework Core: EF Core 官方指南
2. 最佳實務參考
- Microsoft .NET Application Architecture Guides
- Clean Code: Robert C. Martin
- Refactoring: Martin Fowler
- Design Patterns: Gang of Four
3. 社群資源
- Stack Overflow: 問題解決
- GitHub: 開源專案參考
- Medium/Dev.to: 技術部落格
- YouTube: 技術教學影片
❓ 常見問題 FAQ
Q: 如何新增一個新的 AI 服務?
A:
- 在
Services/AI/下建立新的服務目錄 - 實作服務介面和具體類別
- 在
ServiceCollectionExtensions.cs註冊服務 - 撰寫單元測試
- 更新
Services/README.md文檔
Q: 資料庫遷移失敗怎麼辦?
A:
# 檢查遷移狀態
dotnet ef migrations list
# 回滾到特定遷移
dotnet ef database update PreviousMigrationName
# 重新產生遷移
dotnet ef migrations add NewMigrationName
Q: 如何優化 API 效能?
A:
- 使用異步方法 (
async/await) - 實作適當的快取策略
- 最佳化資料庫查詢 (避免 N+1)
- 使用分頁載入大數據集
- 啟用 HTTP 壓縮和快取標頭
Q: 如何處理敏感資訊?
A:
# 使用 User Secrets (開發環境)
dotnet user-secrets set "ApiKey" "your-secret-key"
# 使用環境變數 (生產環境)
export DRAMALING_API_KEY="your-secret-key"
# 絕不在程式碼中硬編碼敏感資訊
🤝 貢獻指南
提交 Pull Request 前檢查清單
- 程式碼遵循編碼規範
- 所有測試通過
- 新功能有對應的測試
- 文檔已更新
- Commit 訊息清楚描述變更
- 沒有合併衝突
Commit 訊息格式
類型(範圍): 簡短描述
詳細描述(如果需要)
- 變更項目 1
- 變更項目 2
Closes #123
類型標籤:
feat: 新功能fix: Bug 修復docs: 文檔更新style: 程式碼格式化refactor: 重構test: 測試相關chore: 構建工具或輔助工具
文檔版本: 1.0 維護者: DramaLing 開發團隊 最後更新: 2025-09-30