07-offline-sync.md
AI 代理程式提醒 (AI Agent Note): 本文件概述認證考試平台中所使用的
PowerSyncConnector與離線優先 (Offline-First) 資料庫機制。修改 Schema、同步規則或衝突解決策略時請參考此文件。
設計理念: 本應用程式以「本地端優先」為原則運作。所有的讀寫操作皆直接針對本地端的 SQLite 資料庫執行,確保 UI 回應即時且具備完整的離線操作能力。資料隨後會透過 PowerSync 同步至 Supabase PostgreSQL 後端。
graph TD
A["應用程式 (Flutter UI & Logic)"] <--> B["本地端 SQLite (SQLCipher 加密)"]
B <--> C["PowerSync 客戶端 SDK"]
C <--> D["PowerSync 雲端服務"]
D <--> E["Supabase PostgreSQL (主資料庫)"]
F["Firebase Auth"] -->|JWT Tokens| A
A -->|傳遞 Token| C
PowerSyncService):
examId, questionText, explanation, topic, correctAnswer, options, images, title, questionNumber, explanationTexts, learnEnglish, videoUrl, type, practicalSolution, dragDropData, dragOptions, dropTargets, isApproved。examId, questionId, userId, username, text, timestamp。PowerSyncConnector)PowerSyncConnector 繼承自 PowerSyncBackendConnector,負責處理自訂的驗證與雙向同步。
fetchCredentials)AuthService().getIdToken() 取得 Firebase ID Token。PowerSyncCredentials(endpoint, token)。EnvConfig 讀取多組端點/備援對應設定,以防主同步伺服器停機。uploadData(database))負責將本地端的變更推送至遠端後端。
database.getNextCrudTransaction() 取得待處理的交易。powersync_rules.yaml)動態儲存桶 (Dynamic Storage Buckets) 依據 JWT 內的 token_parameters.role 控制應同步至客戶端設備的資料。
題庫同步 (儲存桶: questions_by_role):
admin, viewer, internalTester):SELECT * FROM questions (下載所有題目,包含未核准的草稿)。pending, publicTester):SELECT * FROM questions WHERE isApproved = true (僅下載已經過審核、可用於正式環境的題目)。SELECT * FROM questions WHERE isApproved = true AND questionNumber <= 10 (將練習題庫限制在試用範圍內)。討論區同步:
bucket.user_role != 'guest' (訪客使用者無法下載或瀏覽討論串,以節省頻寬並限制進階功能)。role 與 deviceId,用於決定同步儲存桶的行為。uploadData 交易失敗 (如傳輸時斷線),SDK 會採用指數退避 (Exponential Backoff) 自動重試。考量到認證考試內容的專有性質:
requireBioAuth,在啟動時會跳出生物辨識提示,攔截對本地端資料庫的存取。internalTester 與 admin 角色提供 PowerSyncDebugDialog 元件,以便即時檢視原始交易日誌、佇列大小與同步延遲。08-payment-system.md
MockExam 應用程式採用免費增值 (Freemium) 模型,根據使用者的角色和訂閱狀態提供不同層級的功能。
| 功能 | 免費 (Guest/Pending) | 進階 (Viewer) | 管理員 (Admin) |
|---|---|---|---|
| 題庫存取 | 僅限已核准 (訪客: 最多10題) | 所有考題 | 所有考題 |
| 模擬考試 | 選項受限 | 完整自訂功能 | 完整功能 |
| AI 家教 | 次數受限 | 無限制 | 無限制 |
| 廣告 | 顯示 | 隱藏 | 隱藏 |
| 錯題複習 | 每日次數受限 | 無限制 | 無限制 |
| 討論區 | 限制存取 | 完整存取 | 完整存取 |
| 搜尋功能 | 基本過濾器 | 基本 + AI 向量搜尋 | 全部 |
注意:在參考實作中,特定的權限名稱(如「MockExam CCNA Pro」)僅作為範例,但架構設計支援任何通用的認證考試領域。
為了確保高可用性、備援能力及管理便利性,系統實作了雙支付供應商架構。
graph TD
A["支付控制器"] -->|切換| B{"啟用中供應商?"}
B -->|RevenueCat| C["RevenueCat SDK"]
B -->|原生 IAP| D["Google Play Billing SDK"]
B -->|網頁版| E["網頁支付服務"]
C --> F["應用程式商店"]
D --> F
E --> G["網頁支付閘道"]
F --> H["交易稽核"]
G --> H
H --> I["角色升級流程"]
為何採用雙供應商?
kReleaseMode 下使用正式環境金鑰,否則使用測試環境金鑰。CustomerInfoUpdateListener 來監聽即時的訂閱狀態變更。login(uid):將 Firebase UID 綁定至 RevenueCat 客戶,確保訂閱能跨裝置跟隨使用者。logout():清除 RevenueCat 工作階段,防止未經授權的存取。customerInfo.entitlements.active 進行驗證。Purchases.purchasePackage()。premiumProductId, premiumPurchaseDate)。mockexam_premium_1month, mockexam_premium_3month)。_initNativeIAP():訂閱 _inAppPurchase.purchaseStream 以處理 PurchaseDetails 更新並載入產品詳細資訊。架構支援在執行階段動態切換供應商。
RestrictedDefaultsScreen 切換啟用中的供應商。systemConfig 的變更會發送動態廣播,立即在整個應用程式中切換啟用中的供應商。由於 Google Play Billing 無法在網頁版上使用,專屬的 WebPaymentService 負責處理網頁版交易。
所有交易均安全地記錄以供稽核與分析。
id, uid, email, productId, productName, amount, currency, status (pending/completed/failed/refunded), timestamp, purchaseToken
transactions/{uid}/{transactionId}) 與全域路徑 (systemTransactions/{transactionId})。sequenceDiagram
participant U as 使用者
participant A as 應用程式
participant P as 供應商 (RevenueCat/IAP)
participant B as 後端
U->>A: 選擇訂閱方案
A->>P: 啟動購買
P-->>A: 購買成功
A->>B: 驗證收據
B-->>A: 驗證有效
A->>A: 更新角色與權限
A->>B: 記錄交易
A-->>U: 解鎖進階功能
restorePurchases 流程無縫還原跨裝置訂閱。在成功購買事件發生後:
AppUser 角色更新為 'viewer' (或指定的進階角色)。premiumProductId 與 premiumPurchaseDate。TransactionRecord。showTestAds 標籤,並註冊測試廣告裝置 ID 以符合 AdMob 規範。09-state-management.md
本文件概述了認證考試 (certification exam) 應用程式的狀態管理架構。系統主要使用 Provider 和 ChangeNotifier 模式。選擇此方法的考量包含其與 Flutter 原生整合良好、階層式狀態管理的簡單性,以及結合 ValueListenableBuilder 時能提供高效的元件重建機制。
graph TD
A["MultiProvider (main.dart)"] --> B["StreamProvider<AppUser?>"]
A --> C["Provider<AuthService>"]
A --> D["Provider<DatabaseService>"]
A --> E["Provider<PowerSyncService>"]
B --> F["App Components"]
C --> F
D --> F
E --> F
應用程式在根目錄 (main.dart) 被 MultiProvider 包覆。這為整個元件樹提供了核心服務和狀態物件:
StreamProvider<AppUser?>: 監聽 AuthService().user 並廣播當前已驗證使用者的狀態。Provider<AuthService>: 提供身分驗證服務 (authentication service) 單例。Provider<DatabaseService>: 提供資料庫服務 (database service) 單例。Provider<PowerSyncService>: 提供支援離線優先的本機資料庫服務 (PowerSync)。雖然 Riverpod 和 Bloc 功能強大,但專案選擇 Provider 的原因如下:
Selector 和 Consumer,能提供足夠的效能控制。管理進行中考試或練習模式的狀態。
examId、usePowerSync (布林標記)、appUser。RepositoryFactory.getRepository 抽象化資料獲取過程。guest: 僅載入已核准的題目,限制 10 題。pending / publicTester: 僅載入已核准的題目。admin / viewer / internalTester: 載入所有題目。ValueNotifier<int> 代替標準變數來記錄倒數秒數。透過 Timer.periodic 更新 ValueNotifier。UI 層使用 ValueListenableBuilder 訂閱,確保每秒只有計時器元件重建,避免整個頁面觸發重新渲染 (full-page rebuild)。rewardedAdUnlockExpiration 時間戳記,並提供 isExplanationUnlockedByAd getter 以檢查目前是否可透過觀看廣告解鎖解析。currentQuestionIndex 並維護 userAnswers 字典 (map)。處理計時、計分之模擬考的專門邏輯。
correctCount / totalCount)。應用程式利用多個連續串流來管理全域狀態:
為了確保考試期間的流暢效能:
setState()。dispose() 方法會確實取消活動中的計時器、關閉串流並釋放資源,以防止記憶體流失 (memory leaks)。