629 lines
14 KiB
Markdown
629 lines
14 KiB
Markdown
# DramaLing API 開發指南
|
|
|
|
**版本**: 1.0
|
|
**最後更新**: 2025-09-30
|
|
**適用對象**: 後端開發者、新團隊成員
|
|
|
|
## 🚀 快速開始
|
|
|
|
### 開發環境要求
|
|
|
|
- **.NET 8 SDK** (最新 LTS 版本)
|
|
- **Visual Studio Code** 或 **Visual Studio 2022**
|
|
- **Git** 版本控制
|
|
- **SQLite** (開發環境) / **SQL Server** (生產環境)
|
|
|
|
### 環境變數配置
|
|
|
|
建立 `.env` 檔案或設定系統環境變數:
|
|
|
|
```bash
|
|
# 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. 專案設定
|
|
|
|
```bash
|
|
# 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. 開發分支策略
|
|
|
|
```bash
|
|
# 主要分支
|
|
main # 生產環境代碼
|
|
develop # 開發整合分支
|
|
|
|
# 功能分支命名規則
|
|
feature/user-auth # 新功能開發
|
|
bugfix/cache-issue # Bug 修復
|
|
hotfix/security-patch # 緊急修復
|
|
refactor/clean-arch # 重構改善
|
|
```
|
|
|
|
---
|
|
|
|
## 📝 編碼規範
|
|
|
|
### 1. C# 編碼標準
|
|
|
|
**命名規則**:
|
|
```csharp
|
|
// 類別和介面 - 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;
|
|
```
|
|
|
|
**異步方法規範**:
|
|
```csharp
|
|
// ✅ 正確:異步方法使用 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. 文檔註解標準
|
|
|
|
```csharp
|
|
/// <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**
|
|
```csharp
|
|
// 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 (如需要)**
|
|
```csharp
|
|
// 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**
|
|
```csharp
|
|
// 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**
|
|
```csharp
|
|
[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: 註冊服務**
|
|
```csharp
|
|
// Extensions/ServiceCollectionExtensions.cs
|
|
public static IServiceCollection AddBusinessServices(this IServiceCollection services)
|
|
{
|
|
// ... 其他服務
|
|
services.AddScoped<IFeatureRepository, FeatureRepository>();
|
|
services.AddScoped<IFeatureService, FeatureService>();
|
|
|
|
return services;
|
|
}
|
|
```
|
|
|
|
### 2. 撰寫單元測試
|
|
|
|
```csharp
|
|
// 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. 測試分類
|
|
|
|
**單元測試** - 隔離測試個別類別
|
|
```csharp
|
|
[Fact]
|
|
[Trait("Category", "Unit")]
|
|
public async Task ServiceMethod_ValidInput_ReturnsExpectedResult()
|
|
{
|
|
// AAA 模式測試
|
|
}
|
|
```
|
|
|
|
**整合測試** - 測試多個組件協作
|
|
```csharp
|
|
[Fact]
|
|
[Trait("Category", "Integration")]
|
|
public async Task ApiEndpoint_ValidRequest_ReturnsCorrectResponse()
|
|
{
|
|
// 使用 TestServer 測試整個請求流程
|
|
}
|
|
```
|
|
|
|
**端到端測試** - 完整使用者場景
|
|
```csharp
|
|
[Fact]
|
|
[Trait("Category", "E2E")]
|
|
public async Task UserWorkflow_CompleteScenario_WorksCorrectly()
|
|
{
|
|
// 模擬真實使用者操作流程
|
|
}
|
|
```
|
|
|
|
### 2. 測試執行
|
|
|
|
```bash
|
|
# 執行所有測試
|
|
dotnet test
|
|
|
|
# 執行特定分類測試
|
|
dotnet test --filter "Category=Unit"
|
|
dotnet test --filter "Category=Integration"
|
|
|
|
# 產生覆蓋率報告
|
|
dotnet test --collect:"XPlat Code Coverage"
|
|
|
|
# 執行特定測試類別
|
|
dotnet test --filter "ClassName=FeatureServiceTests"
|
|
```
|
|
|
|
---
|
|
|
|
## 🐛 除錯與診斷
|
|
|
|
### 1. 日誌記錄最佳實務
|
|
|
|
```csharp
|
|
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. 效能監控
|
|
|
|
```csharp
|
|
// 使用 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. 常見問題診斷
|
|
|
|
**問題**: 資料庫連接失敗
|
|
```bash
|
|
# 檢查連接字串
|
|
dotnet user-secrets list
|
|
|
|
# 測試資料庫連接
|
|
dotnet ef database update --dry-run
|
|
```
|
|
|
|
**問題**: JWT 驗證失敗
|
|
```csharp
|
|
// 在 Startup/Program.cs 中啟用詳細日誌
|
|
builder.Logging.AddFilter("Microsoft.AspNetCore.Authentication", LogLevel.Debug);
|
|
```
|
|
|
|
**問題**: 快取不工作
|
|
```csharp
|
|
// 檢查快取配置和依賴注入
|
|
services.AddMemoryCache();
|
|
services.AddScoped<ICacheService, HybridCacheService>();
|
|
```
|
|
|
|
---
|
|
|
|
## 🔧 工具和擴展
|
|
|
|
### 1. 推薦 VS Code 擴展
|
|
|
|
```json
|
|
// .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 設定
|
|
|
|
```ini
|
|
# .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 鉤子設定
|
|
|
|
```bash
|
|
# .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:
|
|
1. 在 `Services/AI/` 下建立新的服務目錄
|
|
2. 實作服務介面和具體類別
|
|
3. 在 `ServiceCollectionExtensions.cs` 註冊服務
|
|
4. 撰寫單元測試
|
|
5. 更新 `Services/README.md` 文檔
|
|
|
|
### Q: 資料庫遷移失敗怎麼辦?
|
|
|
|
A:
|
|
```bash
|
|
# 檢查遷移狀態
|
|
dotnet ef migrations list
|
|
|
|
# 回滾到特定遷移
|
|
dotnet ef database update PreviousMigrationName
|
|
|
|
# 重新產生遷移
|
|
dotnet ef migrations add NewMigrationName
|
|
```
|
|
|
|
### Q: 如何優化 API 效能?
|
|
|
|
A:
|
|
1. 使用異步方法 (`async/await`)
|
|
2. 實作適當的快取策略
|
|
3. 最佳化資料庫查詢 (避免 N+1)
|
|
4. 使用分頁載入大數據集
|
|
5. 啟用 HTTP 壓縮和快取標頭
|
|
|
|
### Q: 如何處理敏感資訊?
|
|
|
|
A:
|
|
```bash
|
|
# 使用 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 |