From 4a7c3aec9222ab77e2a2824a602520bd46cd1632 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=84=AD=E6=B2=9B=E8=BB=92?= Date: Tue, 7 Oct 2025 06:21:21 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=BE=A9=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E8=AA=8D=E8=AD=89=20token=20=E7=99=BC=E9=80=81=E5=92=8C?= =?UTF-8?q?=E7=94=A8=E6=88=B6=E8=B3=87=E6=96=99=E9=9A=94=E9=9B=A2=E5=95=8F?= =?UTF-8?q?=E9=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 啟用前端 API client 的認證 header 發送(修復關鍵問題) - 添加 401 錯誤自動清除過期 token 機制 - 設計兩種空狀態畫面:新用戶歡迎 vs 完成慶祝 - 改進錯誤處理:區分認證錯誤和一般錯誤 - 添加詳細除錯日誌追蹤 API 調用過程 - 修復前端條件判斷邏輯,確保正確顯示空狀態 現在用戶資料完全隔離,認證過期會自動處理並引導重新登入。 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../Controllers/FlashcardsController.cs | 6 +- frontend/app/review/page.tsx | 260 +++++++++++++++++- frontend/hooks/review/useReviewSession.ts | 94 ++++++- frontend/lib/api/client.ts | 17 +- 4 files changed, 348 insertions(+), 29 deletions(-) diff --git a/backend/DramaLing.Api/Controllers/FlashcardsController.cs b/backend/DramaLing.Api/Controllers/FlashcardsController.cs index 0f2a1b8..cfd0c47 100644 --- a/backend/DramaLing.Api/Controllers/FlashcardsController.cs +++ b/backend/DramaLing.Api/Controllers/FlashcardsController.cs @@ -5,11 +5,12 @@ using DramaLing.Api.Repositories; using DramaLing.Api.Services.Review; using Microsoft.AspNetCore.Authorization; using DramaLing.Api.Utils; +using DramaLing.Api.Services; namespace DramaLing.Api.Controllers; [Route("api/flashcards")] -[AllowAnonymous] +[Authorize] // 恢復認證要求,確保用戶資料隔離 public class FlashcardsController : BaseController { private readonly IFlashcardRepository _flashcardRepository; @@ -18,7 +19,8 @@ public class FlashcardsController : BaseController public FlashcardsController( IFlashcardRepository flashcardRepository, IReviewService reviewService, - ILogger logger) : base(logger) + IAuthService authService, + ILogger logger) : base(logger, authService) { _flashcardRepository = flashcardRepository; _reviewService = reviewService; diff --git a/frontend/app/review/page.tsx b/frontend/app/review/page.tsx index 67ee1bc..a1fd53a 100644 --- a/frontend/app/review/page.tsx +++ b/frontend/app/review/page.tsx @@ -23,9 +23,21 @@ export default function ReviewPage() { handleRestart, isLoading, error, - flashcards + flashcards, + totalFlashcardsCount } = useReviewSession() + // 除錯日誌 - 檢查狀態 + console.log('🔍 Review Page 狀態檢查:', { + isLoading, + error, + flashcardsLength: flashcards.length, + totalFlashcardsCount, + currentQuizItem: currentQuizItem?.id, + currentCard: currentCard?.word, + isComplete + }) + // 顯示載入狀態 if (isLoading) { return ( @@ -46,21 +58,221 @@ export default function ReviewPage() { // 顯示錯誤狀態 if (error) { + const isAuthError = error.includes('登入已過期') || error.includes('認證') + return (
-
⚠️
-

載入失敗

+
+ {isAuthError ? '🔒' : '⚠️'} +
+

+ {isAuthError ? '需要重新登入' : '載入失敗'} +

{error}

- + +
+ {isAuthError ? ( + + ) : ( + + )} + + +
+
+
+
+
+ ) + } + + // 情境 1: 新用戶 - 一張詞卡都沒有 + if (!isLoading && !error && totalFlashcardsCount === 0) { + return ( +
+ +
+
+
+ {/* 歡迎圖標 */} +
+
+ 👋 +
+
+ + {/* 歡迎標題 */} +

+ 歡迎來到 DramaLing! +

+ + {/* 說明文字 */} +

+ 開始您的英語學習之旅,建立第一張詞卡來開始學習吧! +

+ + {/* 功能介紹卡片 */} +
+
+
🎯
+

智能學習

+

+ AI 驅動的個人化詞彙學習系統 +

+
+ +
+
🧠
+

科學記憶

+

+ 基於遺忘曲線的複習提醒 +

+
+
+ + {/* 主要行動按鈕 */} +
+ +
+ + {/* 次要功能 */} +
+ +
+ + {/* 學習提示 */} +
+

+ 💡 開始提示: 建議從日常生活中的詞彙開始, + 或輸入您感興趣的英文句子讓 AI 協助分析! +

+
+
+
+
+
+ ) + } + + // 情境 2: 有詞卡但都已訓練完成 + if (!isLoading && !error && totalFlashcardsCount && totalFlashcardsCount > 0 && flashcards.length === 0) { + return ( +
+ +
+
+
+ {/* 慶祝圖標 */} +
+
+ 🎉 +
+
+ + {/* 主要標題 */} +

+ 太棒了!所有詞卡都已掌握 +

+ + {/* 次標題 */} +

+ 您已完成所有 {totalFlashcardsCount} 張詞卡的學習! +

+

+ 目前沒有需要複習的詞卡,您的學習進度非常優秀! +

+ + {/* 統計卡片 */} +
+
+
📚
+

持續學習

+

+ 保持每日複習習慣 +

+
+ +
+
📈
+

查看進度

+

+ 檢視學習統計數據 +

+
+ +
+
+

擴展詞庫

+

+ 新增更多詞彙挑戰 +

+
+
+ + {/* 行動按鈕 */} +
+ + + + + +
+ + {/* 提示文字 */} +
+

+ 💡 小提示: 詞卡會根據遺忘曲線算法自動安排複習時間。 + 繼續保持學習,明天可能會有新的複習內容! +

+
@@ -108,6 +320,8 @@ export default function ReviewPage() { } // 主要線性測驗頁面 + // 只有在有可用測驗項目時才顯示測驗界面 + if (!isLoading && !error && totalFlashcardsCount !== null && totalFlashcardsCount > 0 && flashcards.length > 0 && currentQuizItem && currentCard) { return (
@@ -147,4 +361,32 @@ export default function ReviewPage() {
) + } + + // Fallback 狀態 - 處理其他未預期情況 + return ( +
+ +
+
+
+
🤔
+

正在準備學習內容

+

+ 系統正在載入您的學習資料,請稍候片刻。 +

+

+ 如果問題持續,請嘗試重新載入頁面。 +

+ +
+
+
+
+ ) } \ No newline at end of file diff --git a/frontend/hooks/review/useReviewSession.ts b/frontend/hooks/review/useReviewSession.ts index 05bb0a6..39ac706 100644 --- a/frontend/hooks/review/useReviewSession.ts +++ b/frontend/hooks/review/useReviewSession.ts @@ -34,6 +34,7 @@ interface ReviewState { error: string | null pendingWordSubmission: string | null // 等待提交的詞彙ID submittingWords: Set // 正在提交的詞彙ID集合 + totalFlashcardsCount: number | null // 用戶總詞卡數(用於區分空狀態) } type ReviewAction = @@ -42,7 +43,7 @@ type ReviewAction = | { type: 'SKIP_TEST_ITEM'; payload: { quizItemId: string } } | { type: 'RESTART' } | { type: 'LOAD_FLASHCARDS_START' } - | { type: 'LOAD_FLASHCARDS_SUCCESS'; payload: { flashcards: Flashcard[]; quizItems: QuizItem[] } } + | { type: 'LOAD_FLASHCARDS_SUCCESS'; payload: { flashcards: Flashcard[]; quizItems: QuizItem[]; totalCount: number } } | { type: 'LOAD_FLASHCARDS_ERROR'; payload: { error: string } } | { type: 'WORD_SUBMIT_START'; payload: { cardId: string } } | { type: 'WORD_SUBMIT_SUCCESS'; payload: { cardId: string; nextReviewDate: string } } @@ -165,7 +166,8 @@ const reviewReducer = (state: ReviewState, action: ReviewAction): ReviewState => isLoading: false, error: null, flashcards: action.payload.flashcards, - quizItems: action.payload.quizItems + quizItems: action.payload.quizItems, + totalFlashcardsCount: action.payload.totalCount } case 'LOAD_FLASHCARDS_ERROR': @@ -301,10 +303,11 @@ export function useReviewSession() { isLoading: false, error: null, pendingWordSubmission: null, - submittingWords: new Set() + submittingWords: new Set(), + totalFlashcardsCount: null }) - const { quizItems, score, isComplete, flashcards, isLoading, error, pendingWordSubmission, submittingWords } = state + const { quizItems, score, isComplete, flashcards, isLoading, error, pendingWordSubmission, submittingWords, totalFlashcardsCount } = state // 智能排序獲取當前測驗項目 - 使用 useMemo 優化性能 const sortedQuizItems = useMemo(() => sortQuizItemsByPriority(quizItems), [quizItems]) @@ -321,29 +324,95 @@ export function useReviewSession() { dispatch({ type: 'LOAD_FLASHCARDS_START' }) try { - const response = await flashcardsService.getDueFlashcards(10) + console.log('🚀 開始載入詞卡資料...') - if (response.success && response.data) { - const flashcards = response.data - const quizItems = generateQuizItemsFromFlashcards(flashcards) + // 同時獲取待複習詞卡和總詞卡數 + const [dueResponse, totalResponse] = await Promise.all([ + flashcardsService.getDueFlashcards(10), + flashcardsService.getFlashcards() // 獲取所有詞卡來計算總數 + ]) + + console.log('📥 API 回應:', { + dueResponse: { + success: dueResponse.success, + dataType: typeof dueResponse.data, + dataLength: Array.isArray(dueResponse.data) ? dueResponse.data.length : 'not array', + error: dueResponse.error + }, + totalResponse: { + success: totalResponse.success, + dataType: typeof totalResponse.data, + dataLength: Array.isArray(totalResponse.data) ? totalResponse.data.length : 'not array', + error: totalResponse.error + } + }) + + if (dueResponse.success && totalResponse.success) { + // 正確的資料結構處理 + const dueFlashcards = dueResponse.data || [] // getDueFlashcards 返回 Flashcard[] + const totalFlashcardsData = totalResponse.data || { flashcards: [], count: 0 } // getFlashcards 返回 { flashcards: [], count: number } + const totalFlashcards = totalFlashcardsData.flashcards || [] + const totalCount = totalFlashcardsData.count || totalFlashcards.length + + const quizItems = generateQuizItemsFromFlashcards(dueFlashcards) + + console.log('📊 資料統計:', { + dueFlashcards: dueFlashcards.length, + totalFlashcards: totalFlashcards.length, + totalCount: totalCount, + quizItems: quizItems.length + }) dispatch({ type: 'LOAD_FLASHCARDS_SUCCESS', - payload: { flashcards, quizItems } + payload: { + flashcards: dueFlashcards, + quizItems, + totalCount: totalCount + } }) - console.log('✅ 成功載入', flashcards.length, '張詞卡') + console.log('✅ 成功載入', dueFlashcards.length, '張待複習詞卡 / 總共', totalCount, '張詞卡') console.log('🎯 生成', quizItems.length, '個測驗項目') } else { + console.error('❌ API 調用失敗:', { + dueError: dueResponse.error, + totalError: totalResponse.error, + dueSuccess: dueResponse.success, + totalSuccess: totalResponse.success + }) + + // 檢查是否為認證錯誤 + const authError = dueResponse.error?.includes('登入已過期') || + totalResponse.error?.includes('登入已過期') || + dueResponse.error?.includes('認證') || + totalResponse.error?.includes('認證') + dispatch({ type: 'LOAD_FLASHCARDS_ERROR', - payload: { error: response.error || '載入詞卡失敗' } + payload: { + error: authError ? + '登入已過期,請重新登入' : + (dueResponse.error || totalResponse.error || '載入詞卡失敗') + } }) } } catch (error) { + console.error('❌ API 調用異常:', error) + + // 檢查是否為認證錯誤 + const errorMessage = error instanceof Error ? error.message : '載入詞卡失敗' + const isAuthError = errorMessage.includes('登入已過期') || + errorMessage.includes('認證') || + errorMessage.includes('Unauthorized') + dispatch({ type: 'LOAD_FLASHCARDS_ERROR', - payload: { error: error instanceof Error ? error.message : '載入詞卡失敗' } + payload: { + error: isAuthError ? + '登入已過期,請重新登入' : + errorMessage + } }) } } @@ -467,6 +536,7 @@ export function useReviewSession() { isLoading, error, flashcards, + totalFlashcardsCount, // 計算屬性 totalQuizItems: quizItems.length, diff --git a/frontend/lib/api/client.ts b/frontend/lib/api/client.ts index dc69b22..92ecf9c 100644 --- a/frontend/lib/api/client.ts +++ b/frontend/lib/api/client.ts @@ -66,6 +66,12 @@ export class ApiClient { const apiError = await createApiErrorFromResponse(response, context) lastError = apiError + // 處理認證錯誤 - 自動清除過期token + if (response.status === 401) { + localStorage.removeItem('auth_token') + console.log('🔒 認證過期,已清除token') + } + // 如果不可重試,直接返回錯誤 if (attempt === retries || !this.isRetryableStatus(response.status)) { return createErrorResponse(apiError) @@ -138,12 +144,11 @@ export class ApiClient { private getDefaultHeaders(): Record { const headers: Record = {} - // 開發階段:不添加認證Token - // TODO: 生產環境需要添加認證邏輯 - // const token = localStorage.getItem('auth_token') - // if (token) { - // headers.Authorization = `Bearer ${token}` - // } + // 添加認證Token(如果存在) + const token = localStorage.getItem('auth_token') + if (token) { + headers.Authorization = `Bearer ${token}` + } return headers }