811 lines
24 KiB
Markdown
811 lines
24 KiB
Markdown
# 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
|
|
**技術審查**: 每月檢討技術選型和架構決策 |