欢迎来到第十期,也是本系列的第一阶段的收官之作!
到这里,你已经掌握了:
- Episode 01-04:Python 基础(语法、数据结构、OOP、装饰器/生成器)
- Episode 05:爬虫入门
- Episode 06:数据分析(NumPy / Pandas / Matplotlib)
- Episode 07:Web 后端开发(FastAPI + RESTful API)
- Episode 08:AI 实战(LangChain + RAG)
- Episode 09:前端入门(React + 调用 API)
**但知识如果不串联起来,就只是散落的珍珠。** 本期的目标就是把所有珍珠串成一条项链——**从零搭建一个完整的数据分析 Dashboard 项目**。
>**前置知识**:需要熟悉本系列前面 9 期的内容。这个项目将综合运用所有知识点。
---
## 10.1 项目概述
我们要做一个名为 **"FinanceFlow"** 的个人财务管理 Dashboard。
### 功能清单
```
✅ 数据录入:支持添加收入/支出记录(分类、金额、备注、日期)
✅ 数据查询:按时间、分类、类型筛选和分页
✅ 数据可视化:
- 月度收支趋势折线图
- 分类占比饼图
- 每日/每周/每月切换视图
- 余额变化曲线
✅ AI 智能分析:输入自然语言,AI 生成分析报告
✅ 数据导出:支持导出 CSV / PDF 报告
✅ 响应式设计:桌面端和移动端均可使用
```
### 技术栈
```
前端:React + Vite + Chart.js
后端:FastAPI + SQLite
AI:LangChain + RAG
部署:本地开发 → 一键部署到云服务器
```
---
## 10.2 第一步:搭建后端(FastAPI + SQLite)
### 10.2.1 项目结构
```
financeflow/
├── backend/
│ ├── main.py # FastAPI 入口
│ ├── models.py # Pydantic 数据模型
│ ├── database.py # 数据库连接与管理
│ ├── crud.py # 增删改查操作
│ └── ai.py # AI 分析接口
├── frontend/
│ └── ... # React 项目
└── knowledge-base/ # AI 知识库文档
└── finance_guide.md
```
### 10.2.2 数据库模型
```python
# backend/database.py
import sqlite3
from datetime import date
from pathlib import Path
DB_PATH = Path(__file__).parent / "financeflow.db"
defget_connection():
"""获取数据库连接"""
conn = sqlite3.connect(DB_PATH)
conn.execute("PRAGMA journal_mode=WAL") # 提高并发性能
conn.row_factory = sqlite3.Row
return conn
definit_db():
"""初始化数据库表"""
conn = get_connection()
with conn:
conn.executescript("""
-- 交易记录表
CREATE TABLE IF NOT EXISTS transactions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
amount REAL NOT NULL CHECK(amount > 0),
type TEXT NOT NULL CHECK(type IN ('收入', '支出')),
category TEXT NOT NULL,
subcategory TEXT DEFAULT '',
note TEXT DEFAULT '',
transaction_date DATE NOT NULL DEFAULT CURRENT_DATE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 分类配置表
CREATE TABLE IF NOT EXISTS categories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
type TEXT NOT NULL CHECK(type IN ('收入', '支出')),
color TEXT DEFAULT '#4caf50',
icon TEXT DEFAULT '📦',
parent_id INTEGER DEFAULT NULL
);
-- 初始化预设分类
INSERT OR IGNORE INTO categories (name, type, color, icon) VALUES
('工资', '收入', '#4caf50', '💼'),
('奖金', '收入', '#66bb6a', '🎁'),
('投资收益', '收入', '#81c784', '📈'),
('副业收入', '收入', '#a5d6a7', '💻'),
('餐饮', '支出', '#ef5350', '🍜'),
('交通', '支出', '#ff7043', '🚗'),
('购物', '支出', '#ff8a65', '🛒'),
('住房', '支出', '#ffa726', '🏠'),
('娱乐', '支出', '#ffb74d', '🎮'),
('医疗', '支出', '#ffca28', '💊'),
('教育', '支出', '#fff176', '📚'),
('通讯', '支出', '#aed581', '📱');
""")
conn.close()
# 运行时初始化
init_db()
```
### 10.2.3 CRUD 操作层
```python
# backend/crud.py
from database import get_connection
from datetime import date
defadd_transaction(amount: float, tx_type: str, category: str,
subcategory: str = "", note: str = "", tx_date: str = None) -> int:
"""添加交易记录"""
conn = get_connection()
with conn:
cursor = conn.execute(
"""INSERT INTO transactions (amount, type, category, subcategory, note, transaction_date)
VALUES (?, ?, ?, ?, ?, COALESCE(?, DATE('now')))""",
(amount, tx_type, category, subcategory, note, tx_date)
)
conn.commit()
return cursor.lastrowid
defget_transactions(tx_type: str = None, category: str = None,
start_date: str = None, end_date: str = None,
limit: int = 50, offset: int = 0) -> dict:
"""获取交易记录(支持筛选和分页)"""
conn = get_connection()
query = "SELECT * FROM transactions WHERE 1=1"
params = []
if tx_type:
query += " AND type = ?"
params.append(tx_type)
if category:
query += " AND category = ?"
params.append(category)
if start_date:
query += " AND transaction_date >= ?"
params.append(start_date)
if end_date:
query += " AND transaction_date <= ?"
params.append(end_date)
query += " ORDER BY transaction_date DESC LIMIT ? OFFSET ?"
params.extend([limit, offset])
rows = conn.execute(query, params).fetchall()
transactions = [dict(row) for row in rows]
# 获取总数
count_query = "SELECT COUNT(*) FROM transactions WHERE 1=1"
count_params = []
if tx_type:
count_query += " AND type = ?"
count_params.append(tx_type)
if category:
count_query += " AND category = ?"
count_params.append(category)
if start_date:
count_query += " AND transaction_date >= ?"
count_params.append(start_date)
if end_date:
count_query += " AND transaction_date <= ?"
count_params.append(end_date)
total = conn.execute(count_query, count_params).fetchone()[0]
return {
"transactions": transactions,
"total": total,
"limit": limit,
"offset": offset,
"pages": (total + limit - 1) // limit if total > 0else1
}
defget_monthly_summary(year: int, month: int) -> dict:
"""获取月度统计"""
conn = get_connection()
prefix = f"{year}-{month:02d}-"
income = conn.execute(
"SELECT COALESCE(SUM(amount), 0) FROM transactions WHERE type='收入' AND transaction_date LIKE ?",
(prefix + "%",)
).fetchone()[0]
expense = conn.execute(
"SELECT COALESCE(SUM(amount), 0) FROM transactions WHERE type='支出' AND transaction_date LIKE ?",
(prefix + "%",)
).fetchone()[0]
# 按分类统计支出
category_breakdown = conn.execute(
"""SELECT category, SUM(amount) as total
FROM transactions
WHERE type='支出' AND transaction_date LIKE ?
GROUP BY category ORDER BY total DESC""",
(prefix + "%",)
).fetchall()
return {
"year": year,
"month": month,
"income": round(income, 2),
"expense": round(expense, 2),
"balance": round(income - expense, 2),
"category_breakdown": [dict(row) for row in category_breakdown]
}
defget_daily_trend(days: int = 30) -> dict:
"""获取每日趋势(用于折线图)"""
conn = get_connection()
# 获取近 N 天的数据
rows = conn.execute(
"""SELECT transaction_date,
SUM(CASE WHEN type='收入' THEN amount ELSE 0 END) as daily_income,
SUM(CASE WHEN type='支出' THEN amount ELSE 0 END) as daily_expense
FROM transactions
WHERE transaction_date >= DATE('now', ? || ' days')
GROUP BY transaction_date
ORDER BY transaction_date""",
(f"-{days}",)
).fetchall()
return {
"dates": [row["transaction_date"] for row in rows],
"income": [round(row["daily_income"], 2) for row in rows],
"expense": [round(row["daily_expense"], 2) for row in rows]
}
```
### 10.2.4 FastAPI 路由层
```python
# backend/main.py
from fastapi import FastAPI, HTTPException, Query
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field
from typing import Optional
from crud import add_transaction, get_transactions, get_monthly_summary, get_daily_trend
app = FastAPI(
title="FinanceFlow API",
description="个人财务管理 Dashboard 后端服务",
version="1.0.0"
)
# 开启 CORS
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# ============ 数据模型 ============
classTransactionCreate(BaseModel):
amount: float = Field(..., gt=0, description="金额")
type: str = Field(..., pattern="^(收入|支出)$", description="类型:收入 或 支出")
category: str = Field(..., description="分类")
subcategory: str = "",
note: str = Field("", max_length=200),
transaction_date: str = ""
# ============ API 路由 ============
@app.post("/api/transactions", status_code=201)
defcreate_transaction(tx: TransactionCreate):
"""添加交易记录"""
tx_id = add_transaction(
amount=tx.amount,
tx_type=tx.type,
category=tx.category,
subcategory=tx.subcategory,
note=tx.note,
tx_date=tx.transaction_date
)
return {"id": tx_id, "message": "交易记录已添加"}
@app.get("/api/transactions")
deflist_transactions(
tx_type: Optional[str] = Query(None),
category: Optional[str] = Query(None),
start_date: Optional[str] = Query(None),
end_date: Optional[str] = Query(None),
limit: int = Query(50, ge=1, le=100),
offset: int = Query(0, ge=0)
):
"""获取交易记录(分页 + 筛选)"""
result = get_transactions(
tx_type=tx_type,
category=category,
start_date=start_date,
end_date=end_date,
limit=limit,
offset=offset
)
return result
@app.get("/api/summary/monthly")
defmonthly_summary(year: int = Query(2026), month: int = Query(7)):
"""月度统计"""
return get_monthly_summary(year, month)
@app.get("/api/trend/daily")
defdaily_trend(days: int = Query(30, ge=7, le=365)):
"""每日趋势"""
return get_daily_trend(days)
@app.get("/api/health")
defhealth():
return {"status": "running", "version": "1.0.0"}
```
启动后端:
```bash
cdbackend
uvicornmain:app--reload
# 访问 http://localhost:8000/docs 查看交互式文档
```
---
## 10.3 第二步:搭建前端(React + Chart.js)
### 10.3.1 项目初始化
```bash
cdfrontend
npmcreatevite@latestfinanceflow-dashboard----templatereact
cdfinanceflow-dashboard
npminstallaxioschart.jsreact-chartjs-2date-fns
```
### 10.3.2 主应用布局
```jsx
// src/App.jsx
import{ useState, useEffect }from'react';
importaxiosfrom'axios';
importSummaryCardsfrom'./components/SummaryCards';
importTrendChartfrom'./components/TrendChart';
importCategoryPiefrom'./components/CategoryPie';
importTransactionTablefrom'./components/TransactionTable';
importAddTransactionfrom'./components/AddTransaction';
import'./App.css';
constAPI = 'http://localhost:8000/api';
functionApp() {
const [summary, setSummary] =useState(null);
const [trend, setTrend] =useState(null);
const [selectedMonth, setSelectedMonth] =useState(
newDate().toISOString().slice(0, 7) // "2026-07"
);
useEffect(() => {
const [year, month] = selectedMonth.split('-').map(Number);
Promise.all([
axios.get(`${API}/summary/monthly`, { params: { year, month } }),
axios.get(`${API}/trend/daily`, { params: { days:30 } })
])
.then(([summaryRes, trendRes]) => {
setSummary(summaryRes.data);
setTrend(trendRes.data);
})
.catch(err=> console.error('加载数据失败:', err));
}, [selectedMonth]);
return (
<divclassName="app">
<headerclassName="header">
<h1>💰 FinanceFlow</h1>
<input
type="month"
value={selectedMonth}
onChange={e=>setSelectedMonth(e.target.value)}
className="month-picker"
/>
</header>
<mainclassName="main-content">
<SummaryCardssummary={summary}/>
<TrendCharttrend={trend}/>
<divclassName="bottom-row">
<CategoryPiesummary={summary}/>
<TransactionTable/>
</div>
<AddTransaction/>
</main>
</div>
);
}
exportdefaultApp;
```
### 10.3.3 统计卡片组件
```jsx
// src/components/SummaryCards.jsx
functionSummaryCards({ summary }) {
if (!summary) return<div>加载中...</div>;
constcards= [
{ title:'本月收入', value: summary.income, color:'#4caf50', icon:'📈' },
{ title:'本月支出', value: summary.expense, color:'#ef5350', icon:'📉' },
{ title:'本月结余', value: summary.balance, color:'#2196f3', icon:'💰' },
];
return (
<divclassName="summary-cards">
{cards.map(card=> (
<divkey={card.title}className="card"style={{ borderLeftColor: card.color }}>
<divclassName="card-icon">{card.icon}</div>
<divclassName="card-title">{card.title}</div>
<divclassName="card-value"style={{ color: card.color }}>
¥{card.value.toLocaleString()}
</div>
</div>
))}
</div>
);
}
exportdefaultSummaryCards;
```
### 10.3.4 趋势折线图
```jsx
// src/components/TrendChart.jsx
import{ Line }from'react-chartjs-2';
functionTrendChart({ trend }) {
if (!trend || trend.dates.length===0) {
return<div>暂无趋势数据</div>;
}
constdata= {
labels: trend.dates.map(d=> d.slice(5)), // 只显示 MM-DD
datasets: [
{
label:'收入',
data: trend.income,
borderColor:'#4caf50',
backgroundColor:'rgba(76, 175, 80, 0.1)',
tension:0.3,
fill:true
},
{
label:'支出',
data: trend.expense,
borderColor:'#ef5350',
backgroundColor:'rgba(239, 83, 80, 0.1)',
tension:0.3,
fill:true
}
]
};
constoptions= {
responsive:true,
plugins: {
title: { display:true, text:'30天收支趋势', font: { size:16 } }
},
scales: {
y: { beginAtZero:true }
}
};
return<Linedata={data}options={options}/>;
}
exportdefaultTrendChart;
```
### 10.3.5 CSS 样式
```css
/* src/App.css */
.app {
min-height: 100vh;
background: #f0f2f5;
}
.header {
background: white;
padding: 16px24px;
box-shadow: 01px3pxrgba(0,0,0,0.1);
display: flex;
justify-content: space-between;
align-items: center;
}
.main-content {
padding: 24px;
max-width: 1400px;
margin: 0auto;
}
.summary-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.card {
background: white;
border-radius: 12px;
padding: 20px;
border-left: 4pxsolid;
box-shadow: 02px8pxrgba(0,0,0,0.08);
}
.card-title {
color: #888;
font-size: 14px;
margin: 8px0;
}
.card-value {
font-size: 28px;
font-weight: bold;
}
.bottom-row {
display: grid;
grid-template-columns: 1fr1fr;
gap: 24px;
margin-top: 24px;
}
@media (max-width: 768px) {
.bottom-row {
grid-template-columns: 1fr;
}
}
```
---
## 10.4 第三步:接入 AI 智能分析
### 10.4.1 AI 分析接口
```python
# backend/ai.py
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from crud import get_monthly_summary, get_transactions
from datetime import datetime
defanalyze_financial_report(year: int, month: int) -> str:
"""AI 月度财务报告生成"""
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
# 获取当月数据
summary = get_monthly_summary(year, month)
transactions = get_transactions(limit=100, tx_type="支出")["transactions"]
# 构建上下文
context = f"""
当前月份: {year}年{month}月
本月收入: ¥{summary['income']:,.2f}
本月支出: ¥{summary['expense']:,.2f}
本月结余: ¥{summary['balance']:,.2f}
结余率: {summary['income'] / summary['expense'] * 100:.1f}% if summary['expense'] > 0 else 0
主要支出分类:
{chr(10).join([f" - {cat['category']}: ¥{cat['total']:,.2f}"for cat in summary.get('category_breakdown', [])])}
最近支出记录:
{chr(10).join([f" - {tx['category']} ({tx['subcategory']}): ¥{tx['amount']:.2f}{tx['note']}"for tx in transactions[-10:]])}
"""
# 构建 Prompt
prompt = ChatPromptTemplate.from_messages([
("system", """你是一个专业的财务顾问 AI。根据用户提供的财务数据,
给出简洁的分析和建议。请遵循以下规则:
1. 用中文回答
2. 语气友好专业
3. 包含数据洞察(发现了什么趋势或问题)
4. 给出可执行的建议
5. 控制在 300 字以内"""),
("human", "请分析以下财务数据并给出报告:\n{context}")
])
chain = prompt | llm | StrOutputParser()
return chain.invoke({"context": context})
```
### 10.4.2 在 FastAPI 中暴露 AI 接口
```python
# main.py (新增路由)
from ai import analyze_financial_report
@app.get("/api/ai/report")
defai_report(year: int = Query(2026), month: int = Query(7)):
"""AI 生成月度财务分析报告"""
try:
report = analyze_financial_report(year, month)
return {
"year": year,
"month": month,
"report": report
}
exceptExceptionas e:
raise HTTPException(status_code=500, detail=str(e))
```
### 10.4.3 前端展示 AI 报告
```jsx
// src/components/AIReport.jsx
import{ useState }from'react';
importaxiosfrom'axios';
functionAIReport({ year, month }) {
const [report, setReport] =useState(null);
const [loading, setLoading] =useState(false);
constgenerateReport=async () => {
setLoading(true);
try {
constres=await axios.get('http://localhost:8000/api/ai/report', {
params: { year, month }
});
setReport(res.data.report);
} catch (err) {
alert('报告生成失败');
} finally {
setLoading(false);
}
};
return (
<divclassName="ai-report">
<h3>🤖 AI 智能分析报告</h3>
<buttononClick={generateReport}disabled={loading}>
{loading ?'生成中...':'生成月度报告'}
</button>
{report && (
<divclassName="report-content"style={{
marginTop:'16px',
padding:'16px',
background:'#fff8e1',
borderRadius:'8px',
whiteSpace:'pre-wrap',
lineHeight:1.6
}}>
{report}
</div>
)}
</div>
);
}
exportdefaultAIReport;
```
---
## 10.5 第四步:数据导出
```python
# backend/main.py (新增)
import io
from fastapi.responses import StreamingResponse
@app.get("/api/export/csv")
defexport_csv(start_date: str = None, end_date: str = None):
"""导出交易记录为 CSV"""
from crud import get_transactions
result = get_transactions(start_date=start_date, end_date=end_date, limit=10000)
csv_buffer = io.StringIO()
csv_buffer.write("id,transaction_date,type,category,subcategory,amount,note\n")
for tx in result["transactions"]:
csv_buffer.write(
f'{tx["id"]},{tx["transaction_date"]},{tx["type"]},'
f'{tx["category"]},{tx["subcategory"]},{tx["amount"]},{tx["note"]}\n'
)
csv_buffer.seek(0)
return StreamingResponse(
iter([csv_buffer.getvalue()]),
media_type="text/csv",
headers={"Content-Disposition": "attachment; filename=transactions.csv"}
)
```
---
## 10.6 运行项目
```bash
# 终端 1:启动后端
cdbackend
uvicornmain:app--reload
# 终端 2:启动前端
cdfrontend/financeflow-dashboard
npmrundev
# 终端 3(可选):填充一些测试数据
python-c"
from crud import add_transaction
for i in range(50):
add_transaction(
amount=50 + i * 10,
tx_type='支出' if i % 3 != 0 else '收入',
category=['餐饮', '交通', '购物', '工资'][i % 4],
note=f'测试记录 {i+1}'
)
print('已插入 50 条测试数据')
"
# 然后在浏览器打开 http://localhost:5173 查看 Dashboard
```
---
## 10.7 扩展方向
这个项目完成后,你还可以继续扩展:
```
🔄 实时数据同步 —— WebSocket 实现实时更新
👤 用户认证 —— JWT Token + 登录注册
📱 移动端适配 —— PWA / React Native
☁️ 云端部署 —— FastAPI 部署到云 + React 部署到 Vercel
📧 邮件周报 —— 每周自动发送财务摘要
🔔 预算提醒 —— 支出超预算时推送通知
💾 PostgreSQL —— 替代 SQLite 提升并发性能
```
---
## 10.8 知识点小结
| 知识点 | 在本项目中的应用 |
|--------|----------------|
| Python 基础 | CRUD 逻辑、数据清洗、业务规则 |
| 爬虫 | 采集外部财经数据辅助分析 |
| Pandas | 大规模数据分析、报表生成 |
| FastAPI | 后端 RESTful API 服务 |
| React | 前端 Dashboard 用户界面 |
| Chart.js | 数据可视化图表 |
| LangChain | AI 智能分析 + 自然语言问答 |
| SQLite | 本地数据持久化 |
---
## 10.9 第一阶段总结
恭喜你完成了 Python 全栈开发的第一阶段!回顾这条学习路径:
```
Episode 01-04 Python 基础打底
↓
Episode 05 获取外部数据(爬虫)
↓
Episode 06 理解内部数据(分析)
↓
Episode 07 提供服务(后端 API)
↓
Episode 08 赋予智能(AI + RAG)
↓
Episode 09 呈现给用户(前端 React)
↓
Episode 10 融会贯通(完整项目)
```
你现在具备了从零搭建一个完整 Web 应用的能力——从数据库到 API 到 AI 到前端。
---
## 10.10 后续计划预告
第一阶段到此结束,但学习永无止境!以下是后续可能的方向:
-**Episode 11**:Docker 容器化部署 —— 让你的项目一键上线
-**Episode 12**:数据库进阶 —— PostgreSQL + SQLAlchemy + Alembic
-**Episode 13**:Celery 异步任务 —— 定时报表、邮件发送
-**Episode 14**:单元测试与 CI/CD —— 自动化测试 + 持续集成
-**Episode 15**:微服务架构 —— 把大项目拆成独立服务
无论你选择哪条路,请记住:**最好的学习方式就是做一个真正的项目**。FinanceFlow 只是一个开始,你可以用它来分析自己的消费习惯,也可以扩展成公司级的数据平台。
感谢你这十期的陪伴,愿 Python 成为你手中最锋利的工具!🐍🚀✨