# Flutter + .NET Core 整合開發指南

## 概述
針對 Drama Ling 專案使用 Flutter 前端 + .NET Core 後端的技術架構，提供完整的整合開發指引。

## 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<void> 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<void> 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<ApiResponse<LoginResponse>> login(@Body() LoginRequest request);

  @POST('/dialogues/start')
  Future<ApiResponse<Dialogue>> startDialogue(@Body() StartDialogueRequest request);

  @GET('/users/profile')
  Future<ApiResponse<UserProfile>> getUserProfile();

  @GET('/vocabulary')
  Future<ApiResponse<List<Vocabulary>>> 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<UserProfile> getUserProfile();
  Future<void> saveUserProfile(UserProfile profile);
  Future<void> clearUserData();
}

class UserRepositoryImpl implements UserRepository {
  final DramaLingApi _api;
  final Box<UserProfileLocal> _localBox;

  UserRepositoryImpl(this._api, this._localBox);

  @override
  Future<UserProfile> 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<DialoguesController> _logger;

    public DialoguesController(
        IDialogueService dialogueService,
        ILogger<DialoguesController> logger)
    {
        _dialogueService = dialogueService;
        _logger = logger;
    }

    [HttpPost("start")]
    public async Task<ActionResult<ApiResponse<DialogueDto>>> StartDialogue(
        [FromBody] StartDialogueRequest request)
    {
        try {
            var userId = GetCurrentUserId();
            var dialogue = await _dialogueService.StartDialogueAsync(userId, request);
            
            return Ok(ApiResponse<DialogueDto>.Success(dialogue));
        }
        catch (ValidationException ex)
        {
            return BadRequest(ApiResponse<DialogueDto>.Error("VALIDATION_ERROR", ex.Message));
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to start dialogue for user {UserId}", GetCurrentUserId());
            return StatusCode(500, ApiResponse<DialogueDto>.Error("INTERNAL_ERROR", "Internal server error"));
        }
    }

    [HttpPost("{dialogueId}/messages")]
    public async Task<ActionResult<ApiResponse<DialogueMessageDto>>> SendMessage(
        Guid dialogueId,
        [FromBody] SendMessageRequest request)
    {
        var userId = GetCurrentUserId();
        var message = await _dialogueService.SendMessageAsync(userId, dialogueId, request);
        
        return Ok(ApiResponse<DialogueMessageDto>.Success(message));
    }

    private Guid GetCurrentUserId()
    {
        var userIdClaim = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
        return Guid.Parse(userIdClaim ?? throw new UnauthorizedAccessException());
    }
}
```

### 業務服務層設計
```csharp
// ✅ 業務服務實作範例
public interface IDialogueService
{
    Task<DialogueDto> StartDialogueAsync(Guid userId, StartDialogueRequest request);
    Task<DialogueMessageDto> SendMessageAsync(Guid userId, Guid dialogueId, SendMessageRequest request);
    Task<DialogueAnalysisDto> 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<DialogueDto> 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<DialogueDto>(dialogue);
        dto.InitialMessage = initialMessage;

        return dto;
    }

    public async Task<DialogueMessageDto> 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<DialogueMessageDto>(aiMessage);
    }
}
```

### Entity Framework 配置
```csharp
// ✅ DbContext 設定
public class DramaLingDbContext : DbContext
{
    public DbSet<User> Users { get; set; }
    public DbSet<Lesson> Lessons { get; set; }
    public DbSet<Dialogue> Dialogues { get; set; }
    public DbSet<DialogueMessage> DialogueMessages { get; set; }
    public DbSet<Vocabulary> Vocabularies { get; set; }

    public DramaLingDbContext(DbContextOptions<DramaLingDbContext> options)
        : base(options) { }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        // 應用所有配置
        modelBuilder.ApplyConfigurationsFromAssembly(typeof(DramaLingDbContext).Assembly);
    }
}

// ✅ 實體配置範例
public class DialogueConfiguration : IEntityTypeConfiguration<Dialogue>
{
    public void Configure(EntityTypeBuilder<Dialogue> 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<string>();

        // 關聯設定
        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<T>
{
    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<T> Success(T data, string? message = null)
    {
        return new ApiResponse<T>
        {
            Success = true,
            Data = data,
            Message = message,
            Meta = new ApiMeta
            {
                Timestamp = DateTime.UtcNow,
                RequestId = Activity.Current?.Id ?? Guid.NewGuid().ToString()
            }
        };
    }

    public static ApiResponse<T> Error(string code, string message)
    {
        return new ApiResponse<T>
        {
            Success = false,
            Error = new ApiError { Code = code, Message = message }
        };
    }
}
```

```dart
// ✅ Flutter API 回應模型
@freezed
class ApiResponse<T> with _$ApiResponse<T> {
  const factory ApiResponse({
    required bool success,
    T? data,
    String? message,
    ApiError? error,
    ApiMeta? meta,
  }) = _ApiResponse<T>;

  factory ApiResponse.fromJson(
    Map<String, dynamic> json,
    T Function(Object? json) fromJsonT,
  ) => _$ApiResponseFromJson(json, fromJsonT);
}

@freezed
class ApiError with _$ApiError {
  const factory ApiError({
    required String code,
    required String message,
    Map<String, dynamic>? details,
  }) = _ApiError;

  factory ApiError.fromJson(Map<String, dynamic> json) =>
      _$ApiErrorFromJson(json);
}
```

### 錯誤處理機制
```csharp
// ✅ C# 全域錯誤處理中介軟體
public class GlobalExceptionMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<GlobalExceptionMiddleware> _logger;

    public GlobalExceptionMiddleware(RequestDelegate next, ILogger<GlobalExceptionMiddleware> 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<object>.Error("VALIDATION_ERROR", validationEx.Message),
            NotFoundException notFoundEx => ApiResponse<object>.Error("NOT_FOUND", notFoundEx.Message),
            ForbiddenException forbiddenEx => ApiResponse<object>.Error("FORBIDDEN", forbiddenEx.Message),
            UnauthorizedAccessException => ApiResponse<object>.Error("UNAUTHORIZED", "Unauthorized access"),
            _ => ApiResponse<object>.Error("INTERNAL_ERROR", "An internal server error occurred")
        };

        context.Response.StatusCode = exception switch
        {
            ValidationException => 400,
            NotFoundException => 404,
            ForbiddenException => 403,
            UnauthorizedAccessException => 401,
            _ => 500
        };

        context.Response.ContentType = "application/json";
        await context.Response.WriteAsync(JsonSerializer.Serialize(response));
    }
}
```

```dart
// ✅ Flutter 錯誤處理
class ApiException implements Exception {
  final String code;
  final String message;
  final int? statusCode;

  const ApiException({
    required this.code,
    required this.message,
    this.statusCode,
  });

  factory ApiException.fromDioError(DioException error) {
    if (error.response?.data != null) {
      final responseData = error.response!.data as Map<String, dynamic>;
      if (responseData.containsKey('error')) {
        final errorData = responseData['error'] as Map<String, dynamic>;
        return ApiException(
          code: errorData['code'] ?? 'UNKNOWN_ERROR',
          message: errorData['message'] ?? 'An unknown error occurred',
          statusCode: error.response?.statusCode,
        );
      }
    }

    return ApiException(
      code: 'NETWORK_ERROR',
      message: error.message ?? 'Network error occurred',
      statusCode: error.response?.statusCode,
    );
  }
}

// Repository 錯誤處理範例
abstract class BaseRepository {
  Future<T> handleApiCall<T>(Future<T> Function() apiCall) async {
    try {
      return await apiCall();
    } on DioException catch (e) {
      throw ApiException.fromDioError(e);
    } catch (e) {
      throw ApiException(
        code: 'UNKNOWN_ERROR',
        message: e.toString(),
      );
    }
  }
}
```

### JWT 認證整合
```csharp
// ✅ C# JWT 配置
public static class AuthenticationExtensions
{
    public static IServiceCollection AddJwtAuthentication(
        this IServiceCollection services, 
        IConfiguration configuration)
    {
        var jwtSettings = configuration.GetSection("JwtSettings").Get<JwtSettings>()!;

        services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
            .AddJwtBearer(options =>
            {
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateIssuer = true,
                    ValidateAudience = true,
                    ValidateLifetime = true,
                    ValidateIssuerSigningKey = true,
                    ValidIssuer = jwtSettings.Issuer,
                    ValidAudience = jwtSettings.Audience,
                    IssuerSigningKey = new SymmetricSecurityKey(
                        Encoding.UTF8.GetBytes(jwtSettings.SecretKey)),
                    ClockSkew = TimeSpan.Zero
                };
            });

        return services;
    }
}
```

```dart
// ✅ Flutter Token 管理
class TokenManager {
  static const String _accessTokenKey = 'access_token';
  static const String _refreshTokenKey = 'refresh_token';

  final FlutterSecureStorage _secureStorage;

  TokenManager(this._secureStorage);

  Future<String?> getAccessToken() async {
    return await _secureStorage.read(key: _accessTokenKey);
  }

  Future<String?> getRefreshToken() async {
    return await _secureStorage.read(key: _refreshTokenKey);
  }

  Future<void> saveTokens(String accessToken, String refreshToken) async {
    await Future.wait([
      _secureStorage.write(key: _accessTokenKey, value: accessToken),
      _secureStorage.write(key: _refreshTokenKey, value: refreshToken),
    ]);
  }

  Future<void> clearTokens() async {
    await Future.wait([
      _secureStorage.delete(key: _accessTokenKey),
      _secureStorage.delete(key: _refreshTokenKey),
    ]);
  }

  bool isTokenExpired(String token) {
    try {
      final jwt = JWT.verify(token, SecretKey('your-secret'));
      final exp = jwt.payload['exp'] as int;
      final expireDate = DateTime.fromMillisecondsSinceEpoch(exp * 1000);
      return DateTime.now().isAfter(expireDate);
    } catch (e) {
      return true;
    }
  }
}
```

## 開發工具配置

### Flutter 開發環境
```yaml
# pubspec.yaml 關鍵依賴
dependencies:
  flutter:
    sdk: flutter
  
  # 狀態管理
  flutter_riverpod: ^2.4.9
  riverpod_annotation: ^2.3.3
  
  # 網路請求
  dio: ^5.3.3
  retrofit: ^4.0.3
  
  # 本地存儲
  hive: ^2.2.3
  hive_flutter: ^1.1.0
  flutter_secure_storage: ^9.0.0
  
  # JSON 序列化
  freezed_annotation: ^2.4.1
  json_annotation: ^4.8.1
  
  # 路由
  go_router: ^12.1.1
  
  # UI
  flutter_svg: ^2.0.9
  cached_network_image: ^3.3.0
  
dev_dependencies:
  # 程式碼生成
  build_runner: ^2.4.7
  freezed: ^2.4.6
  json_serializable: ^6.7.1
  retrofit_generator: ^7.0.8
  riverpod_generator: ^2.3.9
  hive_generator: ^2.0.1
  
  # 測試
  flutter_test:
    sdk: flutter
  integration_test:
    sdk: flutter
  mockito: ^5.4.3
```

### .NET Core 開發環境
```xml
<!-- DramaLing.Api.csproj -->
<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

  <ItemGroup>
    <!-- Web API -->
    <PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
    <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.0" />
    
    <!-- Entity Framework -->
    <PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.0" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.0" />
    <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0" />
    
    <!-- 序列化和映射 -->
    <PackageReference Include="AutoMapper" Version="12.0.1" />
    <PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
    
    <!-- 驗證 -->
    <PackageReference Include="FluentValidation" Version="11.8.0" />
    <PackageReference Include="FluentValidation.AspNetCore" Version="11.3.0" />
    
    <!-- 日誌 -->
    <PackageReference Include="Serilog.AspNetCore" Version="7.0.0" />
    <PackageReference Include="Serilog.Sinks.Console" Version="4.1.0" />
    
    <!-- 快取 -->
    <PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="8.0.0" />
    
    <!-- 背景服務 -->
    <PackageReference Include="Hangfire" Version="1.8.6" />
    <PackageReference Include="Hangfire.PostgreSql" Version="1.20.6" />
    
    <!-- 測試 -->
    <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.0" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.0" />
  </ItemGroup>
</Project>
```

---

## 待完成任務

### 高優先級
1. [ ] 完成 Flutter 專案初始化和基礎架構設置
2. [ ] 建立 .NET Core Web API 專案和 Entity Framework 配置  
3. [ ] 實現 JWT 認證機制的前後端整合
4. [ ] 設置 API 文檔 (Swagger) 和前端 API 客戶端生成

### 中優先級
1. [ ] 建立前後端的錯誤處理和日誌機制
2. [ ] 實現離線同步和資料快取策略
3. [ ] 設置自動化測試框架 (Flutter + .NET Core)
4. [ ] 建立 CI/CD pipeline 支援 Flutter 和 .NET 部署

### 低優先級
1. [ ] 研究 Flutter Web 版本的可行性
2. [ ] 探索 .NET MAUI 作為 Flutter 的替代方案
3. [ ] 建立效能監控和分析工具整合
4. [ ] 設計微服務架構的擴展計劃

---

**最後更新**: 2024年9月5日  
**負責人**: 前端Lead + 後端Lead  
**技術審查**: 每月檢討技術選型和架構決策