iT邦幫忙

2026 iThome 鐵人賽

DAY 6
0
Build on Google AI

將考國際證照的應用程式變成開源系列 第 6

offline-sync , payment-system , state-management

  • 分享至 

  • xImage
  •  

07-offline-sync.md

離線同步架構與實作規範

AI 代理程式提醒 (AI Agent Note): 本文件概述認證考試平台中所使用的 PowerSyncConnector 與離線優先 (Offline-First) 資料庫機制。修改 Schema、同步規則或衝突解決策略時請參考此文件。

1. 離線優先架構概覽 (Offline-First Architecture Overview)

設計理念: 本應用程式以「本地端優先」為原則運作。所有的讀寫操作皆直接針對本地端的 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

2. PowerSync 客戶端設定 (PowerSync Client Configuration)

  • SQLCipher 加密: 本地端 SQLite 資料庫在靜態儲存時採用 SQLCipher 進行全加密,保護進階題庫與使用者資料的完整性。
  • Schema 定義 (定義於 PowerSyncService):
    • Questions (題目表):
      • 欄位: examId, questionText, explanation, topic, correctAnswer, options, images, title, questionNumber, explanationTexts, learnEnglish, videoUrl, type, practicalSolution, dragDropData, dragOptions, dropTargets, isApproved
    • Discussions (討論表):
      • 欄位: examId, questionId, userId, username, text, timestamp
  • 初始化: 應用程式啟動時立即建立連線;UI 畫面不會因等待網路回應而阻塞。

3. PowerSync 連接器實作 (PowerSyncConnector)

PowerSyncConnector 繼承自 PowerSyncBackendConnector,負責處理自訂的驗證與雙向同步。

憑證管理 (fetchCredentials)

  • 動作: 透過 AuthService().getIdToken() 取得 Firebase ID Token。
  • 回傳: 將 Token 封裝為 PowerSyncCredentials(endpoint, token)
  • 備援機制: 從 EnvConfig 讀取多組端點/備援對應設定,以防主同步伺服器停機。

資料上傳 (uploadData(database))

負責將本地端的變更推送至遠端後端。

  • 處理流程: 透過 database.getNextCrudTransaction() 取得待處理的交易。
  • 執行: 依序處理本地端的異動 (INSERT, UPDATE, DELETE)。
  • 完成: 成功向伺服器發送 HTTP POST 後,將該筆交易標記為完成。

4. 同步規則設計 (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 (僅下載已經過審核、可用於正式環境的題目)。
    • 訪客角色 (Guest):
      SELECT * FROM questions WHERE isApproved = true AND questionNumber <= 10 (將練習題庫限制在試用範圍內)。
  • 討論區同步:

    • 條件過濾: bucket.user_role != 'guest' (訪客使用者無法下載或瀏覽討論串,以節省頻寬並限制進階功能)。

5. 驗證整合 (Authentication Integration)

  • Firebase Auth: 作為主要身分提供者。產生包含 Custom Claims 的 JWT。
  • Custom Claims (自訂聲明): 包含使用者 roledeviceId,用於決定同步儲存桶的行為。
  • Token 刷新: 自動處理 Token 過期,並在工作階段失效時觸發重新驗證流程。

6. 衝突解決策略 (Conflict Resolution Strategy)

  • 最後寫入者獲勝 (Last-Write-Wins, LWW): 依據伺服器端的時間戳記比對,採用標準 LWW 原則解決衝突。
  • 樂觀更新 (Optimistic Updates): 假設同步將會成功,並立即更新客戶端 UI。
  • 重試邏輯: 若 uploadData 交易失敗 (如傳輸時斷線),SDK 會採用指數退避 (Exponential Backoff) 自動重試。

7. 生物辨識與本地端防護 (Biometric Local Protection)

考量到認證考試內容的專有性質:

  • LocalAuthentication: 整合 iOS FaceID/TouchID 與 Android 生物辨識。
  • FlutterSecureStorage: 安全地儲存 256 位元的資料庫加密金鑰。
  • 存取閘道: 若使用者啟用了 requireBioAuth,在啟動時會跳出生物辨識提示,攔截對本地端資料庫的存取。

8. 效能考量 (Performance Considerations)

  • 初始同步大小估算: 依角色計算。訪客約同步 1MB,而標準使用者可能同步約 50MB 的題庫資料。
  • 增量同步 (Incremental Sync): 後續連線僅同步差異變更 (Deltas),大幅節省頻寬。
  • 背景排程: PowerSync 處理背景保活機制 (Keep-alive),在應用程式處於前景時平順地進行同步。
  • 資源最佳化: 經過專門設計,可節省電池消耗並將行動數據使用量降至最低。

9. 錯誤處理與復原 (Error Handling & Recovery)

  • 網路斷線: 在本地端靜默排隊處理異動。UI 不會中斷。
  • 資料庫毀損: 若 SQLCipher 因金鑰遺失導致解密失敗,應用程式將優雅地清除本地快取,並在驗證後重新啟動完整同步。
  • 除錯工具: 為 internalTesteradmin 角色提供 PowerSyncDebugDialog 元件,以便即時檢視原始交易日誌、佇列大小與同步延遲。

08-payment-system.md

08 - 支付系統 (Payment System)

1. 訂閱模型設計

MockExam 應用程式採用免費增值 (Freemium) 模型,根據使用者的角色和訂閱狀態提供不同層級的功能。

免費與進階功能層級矩陣

功能 免費 (Guest/Pending) 進階 (Viewer) 管理員 (Admin)
題庫存取 僅限已核准 (訪客: 最多10題) 所有考題 所有考題
模擬考試 選項受限 完整自訂功能 完整功能
AI 家教 次數受限 無限制 無限制
廣告 顯示 隱藏 隱藏
錯題複習 每日次數受限 無限制 無限制
討論區 限制存取 完整存取 完整存取
搜尋功能 基本過濾器 基本 + AI 向量搜尋 全部

注意:在參考實作中,特定的權限名稱(如「MockExam CCNA Pro」)僅作為範例,但架構設計支援任何通用的認證考試領域。

2. 雙支付供應商架構

為了確保高可用性、備援能力及管理便利性,系統實作了雙支付供應商架構。

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["角色升級流程"]

為何採用雙供應商?

  • RevenueCat 作為主要供應商,因其具備跨平台訂閱管理便利性以及強大的權限 (Entitlement) 追蹤功能。
  • 原生 Google Play Billing (原生 IAP) 作為備援或替代供應商,在必要時提供無第三方依賴的直接整合。

3. RevenueCat 整合 (主要供應商)

SDK 初始化

  • 動態 API 金鑰選擇:系統根據建置模式動態選擇 API 金鑰。在 kReleaseMode 下使用正式環境金鑰,否則使用測試環境金鑰。
  • 日誌層級設定:在除錯模式下啟用詳細日誌以利問題排查。
  • 即時狀態:註冊 CustomerInfoUpdateListener 來監聽即時的訂閱狀態變更。

使用者綁定

  • login(uid):將 Firebase UID 綁定至 RevenueCat 客戶,確保訂閱能跨裝置跟隨使用者。
  • logout():清除 RevenueCat 工作階段,防止未經授權的存取。

權限 (Entitlement) 驗證

  • 啟用中的權限名稱為可配置 (例如在參考實作中可能為 'MockExam CCNA Pro')。
  • 透過檢查 customerInfo.entitlements.active 進行驗證。

方案顯示與購買流程

  • 應用程式從 RevenueCat 獲取可用的方案並顯示。
  • 使用者選擇方案並觸發 Purchases.purchasePackage()
  • 購買成功後,驗證權限是否啟用。
  • 更新本機使用者狀態 (premiumProductId, premiumPurchaseDate)。

4. 原生 Google Play Billing 整合 (次要供應商)

配置與初始化

  • 產品 ID:可配置的進階產品 ID (例如:mockexam_premium_1month, mockexam_premium_3month)。
  • _initNativeIAP():訂閱 _inAppPurchase.purchaseStream 以處理 PurchaseDetails 更新並載入產品詳細資訊。

購買流程

  1. 透過原生 SDK 載入產品。
  2. 向使用者顯示產品。
  3. 使用者點擊購買,啟動原生購買對話框。
  4. 收到成功回呼後處理購買。
  5. 使用安全的後端驗證收據。

5. 供應商切換機制

架構支援在執行階段動態切換供應商。

  • 管理員面板切換:管理員可以從 RestrictedDefaultsScreen 切換啟用中的供應商。
  • 系統配置廣播systemConfig 的變更會發送動態廣播,立即在整個應用程式中切換啟用中的供應商。
  • 優雅處理:系統確保在切換期間不會突然終止正在進行的交易。

6. 網頁平台支付 (WebPaymentService)

由於 Google Play Billing 無法在網頁版上使用,專屬的 WebPaymentService 負責處理網頁版交易。

  • 提供獨立的支付流程。
  • 與外部閘道 (如 Stripe, PayPal 或自訂後端) 整合。

7. 交易紀錄與稽核

所有交易均安全地記錄以供稽核與分析。

資料模型

  • TransactionRecordid, uid, email, productId, productName, amount, currency, status (pending/completed/failed/refunded), timestamp, purchaseToken

儲存

  • 交易記錄寫入 Firebase RTDB,包含使用者特定路徑 (transactions/{uid}/{transactionId}) 與全域路徑 (systemTransactions/{transactionId})。
  • 管理員交易報告畫面提供營收分析,使用者則可透過交易紀錄畫面查看歷史。

8. 訂閱生命週期管理

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: 解鎖進階功能
  • 續訂與取消:透過 RevenueCat/應用程式商店的 Webhook 至後端處理,更新使用者的到期日。
  • 過期:在將使用者角色降級為 'pending' 或 'guest' 之前會給予寬限期。
  • 還原購買:使用者可透過 restorePurchases 流程無縫還原跨裝置訂閱。

9. 角色升級流程

在成功購買事件發生後:

  1. 驗證權限或收據。
  2. AppUser 角色更新為 'viewer' (或指定的進階角色)。
  3. 設定 premiumProductIdpremiumPurchaseDate
  4. 移除所有廣告。
  5. 解鎖進階功能 (無限制 AI 家教、完整模擬考試等)。
  6. 持久化儲存 TransactionRecord

10. 測試與除錯

  • API 金鑰:嚴格區分測試與正式環境的 API 金鑰。
  • Google Play:測試軌道對齊,確保沙盒帳號可以購買且不產生實際扣款。
  • 沙盒測試:充分利用 RevenueCat 沙盒環境。
  • 廣告測試:每位使用者可配置 showTestAds 標籤,並註冊測試廣告裝置 ID 以符合 AdMob 規範。

11. 錯誤處理

  • 購買失敗:針對使用者取消或付款遭拒提供清晰的使用者介面回饋。
  • 網路中斷:交易保持在 'pending' 狀態,並在重新連線時重試。
  • 計費客戶端斷線:原生 IAP 的自動重試與重新連線邏輯。
  • 重複購買:系統嚴格防止對已啟用的權限進行重複購買。
  • 退款:Webhook 處理退款,自動撤銷進階存取權限並記錄 'refunded' 狀態。

09-state-management.md

狀態管理架構 (State Management Architecture)

1. 系統概述 (AI Agent Context)

本文件概述了認證考試 (certification exam) 應用程式的狀態管理架構。系統主要使用 ProviderChangeNotifier 模式。選擇此方法的考量包含其與 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

2. Provider/ChangeNotifier 架構

MultiProvider 設定

應用程式在根目錄 (main.dart) 被 MultiProvider 包覆。這為整個元件樹提供了核心服務和狀態物件:

  • StreamProvider<AppUser?>: 監聽 AuthService().user 並廣播當前已驗證使用者的狀態。
  • Provider<AuthService>: 提供身分驗證服務 (authentication service) 單例。
  • Provider<DatabaseService>: 提供資料庫服務 (database service) 單例。
  • Provider<PowerSyncService>: 提供支援離線優先的本機資料庫服務 (PowerSync)。

為什麼選擇 Provider?

雖然 Riverpod 和 Bloc 功能強大,但專案選擇 Provider 的原因如下:

  1. 簡單性: 管理表單和考試狀態的樣板程式碼較少。
  2. 精細重建: 結合 SelectorConsumer,能提供足夠的效能控制。
  3. 服務定位器 (Service Locator): 它可以有效地作為儲存庫 (repositories) 和服務 (services) 的依賴注入容器。

3. 核心控制器 (Core Controllers)

ExamController (ChangeNotifier)

管理進行中考試或練習模式的狀態。

  • 輸入參數: examIdusePowerSync (布林標記)、appUser
  • 資料存取: 使用 RepositoryFactory.getRepository 抽象化資料獲取過程。
  • 基於角色的過濾 (Role-Based Filtering):
    • guest: 僅載入已核准的題目,限制 10 題。
    • pending / publicTester: 僅載入已核准的題目。
    • admin / viewer / internalTester: 載入所有題目。
  • 計時器管理: 使用 ValueNotifier<int> 代替標準變數來記錄倒數秒數。透過 Timer.periodic 更新 ValueNotifier。UI 層使用 ValueListenableBuilder 訂閱,確保每秒只有計時器元件重建,避免整個頁面觸發重新渲染 (full-page rebuild)。
  • 廣告解鎖: 管理 rewardedAdUnlockExpiration 時間戳記,並提供 isExplanationUnlockedByAd getter 以檢查目前是否可透過觀看廣告解鎖解析。
  • 導覽: 追蹤 currentQuestionIndex 並維護 userAnswers 字典 (map)。

MockExamController (ChangeNotifier)

處理計時、計分之模擬考的專門邏輯。

  • 題目選擇: 從過濾後的題庫中隨機選擇題目,以模擬真實考試環境。
  • 設定: 允許根據考試類型自訂考試時間長度。
  • 計分: 計算最終分數 (correctCount / totalCount)。
  • 報表: 產生考後報表,包含每題的分析與主題分佈。
  • 自動交卷: 當計時器歸零時自動提交考試。

4. 全域狀態串流 (Global State Streams)

應用程式利用多個連續串流來管理全域狀態:

  • AppUser Stream: 在整個應用程式中廣播即時的身分驗證狀態變化。
  • SystemConfig Stream: 提供來自 Firebase Remote Config 或 RTDB 的動態系統設定。
  • 螢幕分享偵測串流 (Screen Share Detection Stream): 每 3 秒輪詢一次的串流,用於偵測使用者是否正在分享螢幕(基於安全防護與防作弊目的)。

5. 效能防護機制 (Performance Safeguards - CRITICAL)

為了確保考試期間的流暢效能:

  • Base64ImageWidget: Base64 字串只解碼一次,將結果快取於記憶體中,並在元件重建時重複使用,以防止昂貴的重複解碼運算。
  • ValueListenableBuilder 用於計時器: 確保在進行考試時,每秒只有單一元件會進行精準重建,而不是在整個頁面上呼叫 setState()
  • 延遲載入 (Lazy Loading): 題目及其大型資源(圖片、影片)皆採隨選載入 (on-demand)。
  • Dispose 清理: 控制器中的 dispose() 方法會確實取消活動中的計時器、關閉串流並釋放資源,以防止記憶體流失 (memory leaks)。

上一篇
運用 Google Skills 新推出的 ADK 2.0 在應用程式內
下一篇
使用 Google ADK 和 Cloud Run,在 Streamlit 中部署 RAG AI 代理程式
系列文
將考國際證照的應用程式變成開源12
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言