01-system-overview
雙層架構說明 (Dual-Layer Context)
- AI 代理規範 (AI Agent Specification):本文檔提供機器可解析的架構限制、依賴樹狀圖與技術堆疊定義。
- 人類開發者背景 (Human Context):為新進開發者提供應用程式生態系、必要建置環境與模組配置的高階視角。
本認證考試應用程式採用現代化、離線優先 (Offline-first) 的行動開發技術堆疊。(例如,在網路認證考試的參考實作中,此架構可確保高可用性與安全性)。
| 類別 | 技術 | 用途 |
|---|---|---|
| 前端 | Flutter 3.x / Dart 3.x | 跨平台使用者介面 (UI) 開發。 |
| 資料庫 | Firebase RTDB | 儲存即時使用者狀態與系統設定。 |
| Supabase PostgreSQL | 關聯式資料與支援語意搜尋的 pgvector。 |
|
| MongoDB Atlas | 非結構化日誌 (Logs) 與效能指標分析。 | |
| PowerSync (SQLCipher) | 加密本地 SQLite,實現離線優先同步功能。 | |
| 身分驗證 | Firebase Auth | 支援 Google、電子郵件/密碼、匿名登入。 |
| AI 引擎 | Gemini 2.5 Flash (Cloud) | 雲端進階大型語言模型 (LLM),提供家教功能。 |
| Gemma 4 LiteRT-LM (Local) | 裝置端備援模型 (2B 參數),支援離線 AI。 | |
| 金流與變現 | RevenueCat + Google Play | 訂閱管理與原生 v8+ 應用程式內購買 (IAP)。 |
| Google AdMob | 廣告投放 (橫幅、插頁、獎勵、原生、開屏)。 | |
| 診斷與監控 | Firebase Crashlytics | 原生與 Dart 層級的崩潰 (Crash) 報告。 |
| Shake SDK | 應用程式內部的錯誤回報與使用者回饋。 | |
| 國際化 | easy_localization | 多國語系支援 (EN, JA, zh-TW, zh-CN)。 |
| 系統安全 | safe_device, local_auth | 越獄 (Jailbreak)/Root 偵測與生物辨識。 |
| check_vpn_connection | 網路安全與 VPN 連線偵測。 | |
| screen_protector | 防截圖與螢幕共享偵測。 | |
| 媒體與機器學習 | flutter_tts, speech_to_text | 語音模式的文字轉語音 (TTS) / 語音轉文字 (STT)。 |
| google_mlkit_text_recognition | 使用者上傳圖片的光學字元辨識 (OCR)。 |
graph TD
User["終端使用者 (End User)"] --> App["認證考試應用程式 (Exam App)"]
subgraph "外部雲端服務 (External Cloud Services)"
App --> Firebase["Firebase (Auth, RTDB, Crashlytics)"]
App --> Supabase["Supabase (PostgreSQL + pgvector)"]
App --> Mongo["MongoDB Atlas"]
App --> GooglePlay["Google Play Billing"]
App --> RevenueCat["RevenueCat SDK"]
App --> Gemini["Google Gemini API"]
App --> AdMob["Google AdMob"]
end
subgraph "裝置端服務 (On-Device Services)"
App --> PowerSync["PowerSync + SQLCipher (加密本地資料庫)"]
App --> Gemma["Gemma 4 LiteRT-LM (邊緣 AI)"]
end
graph TD
Presentation["lib/screens/ & lib/widgets/ (表現層)"] --> Controllers["lib/controllers/ (狀態管理)"]
Controllers --> Services["lib/services/ (單例服務)"]
Controllers --> Repositories["lib/repositories/ (資料存取)"]
Services --> Utils["lib/utils/ (工具模組)"]
Repositories --> Models["lib/models/ (資料模型)"]
Models --> DataSources["Data Source Layer (資料來源層)"]
應用程式嚴格遵守以下 lib/ 目錄佈局:
lib/
├── main.dart # 應用程式進入點
├── constants/ # 全域常數、主題設定、列舉 (Enums)
├── controllers/ # 狀態管理 (ChangeNotifier providers)
├── models/ # 資料結構 (Question, AppUser, Exam 等)
├── repositories/ # 資料存取介面與工廠模式 (Factory)
├── screens/ # 26 個 UI 畫面 (例:DashboardScreen)
├── services/ # 26 個單例服務 (例:AuthService)
├── utils/ # 輔助函式、擴充功能、格式化工具
└── widgets/ # 可共用的 UI 元件 (11 個核心, 13 個考試專用)
為確保建置的一致性,必須符合以下環境規格:
minSdk: 30 (Android 11)compileSdk: 最新穩定版02-architecture.md
雙層架構說明 (Dual-Layer Context)
- AI 代理規範 (AI Agent Specification):本文檔定義了設計模式、流程順序、路由閘道以及嚴格的應用程式分層。任何程式碼修改都必須符合這些架構約束。
- 人類開發者背景 (Human Context):了解不同元件之間的通訊方式、導航安全的執行機制,以及如何最佳化初始化流程以避免 UI 阻塞。
本應用程式採用嚴格的五層式架構以確保關注點分離 (Separation of Concerns)。(在現代化的認證考試題庫應用程式中,這是處理複雜狀態與離線/連線資料的標準做法)。
graph TD
subgraph "五層式架構 (5-Layer Architecture)"
UI["表現層 (Presentation Layer: screens/, widgets/)"]
Controller["控制器層 (Controller Layer: controllers/ - ChangeNotifier)"]
Service["服務層 (Service Layer: services/ - 單例模式)"]
Repository["儲存庫層 (Repository Layer: repositories/ - 工廠模式)"]
DataSource["資料來源層 (Data Source Layer: Firebase, Supabase, PowerSync)"]
UI --> Controller
Controller --> Service
Controller --> Repository
Repository --> DataSource
Service --> DataSource
end
架構大量採用成熟的設計模式以維持系統的擴充性:
RepositoryFactory 類別處理。它負責根據系統設定或網路連線狀態動態切換資料提供者 (Provider)。PaymentService, DeviceSecurityService) 皆實作為單例。存取方式嚴格限制為 ServiceName.instance。main.dart 中的 MultiProvider 設定進行。控制器 (Controllers) 繼承 ChangeNotifier,負責將狀態更新廣播至表現層 (UI)。examId 的前綴與標準 systemConfig 動態切換資料來源 (例如,從 Supabase 獲取進階考題,從 Firebase 獲取使用者設定)。所有的導航流程都必須通過一個中央安全閘道:AuthWrapper。此元件負責在呈現應用程式儀表板前,確保系統安全性未受到損害。
該閘道強制執行嚴格的 七步驟驗證鏈 (7-step verification chain):
graph TD
Start["AuthWrapper (安全閘道)"] --> Step1{"1. 裝置安全 (Device Security)"}
Step1 -- "通過" --> Step2{"2. NTP 時間驗證 (NTP Time)"}
Step2 -- "通過" --> Step3{"3. 身分驗證狀態 (Auth State)"}
Step3 -- "已登入" --> Step4{"4. 單一裝置登入 (Single Device)"}
Step4 -- "通過" --> Step5{"5. 帳戶到期日 (Account Expiry)"}
Step5 -- "有效" --> Step6{"6. 平台限制 (Platform Restriction)"}
Step6 -- "通過" --> Dashboard["DashboardScreen (儀表板)"]
Step1 -- "失敗 (Root/模擬器/分享)" --> Reject1["DeviceSecurityScreen"]
Step2 -- "失敗 (時間回滾)" --> Reject2["UnauthorizedScreen"]
Step3 -- "未登入" --> Reject3["LoginScreen (登入畫面)"]
Step4 -- "裝置 ID 不符" --> Reject4["UnauthorizedScreen"]
Step5 -- "Viewer 過期" --> Downgrade["自動降級為 Pending 權限"]
Step6 -- "平台不允許" --> Reject6["UnauthorizedScreen"]
系統在多個層級優雅地攔截錯誤:
由於應用程式包含較重的依賴項 (AI 模型、SQLite 加密、金流 SDK),main.dart 中的初始化流程採用延遲渲染 (Delayed Rendering) 技術,以防止作業系統層級的 ANR (應用程式無回應)。
sequenceDiagram
participant Main as main.dart
participant Services as 核心服務 (Core Services)
participant UI as 根視圖 (Root Widget / MultiProvider)
participant Post as PostFrameCallback (延遲執行)
Main->>Services: EasyLocalization.ensureInitialized()
Main->>Services: 初始化 FlutterGemma + LiteRtLmEngine (非 Web 環境)
Main->>Services: Firebase.initializeApp() & Supabase.initialize()
Main->>Services: 註冊全域錯誤處理常式 (Global Error Handlers)
Main->>Services: DeviceSecurityService 安全檢查 (2 秒超時限制)
Main->>UI: runApp(EasyLocalization -> MultiProvider -> ErrorBoundary -> ObscuredBlockerScreen)
UI->>Post: WidgetsBinding.instance.addPostFrameCallback
Note over Post: 延遲初始化非關鍵性 SDK
Post->>Post: 初始化 Shake SDK
Post->>Post: 初始化 Crashlytics
Post->>Post: 初始化 AdMob
Post->>Post: 初始化 RevenueCat
Post->>Post: 初始化 Screen Protector (防截圖)
Post->>Post: 初始化 TTS & Speech-to-Text
使用 addPostFrameCallback 可將非關鍵的 SDK (如 AdMob, RevenueCat, Shake) 的初始化延後至初始 Flutter 渲染幀 (Frame) 之後。此策略能夠打破啟動期間同步佔用 CPU 的狀況,完全避免低階裝置上發生 ANR 死結,同時確保 ObscuredBlockerScreen (啟動畫面 UI) 能立即被渲染。
03-data-models.md
本文檔詳細說明了通用考試題庫應用程式中使用的核心資料模型。此文件同時作為人類開發者與 AI Agent 的權威綱要參考。
Question 模型代表考試題庫中的單一測驗題目。
id: int - 唯一識別碼 (例如從 'q100' 格式解析而來)。examId: String - 認證考試識別碼 (預設:參考實作中為 'CCNA',但可為任何考試代碼)。type: QuestionType (列舉) - 題目類型:SINGLE_CHOICE (單選題)、MULTIPLE_CHOICE (複選題)、DRAG_AND_DROP (拖曳題)、SIMULATION (模擬題)。title: String - 題目的主要文字或敘述。questionImage: String? - 題目附帶圖片的選用檔案名稱/路徑。options: List<String>? - 單選/複選題的可用選項。dragOptions: List<DragAndDropOption>? - 可供拖曳的選項 (id, content)。dropTargets: List<DropTarget>? - 選項可以放置的目標位置 (id, label)。correctAnswer: dynamic - 正確答案。結構依類型而異:
int 適用於 SINGLE_CHOICE
List<int> 適用於 MULTIPLE_CHOICE
Map<String,String> 適用於 DRAG_AND_DROP
String 適用於 SIMULATION
explanation: String - 正確答案的詳細解析。discussion: List<Comment> - 與該考題相關的使用者評論與討論。topic: int - 考試領域或主題分類 (例如 Domain 1-6)。communityVotes: Map<String, int>? - 該考題的社群投票資料。isApproved: bool - 審核狀態,預設為 false。questionNumber: int? - 特定考卷中考題的流水號。images: List<String>? - 與考題相關的額外圖片。explanationTexts: Map<String, String>? - 支援多國語言的解析文字。learnEnglish: String? - AI 生成的考題文字英文學習分析。videoUrl: String? - AI 生成的解說影片連結。videoStatus: String? - 影片生成狀態 (null, generating, completed, failed)。toMap(): 將物件序列化為 Map 以供資料庫儲存。列舉會轉換為字串,而巢狀物件 (如評論) 會映射為其對應的 Map 表示。fromMap(Map<String, dynamic> map): 工廠建構子,用於從資料庫輸出實例化 Question。classDiagram
class Question {
+int id
+String examId
+QuestionType type
+String title
+String? questionImage
+List~String~? options
+List~DragAndDropOption~? dragOptions
+List~DropTarget~? dropTargets
+dynamic correctAnswer
+String explanation
+List~Comment~ discussion
+int topic
+Map~String, int~? communityVotes
+bool isApproved
+int? questionNumber
+List~String~? images
+Map~String, String~? explanationTexts
+String? learnEnglish
+String? videoUrl
+String? videoStatus
+toMap() Map
+fromMap(Map) Question
}
AppUser 模型處理使用者個人檔案、角色及應用程式層級的偏好設定。
admin: 完整管理員權限。viewer: 僅具備進階/內部功能的檢視權限。pending: 帳號等待審核中。internalTester: 可存取發布前功能與未審核考題。publicTester: 具有特定限制的 Beta 測試人員。guest: 未驗證或具最低存取權限的訪客。firebaseUser: User - 底層 Firebase Authentication 的使用者物件。username: String - 顯示名稱。role: UserRole - 指派的使用者角色。picture: String? - 個人資料圖片 URL。roleDescription: String - 本地化的使用者角色描述。expiryDate: DateTime? - 進階會員或存取權限到期日。activeDeviceId: String? - 綁定 ID 以強制單一裝置使用。allowedPlatforms: List<String> - 允許平台的白名單 (例如 ["ios", "android"])。allowedDatabases: List<String> - 使用者可連線的資料庫白名單。requireBioAuth: bool - 強制生物辨識驗證的旗標 (預設:true)。isGoogleUser: bool - 若透過 Google 進行驗證則為 true。isAnonymous: bool - 若以訪客身分登入則為 true。premiumProductId: String? - 生效中的進階訂閱產品識別碼。premiumPurchaseDate: DateTime? - 進階方案購買日期。showTestAds: bool - 顯示測試廣告的旗標 (預設:true)。email: String? - 衍生自 firebaseUser.email 的 getter。| 功能 / 能力 | admin | viewer | pending | internalTester | publicTester | guest |
|---|---|---|---|---|---|---|
| 登入 / 基本驗證 | 是 | 是 | 是 | 是 | 是 | 是 |
| 檢視已審核考題 | 是 | 是 | 否 | 是 | 是 | 限制 |
| 檢視未審核考題 | 是 | 是 | 否 | 是 | 否 | 否 |
| 編輯考題 | 是 | 否 | 否 | 否 | 否 | 否 |
| 發布評論 | 是 | 否 | 否 | 是 | 是 | 否 |
| 進行購買 | 不適用 | 不適用 | 否 | 否 | 是 | 是 |
| 存取管理員面板 | 是 | 是 | 否 | 否 | 否 | 否 |
classDiagram
class AppUser {
+User firebaseUser
+String username
+UserRole role
+String? picture
+String roleDescription
+DateTime? expiryDate
+String? activeDeviceId
+List~String~ allowedPlatforms
+List~String~ allowedDatabases
+bool requireBioAuth
+bool isGoogleUser
+bool isAnonymous
+String? premiumProductId
+DateTime? premiumPurchaseDate
+bool showTestAds
+String? email
}
代表完整的考試集合。
id: String - 唯一識別碼。vendor: String - 認證廠商 (例如 Cisco, AWS)。title: String - 考試標題。questions: List<Question> - 相關考題的完整清單。lastUpdated: DateTime - 最後修改時間戳記。用於清單與導覽的輕量級模型。
id: String
vendor: String
title: String
lastUpdated: DateTime
questionCount: int
isApproved: bool
處理購買紀錄與進階功能解鎖。
pending, completed, failed, refunded
id: String - 唯一交易 ID。uid: String - 進行購買的使用者 ID。email: String? - 使用者電子郵件。productId: String - 來自商店的產品識別碼。productName: String - 易於閱讀的產品名稱。amount: double - 購買金額。currency: String - ISO 貨幣代碼。status: TransactionStatus
timestamp: DateTime - 交易時間。purchaseToken: String - 來自 Google Play / App Store 的驗證權杖。時間戳記使用 millisecondsSinceEpoch 轉換為整數與還原,以確保跨 NoSQL 與關聯式資料庫 (SQL) 儲存的相容性。
id: String - 唯一識別碼。user: String - 評論者的使用者名稱。comment: String - 評論本文。timestamp: int - 建立時間 (自 epoch 以來的毫秒數)。upvotes: int - 獲得的推文(讚)數。uid: String - 評論者的使用者 ID。copyWith(): 建立該實例修改後複本的方法。在 Firebase RTDB、Supabase PostgreSQL、MongoDB Atlas 與 PowerSync 之間同步時,映射必須嚴格遵守下表。
| Dart 模型 (camelCase) | SQL/PowerSync (snake_case) | Firebase RTDB (camelCase) |
|---|---|---|
id |
id |
id |
examId |
exam_id |
examId |
questionImage |
question_image |
questionImage |
correctAnswer |
correct_answer |
correctAnswer |
isApproved |
is_approved |
isApproved |
questionNumber |
question_number |
questionNumber |
explanationTexts |
explanation_texts |
explanationTexts |
learnEnglish |
learn_english |
learnEnglish |
videoUrl |
video_url |
videoUrl |
videoStatus |
video_status |
videoStatus |
activeDeviceId |
active_device_id |
activeDeviceId |
allowedPlatforms |
allowed_platforms |
allowedPlatforms |
requireBioAuth |
require_bio_auth |
requireBioAuth |
premiumProductId |
premium_product_id |
premiumProductId |
注意:Firebase RTDB 依賴與 Dart 模型完全一致的 camelCase 命名,而關聯式資料庫 (SQL) 則需要在 Repository 層中翻譯為 snake_case。