# 多平台整合開發指南
## 概述
針對 Drama Ling 專案採用**前後端分離架構**,提供Web前端 + Flutter移動端 + 統一.NET Core API的完整整合開發指引。
## 整體架構設計
### 多平台架構圖
```mermaid
graph TB
subgraph "前端客戶端"
WEB[Web應用
Modern JavaScript + Vite]
MOBILE[Flutter移動應用
iOS + Android]
end
subgraph "統一後端服務"
API[.NET Core Web API
統一API服務]
subgraph "差異化端點"
WEB_EP[/api/v1/web/*]
MOBILE_EP[/api/v1/mobile/*]
COMMON_EP[/api/v1/common/*]
end
end
WEB --> WEB_EP
MOBILE --> MOBILE_EP
WEB --> COMMON_EP
MOBILE --> COMMON_EP
WEB_EP --> API
MOBILE_EP --> API
COMMON_EP --> API
```
## Web前端架構 (主要平台)
### 技術棧
- **框架**: 原生Modern JavaScript + ES6 Modules
- **構建工具**: Vite 5.x/7.x
- **樣式**: SCSS/Sass + CSS3
- **狀態管理**: 原生JavaScript類別
- **API通信**: Fetch API + RESTful設計
### 項目結構
```
apps/web/
├── src/
│ ├── modules/ # 核心業務模組
│ ├── components/ # 可重用組件
│ ├── utils/ # 工具函數和API客戶端
│ └── styles/ # SCSS樣式文件
├── index.html
└── vite.config.js
```
## Flutter移動端架構
### 專案結構設計
```
lib/
├── main.dart # 應用程式進入點
├── app/
│ ├── app.dart # App主要配置
│ ├── routes/ # 路由配置
│ └── themes/ # 主題設定
├── core/
│ ├── constants/ # 常數定義
│ ├── utils/ # 工具類
│ ├── errors/ # 錯誤處理
│ └── network/ # 網路層配置
├── features/ # 功能模組
│ ├── authentication/ # 用戶認證
│ ├── dialogue/ # 對話練習
│ ├── vocabulary/ # 詞彙管理
│ ├── gamification/ # 遊戲化功能
│ └── subscription/ # 訂閱功能
└── shared/
├── widgets/ # 共用元件
├── models/ # 資料模型
└── services/ # 共用服務
```
### 狀態管理架構 (Riverpod)
```dart
// ✅ Provider 定義範例
@riverpod
class DialogueNotifier extends _$DialogueNotifier {
@override
DialogueState build() {
return const DialogueState.initial();
}
Future startDialogue(String scenarioId) async {
state = const DialogueState.loading();
try {
final dialogue = await ref.read(dialogueRepositoryProvider)
.startDialogue(scenarioId);
state = DialogueState.success(dialogue);
} catch (error) {
state = DialogueState.error(error.toString());
}
}
Future sendMessage(String message) async {
// 發送訊息邏輯
}
}
// State 類定義
@freezed
class DialogueState with _$DialogueState {
const factory DialogueState.initial() = _Initial;
const factory DialogueState.loading() = _Loading;
const factory DialogueState.success(Dialogue dialogue) = _Success;
const factory DialogueState.error(String message) = _Error;
}
```
### 網路層設計 (Dio + Retrofit)
```dart
// ✅ API 客戶端設定
@RestApi(baseUrl: 'https://api.dramaling.com/api/v1')
abstract class DramaLingApi {
factory DramaLingApi(Dio dio) = _DramaLingApi;
@POST('/auth/login')
Future> login(@Body() LoginRequest request);
@POST('/dialogues/start')
Future> startDialogue(@Body() StartDialogueRequest request);
@GET('/users/profile')
Future> getUserProfile();
@GET('/vocabulary')
Future>> getVocabulary(
@Query('category') String? category,
@Query('difficulty') String? difficulty,
);
}
// 攔截器設置
class AuthInterceptor extends Interceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
final token = getStoredToken();
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
handler.next(options);
}
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
if (err.response?.statusCode == 401) {
// 處理 Token 過期
logout();
}
handler.next(err);
}
}
```
### 本地存儲設計 (Hive)
```dart
// ✅ Hive 資料模型
@HiveType(typeId: 0)
class UserProfileLocal extends HiveObject {
@HiveField(0)
late String userId;
@HiveField(1)
late String username;
@HiveField(2)
late String email;
@HiveField(3)
late int totalScore;
@HiveField(4)
late DateTime lastSyncAt;
}
// Repository 模式
abstract class UserRepository {
Future getUserProfile();
Future saveUserProfile(UserProfile profile);
Future clearUserData();
}
class UserRepositoryImpl implements UserRepository {
final DramaLingApi _api;
final Box _localBox;
UserRepositoryImpl(this._api, this._localBox);
@override
Future getUserProfile() async {
try {
// 嘗試從 API 獲取最新資料
final response = await _api.getUserProfile();
// 存儲到本地
final localProfile = UserProfileLocal()
..userId = response.data.userId
..username = response.data.username
..email = response.data.email
..totalScore = response.data.totalScore
..lastSyncAt = DateTime.now();
await _localBox.put('user_profile', localProfile);
return response.data;
} catch (e) {
// API 失敗時使用本地快取
final localProfile = _localBox.get('user_profile');
if (localProfile != null) {
return UserProfile.fromLocal(localProfile);
}
rethrow;
}
}
}
```
## .NET Core 後端架構
### 專案結構設計
```
DramaLing.Api/
├── DramaLing.Api/ # Web API 專案
│ ├── Controllers/ # API 控制器
│ ├── Middleware/ # 自定義中介軟體
│ ├── Filters/ # 過濾器
│ └── Program.cs # 應用程式啟動
├── DramaLing.Core/ # 核心業務邏輯
│ ├── Entities/ # 實體類別
│ ├── Interfaces/ # 介面定義
│ ├── Services/ # 業務服務
│ └── DTOs/ # 資料傳輸物件
├── DramaLing.Infrastructure/ # 基礎設施層
│ ├── Data/ # 資料存取
│ ├── Repositories/ # Repository 實作
│ ├── External/ # 外部服務整合
│ └── Configurations/ # 設定檔
└── DramaLing.Tests/ # 測試專案
├── Unit/ # 單元測試
├── Integration/ # 整合測試
└── Common/ # 測試共用程式碼
```
### API 控制器設計
```csharp
// ✅ RESTful API 控制器範例
[ApiController]
[Route("api/v1/[controller]")]
[Authorize]
public class DialoguesController : ControllerBase
{
private readonly IDialogueService _dialogueService;
private readonly ILogger _logger;
public DialoguesController(
IDialogueService dialogueService,
ILogger logger)
{
_dialogueService = dialogueService;
_logger = logger;
}
[HttpPost("start")]
public async Task>> StartDialogue(
[FromBody] StartDialogueRequest request)
{
try {
var userId = GetCurrentUserId();
var dialogue = await _dialogueService.StartDialogueAsync(userId, request);
return Ok(ApiResponse.Success(dialogue));
}
catch (ValidationException ex)
{
return BadRequest(ApiResponse.Error("VALIDATION_ERROR", ex.Message));
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to start dialogue for user {UserId}", GetCurrentUserId());
return StatusCode(500, ApiResponse.Error("INTERNAL_ERROR", "Internal server error"));
}
}
[HttpPost("{dialogueId}/messages")]
public async Task>> SendMessage(
Guid dialogueId,
[FromBody] SendMessageRequest request)
{
var userId = GetCurrentUserId();
var message = await _dialogueService.SendMessageAsync(userId, dialogueId, request);
return Ok(ApiResponse.Success(message));
}
private Guid GetCurrentUserId()
{
var userIdClaim = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
return Guid.Parse(userIdClaim ?? throw new UnauthorizedAccessException());
}
}
```
### 業務服務層設計
```csharp
// ✅ 業務服務實作範例
public interface IDialogueService
{
Task StartDialogueAsync(Guid userId, StartDialogueRequest request);
Task SendMessageAsync(Guid userId, Guid dialogueId, SendMessageRequest request);
Task GetDialogueAnalysisAsync(Guid userId, Guid dialogueId);
}
public class DialogueService : IDialogueService
{
private readonly IDialogueRepository _dialogueRepository;
private readonly ILessonRepository _lessonRepository;
private readonly IAiAnalysisService _aiAnalysisService;
private readonly IMapper _mapper;
public DialogueService(
IDialogueRepository dialogueRepository,
ILessonRepository lessonRepository,
IAiAnalysisService aiAnalysisService,
IMapper mapper)
{
_dialogueRepository = dialogueRepository;
_lessonRepository = lessonRepository;
_aiAnalysisService = aiAnalysisService;
_mapper = mapper;
}
public async Task StartDialogueAsync(Guid userId, StartDialogueRequest request)
{
// 驗證場景是否存在
var lesson = await _lessonRepository.GetByCodeAsync(request.ScenarioId)
?? throw new ValidationException($"Scenario {request.ScenarioId} not found");
// 檢查用戶權限
await ValidateUserAccessAsync(userId, lesson);
// 創建對話實例
var dialogue = new Dialogue
{
UserId = userId,
LessonId = lesson.Id,
SessionToken = Guid.NewGuid().ToString(),
Status = DialogueStatus.InProgress,
StartedAt = DateTime.UtcNow
};
await _dialogueRepository.AddAsync(dialogue);
await _dialogueRepository.SaveChangesAsync();
// 生成初始 AI 訊息
var initialMessage = await GenerateInitialMessageAsync(dialogue, lesson);
var dto = _mapper.Map(dialogue);
dto.InitialMessage = initialMessage;
return dto;
}
public async Task SendMessageAsync(
Guid userId,
Guid dialogueId,
SendMessageRequest request)
{
var dialogue = await _dialogueRepository.GetByIdAsync(dialogueId)
?? throw new NotFoundException($"Dialogue {dialogueId} not found");
if (dialogue.UserId != userId)
throw new ForbiddenException("Access denied");
// 保存用戶訊息
var userMessage = new DialogueMessage
{
DialogueId = dialogueId,
TurnNumber = await GetNextTurnNumberAsync(dialogueId),
SenderType = MessageSenderType.User,
MessageText = request.Message,
CreatedAt = DateTime.UtcNow
};
await _dialogueRepository.AddMessageAsync(userMessage);
// AI 分析和回應
var analysisResult = await _aiAnalysisService.AnalyzeMessageAsync(
dialogue, userMessage);
// 生成 AI 回應
var aiResponse = await _aiAnalysisService.GenerateResponseAsync(
dialogue, userMessage, analysisResult);
var aiMessage = new DialogueMessage
{
DialogueId = dialogueId,
TurnNumber = userMessage.TurnNumber,
SenderType = MessageSenderType.AI,
MessageText = aiResponse.Message,
CreatedAt = DateTime.UtcNow
};
await _dialogueRepository.AddMessageAsync(aiMessage);
await _dialogueRepository.SaveChangesAsync();
return _mapper.Map(aiMessage);
}
}
```
### Entity Framework 配置
```csharp
// ✅ DbContext 設定
public class DramaLingDbContext : DbContext
{
public DbSet Users { get; set; }
public DbSet Lessons { get; set; }
public DbSet Dialogues { get; set; }
public DbSet DialogueMessages { get; set; }
public DbSet Vocabularies { get; set; }
public DramaLingDbContext(DbContextOptions options)
: base(options) { }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// 應用所有配置
modelBuilder.ApplyConfigurationsFromAssembly(typeof(DramaLingDbContext).Assembly);
}
}
// ✅ 實體配置範例
public class DialogueConfiguration : IEntityTypeConfiguration
{
public void Configure(EntityTypeBuilder builder)
{
builder.ToTable("dialogues");
builder.HasKey(d => d.Id);
builder.Property(d => d.Id)
.HasColumnName("dialogue_id")
.HasDefaultValueSql("gen_random_uuid()");
builder.Property(d => d.SessionToken)
.HasColumnName("session_token")
.HasMaxLength(255)
.IsRequired();
builder.Property(d => d.Status)
.HasColumnName("status")
.HasConversion();
// 關聯設定
builder.HasOne(d => d.User)
.WithMany(u => u.Dialogues)
.HasForeignKey(d => d.UserId)
.OnDelete(DeleteBehavior.Cascade);
builder.HasOne(d => d.Lesson)
.WithMany()
.HasForeignKey(d => d.LessonId)
.OnDelete(DeleteBehavior.SetNull);
// 索引設定
builder.HasIndex(d => d.UserId);
builder.HasIndex(d => d.SessionToken).IsUnique();
builder.HasIndex(d => d.CreatedAt);
}
}
```
## 前後端整合最佳實踐
### API 回應格式統一
```csharp
// ✅ C# API 回應模型
public class ApiResponse
{
public bool Success { get; set; }
public T? Data { get; set; }
public string? Message { get; set; }
public ApiError? Error { get; set; }
public ApiMeta? Meta { get; set; }
public static ApiResponse Success(T data, string? message = null)
{
return new ApiResponse
{
Success = true,
Data = data,
Message = message,
Meta = new ApiMeta
{
Timestamp = DateTime.UtcNow,
RequestId = Activity.Current?.Id ?? Guid.NewGuid().ToString()
}
};
}
public static ApiResponse Error(string code, string message)
{
return new ApiResponse
{
Success = false,
Error = new ApiError { Code = code, Message = message }
};
}
}
```
```dart
// ✅ Flutter API 回應模型
@freezed
class ApiResponse with _$ApiResponse {
const factory ApiResponse({
required bool success,
T? data,
String? message,
ApiError? error,
ApiMeta? meta,
}) = _ApiResponse;
factory ApiResponse.fromJson(
Map json,
T Function(Object? json) fromJsonT,
) => _$ApiResponseFromJson(json, fromJsonT);
}
@freezed
class ApiError with _$ApiError {
const factory ApiError({
required String code,
required String message,
Map? details,
}) = _ApiError;
factory ApiError.fromJson(Map json) =>
_$ApiErrorFromJson(json);
}
```
### 錯誤處理機制
```csharp
// ✅ C# 全域錯誤處理中介軟體
public class GlobalExceptionMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger _logger;
public GlobalExceptionMiddleware(RequestDelegate next, ILogger logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
await HandleExceptionAsync(context, ex);
}
}
private async Task HandleExceptionAsync(HttpContext context, Exception exception)
{
_logger.LogError(exception, "An unhandled exception occurred");
var response = exception switch
{
ValidationException validationEx => ApiResponse