12、Python项目实战

Python项目实战

1. 项目概述

在前几天的学习中,我们已经掌握了Python的基础知识、Web开发框架、数据分析与可视化以及测试与调试技术。今天,我们将把这些知识整合起来,构建一个完整的实际项目。

项目开发流程图

graph TD A[需求分析] -->|明确项目目标| B[系统设计] B -->|架构设计| C[数据库设计] B -->|界面设计| D[API设计] C --> E[后端开发] D --> E D --> F[前端开发] E --> G[单元测试] F --> H[集成测试] G --> I[系统测试] H --> I I --> J[部署上线] J --> K[维护与迭代] style A fill:#f9f,stroke:#333,stroke-width:2px style B fill:#bbf,stroke:#333,stroke-width:2px style C fill:#dfd,stroke:#333,stroke-width:2px style D fill:#fdd,stroke:#333,stroke-width:2px style E fill:#ddf,stroke:#333,stroke-width:2px style F fill:#ffd,stroke:#333,stroke-width:2px style G fill:#dff,stroke:#333,stroke-width:2px style H fill:#fdf,stroke:#333,stroke-width:2px style I fill:#dff,stroke:#333,stroke-width:2px style J fill:#ffd,stroke:#333,stroke-width:2px style K fill:#dfd,stroke:#333,stroke-width:2px

项目选择:个人财务管理系统

我们将构建一个个人财务管理系统,具有以下功能:

  1. 用户认证:注册、登录和注销
  2. 交易管理:添加、编辑、删除和查看交易记录
  3. 分类管理:创建和管理交易分类
  4. 数据可视化:展示收入和支出的图表
  5. 报表生成:生成财务报表
  6. 预算设置:设置和跟踪预算

这个项目将使用以下技术:

  • 后端:Flask框架
  • 数据库:SQLite(简单部署)或PostgreSQL(生产环境)
  • ORM:SQLAlchemy
  • 前端:HTML、CSS、JavaScript和Bootstrap
  • 数据可视化:Chart.js
  • 测试:pytest

2. 项目规划

需求分析

在开始编码之前,我们需要明确项目的需求:

功能需求
  1. 用户管理

    • 用户注册(用户名、电子邮件、密码)
    • 用户登录和注销
    • 用户个人资料管理
  2. 交易管理

    • 添加交易(日期、金额、类别、描述)
    • 编辑和删除交易
    • 查看交易列表(支持筛选和排序)
  3. 分类管理

    • 创建交易分类(名称、类型:收入/支出)
    • 编辑和删除分类
    • 查看分类列表
  4. 数据可视化

    • 按类别的收入和支出饼图
    • 按时间的收入和支出折线图
    • 预算与实际支出的对比图
  5. 报表

    • 月度和年度财务报表
    • 导出报表(CSV、PDF)
  6. 预算

    • 设置月度预算(总体和分类)
    • 跟踪预算使用情况
非功能需求
  1. 安全性:密码加密、CSRF保护、输入验证
  2. 性能:页面加载时间小于2秒
  3. 可用性:响应式设计,适配移动设备
  4. 可靠性:数据备份和恢复机制

系统架构

我们将采用经典的MVC(模型-视图-控制器)架构:

  1. 模型(Model):数据库模型和业务逻辑
  2. 视图(View):HTML模板和前端代码
  3. 控制器(Controller):Flask路由和视图函数

数据库设计

我们需要以下数据表:

  1. 用户(users)

    • id: 主键
    • username: 用户名
    • email: 电子邮件
    • password_hash: 密码哈希
    • created_at: 创建时间
  2. 分类(categories)

    • id: 主键
    • name: 分类名称
    • type: 类型(收入/支出)
    • user_id: 外键,关联用户
  3. 交易(transactions)

    • id: 主键
    • amount: 金额
    • date: 日期
    • description: 描述
    • category_id: 外键,关联分类
    • user_id: 外键,关联用户
    • created_at: 创建时间
  4. 预算(budgets)

    • id: 主键
    • amount: 金额
    • month: 月份
    • year: 年份
    • category_id: 外键,关联分类(可选)
    • user_id: 外键,关联用户

项目结构

csharp 复制代码
finance_app/
  ├── app/
  │   ├── __init__.py          # 应用初始化
  │   ├── models.py            # 数据库模型
  │   ├── forms.py             # 表单定义
  │   ├── routes/              # 路由和视图函数
  │   │   ├── __init__.py
  │   │   ├── auth.py          # 认证相关路由
  │   │   ├── main.py          # 主页和通用路由
  │   │   ├── transactions.py  # 交易相关路由
  │   │   ├── categories.py    # 分类相关路由
  │   │   ├── reports.py       # 报表相关路由
  │   │   └── budgets.py       # 预算相关路由
  │   ├── static/              # 静态文件
  │   │   ├── css/
  │   │   ├── js/
  │   │   └── img/
  │   └── templates/           # HTML模板
  │       ├── base.html
  │       ├── index.html
  │       ├── auth/
  │       ├── transactions/
  │       ├── categories/
  │       ├── reports/
  │       └── budgets/
  ├── migrations/              # 数据库迁移文件
  ├── tests/                   # 测试文件
  │   ├── __init__.py
  │   ├── test_models.py
  │   ├── test_routes.py
  │   └── test_forms.py
  ├── config.py                # 配置文件
  ├── run.py                   # 应用入口
  ├── requirements.txt         # 依赖列表
  └── README.md                # 项目说明

3. 项目实现

现在,让我们开始实现这个项目。我们将按照以下步骤进行:

  1. 设置项目环境
  2. 实现数据库模型
  3. 创建用户认证功能
  4. 实现交易和分类管理
  5. 添加数据可视化和报表功能
  6. 实现预算管理
  7. 编写测试
  8. 部署应用

3.1 设置项目环境

首先,我们需要创建项目目录并安装必要的依赖:

bash 复制代码
# 创建项目目录
mkdir finance_app
cd finance_app

# 创建虚拟环境
python -m venv venv

# 激活虚拟环境
# Windows
venv\Scripts\activate
# macOS/Linux
source venv/bin/activate

# 安装依赖
pip install flask flask-sqlalchemy flask-migrate flask-login flask-wtf email-validator flask-bcrypt
pip install pytest pytest-flask

创建基本的项目结构:

bash 复制代码
mkdir -p app/static/{css,js,img}
mkdir -p app/templates/{auth,transactions,categories,reports,budgets}
mkdir -p app/routes
mkdir tests

3.2 配置文件

创建配置文件config.py

python 复制代码
import os
from datetime import timedelta

class Config:
    # 基本配置
    SECRET_KEY = os.environ.get('SECRET_KEY') or 'hard-to-guess-string'
    
    # 数据库配置
    SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
        'sqlite:///' + os.path.join(os.path.abspath(os.path.dirname(__file__)), 'app.db')
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    
    # 会话配置
    PERMANENT_SESSION_LIFETIME = timedelta(days=7)
    
    # 邮件配置
    MAIL_SERVER = os.environ.get('MAIL_SERVER') or 'smtp.googlemail.com'
    MAIL_PORT = int(os.environ.get('MAIL_PORT') or 587)
    MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS') or True
    MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
    MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
    MAIL_DEFAULT_SENDER = os.environ.get('MAIL_DEFAULT_SENDER')

class DevelopmentConfig(Config):
    DEBUG = True

class TestingConfig(Config):
    TESTING = True
    SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'
    WTF_CSRF_ENABLED = False

class ProductionConfig(Config):
    DEBUG = False
    # 生产环境可以使用更安全的数据库
    # SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL')

config = {
    'development': DevelopmentConfig,
    'testing': TestingConfig,
    'production': ProductionConfig,
    'default': DevelopmentConfig
}

3.3 应用初始化

创建app/__init__.py

python 复制代码
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_login import LoginManager
from flask_bcrypt import Bcrypt
from config import config

# 初始化扩展
db = SQLAlchemy()
migrate = Migrate()
login_manager = LoginManager()
login_manager.login_view = 'auth.login'
login_manager.login_message = '请先登录'
bcrypt = Bcrypt()

def create_app(config_name='default'):
    app = Flask(__name__)
    app.config.from_object(config[config_name])
    
    # 初始化扩展
    db.init_app(app)
    migrate.init_app(app, db)
    login_manager.init_app(app)
    bcrypt.init_app(app)
    
    # 注册蓝图
    from app.routes.auth import auth as auth_blueprint
    from app.routes.main import main as main_blueprint
    from app.routes.transactions import transactions as transactions_blueprint
    from app.routes.categories import categories as categories_blueprint
    from app.routes.reports import reports as reports_blueprint
    from app.routes.budgets import budgets as budgets_blueprint
    
    app.register_blueprint(auth_blueprint)
    app.register_blueprint(main_blueprint)
    app.register_blueprint(transactions_blueprint)
    app.register_blueprint(categories_blueprint)
    app.register_blueprint(reports_blueprint)
    app.register_blueprint(budgets_blueprint)
    
    return app

3.4 数据库模型

创建app/models.py

python 复制代码
from datetime import datetime
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
from app import db, login_manager

@login_manager.user_loader
def load_user(user_id):
    return User.query.get(int(user_id))

class User(UserMixin, db.Model):
    __tablename__ = 'users'
    
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(64), unique=True, index=True)
    email = db.Column(db.String(120), unique=True, index=True)
    password_hash = db.Column(db.String(128))
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    
    # 关系
    transactions = db.relationship('Transaction', backref='user', lazy='dynamic')
    categories = db.relationship('Category', backref='user', lazy='dynamic')
    budgets = db.relationship('Budget', backref='user', lazy='dynamic')
    
    @property
    def password(self):
        raise AttributeError('密码不是可读属性')
    
    @password.setter
    def password(self, password):
        self.password_hash = generate_password_hash(password)
    
    def verify_password(self, password):
        return check_password_hash(self.password_hash, password)
    
    def __repr__(self):
        return f'<User {self.username}>'

class Category(db.Model):
    __tablename__ = 'categories'
    
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(64), index=True)
    type = db.Column(db.String(10))  # 'income' 或 'expense'
    user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
    
    # 关系
    transactions = db.relationship('Transaction', backref='category', lazy='dynamic')
    budgets = db.relationship('Budget', backref='category', lazy='dynamic')
    
    def __repr__(self):
        return f'<Category {self.name}>'

class Transaction(db.Model):
    __tablename__ = 'transactions'
    
    id = db.Column(db.Integer, primary_key=True)
    amount = db.Column(db.Float)
    date = db.Column(db.Date, index=True)
    description = db.Column(db.String(255))
    category_id = db.Column(db.Integer, db.ForeignKey('categories.id'))
    user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    
    def __repr__(self):
        return f'<Transaction {self.amount}>'

class Budget(db.Model):
    __tablename__ = 'budgets'
    
    id = db.Column(db.Integer, primary_key=True)
    amount = db.Column(db.Float)
    month = db.Column(db.Integer)
    year = db.Column(db.Integer)
    category_id = db.Column(db.Integer, db.ForeignKey('categories.id'), nullable=True)
    user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
    
    def __repr__(self):
        return f'<Budget {self.amount}>'

3.5 表单定义

创建app/forms.py

python 复制代码
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField, FloatField, SelectField, TextAreaField, DateField
from wtforms.validators import DataRequired, Email, EqualTo, Length, ValidationError
from app.models import User

class RegistrationForm(FlaskForm):
    username = StringField('用户名', validators=[DataRequired(), Length(min=2, max=20)])
    email = StringField('电子邮件', validators=[DataRequired(), Email()])
    password = PasswordField('密码', validators=[DataRequired()])
    confirm_password = PasswordField('确认密码', validators=[DataRequired(), EqualTo('password')])
    submit = SubmitField('注册')
    
    def validate_username(self, username):
        user = User.query.filter_by(username=username.data).first()
        if user:
            raise ValidationError('该用户名已被使用')
    
    def validate_email(self, email):
        user = User.query.filter_by(email=email.data).first()
        if user:
            raise ValidationError('该电子邮件已被注册')

class LoginForm(FlaskForm):
    email = StringField('电子邮件', validators=[DataRequired(), Email()])
    password = PasswordField('密码', validators=[DataRequired()])
    submit = SubmitField('登录')

class CategoryForm(FlaskForm):
    name = StringField('名称', validators=[DataRequired(), Length(max=64)])
    type = SelectField('类型', choices=[('income', '收入'), ('expense', '支出')], validators=[DataRequired()])
    submit = SubmitField('保存')

class TransactionForm(FlaskForm):
    amount = FloatField('金额', validators=[DataRequired()])
    date = DateField('日期', format='%Y-%m-%d', validators=[DataRequired()])
    description = TextAreaField('描述', validators=[Length(max=255)])
    category_id = SelectField('分类', coerce=int, validators=[DataRequired()])
    submit = SubmitField('保存')

class BudgetForm(FlaskForm):
    amount = FloatField('金额', validators=[DataRequired()])
    month = SelectField('月份', choices=[(i, i) for i in range(1, 13)], coerce=int, validators=[DataRequired()])
    year = SelectField('年份', coerce=int, validators=[DataRequired()])
    category_id = SelectField('分类', coerce=int)
    submit = SubmitField('保存')
    
    def __init__(self, *args, **kwargs):
        super(BudgetForm, self).__init__(*args, **kwargs)
        # 动态设置年份选项
        import datetime
        current_year = datetime.datetime.now().year
        self.year.choices = [(i, i) for i in range(current_year - 5, current_year + 6)]

3.6 路由和视图函数

主页路由

创建app/routes/main.py

python 复制代码
from flask import Blueprint, render_template
from flask_login import login_required, current_user
from app.models import Transaction, Category, Budget
from sqlalchemy import func
from app import db
import datetime

main = Blueprint('main', __name__)

@main.route('/')
def index():
    if current_user.is_authenticated:
        return render_template('index.html')
    return render_template('landing.html')

@main.route('/dashboard')
@login_required
def dashboard():
    # 获取当前月份的交易
    today = datetime.date.today()
    start_date = datetime.date(today.year, today.month, 1)
    end_date = datetime.date(today.year if today.month < 12 else today.year + 1,
                           (today.month + 1) if today.month < 12 else 1, 1)
    
    # 收入和支出总额
    income = db.session.query(func.sum(Transaction.amount)).\
        join(Category).filter(Category.type == 'income',
                             Transaction.user_id == current_user.id,
                             Transaction.date >= start_date,
                             Transaction.date < end_date).scalar() or 0
    
    expense = db.session.query(func.sum(Transaction.amount)).\
        join(Category).filter(Category.type == 'expense',
                             Transaction.user_id == current_user.id,
                             Transaction.date >= start_date,
                             Transaction.date < end_date).scalar() or 0
    
    # 最近的交易
    recent_transactions = Transaction.query.\
        filter_by(user_id=current_user.id).\
        order_by(Transaction.date.desc()).\
        limit(5).all()
    
    # 预算使用情况
    budgets = Budget.query.filter_by(
        user_id=current_user.id,
        month=today.month,
        year=today.year
    ).all()
    
    # 计算预算使用百分比
    budget_usage = []
    for budget in budgets:
        if budget.category_id:
            # 特定分类的预算
            spent = db.session.query(func.sum(Transaction.amount)).\
                filter(Transaction.category_id == budget.category_id,
                      Transaction.user_id == current_user.id,
                      Transaction.date >= start_date,
                      Transaction.date < end_date).scalar() or 0
            
            category_name = Category.query.get(budget.category_id).name
            budget_usage.append({
                'category': category_name,
                'budget': budget.amount,
                'spent': spent,
                'percentage': min(100, int(spent / budget.amount * 100)) if budget.amount > 0 else 0
            })
        else:
            # 总体预算
            budget_usage.append({
                'category': '总计',
                'budget': budget.amount,
                'spent': expense,
                'percentage': min(100, int(expense / budget.amount * 100)) if budget.amount > 0 else 0
            })
    
    return render_template('dashboard.html',
                          income=income,
                          expense=expense,
                          balance=income-expense,
                          recent_transactions=recent_transactions,
                          budget_usage=budget_usage)
认证路由

创建app/routes/auth.py

python 复制代码
from flask import Blueprint, render_template, redirect, url_for, flash, request
from flask_login import login_user, logout_user, login_required, current_user
from app import db
from app.models import User
from app.forms import RegistrationForm, LoginForm

auth = Blueprint('auth', __name__)

@auth.route('/register', methods=['GET', 'POST'])
def register():
    if current_user.is_authenticated:
        return redirect(url_for('main.dashboard'))
    
    form = RegistrationForm()
    if form.validate_on_submit():
        user = User(username=form.username.data,
                   email=form.email.data)
        user.password = form.password.data
        db.session.add(user)
        db.session.commit()
        flash('注册成功!现在您可以登录了。', 'success')
        return redirect(url_for('auth.login'))
    
    return render_template('auth/register.html', form=form)

@auth.route('/login', methods=['GET', 'POST'])
def login():
    if current_user.is_authenticated:
        return redirect(url_for('main.dashboard'))
    
    form = LoginForm()
    if form.validate_on_submit():
        user = User.query.filter_by(email=form.email.data).first()
        if user and user.verify_password(form.password.data):
            login_user(user)
            next_page = request.args.get('next')
            return redirect(next_page or url_for('main.dashboard'))
        else:
            flash('登录失败。请检查电子邮件和密码。', 'danger')
    
    return render_template('auth/login.html', form=form)

@auth.route('/logout')
@login_required
def logout():
    logout_user()
    flash('您已成功注销。', 'success')
    return redirect(url_for('main.index'))

@auth.route('/profile')
@login_required
def profile():
    return render_template('auth/profile.html')
交易路由

创建app/routes/transactions.py

python 复制代码
from flask import Blueprint, render_template, redirect, url_for, flash, request
from flask_login import login_required, current_user
from app import db
from app.models import Transaction, Category
from app.forms import TransactionForm
from sqlalchemy import desc
import datetime

transactions = Blueprint('transactions', __name__)

@transactions.route('/transactions')
@login_required
def index():
    # 获取筛选参数
    category_id = request.args.get('category_id', type=int)
    start_date = request.args.get('start_date')
    end_date = request.args.get('end_date')
    transaction_type = request.args.get('type')  # 'income' 或 'expense'
    
    # 构建查询
    query = Transaction.query.filter_by(user_id=current_user.id)
    
    if category_id:
        query = query.filter_by(category_id=category_id)
    
    if start_date:
        query = query.filter(Transaction.date >= datetime.datetime.strptime(start_date, '%Y-%m-%d').date())
    
    if end_date:
        query = query.filter(Transaction.date <= datetime.datetime.strptime(end_date, '%Y-%m-%d').date())
    
    if transaction_type:
        query = query.join(Category).filter(Category.type == transaction_type)
    
    # 排序
    sort_by = request.args.get('sort_by', 'date')
    sort_order = request.args.get('sort_order', 'desc')
    
    if sort_by == 'amount':
        query = query.order_by(desc(Transaction.amount) if sort_order == 'desc' else Transaction.amount)
    elif sort_by == 'date':
        query = query.order_by(desc(Transaction.date) if sort_order == 'desc' else Transaction.date)
    else:
        query = query.order_by(desc(Transaction.date))
    
    # 分页
    page = request.args.get('page', 1, type=int)
    per_page = 20
    pagination = query.paginate(page=page, per_page=per_page, error_out=False)
    transactions_list = pagination.items
    
    # 获取所有分类,用于筛选
    categories = Category.query.filter_by(user_id=current_user.id).all()
    
    return render_template('transactions/index.html',
                          transactions=transactions_list,
                          pagination=pagination,
                          categories=categories)

@transactions.route('/transactions/create', methods=['GET', 'POST'])
@login_required
def create():
    form = TransactionForm()
    
    # 动态设置分类选项
    form.category_id.choices = [(c.id, c.name) for c in 
                               Category.query.filter_by(user_id=current_user.id).all()]
    
    if form.validate_on_submit():
        transaction = Transaction(
            amount=form.amount.data,
            date=form.date.data,
            description=form.description.data,
            category_id=form.category_id.data,
            user_id=current_user.id
        )
        db.session.add(transaction)
        db.session.commit()
        flash('交易已添加!', 'success')
        return redirect(url_for('transactions.index'))
    
    return render_template('transactions/create.html', form=form)

@transactions.route('/transactions/<int:id>/edit', methods=['GET', 'POST'])
@login_required
def edit(id):
    transaction = Transaction.query.get_or_404(id)
    
    # 确保用户只能编辑自己的交易
    if transaction.user_id != current_user.id:
        flash('您无权编辑此交易。', 'danger')
        return redirect(url_for('transactions.index'))
    
    form = TransactionForm()
    
    # 动态设置分类选项
    form.category_id.choices = [(c.id, c.name) for c in 
                               Category.query.filter_by(user_id=current_user.id).all()]
    
    if form.validate_on_submit():
        transaction.amount = form.amount.data
        transaction.date = form.date.data
        transaction.description = form.description.data
        transaction.category_id = form.category_id.data
        db.session.commit()
        flash('交易已更新!', 'success')
        return redirect(url_for('transactions.index'))
    
    # 填充表单
    if request.method == 'GET':
        form.amount.data = transaction.amount
        form.date.data = transaction.date
        form.description.data = transaction.description
        form.category_id.data = transaction.category_id
    
    return render_template('transactions/edit.html', form=form, transaction=transaction)

@transactions.route('/transactions/<int:id>/delete', methods=['POST'])
@login_required
def delete(id):
    transaction = Transaction.query.get_or_404(id)
    
    # 确保用户只能删除自己的交易
    if transaction.user_id != current_user.id:
        flash('您无权删除此交易。', 'danger')
        return redirect(url_for('transactions.index'))
    
    db.session.delete(transaction)
    db.session.commit()
    flash('交易已删除!', 'success')
    return redirect(url_for('transactions.index'))
分类路由

创建app/routes/categories.py

python 复制代码
from flask import Blueprint, render_template, redirect, url_for, flash, request
from flask_login import login_required, current_user
from app import db
from app.models import Category
from app.forms import CategoryForm

categories = Blueprint('categories', __name__)

@categories.route('/categories')
@login_required
def index():
    categories_list = Category.query.filter_by(user_id=current_user.id).all()
    return render_template('categories/index.html', categories=categories_list)

@categories.route('/categories/create', methods=['GET', 'POST'])
@login_required
def create():
    form = CategoryForm()
    
    if form.validate_on_submit():
        category = Category(
            name=form.name.data,
            type=form.type.data,
            user_id=current_user.id
        )
        db.session.add(category)
        db.session.commit()
        flash('分类已添加!', 'success')
        return redirect(url_for('categories.index'))
    
    return render_template('categories/create.html', form=form)

@categories.route('/categories/<int:id>/edit', methods=['GET', 'POST'])
@login_required
def edit(id):
    category = Category.query.get_or_404(id)
    
    # 确保用户只能编辑自己的分类
    if category.user_id != current_user.id:
        flash('您无权编辑此分类。', 'danger')
        return redirect(url_for('categories.index'))
    
    form = CategoryForm()
    
    if form.validate_on_submit():
        category.name = form.name.data
        category.type = form.type.data
        db.session.commit()
        flash('分类已更新!', 'success')
        return redirect(url_for('categories.index'))
    
    # 填充表单
    if request.method == 'GET':
        form.name.data = category.name
        form.type.data = category.type
    
    return render_template('categories/edit.html', form=form, category=category)

@categories.route('/categories/<int:id>/delete', methods=['POST'])
@login_required
def delete(id):
    category = Category.query.get_or_404(id)
    
    # 确保用户只能删除自己的分类
    if category.user_id != current_user.id:
        flash('您无权删除此分类。', 'danger')
        return redirect(url_for('categories.index'))
    
    # 检查是否有关联的交易
    if category.transactions.count() > 0:
        flash('无法删除此分类,因为它已被交易使用。', 'danger')
        return redirect(url_for('categories.index'))
    
    db.session.delete(category)
    db.session.commit()
    flash('分类已删除!', 'success')
    return redirect(url_for('categories.index'))
报表路由

创建app/routes/reports.py

python 复制代码
from flask import Blueprint, render_template, request, jsonify, send_file
from flask_login import login_required, current_user
from app import db
from app.models import Transaction, Category
from sqlalchemy import func, extract
import datetime
import io
import csv

reports = Blueprint('reports', __name__)

@reports.route('/reports')
@login_required
def index():
    return render_template('reports/index.html')

@reports.route('/reports/monthly')
@login_required
def monthly():
    # 获取年份和月份参数
    year = request.args.get('year', datetime.datetime.now().year, type=int)
    month = request.args.get('month', datetime.datetime.now().month, type=int)
    
    # 计算日期范围
    start_date = datetime.date(year, month, 1)
    if month == 12:
        end_date = datetime.date(year + 1, 1, 1)
    else:
        end_date = datetime.date(year, month + 1, 1)
    
    # 获取收入和支出
    income_by_category = db.session.query(
        Category.name,
        func.sum(Transaction.amount).label('total')
    ).join(Transaction).filter(
        Transaction.user_id == current_user.id,
        Transaction.date >= start_date,
        Transaction.date < end_date,
        Category.type == 'income'
    ).group_by(Category.name).all()
    
    expense_by_category = db.session.query(
        Category.name,
        func.sum(Transaction.amount).label('total')
    ).join(Transaction).filter(
        Transaction.user_id == current_user.id,
        Transaction.date >= start_date,
        Transaction.date < end_date,
        Category.type == 'expense'
    ).group_by(Category.name).all()
    
    # 计算总收入和总支出
    total_income = sum(item.total for item in income_by_category)
    total_expense = sum(item.total for item in expense_by_category)
    
    # 按日期的收入和支出
    daily_data = db.session.query(
        Transaction.date,
        Category.type,
        func.sum(Transaction.amount).label('total')
    ).join(Category).filter(
        Transaction.user_id == current_user.id,
        Transaction.date >= start_date,
        Transaction.date < end_date
    ).group_by(Transaction.date, Category.type).all()
    
    # 处理数据用于图表
    dates = sorted(set(item.date for item in daily_data))
    daily_income = {date: 0 for date in dates}
    daily_expense = {date: 0 for date in dates}
    
    for item in daily_data:
        if item.type == 'income':
            daily_income[item.date] = item.total
        else:
            daily_expense[item.date] = item.total
    
    chart_dates = [date.strftime('%Y-%m-%d') for date in dates]
    chart_income = [daily_income[date] for date in dates]
    chart_expense = [daily_expense[date] for date in dates]
    
    return render_template('reports/monthly.html',
                          year=year,
                          month=month,
                          income_by_category=income_by_category,
                          expense_by_category=expense_by_category,
                          total_income=total_income,
                          total_expense=total_expense,
                          chart_dates=chart_dates,
                          chart_income=chart_income,
                          chart_expense=chart_expense)

@reports.route('/reports/annual')
@login_required
def annual():
    # 获取年份参数
    year = request.args.get('year', datetime.datetime.now().year, type=int)
    
    # 按月份和类型的收入和支出
    monthly_data = db.session.query(
        extract('month', Transaction.date).label('month'),
        Category.type,
        func.sum(Transaction.amount).label('total')
    ).join(Category).filter(
        Transaction.user_id == current_user.id,
        extract('year', Transaction.date) == year
    ).group_by('month', Category.type).all()
    
    # 处理数据用于图表
    months = list(range(1, 13))
    monthly_income = {month: 0 for month in months}
    monthly_expense = {month: 0 for month in months}
    
    for item in monthly_data:
        if item.type == 'income':
            monthly_income[item.month] = item.total
        else:
            monthly_expense[item.month] = item.total
    
    chart_months = [f"{year}-{month:02d}" for month in months]
    chart_income = [monthly_income[month] for month in months]
    chart_expense = [monthly_expense[month] for month in months]
    
    # 计算年度总收入和总支出
    total_income = sum(monthly_income.values())
    total_expense = sum(monthly_expense.values())
    
    # 按类别的年度收入和支出
    income_by_category = db.session.query(
        Category.name,
        func.sum(Transaction.amount).label('total')
    ).join(Transaction).filter(
        Transaction.user_id == current_user.id,
        extract('year', Transaction.date) == year,
        Category.type == 'income'
    ).group_by(Category.name).all()
    
    expense_by_category = db.session.query(
        Category.name,
        func.sum(Transaction.amount).label('total')
    ).join(Transaction).filter(
        Transaction.user_id == current_user.id,
        extract('year', Transaction.date) == year,
        Category.type == 'expense'
    ).group_by(Category.name).all()
    
    return render_template('reports/annual.html',
                          year=year,
                          monthly_income=monthly_income,
                          monthly_expense=monthly_expense,
                          chart_months=chart_months,
                          chart_income=chart_income,
                          chart_expense=chart_expense,
                          total_income=total_income,
                          total_expense=total_expense,
                          income_by_category=income_by_category,
                          expense_by_category=expense_by_category)

@reports.route('/reports/export')
@login_required
def export():
    # 获取日期范围
    start_date = request.args.get('start_date')
    end_date = request.args.get('end_date')
    
    if start_date:
        start_date = datetime.datetime.strptime(start_date, '%Y-%m-%d').date()
    else:
        # 默认为当年开始
        start_date = datetime.date(datetime.date.today().year, 1, 1)
    
    if end_date:
        end_date = datetime.datetime.strptime(end_date, '%Y-%m-%d').date()
    else:
        # 默认为今天
        end_date = datetime.date.today()
    
    # 查询交易
    transactions = Transaction.query.filter(
        Transaction.user_id == current_user.id,
        Transaction.date >= start_date,
        Transaction.date <= end_date
    ).order_by(Transaction.date).all()
    
    # 创建CSV文件
    output = io.StringIO()
    writer = csv.writer(output)
    
    # 写入标题行
    writer.writerow(['日期', '金额', '类别', '类型', '描述'])
    
    # 写入数据行
    for transaction in transactions:
        category = Category.query.get(transaction.category_id)
        writer.writerow([
            transaction.date.strftime('%Y-%m-%d'),
            transaction.amount,
            category.name,
            '收入' if category.type == 'income' else '支出',
            transaction.description
        ])
    
    # 准备响应
    output.seek(0)
    return send_file(
        io.BytesIO(output.getvalue().encode('utf-8-sig')),
        mimetype='text/csv',
        as_attachment=True,
        download_name=f'transactions_{start_date}_{end_date}.csv'
    )
预算路由

创建app/routes/budgets.py

python 复制代码
from flask import Blueprint, render_template, redirect, url_for, flash, request
from flask_login import login_required, current_user
from app import db
from app.models import Budget, Category, Transaction
from app.forms import BudgetForm
from sqlalchemy import func
import datetime

budgets = Blueprint('budgets', __name__)

@budgets.route('/budgets')
@login_required
def index():
    # 获取当前月份和年份
    today = datetime.date.today()
    month = request.args.get('month', today.month, type=int)
    year = request.args.get('year', today.year, type=int)
    
    # 查询预算
    budgets_list = Budget.query.filter_by(
        user_id=current_user.id,
        month=month,
        year=year
    ).all()
    
    # 计算每个预算的使用情况
    start_date = datetime.date(year, month, 1)
    if month == 12:
        end_date = datetime.date(year + 1, 1, 1)
    else:
        end_date = datetime.date(year, month + 1, 1)
    
    budget_usage = []
    for budget in budgets_list:
        if budget.category_id:
            # 特定分类的预算
            spent = db.session.query(func.sum(Transaction.amount)).\
                filter(Transaction.category_id == budget.category_id,
                      Transaction.user_id == current_user.id,
                      Transaction.date >= start_date,
                      Transaction.date < end_date).scalar() or 0
            
            category = Category.query.get(budget.category_id)
            budget_usage.append({
                'id': budget.id,
                'category': category.name if category else '未知',
                'budget': budget.amount,
                'spent': spent,
                'remaining': budget.amount - spent,
                'percentage': min(100, int(spent / budget.amount * 100)) if budget.amount > 0 else 0
            })
        else:
            # 总体预算
            spent = db.session.query(func.sum(Transaction.amount)).\
                join(Category).filter(Category.type == 'expense',
                                    Transaction.user_id == current_user.id,
                                    Transaction.date >= start_date,
                                    Transaction.date < end_date).scalar() or 0
            
            budget_usage.append({
                'id': budget.id,
                'category': '总支出',
                'budget': budget.amount,
                'spent': spent,
                'remaining': budget.amount - spent,
                'percentage': min(100, int(spent / budget.amount * 100)) if budget.amount > 0 else 0
            })
    
    return render_template('budgets/index.html',
                          budgets=budget_usage,
                          month=month,
                          year=year)

@budgets.route('/budgets/create', methods=['GET', 'POST'])
@login_required
def create():
    form = BudgetForm()
    
    # 动态设置分类选项
    categories = Category.query.filter_by(user_id=current_user.id, type='expense').all()
    form.category_id.choices = [(0, '总体预算')] + [(c.id, c.name) for c in categories]
    
    if form.validate_on_submit():
        # 检查是否已存在相同月份、年份和分类的预算
        existing_budget = Budget.query.filter_by(
            user_id=current_user.id,
            month=form.month.data,
            year=form.year.data,
            category_id=form.category_id.data if form.category_id.data != 0 else None
        ).first()
        
        if existing_budget:
            flash('该月份已存在此类别的预算。', 'danger')
            return redirect(url_for('budgets.create'))
        
        budget = Budget(
            amount=form.amount.data,
            month=form.month.data,
            year=form.year.data,
            category_id=form.category_id.data if form.category_id.data != 0 else None,
            user_id=current_user.id
        )
        db.session.add(budget)
        db.session.commit()
        flash('预算已添加!', 'success')
        return redirect(url_for('budgets.index'))
    
    return render_template('budgets/create.html', form=form)

@budgets.route('/budgets/<int:id>/edit', methods=['GET', 'POST'])
@login_required
def edit(id):
    budget = Budget.query.get_or_404(id)
    
    # 确保用户只能编辑自己的预算
    if budget.user_id != current_user.id:
        flash('您无权编辑此预算。', 'danger')
        return redirect(url_for('budgets.index'))
    
    form = BudgetForm()
    
    # 动态设置分类选项
    categories = Category.query.filter_by(user_id=current_user.id, type='expense').all()
    form.category_id.choices = [(0, '总体预算')] + [(c.id, c.name) for c in categories]
    
    if form.validate_on_submit():
        budget.amount = form.amount.data
        budget.month = form.month.data
        budget.year = form.year.data
        budget.category_id = form.category_id.data if form.category_id.data != 0 else None
        db.session.commit()
        flash('预算已更新!', 'success')
        return redirect(url_for('budgets.index'))
    
    # 填充表单
    if request.method == 'GET':
        form.amount.data = budget.amount
        form.month.data = budget.month
        form.year.data = budget.year
        form.category_id.data = budget.category_id if budget.category_id else 0
    
    return render_template('budgets/edit.html', form=form, budget=budget)

@budgets.route('/budgets/<int:id>/delete', methods=['POST'])
@login_required
def delete(id):
    budget = Budget.query.get_or_404(id)
    
    # 确保用户只能删除自己的预算
    if budget.user_id != current_user.id:
        flash('您无权删除此预算。', 'danger')
        return redirect(url_for('budgets.index'))
    
    db.session.delete(budget)
    db.session.commit()
    flash('预算已删除!', 'success')
    return redirect(url_for('budgets.index'))

3.7 HTML模板

基础模板

创建app/templates/base.html

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>
    <!-- Bootstrap CSS -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
    <!-- Font Awesome -->
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
    <!-- 自定义CSS -->
    <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
    {% block styles %}{% endblock %}
</head>
<body>
    <!-- 导航栏 -->
    <nav class="navbar navbar-expand-lg navbar-dark bg-primary">
        <div class="container">
            <a class="navbar-brand" href="{{ url_for('main.index') }}">
                <i class="fas fa-wallet"></i> 财务管理
            </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 me-auto">
                    {% if current_user.is_authenticated %}
                        <li class="nav-item">
                            <a class="nav-link" href="{{ url_for('main.dashboard') }}">
                                <i class="fas fa-tachometer-alt"></i> 仪表盘
                            </a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link" href="{{ url_for('transactions.index') }}">
                                <i class="fas fa-exchange-alt"></i> 交易
                            </a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link" href="{{ url_for('categories.index') }}">
                                <i class="fas fa-tags"></i> 分类
                            </a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link" href="{{ url_for('budgets.index') }}">
                                <i class="fas fa-chart-pie"></i> 预算
                            </a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link" href="{{ url_for('reports.index') }}">
                                <i class="fas fa-chart-bar"></i> 报表
                            </a>
                        </li>
                    {% endif %}
                </ul>
                <ul class="navbar-nav">
                    {% if current_user.is_authenticated %}
                        <li class="nav-item dropdown">
                            <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown">
                                <i class="fas fa-user"></i> {{ current_user.username }}
                            </a>
                            <ul class="dropdown-menu dropdown-menu-end">
                                <li><a class="dropdown-item" href="{{ url_for('auth.profile') }}">个人资料</a></li>
                                <li><hr class="dropdown-divider"></li>
                                <li><a class="dropdown-item" href="{{ url_for('auth.logout') }}">注销</a></li>
                            </ul>
                        </li>
                    {% else %}
                        <li class="nav-item">
                            <a class="nav-link" href="{{ url_for('auth.login') }}">登录</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link" href="{{ url_for('auth.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">
                        {{ message }}
                        <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
                    </div>
                {% endfor %}
            {% endif %}
        {% endwith %}

        <!-- 页面内容 -->
        {% block content %}{% endblock %}
    </div>

    <!-- 页脚 -->
    <footer class="footer mt-5 py-3 bg-light">
        <div class="container text-center">
            <span class="text-muted">© 2025 个人财务管理系统</span>
        </div>
    </footer>

    <!-- Bootstrap JS -->
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
    <!-- Chart.js -->
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    <!-- 自定义JS -->
    <script src="{{ url_for('static', filename='js/script.js') }}"></script>
    {% block scripts %}{% endblock %}
</body>
</html>
仪表盘模板

创建app/templates/dashboard.html

html 复制代码
{% extends 'base.html' %}

{% block title %}仪表盘 - 个人财务管理系统{% endblock %}

{% block content %}
<h1 class="mb-4">仪表盘</h1>

<!-- 财务概览 -->
<div class="row mb-4">
    <div class="col-md-4">
        <div class="card bg-success text-white">
            <div class="card-body">
                <h5 class="card-title">收入</h5>
                <h2 class="card-text">¥{{ "%.2f"|format(income) }}</h2>
            </div>
        </div>
    </div>
    <div class="col-md-4">
        <div class="card bg-danger text-white">
            <div class="card-body">
                <h5 class="card-title">支出</h5>
                <h2 class="card-text">¥{{ "%.2f"|format(expense) }}</h2>
            </div>
        </div>
    </div>
    <div class="col-md-4">
        <div class="card {% if balance >= 0 %}bg-primary{% else %}bg-warning{% endif %} text-white">
            <div class="card-body">
                <h5 class="card-title">结余</h5>
                <h2 class="card-text">¥{{ "%.2f"|format(balance) }}</h2>
            </div>
        </div>
    </div>
</div>

<div class="row">
    <!-- 最近交易 -->
    <div class="col-md-6">
        <div class="card mb-4">
            <div class="card-header">
                <h5 class="card-title mb-0">最近交易</h5>
            </div>
            <div class="card-body">
                {% if recent_transactions %}
                    <div class="table-responsive">
                        <table class="table table-striped">
                            <thead>
                                <tr>
                                    <th>日期</th>
                                    <th>分类</th>
                                    <th>金额</th>
                                </tr>
                            </thead>
                            <tbody>
                                {% for transaction in recent_transactions %}
                                    <tr>
                                        <td>{{ transaction.date.strftime('%Y-%m-%d') }}</td>
                                        <td>{{ transaction.category.name }}</td>
                                        <td class="{% if transaction.category.type == 'income' %}text-success{% else %}text-danger{% endif %}">
                                            {% if transaction.category.type == 'income' %}+{% else %}-{% endif %}
                                            ¥{{ "%.2f"|format(transaction.amount) }}
                                        </td>
                                    </tr>
                                {% endfor %}
                            </tbody>
                        </table>
                    </div>
                    <a href="{{ url_for('transactions.index') }}" class="btn btn-sm btn-primary">查看所有交易</a>
                {% else %}
                    <p class="text-muted">暂无交易记录</p>
                    <a href="{{ url_for('transactions.create') }}" class="btn btn-sm btn-primary">添加交易</a>
                {% endif %}
            </div>
        </div>
    </div>
    
    <!-- 预算使用情况 -->
    <div class="col-md-6">
        <div class="card mb-4">
            <div class="card-header">
                <h5 class="card-title mb-0">预算使用情况</h5>
            </div>
            <div class="card-body">
                {% if budget_usage %}
                    {% for item in budget_usage %}
                        <div class="mb-3">
                            <div class="d-flex justify-content-between">
                                <span>{{ item.category }}</span>
                                <span>¥{{ "%.2f"|format(item.spent) }} / ¥{{ "%.2f"|format(item.budget) }}</span>
                            </div>
                            <div class="progress">
                                <div class="progress-bar {% if item.percentage >= 100 %}bg-danger{% elif item.percentage >= 80 %}bg-warning{% else %}bg-success{% endif %}" 
                                     role="progressbar" 
                                     style="width: {{ item.percentage }}%" 
                                     aria-valuenow="{{ item.percentage }}" 
                                     aria-valuemin="0" 
                                     aria-valuemax="100">
                                    {{ item.percentage }}%
                                </div>
                            </div>
                        </div>
                    {% endfor %}
                    <a href="{{ url_for('budgets.index') }}" class="btn btn-sm btn-primary">管理预算</a>
                {% else %}
                    <p class="text-muted">暂无预算设置</p>
                    <a href="{{ url_for('budgets.create') }}" class="btn btn-sm btn-primary">设置预算</a>
                {% endif %}
            </div>
        </div>
    </div>
</div>
{% endblock %}

3.8 静态文件

创建app/static/css/style.css

css 复制代码
/* 全局样式 */
body {
    min-height: 100vh;
    display: flex;
    flex-direction: column;
}

.container {
    flex: 1;
}

/* 导航栏 */
.navbar-brand i {
    margin-right: 5px;
}

/* 卡片样式 */
.card {
    box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
    margin-bottom: 1.5rem;
}

.card-header {
    background-color: #f8f9fa;
}

/* 表单样式 */
.form-group {
    margin-bottom: 1rem;
}

/* 按钮样式 */
.btn-action {
    margin-right: 5px;
}

/* 表格样式 */
.table-responsive {
    margin-bottom: 1rem;
}

/* 页脚 */
.footer {
    margin-top: auto;
}

/* 图表容器 */
.chart-container {
    position: relative;
    height: 300px;
    margin-bottom: 2rem;
}

创建app/static/js/script.js

javascript 复制代码
// 通用函数
document.addEventListener('DOMContentLoaded', function() {
    // 自动关闭警告消息
    setTimeout(function() {
        const alerts = document.querySelectorAll('.alert');
        alerts.forEach(function(alert) {
            const bsAlert = new bootstrap.Alert(alert);
            bsAlert.close();
        });
    }, 5000);
    
    // 日期选择器初始化
    const dateInputs = document.querySelectorAll('input[type="date"]');
    dateInputs.forEach(function(input) {
        if (!input.value) {
            const today = new Date();
            const year = today.getFullYear();
            const month = String(today.getMonth() + 1).padStart(2, '0');
            const day = String(today.getDate()).padStart(2, '0');
            input.value = `${year}-${month}-${day}`;
        }
    });
    
    // 确认删除
    const deleteButtons = document.querySelectorAll('.btn-delete');
    deleteButtons.forEach(function(button) {
        button.addEventListener('click', function(e) {
            if (!confirm('确定要删除吗?此操作无法撤销。')) {
                e.preventDefault();
            }
        });
    });
});

// 创建饼图
function createPieChart(elementId, labels, data, backgroundColor) {
    const ctx = document.getElementById(elementId).getContext('2d');
    new Chart(ctx, {
        type: 'pie',
        data: {
            labels: labels,
            datasets: [{
                data: data,
                backgroundColor: backgroundColor,
                borderWidth: 1
            }]
        },
        options: {
            responsive: true,
            maintainAspectRatio: false,
            plugins: {
                legend: {
                    position: 'right'
                }
            }
        }
    });
}

// 创建折线图
function createLineChart(elementId, labels, datasets) {
    const ctx = document.getElementById(elementId).getContext('2d');
    new Chart(ctx, {
        type: 'line',
        data: {
            labels: labels,
            datasets: datasets
        },
        options: {
            responsive: true,
            maintainAspectRatio: false,
            scales: {
                y: {
                    beginAtZero: true
                }
            }
        }
    });
}

3.9 应用入口

创建run.py

python 复制代码
from app import create_app, db
from app.models import User, Category, Transaction, Budget

app = create_app()

@app.shell_context_processor
def make_shell_context():
    return {
        'db': db,
        'User': User,
        'Category': Category,
        'Transaction': Transaction,
        'Budget': Budget
    }

if __name__ == '__main__':
    app.run(debug=True)

4. 测试

4.1 单元测试

创建tests/test_models.py

python 复制代码
import unittest
from app import create_app, db
from app.models import User, Category, Transaction, Budget
import datetime

class ModelsTestCase(unittest.TestCase):
    def setUp(self):
        self.app = create_app('testing')
        self.app_context = self.app.app_context()
        self.app_context.push()
        db.create_all()
        
        # 创建测试用户
        self.user = User(username='testuser', email='test@example.com')
        self.user.password = 'password'
        db.session.add(self.user)
        db.session.commit()
    
    def tearDown(self):
        db.session.remove()
        db.drop_all()
        self.app_context.pop()
    
    def test_user_password(self):
        self.assertTrue(self.user.verify_password('password'))
        self.assertFalse(self.user.verify_password('wrong_password'))
    
    def test_category_creation(self):
        category = Category(name='Food', type='expense', user=self.user)
        db.session.add(category)
        db.session.commit()
        
        self.assertEqual(category.name, 'Food')
        self.assertEqual(category.type, 'expense')
        self.assertEqual(category.user, self.user)
    
    def test_transaction_creation(self):
        category = Category(name='Food', type='expense', user=self.user)
        db.session.add(category)
        db.session.commit()
        
        transaction = Transaction(
            amount=50.0,
            date=datetime.date.today(),
            description='Groceries',
            category=category,
            user=self.user
        )
        db.session.add(transaction)
        db.session.commit()
        
        self.assertEqual(transaction.amount, 50.0)
        self.assertEqual(transaction.description, 'Groceries')
        self.assertEqual(transaction.category, category)
        self.assertEqual(transaction.user, self.user)
    
    def test_budget_creation(self):
        category = Category(name='Food', type='expense', user=self.user)
        db.session.add(category)
        db.session.commit()
        
        budget = Budget(
            amount=500.0,
            month=datetime.date.today().month,
            year=datetime.date.today().year,
            category=category,
            user=self.user
        )
        db.session.add(budget)
        db.session.commit()
        
        self.assertEqual(budget.amount, 500.0)
        self.assertEqual(budget.category, category)
        self.assertEqual(budget.user, self.user)

创建tests/test_routes.py

python 复制代码
import unittest
from flask import url_for
from app import create_app, db
from app.models import User, Category, Transaction
import datetime

class RoutesTestCase(unittest.TestCase):
    def setUp(self):
        self.app = create_app('testing')
        self.app_context = self.app.app_context()
        self.app_context.push()
        db.create_all()
        
        # 创建测试客户端
        self.client = self.app.test_client(use_cookies=True)
        
        # 创建测试用户
        self.user = User(username='testuser', email='test@example.com')
        self.user.password = 'password'
        db.session.add(self.user)
        db.session.commit()
    
    def tearDown(self):
        db.session.remove()
        db.drop_all()
        self.app_context.pop()
    
    def login(self):
        return self.client.post(url_for('auth.login'), data={
            'email': 'test@example.com',
            'password': 'password'
        }, follow_redirects=True)
    
    def test_home_page(self):
        response = self.client.get(url_for('main.index'))
        self.assertEqual(response.status_code, 200)
    
    def test_login_and_logout(self):
        # 登录
        response = self.login()
        self.assertEqual(response.status_code, 200)
        
        # 注销
        response = self.client.get(url_for('auth.logout'), follow_redirects=True)
        self.assertEqual(response.status_code, 200)
    
    def test_dashboard_access(self):
        # 未登录时访问仪表盘
        response = self.client.get(url_for('main.dashboard'), follow_redirects=True)
        self.assertEqual(response.status_code, 200)
        self.assertIn(b'login', response.data)
        
        # 登录后访问仪表盘
        self.login()
        response = self.client.get(url_for('main.dashboard'))
        self.assertEqual(response.status_code, 200)
        self.assertIn(b'Dashboard', response.data)
    
    def test_category_management(self):
        self.login()
        
        # 创建分类
        response = self.client.post(url_for('categories.create'), data={
            'name': 'Food',
            'type': 'expense'
        }, follow_redirects=True)
        self.assertEqual(response.status_code, 200)
        self.assertIn(b'Food', response.data)
        
        # 验证分类已创建
        category = Category.query.filter_by(name='Food').first()
        self.assertIsNotNone(category)
        self.assertEqual(category.name, 'Food')
        self.assertEqual(category.type, 'expense')
    
    def test_transaction_management(self):
        self.login()
        
        # 创建分类
        category = Category(name='Food', type='expense', user=self.user)
        db.session.add(category)
        db.session.commit()
        
        # 创建交易
        response = self.client.post(url_for('transactions.create'), data={
            'amount': '50.0',
            'date': datetime.date.today().strftime('%Y-%m-%d'),
            'description': 'Groceries',
            'category_id': category.id
        }, follow_redirects=True)
        self.assertEqual(response.status_code, 200)
        
        # 验证交易已创建
        transaction = Transaction.query.filter_by(description='Groceries').first()
        self.assertIsNotNone(transaction)
        self.assertEqual(transaction.amount, 50.0)
        self.assertEqual(transaction.category, category)

4.2 运行测试

bash 复制代码
# 运行所有测试
python -m unittest discover tests

# 运行特定测试文件
python -m unittest tests/test_models.py

5. 部署

5.1 准备生产环境

  1. 创建requirements.txt
bash 复制代码
pip freeze > requirements.txt
  1. 配置环境变量:
bash 复制代码
export FLASK_APP=run.py
export FLASK_ENV=production
export SECRET_KEY=your-secret-key

5.2 使用Gunicorn和Nginx部署

  1. 安装Gunicorn:
bash 复制代码
pip install gunicorn
  1. 创建Gunicorn配置文件gunicorn_config.py
python 复制代码
bind = "127.0.0.1:8000"
workers = 4
worker_class = "gevent"
timeout = 120
  1. 启动Gunicorn:
bash 复制代码
gunicorn -c gunicorn_config.py run:app
  1. Nginx配置:
nginx 复制代码
server {
    listen 80;
    server_name yourdomain.com;

    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    location /static {
        alias /path/to/your/app/static;
    }
}

5.3 使用Docker部署

  1. 创建Dockerfile
dockerfile 复制代码
FROM python:3.9-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

ENV FLASK_APP=run.py
ENV FLASK_ENV=production

EXPOSE 5000

CMD ["gunicorn", "--bind", "0.0.0.0:5000", "run:app"]
  1. 创建docker-compose.yml
yaml 复制代码
version: '3'

services:
  web:
    build: .
    ports:
      - "5000:5000"
    environment:
      - SECRET_KEY=your-secret-key
      - DATABASE_URL=sqlite:///app.db
    volumes:
      - ./app.db:/app/app.db
  1. 构建和运行Docker容器:
bash 复制代码
docker-compose up -d

6. 项目扩展

完成基本功能后,可以考虑以下扩展:

  1. 多币种支持:添加不同货币的支持和汇率转换
  2. 定期交易:支持设置定期重复的交易
  3. 数据导入/导出:支持从其他财务软件导入数据
  4. 移动应用:开发配套的移动应用
  5. 投资跟踪:添加投资组合管理功能
  6. 目标设置:设置财务目标并跟踪进度
  7. 通知系统:预算超支提醒、账单到期提醒等

7. 今日总结

在今天的学习中,我们完成了一个完整的个人财务管理系统,涵盖了以下内容:

  1. 项目规划:需求分析、系统架构设计、数据库设计
  2. 后端开发:使用Flask框架实现MVC架构
  3. 前端开发:使用Bootstrap和Chart.js创建响应式界面和数据可视化
  4. 数据库操作:使用SQLAlchemy ORM进行数据库操作
  5. 用户认证:实现用户注册、登录和权限控制
  6. 测试:编写单元测试和集成测试
  7. 部署:使用Gunicorn、Nginx和Docker部署应用

通过这个项目,我们将前几天学习的Python知识应用到实际开发中,包括Web开发、数据处理、测试和部署等方面。这种端到端的项目实践有助于巩固所学知识,并了解如何将不同技术整合起来构建完整的应用。

8. 学习总结

经过两周的Python学习,我们已经从基础知识到实际项目开发,系统地学习了Python编程。以下是这两周学习的总结:

第一周:Python基础

  1. Python基础入门:语法、数据类型、变量、运算符
  2. Python数据结构:列表、元组、字典、集合
  3. Python控制流与函数:条件语句、循环、函数定义和调用
  4. Python面向对象编程:类、对象、继承、多态
  5. Python模块与包:模块导入、包管理、虚拟环境
  6. Python文件操作与异常处理:文件读写、异常捕获
  7. Python高级特性:装饰器、生成器、迭代器、上下文管理器

第二周:Python应用开发

  1. Python Web开发基础:Flask框架
  2. Python Web开发进阶:Django框架
  3. Python数据分析与可视化:NumPy、pandas、Matplotlib、Seaborn
  4. Python测试与调试:unittest、pytest、调试技术、性能分析
  5. Python项目实战:个人财务管理系统

通过这两周的学习,我们已经具备了使用Python进行实际开发的能力。Python的简洁语法和丰富的生态系统使其成为一种强大而灵活的编程语言,适用于Web开发、数据分析、人工智能等多个领域。

希望这个学习计划对您从Java转向Python的旅程有所帮助。继续实践和探索,您将能够充分发挥Python的潜力,并在各种项目中应用它。

Python项目实战

1. 项目概述

在前几天的学习中,我们已经掌握了Python的基础知识、Web开发框架、数据分析与可视化以及测试与调试技术。今天,我们将把这些知识整合起来,构建一个完整的实际项目。

项目选择:个人财务管理系统

我们将构建一个个人财务管理系统,具有以下功能:

  1. 用户认证:注册、登录和注销
  2. 交易管理:添加、编辑、删除和查看交易记录
  3. 分类管理:创建和管理交易分类
  4. 数据可视化:展示收入和支出的图表
  5. 报表生成:生成财务报表
  6. 预算设置:设置和跟踪预算

这个项目将使用以下技术:

  • 后端:Flask框架
  • 数据库:SQLite(简单部署)或PostgreSQL(生产环境)
  • ORM:SQLAlchemy
  • 前端:HTML、CSS、JavaScript和Bootstrap
  • 数据可视化:Chart.js
  • 测试:pytest

2. 项目规划

需求分析

在开始编码之前,我们需要明确项目的需求:

功能需求
  1. 用户管理

    • 用户注册(用户名、电子邮件、密码)
    • 用户登录和注销
    • 用户个人资料管理
  2. 交易管理

    • 添加交易(日期、金额、类别、描述)
    • 编辑和删除交易
    • 查看交易列表(支持筛选和排序)
  3. 分类管理

    • 创建交易分类(名称、类型:收入/支出)
    • 编辑和删除分类
    • 查看分类列表
  4. 数据可视化

    • 按类别的收入和支出饼图
    • 按时间的收入和支出折线图
    • 预算与实际支出的对比图
  5. 报表

    • 月度和年度财务报表
    • 导出报表(CSV、PDF)
  6. 预算

    • 设置月度预算(总体和分类)
    • 跟踪预算使用情况
非功能需求
  1. 安全性:密码加密、CSRF保护、输入验证
  2. 性能:页面加载时间小于2秒
  3. 可用性:响应式设计,适配移动设备
  4. 可靠性:数据备份和恢复机制

系统架构

我们将采用经典的MVC(模型-视图-控制器)架构:

  1. 模型(Model):数据库模型和业务逻辑
  2. 视图(View):HTML模板和前端代码
  3. 控制器(Controller):Flask路由和视图函数

数据库设计

我们需要以下数据表:

  1. 用户(users)

    • id: 主键
    • username: 用户名
    • email: 电子邮件
    • password_hash: 密码哈希
    • created_at: 创建时间
  2. 分类(categories)

    • id: 主键
    • name: 分类名称
    • type: 类型(收入/支出)
    • user_id: 外键,关联用户
  3. 交易(transactions)

    • id: 主键
    • amount: 金额
    • date: 日期
    • description: 描述
    • category_id: 外键,关联分类
    • user_id: 外键,关联用户
    • created_at: 创建时间
  4. 预算(budgets)

    • id: 主键
    • amount: 金额
    • month: 月份
    • year: 年份
    • category_id: 外键,关联分类(可选)
    • user_id: 外键,关联用户

项目结构

csharp 复制代码
finance_app/
  ├── app/
  │   ├── __init__.py          # 应用初始化
  │   ├── models.py            # 数据库模型
  │   ├── forms.py             # 表单定义
  │   ├── routes/              # 路由和视图函数
  │   │   ├── __init__.py
  │   │   ├── auth.py          # 认证相关路由
  │   │   ├── main.py          # 主页和通用路由
  │   │   ├── transactions.py  # 交易相关路由
  │   │   ├── categories.py    # 分类相关路由
  │   │   ├── reports.py       # 报表相关路由
  │   │   └── budgets.py       # 预算相关路由
  │   ├── static/              # 静态文件
  │   │   ├── css/
  │   │   ├── js/
  │   │   └── img/
  │   └── templates/           # HTML模板
  │       ├── base.html
  │       ├── index.html
  │       ├── auth/
  │       ├── transactions/
  │       ├── categories/
  │       ├── reports/
  │       └── budgets/
  ├── migrations/              # 数据库迁移文件
  ├── tests/                   # 测试文件
  │   ├── __init__.py
  │   ├── test_models.py
  │   ├── test_routes.py
  │   └── test_forms.py
  ├── config.py                # 配置文件
  ├── run.py                   # 应用入口
  ├── requirements.txt         # 依赖列表
  └── README.md                # 项目说明

3. 项目实现

现在,让我们开始实现这个项目。我们将按照以下步骤进行:

  1. 设置项目环境
  2. 实现数据库模型
  3. 创建用户认证功能
  4. 实现交易和分类管理
  5. 添加数据可视化和报表功能
  6. 实现预算管理
  7. 编写测试
  8. 部署应用

3.1 设置项目环境

首先,我们需要创建项目目录并安装必要的依赖:

bash 复制代码
# 创建项目目录
mkdir finance_app
cd finance_app

# 创建虚拟环境
python -m venv venv

# 激活虚拟环境
# Windows
venv\Scripts\activate
# macOS/Linux
source venv/bin/activate

# 安装依赖
pip install flask flask-sqlalchemy flask-migrate flask-login flask-wtf email-validator flask-bcrypt
pip install pytest pytest-flask

创建基本的项目结构:

bash 复制代码
mkdir -p app/static/{css,js,img}
mkdir -p app/templates/{auth,transactions,categories,reports,budgets}
mkdir -p app/routes
mkdir tests

3.2 配置文件

创建配置文件config.py

python 复制代码
import os
from datetime import timedelta

class Config:
    # 基本配置
    SECRET_KEY = os.environ.get('SECRET_KEY') or 'hard-to-guess-string'
    
    # 数据库配置
    SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
        'sqlite:///' + os.path.join(os.path.abspath(os.path.dirname(__file__)), 'app.db')
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    
    # 会话配置
    PERMANENT_SESSION_LIFETIME = timedelta(days=7)
    
    # 邮件配置
    MAIL_SERVER = os.environ.get('MAIL_SERVER') or 'smtp.googlemail.com'
    MAIL_PORT = int(os.environ.get('MAIL_PORT') or 587)
    MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS') or True
    MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
    MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
    MAIL_DEFAULT_SENDER = os.environ.get('MAIL_DEFAULT_SENDER')

class DevelopmentConfig(Config):
    DEBUG = True

class TestingConfig(Config):
    TESTING = True
    SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'
    WTF_CSRF_ENABLED = False

class ProductionConfig(Config):
    DEBUG = False
    # 生产环境可以使用更安全的数据库
    # SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL')

config = {
    'development': DevelopmentConfig,
    'testing': TestingConfig,
    'production': ProductionConfig,
    'default': DevelopmentConfig
}

3.3 应用初始化

创建app/__init__.py

python 复制代码
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_login import LoginManager
from flask_bcrypt import Bcrypt
from config import config

# 初始化扩展
db = SQLAlchemy()
migrate = Migrate()
login_manager = LoginManager()
login_manager.login_view = 'auth.login'
login_manager.login_message = '请先登录'
bcrypt = Bcrypt()

def create_app(config_name='default'):
    app = Flask(__name__)
    app.config.from_object(config[config_name])
    
    # 初始化扩展
    db.init_app(app)
    migrate.init_app(app, db)
    login_manager.init_app(app)
    bcrypt.init_app(app)
    
    # 注册蓝图
    from app.routes.auth import auth as auth_blueprint
    from app.routes.main import main as main_blueprint
    from app.routes.transactions import transactions as transactions_blueprint
    from app.routes.categories import categories as categories_blueprint
    from app.routes.reports import reports as reports_blueprint
    from app.routes.budgets import budgets as budgets_blueprint
    
    app.register_blueprint(auth_blueprint)
    app.register_blueprint(main_blueprint)
    app.register_blueprint(transactions_blueprint)
    app.register_blueprint(categories_blueprint)
    app.register_blueprint(reports_blueprint)
    app.register_blueprint(budgets_blueprint)
    
    return app

3.4 数据库模型

创建app/models.py

python 复制代码
from datetime import datetime
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
from app import db, login_manager

@login_manager.user_loader
def load_user(user_id):
    return User.query.get(int(user_id))

class User(UserMixin, db.Model):
    __tablename__ = 'users'
    
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(64), unique=True, index=True)
    email = db.Column(db.String(120), unique=True, index=True)
    password_hash = db.Column(db.String(128))
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    
    # 关系
    transactions = db.relationship('Transaction', backref='user', lazy='dynamic')
    categories = db.relationship('Category', backref='user', lazy='dynamic')
    budgets = db.relationship('Budget', backref='user', lazy='dynamic')
    
    @property
    def password(self):
        raise AttributeError('密码不是可读属性')
    
    @password.setter
    def password(self, password):
        self.password_hash = generate_password_hash(password)
    
    def verify_password(self, password):
        return check_password_hash(self.password_hash, password)
    
    def __repr__(self):
        return f'<User {self.username}>'

class Category(db.Model):
    __tablename__ = 'categories'
    
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(64), index=True)
    type = db.Column(db.String(10))  # 'income' 或 'expense'
    user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
    
    # 关系
    transactions = db.relationship('Transaction', backref='category', lazy='dynamic')
    budgets = db.relationship('Budget', backref='category', lazy='dynamic')
    
    def __repr__(self):
        return f'<Category {self.name}>'

class Transaction(db.Model):
    __tablename__ = 'transactions'
    
    id = db.Column(db.Integer, primary_key=True)
    amount = db.Column(db.Float)
    date = db.Column(db.Date, index=True)
    description = db.Column(db.String(255))
    category_id = db.Column(db.Integer, db.ForeignKey('categories.id'))
    user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    
    def __repr__(self):
        return f'<Transaction {self.amount}>'

class Budget(db.Model):
    __tablename__ = 'budgets'
    
    id = db.Column(db.Integer, primary_key=True)
    amount = db.Column(db.Float)
    month = db.Column(db.Integer)
    year = db.Column(db.Integer)
    category_id = db.Column(db.Integer, db.ForeignKey('categories.id'), nullable=True)
    user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
    
    def __repr__(self):
        return f'<Budget {self.amount}>'

3.5 表单定义

创建app/forms.py

python 复制代码
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField, FloatField, SelectField, TextAreaField, DateField
from wtforms.validators import DataRequired, Email, EqualTo, Length, ValidationError
from app.models import User

class RegistrationForm(FlaskForm):
    username = StringField('用户名', validators=[DataRequired(), Length(min=2, max=20)])
    email = StringField('电子邮件', validators=[DataRequired(), Email()])
    password = PasswordField('密码', validators=[DataRequired()])
    confirm_password = PasswordField('确认密码', validators=[DataRequired(), EqualTo('password')])
    submit = SubmitField('注册')
    
    def validate_username(self, username):
        user = User.query.filter_by(username=username.data).first()
        if user:
            raise ValidationError('该用户名已被使用')
    
    def validate_email(self, email):
        user = User.query.filter_by(email=email.data).first()
        if user:
            raise ValidationError('该电子邮件已被注册')

class LoginForm(FlaskForm):
    email = StringField('电子邮件', validators=[DataRequired(), Email()])
    password = PasswordField('密码', validators=[DataRequired()])
    submit = SubmitField('登录')

class CategoryForm(FlaskForm):
    name = StringField('名称', validators=[DataRequired(), Length(max=64)])
    type = SelectField('类型', choices=[('income', '收入'), ('expense', '支出')], validators=[DataRequired()])
    submit = SubmitField('保存')

class TransactionForm(FlaskForm):
    amount = FloatField('金额', validators=[DataRequired()])
    date = DateField('日期', format='%Y-%m-%d', validators=[DataRequired()])
    description = TextAreaField('描述', validators=[Length(max=255)])
    category_id = SelectField('分类', coerce=int, validators=[DataRequired()])
    submit = SubmitField('保存')

class BudgetForm(FlaskForm):
    amount = FloatField('金额', validators=[DataRequired()])
    month = SelectField('月份', choices=[(i, i) for i in range(1, 13)], coerce=int, validators=[DataRequired()])
    year = SelectField('年份', coerce=int, validators=[DataRequired()])
    category_id = SelectField('分类', coerce=int)
    submit = SubmitField('保存')
    
    def __init__(self, *args, **kwargs):
        super(BudgetForm, self).__init__(*args, **kwargs)
        # 动态设置年份选项
        import datetime
        current_year = datetime.datetime.now().year
        self.year.choices = [(i, i) for i in range(current_year - 5, current_year + 6)]

3.6 路由和视图函数

主页路由

创建app/routes/main.py

python 复制代码
from flask import Blueprint, render_template
from flask_login import login_required, current_user
from app.models import Transaction, Category, Budget
from sqlalchemy import func
from app import db
import datetime

main = Blueprint('main', __name__)

@main.route('/')
def index():
    if current_user.is_authenticated:
        return render_template('index.html')
    return render_template('landing.html')

@main.route('/dashboard')
@login_required
def dashboard():
    # 获取当前月份的交易
    today = datetime.date.today()
    start_date = datetime.date(today.year, today.month, 1)
    end_date = datetime.date(today.year if today.month < 12 else today.year + 1,
                           (today.month + 1) if today.month < 12 else 1, 1)
    
    # 收入和支出总额
    income = db.session.query(func.sum(Transaction.amount)).\
        join(Category).filter(Category.type == 'income',
                             Transaction.user_id == current_user.id,
                             Transaction.date >= start_date,
                             Transaction.date < end_date).scalar() or 0
    
    expense = db.session.query(func.sum(Transaction.amount)).\
        join(Category).filter(Category.type == 'expense',
                             Transaction.user_id == current_user.id,
                             Transaction.date >= start_date,
                             Transaction.date < end_date).scalar() or 0
    
    # 最近的交易
    recent_transactions = Transaction.query.\
        filter_by(user_id=current_user.id).\
        order_by(Transaction.date.desc()).\
        limit(5).all()
    
    # 预算使用情况
    budgets = Budget.query.filter_by(
        user_id=current_user.id,
        month=today.month,
        year=today.year
    ).all()
    
    # 计算预算使用百分比
    budget_usage = []
    for budget in budgets:
        if budget.category_id:
            # 特定分类的预算
            spent = db.session.query(func.sum(Transaction.amount)).\
                filter(Transaction.category_id == budget.category_id,
                      Transaction.user_id == current_user.id,
                      Transaction.date >= start_date,
                      Transaction.date < end_date).scalar() or 0
            
            category_name = Category.query.get(budget.category_id).name
            budget_usage.append({
                'category': category_name,
                'budget': budget.amount,
                'spent': spent,
                'percentage': min(100, int(spent / budget.amount * 100)) if budget.amount > 0 else 0
            })
        else:
            # 总体预算
            budget_usage.append({
                'category': '总计',
                'budget': budget.amount,
                'spent': expense,
                'percentage': min(100, int(expense / budget.amount * 100)) if budget.amount > 0 else 0
            })
    
    return render_template('dashboard.html',
                          income=income,
                          expense=expense,
                          balance=income-expense,
                          recent_transactions=recent_transactions,
                          budget_usage=budget_usage)
认证路由

创建app/routes/auth.py

python 复制代码
from flask import Blueprint, render_template, redirect, url_for, flash, request
from flask_login import login_user, logout_user, login_required, current_user
from app import db
from app.models import User
from app.forms import RegistrationForm, LoginForm

auth = Blueprint('auth', __name__)

@auth.route('/register', methods=['GET', 'POST'])
def register():
    if current_user.is_authenticated:
        return redirect(url_for('main.dashboard'))
    
    form = RegistrationForm()
    if form.validate_on_submit():
        user = User(username=form.username.data,
                   email=form.email.data)
        user.password = form.password.data
        db.session.add(user)
        db.session.commit()
        flash('注册成功!现在您可以登录了。', 'success')
        return redirect(url_for('auth.login'))
    
    return render_template('auth/register.html', form=form)

@auth.route('/login', methods=['GET', 'POST'])
def login():
    if current_user.is_authenticated:
        return redirect(url_for('main.dashboard'))
    
    form = LoginForm()
    if form.validate_on_submit():
        user = User.query.filter_by(email=form.email.data).first()
        if user and user.verify_password(form.password.data):
            login_user(user)
            next_page = request.args.get('next')
            return redirect(next_page or url_for('main.dashboard'))
        else:
            flash('登录失败。请检查电子邮件和密码。', 'danger')
    
    return render_template('auth/login.html', form=form)

@auth.route('/logout')
@login_required
def logout():
    logout_user()
    flash('您已成功注销。', 'success')
    return redirect(url_for('main.index'))

@auth.route('/profile')
@login_required
def profile():
    return render_template('auth/profile.html')
交易路由

创建app/routes/transactions.py

python 复制代码
from flask import Blueprint, render_template, redirect, url_for, flash, request
from flask_login import login_required, current_user
from app import db
from app.models import Transaction, Category
from app.forms import TransactionForm
from sqlalchemy import desc
import datetime

transactions = Blueprint('transactions', __name__)

@transactions.route('/transactions')
@login_required
def index():
    # 获取筛选参数
    category_id = request.args.get('category_id', type=int)
    start_date = request.args.get('start_date')
    end_date = request.args.get('end_date')
    transaction_type = request.args.get('type')  # 'income' 或 'expense'
    
    # 构建查询
    query = Transaction.query.filter_by(user_id=current_user.id)
    
    if category_id:
        query = query.filter_by(category_id=category_id)
    
    if start_date:
        query = query.filter(Transaction.date >= datetime.datetime.strptime(start_date, '%Y-%m-%d').date())
    
    if end_date:
        query = query.filter(Transaction.date <= datetime.datetime.strptime(end_date, '%Y-%m-%d').date())
    
    if transaction_type:
        query = query.join(Category).filter(Category.type == transaction_type)
    
    # 排序
    sort_by = request.args.get('sort_by', 'date')
    sort_order = request.args.get('sort_order', 'desc')
    
    if sort_by == 'amount':
        query = query.order_by(desc(Transaction.amount) if sort_order == 'desc' else Transaction.amount)
    elif sort_by == 'date':
        query = query.order_by(desc(Transaction.date) if sort_order == 'desc' else Transaction.date)
    else:
        query = query.order_by(desc(Transaction.date))
    
    # 分页
    page = request.args.get('page', 1, type=int)
    per_page = 20
    pagination = query.paginate(page=page, per_page=per_page, error_out=False)
    transactions_list = pagination.items
    
    # 获取所有分类,用于筛选
    categories = Category.query.filter_by(user_id=current_user.id).all()
    
    return render_template('transactions/index.html',
                          transactions=transactions_list,
                          pagination=pagination,
                          categories=categories)

@transactions.route('/transactions/create', methods=['GET', 'POST'])
@login_required
def create():
    form = TransactionForm()
    
    # 动态设置分类选项
    form.category_id.choices = [(c.id, c.name) for c in 
                               Category.query.filter_by(user_id=current_user.id).all()]
    
    if form.validate_on_submit():
        transaction = Transaction(
            amount=form.amount.data,
            date=form.date.data,
            description=form.description.data,
            category_id=form.category_id.data,
            user_id=current_user.id
        )
        db.session.add(transaction)
        db.session.commit()
        flash('交易已添加!', 'success')
        return redirect(url_for('transactions.index'))
    
    return render_template('transactions/create.html', form=form)

@transactions.route('/transactions/<int:id>/edit', methods=['GET', 'POST'])
@login_required
def edit(id):
    transaction = Transaction.query.get_or_404(id)
    
    # 确保用户只能编辑自己的交易
    if transaction.user_id != current_user.id:
        flash('您无权编辑此交易。', 'danger')
        return redirect(url_for('transactions.index'))
    
    form = TransactionForm()
    
    # 动态设置分类选项
    form.category_id.choices = [(c.id, c.name) for c in 
                               Category.query.filter_by(user_id=current_user.id).all()]
    
    if form.validate_on_submit():
        transaction.amount = form.amount.data
        transaction.date = form.date.data
        transaction.description = form.description.data
        transaction.category_id = form.category_id.data
        db.session.commit()
        flash('交易已更新!', 'success')
        return redirect(url_for('transactions.index'))
    
    # 填充表单
    if request.method == 'GET':
        form.amount.data = transaction.amount
        form.date.data = transaction.date
        form.description.data = transaction.description
        form.category_id.data = transaction.category_id
    
    return render_template('transactions/edit.html', form=form, transaction=transaction)

@transactions.route('/transactions/<int:id>/delete', methods=['POST'])
@login_required
def delete(id):
    transaction = Transaction.query.get_or_404(id)
    
    # 确保用户只能删除自己的交易
    if transaction.user_id != current_user.id:
        flash('您无权删除此交易。', 'danger')
        return redirect(url_for('transactions.index'))
    
    db.session.delete(transaction)
    db.session.commit()
    flash('交易已删除!', 'success')
    return redirect(url_for('transactions.index'))
分类路由

创建app/routes/categories.py

python 复制代码
from flask import Blueprint, render_template, redirect, url_for, flash, request
from flask_login import login_required, current_user
from app import db
from app.models import Category
from app.forms import CategoryForm

categories = Blueprint('categories', __name__)

@categories.route('/categories')
@login_required
def index():
    categories_list = Category.query.filter_by(user_id=current_user.id).all()
    return render_template('categories/index.html', categories=categories_list)

@categories.route('/categories/create', methods=['GET', 'POST'])
@login_required
def create():
    form = CategoryForm()
    
    if form.validate_on_submit():
        category = Category(
            name=form.name.data,
            type=form.type.data,
            user_id=current_user.id
        )
        db.session.add(category)
        db.session.commit()
        flash('分类已添加!', 'success')
        return redirect(url_for('categories.index'))
    
    return render_template('categories/create.html', form=form)

@categories.route('/categories/<int:id>/edit', methods=['GET', 'POST'])
@login_required
def edit(id):
    category = Category.query.get_or_404(id)
    
    # 确保用户只能编辑自己的分类
    if category.user_id != current_user.id:
        flash('您无权编辑此分类。', 'danger')
        return redirect(url_for('categories.index'))
    
    form = CategoryForm()
    
    if form.validate_on_submit():
        category.name = form.name.data
        category.type = form.type.data
        db.session.commit()
        flash('分类已更新!', 'success')
        return redirect(url_for('categories.index'))
    
    # 填充表单
    if request.method == 'GET':
        form.name.data = category.name
相关推荐
回家路上绕了弯14 分钟前
深度理解 volatile 与 synchronized:并发编程的两把钥匙
java·后端
程序员清风15 分钟前
ThreadLocal在什么情况下会导OOM?
java·后端·面试
就是帅我不改21 分钟前
基于领域事件驱动的微服务架构设计与实践
后端·面试·架构
JohnYan22 分钟前
Bun技术评估 - 25 Utils(实用工具)
javascript·后端·bun
我要成为Java糕手1 小时前
支付宝芝麻免押支付集成指南及技术对接验收(Java版)
javascript·后端
anthem371 小时前
3、Python持续集成与部署
后端
用户4099322502121 小时前
如何让你的FastAPI Celery Worker在压力下优雅起舞?
后端·github·trae
anthem371 小时前
5、Python文档生成与API设计
后端
ruokkk1 小时前
当你配置了feign.sentinel.enable=true时发生什么
后端·架构