大家好,歡迎來到第二十天!昨天,我們成功打通了從拍照到記帳的完整智慧流程,實現了「省錢拍拍」的核心功能。我們的 App 現在已經是一個非常高效的資料輸入工具。
但我們不禁要問:辛苦記帳的目的,僅僅是為了記錄嗎?當然不是。記帳的最終目的是為了從資料中學習,發現我們的消費模式,並做出更好的財務決策。
今天,我們將讓 App 的 AI 大腦再次升級。我們不再滿足於分析單張發票,而是要將使用者最近的一批消費紀錄打包發送給 Gemini,並命令它扮演一位個人理財顧問,為我們產生一份客製化的消費分析報告與省錢建議。
要進行分析,AI 首先需要數據。我們需要在 FirestoreService
中新增一個方法,專門用來獲取使用者最近的 N 筆交易紀錄。這次我們使用 .get() 來做一次性讀取,因為分析報告不需要像主頁列表那樣即時更新。
打開 lib/services/firestore_service.dart
,加入新方法 getRecentTransactions
:
Future<List<Transaction>> getRecentTransactions({
required String userId,
int limit = 20, // 預設讀取最近 20 筆
}) async {
try {
final snapshot =
await _usersCollection
.doc(userId)
.collection('transactions')
.orderBy('date', descending: true)
.limit(limit)
.get(); // 使用 .get() 進行一次性讀取
// 將讀取到的文件使用我們模型的 fromFirestore 方法,轉換為 Transaction 物件列表
return snapshot.docs
// 使用 hide 關鍵字 「隱藏」某個套件中的特定類別,以消除衝突。
.map((doc) => Transaction.fromFirestore(doc))
.toList();
} catch (e) {
print("讀取近期交易失敗: $e");
return []; // 發生錯誤時回傳空列表
}
}
分析型的 Prompt 與擷取型的 Prompt 在思維上完全不同。我們不再是要求 AI 找特定欄位,而是要求它進行歸納、總結和建議。
打開 lib/services/gemini_service.dart,加入新方法 getExpenseAnalysis:
Future<String?> getExpenseAnalysis(List<Transaction> transactions) async {
if (transactions.length < 5) {
// 資料太少就沒有分析意義
return '消費紀錄不足 5 筆,暫時無法進行分析。';
}
try {
final model = GenerativeModel(
model: 'gemini-1.5-flash-latest',
apiKey: _apiKey,
);
final transactionsText = transactions
.map(
(t) =>
"日期: ${t.date.toIso8601String().substring(0, 10)}, 分類: ${t.category}, 金額: ${t.amount.toInt()}",
)
.join('\n');
final prompt = '''
你是一位專業且友善的個人理財顧問。
以下是我最近的一些消費紀錄,格式為 "日期, 分類, 金額"。
--- 消費紀錄開始 ---
$transactionsText
--- 消費紀錄結束 ---
請根據以上資料,嚴格遵循以下指示,用繁體中文為我提供一份簡短(總字數不超過 200 字)的消費分析:
1. 分析的開頭,請用一個 emoji 和一句鼓勵的話開始。
2. 找出我花費最多的前三大消費類別,並用條列式列出。
3. 根據我的消費習慣,提供一個最直接、最實用的省錢建議。
4. 結尾請保持正向鼓勵。
''';
final content = [Content.text(prompt)];
final response = await model.generateContent(content);
return response.text;
} catch (e) {
print('Gemini 分析請求失敗: $e');
return 'AI 分析失敗,請稍後再試。';
}
}
List<Transaction>
轉換為一個簡潔的純文字字串,方便 AI 閱讀。我們需要一個新頁面來呈現 AI 生成的分析報告。
// lib/analysis_page.dart
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:snapsaver/services/firestore_service.dart';
import 'package:snapsaver/services/gemini_service.dart';
class AnalysisPage extends StatefulWidget {
const AnalysisPage({super.key});
@override
State<AnalysisPage> createState() => _AnalysisPageState();
}
class _AnalysisPageState extends State<AnalysisPage> {
bool _isLoading = true;
String? _analysisResult;
final FirestoreService _firestoreService = FirestoreService();
final GeminiService _geminiService = GeminiService();
@override
void initState() {
super.initState();
_fetchAndAnalyzeData(); // 頁面一載入就開始分析
}
Future<void> _fetchAndAnalyzeData() async {
final user = FirebaseAuth.instance.currentUser;
if (user == null) {
setState(() {
_isLoading = false;
_analysisResult = '錯誤:使用者未登入。';
});
return;
}
// 1. 從 Firestore 獲取資料
final transactions = await _firestoreService.getRecentTransactions(userId: user.uid);
if (!mounted) return;
// 2. 將資料交給 Gemini 分析
final result = await _geminiService.getExpenseAnalysis(transactions);
if (!mounted) return;
// 3. 更新 UI
setState(() {
_analysisResult = result;
_isLoading = false;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('智慧消費分析')),
body: Center(
child: _isLoading
? const CircularProgressIndicator()
: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Card(
elevation: 4.0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Text(
_analysisResult ?? '無法產生分析報告。',
style: Theme.of(context).textTheme.bodyLarge?.copyWith(height: 1.5),
),
),
),
),
),
);
}
}
最後一步,讓我們把 Day 5 建立的「查看紀錄」按鈕,賦予它新的生命!
打開 lib/main.dart,找到 _HomePageState 中對應的按鈕,為它加上導航功能。
// lib/main.dart
import 'package:snapsaver/analysis_page.dart'; // 導入新頁面
// ... _HomePageState -> build -> 中間功能按鈕區塊 ...
// 找到代表「查看紀錄」的那個 Column
child: GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const AnalysisPage()),
);
},
child: const Column(
children: [
Icon(Icons.bar_chart, size: 40, color: Colors.teal), // 圖示可以換成 bar_chart
Text('智慧分析'), // 文字也可以更新
],
),
),
// ...
讓我們重啟 App,確保你已經有好幾筆消費紀錄,然後點擊「智慧分析」按鈕。稍待片刻,你就能看到 Gemini 為你量身打造的專屬消費分析報告了!
今日結語
今天,我們的 AI 夥伴正式從「資料擷取員」晉升為了「理財分析師」。我們成功地:
「省錢拍拍」不再只是一個被動的記帳工具,它已經開始能主動地為使用者提供價值。
至此,我們已經完成了從 UI 設計、後端整合到 AI 賦能的完整開發旅程。在接下來的鐵人賽最終篇章中,我們將會聚焦於專案的打磨與完善,例如處理更多的錯誤邊界、優化 UI/UX、以及為最終上架做準備。