
文章目录
📖 开篇导读
恭喜你来到了综合实战阶段!在前面的48节课中,我们一步一步学习了Python的基础语法、面向对象、文件操作、数据库、网络爬虫、Web框架(Flask和Django)。现在,是时候将这些知识融会贯通,独立完成一个完整的Web应用——个人记账系统。
为什么选择记账系统?因为它涵盖了Web开发的核心功能:
- 用户注册与登录(认证与授权)
- 数据的增删改查(记账记录的添加、编辑、删除)
- 数据分类与统计(支出分类统计)
- 数据可视化(图表展示收支趋势)
- 数据库设计(一对多关系:用户→分类→记账记录)
通过这个项目,你将完全理解一个真实Web应用的开发流程:需求分析→数据库设计→后端API开发→前端页面制作→项目部署。无论你是想找一份Python全栈开发的工作,还是想独立开发自己的产品,这个项目都会成为你简历上的亮点。
💡 工作场景:记账系统是典型的企业级管理系统的缩影。掌握它,你就能触类旁通地开发待办清单、库存管理、客户关系管理等系统。很多公司的内部工具原型就是从这个模式开始的。
本课将使用Flask作为Web框架(轻量、灵活),SQLite作为数据库(无需额外安装),ECharts进行图表展示。我们会从零开始,一行一行代码搭建整个项目。学完本课,你将拥有一个可运行的个人记账系统,并能根据需求自行扩展。
🎯 学习目标
| 目标编号 | 具体掌握内容 | 对应面试/工作价值 |
|---|---|---|
| 1️⃣ | 独立进行需求分析和数据库设计 | 项目初步设计能力 |
| 2️⃣ | 实现用户认证模块(注册、登录、会话管理) | 大多数Web应用的必备功能 |
| 3️⃣ | 实现记账记录的增删改查(CRUD) | 核心业务逻辑 |
| 4️⃣ | 实现收支分类管理(分类添加、选择) | 数据关联查询 |
| 5️⃣ | 使用ECharts展示月度收支趋势图 | 数据可视化 |
| 6️⃣ | 综合运用Flask、SQLite、HTML/CSS/JS | 全栈开发能力 |
🔥 面试考点:“如何设计数据库表来实现记账系统?”“用户认证如何实现?”“Flask中如何处理一对多的关系?”“ECharts如何与后端数据对接?”
📚 知识点理论精讲(设计阶段)
一、需求分析
我们要开发的个人记账系统,主要功能如下:
- 用户注册与登录:用户可以注册账号,登录后才能使用记账功能。
- 收支分类管理:用户可以自定义分类(如餐饮、购物、工资等),分为收入类和支出类。
- 记账记录管理:
- 添加记录:选择类型(收入/支出)、分类、金额、日期、备注。
- 编辑记录:修改已有记录的信息。
- 删除记录:删除无用记录。
- 查看记录:列表展示,可按日期排序,支持分页。
- 统计分析:
- 月度收支总览:总收入、总支出、结余。
- 分类支出占比饼图。
- 近6个月收支趋势折线图。
- 数据持久化:使用SQLite数据库存储用户、分类、记账记录。
二、技术选型
- 后端框架:Flask(轻量级,易于上手)
- 数据库:SQLite(无需单独安装,适合小型项目)
- ORM:Flask-SQLAlchemy(简化数据库操作)
- 表单处理:Flask-WTF(提供CSRF保护和表单验证)
- 用户认证:Flask-Login(简化会话管理)
- 前端:Bootstrap 5(快速搭建界面),ECharts 5(生成图表)
- 模板引擎:Jinja2(Flask内置)
安装依赖:
pip install flask flask-sqlalchemy flask-wtf flask-login werkzeug
三、数据库设计
3.1 表结构
| 表名 | 字段 | 说明 |
|---|---|---|
| user | id (主键), username (唯一), password_hash (加密), email | 用户信息 |
| category | id, name, type (income/expense), user_id (外键) | 分类,关联用户 |
| record | id, amount, date, note, user_id, category_id | 记账记录 |
- 一个用户拥有多个分类和多个记录。
- 分类的
type字段区分收入/支出,方便前端筛选。 - 记录的金额正数表示收入,负数表示支出?为了统一,我们约定无论收支都用正数,由
record_type字段决定。更通用的做法:每条记录关联的category已经有类型,可知是收入还是支出。但直接存储type冗余查询快。我们采用记录自身的type(收入/支出)字段。
简化设计:record 表包含 type (income/expense) 和 amount。
最终表结构:
user
- id INT PRIMARY KEY
- username VARCHAR(80) UNIQUE
- password_hash VARCHAR(128)
- email VARCHAR(120)
category
- id INT PRIMARY KEY
- name VARCHAR(50)
- type VARCHAR(10) – ‘income’ or ‘expense’
- user_id INT FOREIGN KEY REFERENCES user(id)
record
- id INT PRIMARY KEY
- amount FLOAT
- date DATE
- note TEXT
- record_type VARCHAR(10) – ‘income’ or ‘expense’ (冗余,也可从category.type获得)
- user_id INT FOREIGN KEY REFERENCES user(id)
- category_id INT FOREIGN KEY REFERENCES category(id)
3.2 表关系
- User → Category: 一对多(一个用户有多个分类)
- User → Record: 一对多(一个用户有多个记账记录)
- Category → Record: 一对多(一个分类下有多条记录)
💻 代码实现(全流程)
我们将项目命名为 AccountBook,按以下步骤实现。
步骤1:项目初始化
创建项目文件夹:
AccountBook/
├── app.py # 主入口
├── models.py # 数据库模型
├── forms.py # 表单类
├── templates/ # HTML模板
│ ├── base.html
│ ├── index.html
│ ├── login.html
│ ├── register.html
│ ├── records.html
│ ├── record_form.html
│ ├── categories.html
│ └── statistics.html
├── static/ # CSS, JS, 图表库
│ ├── style.css
│ └── echarts.min.js
└── instance/ # SQLite数据库文件自动生成
1. 安装依赖并创建文件
mkdir AccountBook && cd AccountBook
pip install flask flask-sqlalchemy flask-wtf flask-login
2. 编写 app.py
"""
app.py - 应用主入口,配置、路由、视图函数
"""
from flask import Flask, render_template, redirect, url_for, flash, request
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager, login_user, logout_user, login_required, current_user
from werkzeug.security import generate_password_hash, check_password_hash
from forms import RegistrationForm, LoginForm, RecordForm, CategoryForm
from models import User, Category, Record
from datetime import datetime
import json
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key-change-in-production'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///account.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
login_manager = LoginManager(app)
login_manager.login_view = 'login'
login_manager.login_message = '请先登录'
# 导入模型(确保在db定义之后)
from models import User, Category, Record
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
# ---------------------------- 首页 ----------------------------
@app.route('/')
def index():
"""首页,重定向到记录列表(需要登录)"""
if current_user.is_authenticated:
return redirect(url_for('records'))
return redirect(url_for('login'))
# ---------------------------- 用户认证 ----------------------------
@app.route('/register', methods=['GET', 'POST'])
def register():
if current_user.is_authenticated:
return redirect(url_for('records'))
form = RegistrationForm()
if form.validate_on_submit():
# 检查用户名是否存在
existing_user = User.query.filter_by(username=form.username.data).first()
if existing_user:
flash('用户名已存在', 'danger')
return render_template('register.html', form=form)
# 创建新用户
hashed_pw = generate_password_hash(form.password.data)
user = User(username=form.username.data, email=form.email.data, password_hash=hashed_pw)
db.session.add(user)
db.session.commit()
# 为新用户创建默认分类
default_categories = [
('餐饮', 'expense'), ('购物', 'expense'), ('交通', 'expense'),
('工资', 'income'), ('奖金', 'income')
]
for name, cat_type in default_categories:
cat = Category(name=name, type=cat_type, user_id=user.id)
db.session.add(cat)
db.session.commit()
flash('注册成功,请登录', 'success')
return redirect(url_for('login'))
return render_template('register.html', form=form)
@app.route('/login', methods=['GET', 'POST'])
def login():
if current_user.is_authenticated:
return redirect(url_for('records'))
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(username=form.username.data).first()
if user and check_password_hash(user.password_hash, form.password.data):
login_user(user, remember=form.remember.data)
next_page = request.args.get('next')
flash(f'欢迎回来,{user.username}', 'success')
return redirect(next_page) if next_page else redirect(url_for('records'))
else:
flash('用户名或密码错误', 'danger')
return render_template('login.html', form=form)
@app.route('/logout')
@login_required
def logout():
logout_user()
flash('您已退出登录', 'info')
return redirect(url_for('login'))
# ---------------------------- 记账记录管理 ----------------------------
@app.route('/records')
@login_required
def records():
"""显示当前用户的所有记账记录,支持按日期降序"""
# 获取查询参数(用于筛选,可选)
year = request.args.get('year', type=int)
month = request.args.get('month', type=int)
query = Record.query.filter_by(user_id=current_user.id)
if year and month:
query = query.filter(db.extract('year', Record.date) == year,
db.extract('month', Record.date) == month)
records = query.order_by(Record.date.desc()).all()
# 计算总收入和总支出
total_income = sum(r.amount for r in records if r.record_type == 'income')
total_expense = sum(r.amount for r in records if r.record_type == 'expense')
balance = total_income - total_expense
return render_template('records.html', records=records,
total_income=total_income,
total_expense=total_expense,
balance=balance)
@app.route('/record/add', methods=['GET', 'POST'])
@login_required
def add_record():
form = RecordForm()
# 动态设置分类选择框的选项(根据收入/支出类型动态过滤)
if request.method == 'GET':
# 根据初始的record_type显示对应的分类
income_cats = Category.query.filter_by(user_id=current_user.id, type='income').all()
expense_cats = Category.query.filter_by(user_id=current_user.id, type='expense').all()
form.category_id.choices = [(c.id, c.name) for c in income_cats+expense_cats]
else:
# POST时,根据选择的record_type更新分类选项,但表单已提交,分类ID应该是有效的
# 更完善的做法:前端联动,简化:直接验证category属于当前用户
pass
if form.validate_on_submit():
# 验证分类属于当前用户
category = Category.query.get(form.category_id.data)
if not category or category.user_id != current_user.id:
flash('无效的分类', 'danger')
return redirect(url_for('add_record'))
# 创建记录
record = Record(
amount=form.amount.data,
date=form.date.data,
note=form.note.data,
record_type=form.record_type.data,
user_id=current_user.id,
category_id=form.category_id.data
)
db.session.add(record)
db.session.commit()
flash('记账记录添加成功', 'success')
return redirect(url_for('records'))
# 重新设置choices
income_cats = Category.query.filter_by(user_id=current_user.id, type='income').all()
expense_cats = Category.query.filter_by(user_id=current_user.id, type='expense').all()
form.category_id.choices = [(c.id, c.name) for c in income_cats+expense_cats]
return render_template('record_form.html', form=form, title='添加记录')
@app.route('/record/edit/<int:record_id>', methods=['GET', 'POST'])
@login_required
def edit_record(record_id):
record = Record.query.get_or_404(record_id)
if record.user_id != current_user.id:
abort(403)
form = RecordForm(obj=record)
# 设置分类choices
income_cats = Category.query.filter_by(user_id=current_user.id, type='income').all()
expense_cats = Category.query.filter_by(user_id=current_user.id, type='expense').all()
form.category_id.choices = [(c.id, c.name) for c in income_cats+expense_cats]
if form.validate_on_submit():
record.amount = form.amount.data
record.date = form.date.data
record.note = form.note.data
record.record_type = form.record_type.data
record.category_id = form.category_id.data
db.session.commit()
flash('记录已更新', 'success')
return redirect(url_for('records'))
return render_template('record_form.html', form=form, title='编辑记录')
@app.route('/record/delete/<int:record_id>')
@login_required
def delete_record(record_id):
record = Record.query.get_or_404(record_id)
if record.user_id != current_user.id:
abort(403)
db.session.delete(record)
db.session.commit()
flash('记录已删除', 'success')
return redirect(url_for('records'))
# ---------------------------- 分类管理 ----------------------------
@app.route('/categories')
@login_required
def categories():
"""分类列表"""
cats = Category.query.filter_by(user_id=current_user.id).all()
return render_template('categories.html', cats=cats)
@app.route('/category/add', methods=['GET', 'POST'])
@login_required
def add_category():
form = CategoryForm()
if form.validate_on_submit():
cat = Category(name=form.name.data, type=form.type.data, user_id=current_user.id)
db.session.add(cat)
db.session.commit()
flash('分类添加成功', 'success')
return redirect(url_for('categories'))
return render_template('category_form.html', form=form, title='添加分类')
@app.route('/category/delete/<int:cat_id>')
@login_required
def delete_category(cat_id):
cat = Category.query.get_or_404(cat_id)
if cat.user_id != current_user.id:
abort(403)
# 检查该分类下是否有记录,如果有则不能删除
if Record.query.filter_by(category_id=cat.id).count() > 0:
flash('该分类下还有记账记录,请先删除或转移记录', 'danger')
else:
db.session.delete(cat)
db.session.commit()
flash('分类已删除', 'success')
return redirect(url_for('categories'))
# ---------------------------- 统计分析 ----------------------------
@app.route('/statistics')
@login_required
def statistics():
# 获取近6个月的月度统计
from sqlalchemy import func
# 计算近6个月各月收支总额
# 注意:SQLite日期函数,提取年月
records = Record.query.filter_by(user_id=current_user.id).all()
# 在Python中聚合更为通用
monthly_data = {}
for r in records:
key = r.date.strftime('%Y-%m')
if key not in monthly_data:
monthly_data[key] = {'income': 0, 'expense': 0}
if r.record_type == 'income':
monthly_data[key]['income'] += r.amount
else:
monthly_data[key]['expense'] += r.amount
# 排序最近6个月
sorted_months = sorted(monthly_data.keys(), reverse=True)[:6][::-1]
months = sorted_months
income_list = [monthly_data[m]['income'] for m in months]
expense_list = [monthly_data[m]['expense'] for m in months]
# 支出分类占比(当前年份,可选)
category_expense = db.session.query(
Category.name, func.sum(Record.amount).label('total')
).join(Record).filter(
Record.user_id == current_user.id,
Record.record_type == 'expense'
).group_by(Category.id).all()
pie_data = [{'name': c.name, 'value': float(c.total)} for c in category_expense]
return render_template('statistics.html',
months=json.dumps(months),
income_list=json.dumps(income_list),
expense_list=json.dumps(expense_list),
pie_data=json.dumps(pie_data))
if __name__ == '__main__':
with app.app_context():
db.create_all()
app.run(debug=True)
3. 编写 models.py
from flask_sqlalchemy import SQLAlchemy
from flask_login import UserMixin
from datetime import datetime
db = SQLAlchemy() # 将在app.py中初始化
class User(UserMixin, db.Model):
__tablename__ = 'user'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=True)
password_hash = db.Column(db.String(128), nullable=False)
# 关系
categories = db.relationship('Category', backref='user', lazy=True, cascade='all, delete-orphan')
records = db.relationship('Record', backref='user', lazy=True, cascade='all, delete-orphan')
class Category(db.Model):
__tablename__ = 'category'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50), nullable=False)
type = db.Column(db.String(10), nullable=False) # 'income' or 'expense'
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
# 关系
records = db.relationship('Record', backref='category', lazy=True, cascade='all, delete-orphan')
class Record(db.Model):
__tablename__ = 'record'
id = db.Column(db.Integer, primary_key=True)
amount = db.Column(db.Float, nullable=False)
date = db.Column(db.Date, nullable=False, default=datetime.today)
note = db.Column(db.Text, nullable=True)
record_type = db.Column(db.String(10), nullable=False) # 'income' or 'expense'
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
category_id = db.Column(db.Integer, db.ForeignKey('category.id'), nullable=False)
4. 编写 forms.py
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField, BooleanField, FloatField, DateField, TextAreaField, SelectField
from wtforms.validators import DataRequired, Length, Email, EqualTo, ValidationError
from datetime import datetime
class RegistrationForm(FlaskForm):
username = StringField('用户名', validators=[DataRequired(), Length(min=2, max=20)])
email = StringField('邮箱', validators=[Email(), Length(max=120)])
password = PasswordField('密码', validators=[DataRequired(), Length(min=6)])
confirm_password = PasswordField('确认密码', validators=[DataRequired(), EqualTo('password')])
submit = SubmitField('注册')
class LoginForm(FlaskForm):
username = StringField('用户名', validators=[DataRequired()])
password = PasswordField('密码', validators=[DataRequired()])
remember = BooleanField('记住我')
submit = SubmitField('登录')
class RecordForm(FlaskForm):
amount = FloatField('金额', validators=[DataRequired()])
date = DateField('日期', validators=[DataRequired()], default=datetime.today, format='%Y-%m-%d')
note = TextAreaField('备注')
record_type = SelectField('类型', choices=[('income', '收入'), ('expense', '支出')], validators=[DataRequired()])
category_id = SelectField('分类', coerce=int, validators=[DataRequired()])
submit = SubmitField('保存')
class CategoryForm(FlaskForm):
name = StringField('分类名称', validators=[DataRequired(), Length(max=50)])
type = SelectField('类型', choices=[('income', '收入'), ('expense', '支出')], validators=[DataRequired()])
submit = SubmitField('添加')
5. 编写模板
由于篇幅,仅列出关键模板内容。所有模板放在 templates/ 目录下。
base.html 基础模板
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}个人记账系统{% endblock %}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container">
<a class="navbar-brand" href="{{ url_for('records') }}">记账系统</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
{% if current_user.is_authenticated %}
<li class="nav-item"><a class="nav-link" href="{{ url_for('records') }}">记账流水</a></li>
<li class="nav-item"><a class="nav-link" href="{{ url_for('statistics') }}">统计图表</a></li>
<li class="nav-item"><a class="nav-link" href="{{ url_for('categories') }}">分类管理</a></li>
<li class="nav-item"><a class="nav-link" href="{{ url_for('logout') }}">退出</a></li>
{% else %}
<li class="nav-item"><a class="nav-link" href="{{ url_for('login') }}">登录</a></li>
<li class="nav-item"><a class="nav-link" href="{{ url_for('register') }}">注册</a></li>
{% endif %}
</ul>
</div>
</div>
</nav>
<div class="container mt-4">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
{% block scripts %}{% endblock %}
</body>
</html>
records.html 记账列表
{% extends "base.html" %}
{% block title %}记账流水{% endblock %}
{% block content %}
<h1 class="mb-4">记账记录</h1>
<div class="row mb-3">
<div class="col-md-6">
<a href="{{ url_for('add_record') }}" class="btn btn-success">+ 新增记录</a>
</div>
<div class="col-md-6 text-end">
<span class="badge bg-info">总收入: {{ total_income }} 元</span>
<span class="badge bg-warning">总支出: {{ total_expense }} 元</span>
<span class="badge bg-primary">结余: {{ balance }} 元</span>
</div>
</div>
<table class="table table-striped table-hover">
<thead>
<tr>
<th>日期</th><th>类型</th><th>分类</th><th>金额</th><th>备注</th><th>操作</th>
</tr>
</thead>
<tbody>
{% for r in records %}
<tr>
<td>{{ r.date.strftime('%Y-%m-%d') }}</td>
<td>{{ '收入' if r.record_type=='income' else '支出' }}</td>
<td>{{ r.category.name }}</td>
<td class="{{ 'text-success' if r.record_type=='income' else 'text-danger' }}">{{ r.amount }}</td>
<td>{{ r.note or '' }}</td>
<td>
<a href="{{ url_for('edit_record', record_id=r.id) }}" class="btn btn-sm btn-secondary">编辑</a>
<a href="{{ url_for('delete_record', record_id=r.id) }}" class="btn btn-sm btn-danger" onclick="return confirm('确定删除吗?')">删除</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
record_form.html 添加/编辑界面
{% extends "base.html" %}
{% block title %}{{ title }}{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-header">{{ title }}</div>
<div class="card-body">
<form method="post">
{{ form.hidden_tag() }}
<div class="mb-3">
{{ form.amount.label }} {{ form.amount(class="form-control") }}
{% for error in form.amount.errors %}<span class="text-danger">{{ error }}</span>{% endfor %}
</div>
<div class="mb-3">
{{ form.date.label }} {{ form.date(class="form-control") }}
</div>
<div class="mb-3">
{{ form.record_type.label }} {{ form.record_type(class="form-select") }}
</div>
<div class="mb-3">
{{ form.category_id.label }} {{ form.category_id(class="form-select") }}
</div>
<div class="mb-3">
{{ form.note.label }} {{ form.note(class="form-control", rows=3) }}
</div>
{{ form.submit(class="btn btn-primary") }}
</form>
</div>
</div>
</div>
</div>
{% endblock %}
statistics.html 统计图表
{% extends "base.html" %}
{% block title %}统计分析{% endblock %}
{% block content %}
<h1>收支统计</h1>
<div class="row">
<div class="col-md-8">
<div id="trendChart" style="height: 400px;"></div>
</div>
<div class="col-md-4">
<div id="pieChart" style="height: 400px;"></div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>
<script>
// 折线图 - 近6个月收支趋势
var months = {{ months|safe }};
var incomeData = {{ income_list|safe }};
var expenseData = {{ expense_list|safe }};
var trendChart = echarts.init(document.getElementById('trendChart'));
trendChart.setOption({
title: { text: '月度收支趋势' },
tooltip: { trigger: 'axis' },
legend: { data: ['收入', '支出'] },
xAxis: { type: 'category', data: months },
yAxis: { type: 'value' },
series: [
{ name: '收入', type: 'line', data: incomeData, lineStyle: { color: 'green' } },
{ name: '支出', type: 'line', data: expenseData, lineStyle: { color: 'red' } }
]
});
// 饼图 - 支出分类占比
var pieData = {{ pie_data|safe }};
var pieChart = echarts.init(document.getElementById('pieChart'));
pieChart.setOption({
title: { text: '支出分类占比' },
tooltip: { trigger: 'item' },
series: [{
type: 'pie', radius: '55%',
data: pieData,
label: { show: true, formatter: '{b}: {d}%' }
}]
});
</script>
{% endblock %}
其他模板(login.html, register.html, categories.html等)类似,使用Bootstrap表单,篇幅原因不再列出。
步骤2:运行项目
- 在项目目录下执行
python app.py。 - 首次运行会自动创建数据库和表。
- 访问
http://127.0.0.1:5000开始使用。
⚠️ 易错点避坑总结
| 序号 | 常见错误 | 解决方法 |
|---|---|---|
| 1 | 登录后跳转循环 | 检查login_required装饰器,确保主页等需要认证 |
| 2 | 表单验证失败但没有错误提示 | 在模板中循环显示form.errors |
| 3 | 数据库表未创建 | 在app.run之前调用db.create_all(),或使用Flask-Migrate |
| 4 | ECharts图表数据为空 | 检查后端是否正确传递JSON,前端` |
| 5 | 分类动态加载问题 | 编辑记录时,分类下拉框需要根据收支类型动态筛选,本实现简化了,可改进为AJAX |
| 6 | 删除分类时关联记录导致外键约束失败 | 先删除记录或设置级联删除(模型中已设置) |
| 7 | 日期格式错误 | 表单使用DateField,模板中显示用strftime |
| 8 | 金额浮点数精度 | SQLite中Float可能导致小数位问题,可用Decimal |
| 9 | 用户注册后默认分类重复 | 检查数据库中是否已有默认分类,避免重复插入 |
| 10 | 静态文件404 | 确保static文件夹存在,且路径引用正确 |
📝 课后实战练习题
- 增加月度预算功能:为每个分类设定预算,月度报表中显示超支警告。
- 导出数据报表:支持按月份CSV导出记账记录。
- 增加账户管理:多个账户(现金、银行卡),记账时选择账户。
- 增加标签功能:为记录添加多个标签,支持标签统计。
- 实现收支趋势周视图:通过选择日期范围展示折线图。
- 添加用户头像上传:在用户资料页上传头像并显示。
- 部署项目到云服务器:尝试使用Gunicorn+nginx部署。
🧠 知识点思维导图总结
🔜 下节课预告
本项目将Python基础、Web框架、数据库、前端技术融为一体,是进入企业级开发的里程碑。下一节课我们将进行第二个综合项目:小型爬虫+数据分析完整商业级项目实战,爬取电商网站数据并进行分析可视化。
第50课:综合项目二:小型爬虫+数据分析完整商业级项目实战
内容包括:
- 爬取商品信息(价格、评论)
- 数据清洗与存储
- 数据分析(价格分布、评分相关性)
- 可视化报表生成
- 定时任务自动爬取
学完本课,你的数据采集和分析能力将达到新高度。
🌟 学习鼓励:恭喜你完成了第一个完整Web项目!不要满足于此,尝试增加新功能、优化UI、部署到公网。每一个bug的解决都是成长的阶梯。坚持下去,你离全栈工程师越来越近!
🔗《50节课 Python 从入门到精通》系列课程导航
🌟 感谢您耐心阅读到这里!
💡 如果本文对您有所启发欢迎:
👍 点赞📌 收藏 📤 分享给更多需要的伙伴。
🗣️ 期待在评论区看到您的想法, 共同进步。
🔔 关注我,持续获取更多干货内容~
🤗 我们下篇文章见~

966

被折叠的 条评论
为什么被折叠?



