861 lines
25 KiB
Markdown
861 lines
25 KiB
Markdown
# 多平台整合開發指南
|
||
|
||
## 概述
|
||
針對 Drama Ling 專案採用**前後端分離架構**,提供Web前端 + Flutter移動端 + 統一.NET Core API的完整整合開發指引。
|
||
|
||
## 整體架構設計
|
||
|
||
### 多平台架構圖
|
||
```mermaid
|
||
graph TB
|
||
subgraph "前端客戶端"
|
||
WEB[Web應用<br/>Modern JavaScript + Vite]
|
||
MOBILE[Flutter移動應用<br/>iOS + Android]
|
||
end
|
||
|
||
subgraph "統一後端服務"
|
||
API[.NET Core Web API<br/>統一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<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. [ ] 設計微服務架構的擴展計劃
|
||
|
||
---
|
||
|
||
**最後更新**: 2025年9月10日
|
||
**負責人**: 前端Lead + 後端Lead
|
||
**技術審查**: 每月檢討技術選型和架構決策 |