Flask Web 框架详细指南

1. Flask 简介

Flask 是一个轻量级的 Python Web 应用框架,基于 Werkzeug WSGI 工具库和 Jinja2 模板引擎。它被称为"微框架",因为它不需要特定的工具或库,但可以通过扩展来添加功能。

1.1 Flask 的特点

  • 轻量级:核心功能简单,易于学习
  • 灵活性:可以根据需求选择组件
  • 可扩展:丰富的扩展生态系统
  • RESTful:内置对 RESTful 请求分发的支持
  • 调试友好:内置开发服务器和调试器

2. Flask 安装和环境配置

2.1 基础安装

bash 复制代码
# 创建虚拟环境
python -m venv flask_env
source flask_env/bin/activate  # Linux/Mac
# flask_env\Scripts\activate  # Windows

# 安装 Flask
pip install Flask

# 安装常用扩展
pip install Flask-SQLAlchemy Flask-Migrate Flask-Login Flask-WTF Flask-Mail

2.2 项目结构

arduino 复制代码
my_flask_app/
├── app/
│   ├── __init__.py
│   ├── models.py
│   ├── routes.py
│   ├── forms.py
│   ├── templates/
│   │   ├── base.html
│   │   └── index.html
│   └── static/
│       ├── css/
│       ├── js/
│       └── images/
├── migrations/
├── config.py
├── requirements.txt
└── run.py

3. Flask 基础概念

3.1 最简单的 Flask 应用

python 复制代码
# app.py
from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello_world():
    return 'Hello, World!'

@app.route('/user/<name>')
def user_profile(name):
    return f'Hello, {name}!'

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

3.2 路由和视图函数

python 复制代码
from flask import Flask, request, jsonify, render_template

app = Flask(__name__)

# 基本路由
@app.route('/')
def index():
    return render_template('index.html')

# 带参数的路由
@app.route('/user/<int:user_id>')
def user_detail(user_id):
    return f'User ID: {user_id}'

# 多种 HTTP 方法
@app.route('/api/users', methods=['GET', 'POST'])
def users_api():
    if request.method == 'GET':
        return jsonify({'users': []})
    elif request.method == 'POST':
        data = request.get_json()
        return jsonify({'message': 'User created', 'data': data}), 201

# URL 参数类型
@app.route('/post/<string:title>')
def post_detail(title):
    return f'Post: {title}'

@app.route('/category/<path:category_path>')
def category_detail(category_path):
    return f'Category: {category_path}'

3.3 请求处理

python 复制代码
from flask import request, session, redirect, url_for

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']
        
        # 验证用户凭据
        if validate_user(username, password):
            session['user_id'] = get_user_id(username)
            return redirect(url_for('dashboard'))
        else:
            return render_template('login.html', error='Invalid credentials')
    
    return render_template('login.html')

@app.route('/upload', methods=['POST'])
def upload_file():
    if 'file' not in request.files:
        return 'No file uploaded', 400
    
    file = request.files['file']
    if file.filename == '':
        return 'No file selected', 400
    
    if file and allowed_file(file.filename):
        filename = secure_filename(file.filename)
        file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
        return 'File uploaded successfully'

4. 模板系统 (Jinja2)

4.1 基础模板语法

html 复制代码
<!-- templates/base.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{% block title %}Default Title{% endblock %}</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
    <nav>
        <ul>
            <li><a href="{{ url_for('index') }}">Home</a></li>
            <li><a href="{{ url_for('about') }}">About</a></li>
            {% if current_user.is_authenticated %}
                <li><a href="{{ url_for('profile') }}">Profile</a></li>
                <li><a href="{{ url_for('logout') }}">Logout</a></li>
            {% else %}
                <li><a href="{{ url_for('login') }}">Login</a></li>
            {% endif %}
        </ul>
    </nav>
    
    <main>
        {% with messages = get_flashed_messages() %}
            {% if messages %}
                <div class="flash-messages">
                    {% for message in messages %}
                        <div class="alert">{{ message }}</div>
                    {% endfor %}
                </div>
            {% endif %}
        {% endwith %}
        
        {% block content %}{% endblock %}
    </main>
</body>
</html>
html 复制代码
<!-- templates/index.html -->
{% extends "base.html" %}

{% block title %}Home Page{% endblock %}

{% block content %}
    <h1>Welcome to My App</h1>
    
    {% if users %}
        <h2>Users List</h2>
        <ul>
            {% for user in users %}
                <li>
                    <a href="{{ url_for('user_detail', user_id=user.id) }}">
                        {{ user.username }}
                    </a>
                    - {{ user.email }}
                </li>
            {% endfor %}
        </ul>
    {% else %}
        <p>No users found.</p>
    {% endif %}
    
    <!-- 条件判断 -->
    {% if current_user.is_admin %}
        <a href="{{ url_for('admin_panel') }}" class="btn btn-admin">Admin Panel</a>
    {% endif %}
    
    <!-- 循环和过滤器 -->
    {% for post in posts %}
        <article>
            <h3>{{ post.title|title }}</h3>
            <p>{{ post.content|truncate(100) }}</p>
            <small>Published on {{ post.created_at|strftime('%Y-%m-%d') }}</small>
        </article>
    {% endfor %}
{% endblock %}

4.2 自定义过滤器和函数

python 复制代码
from datetime import datetime
import markdown

@app.template_filter('strftime')
def strftime_filter(date, format='%Y-%m-%d'):
    """自定义日期格式化过滤器"""
    return date.strftime(format)

@app.template_filter('markdown')
def markdown_filter(text):
    """Markdown 转 HTML 过滤器"""
    return markdown.markdown(text)

@app.context_processor
def utility_processor():
    """全局模板函数"""
    def format_price(amount):
        return f"${amount:.2f}"
    
    def is_weekend():
        return datetime.now().weekday() >= 5
    
    return dict(format_price=format_price, is_weekend=is_weekend)

5. 数据库集成 (SQLAlchemy)

5.1 配置数据库

python 复制代码
# config.py
import os

class Config:
    SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key'
    SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
        'sqlite:///app.db'
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    
class DevelopmentConfig(Config):
    DEBUG = True
    SQLALCHEMY_DATABASE_URI = 'sqlite:///dev.db'

class ProductionConfig(Config):
    DEBUG = False
    SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL')

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

5.2 定义模型

python 复制代码
# app/models.py
from flask_sqlalchemy import SQLAlchemy
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
from datetime import datetime

db = SQLAlchemy()

# 多对多关系表
user_roles = db.Table('user_roles',
    db.Column('user_id', db.Integer, db.ForeignKey('user.id'), primary_key=True),
    db.Column('role_id', db.Integer, db.ForeignKey('role.id'), primary_key=True)
)

class User(UserMixin, db.Model):
    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=False)
    password_hash = db.Column(db.String(255), nullable=False)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    is_active = db.Column(db.Boolean, default=True)
    
    # 关系
    posts = db.relationship('Post', backref='author', lazy='dynamic')
    roles = db.relationship('Role', secondary=user_roles, backref='users')
    profile = db.relationship('UserProfile', uselist=False, backref='user')
    
    def set_password(self, password):
        self.password_hash = generate_password_hash(password)
    
    def check_password(self, password):
        return check_password_hash(self.password_hash, password)
    
    def has_role(self, role_name):
        return any(role.name == role_name for role in self.roles)
    
    def __repr__(self):
        return f'<User {self.username}>'

class Role(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(50), unique=True, nullable=False)
    description = db.Column(db.String(200))

class UserProfile(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
    first_name = db.Column(db.String(50))
    last_name = db.Column(db.String(50))
    bio = db.Column(db.Text)
    avatar_url = db.Column(db.String(200))
    birth_date = db.Column(db.Date)

class Post(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(200), nullable=False)
    content = db.Column(db.Text, nullable=False)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
    published = db.Column(db.Boolean, default=False)
    
    # 外键
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
    category_id = db.Column(db.Integer, db.ForeignKey('category.id'))
    
    # 关系
    comments = db.relationship('Comment', backref='post', lazy='dynamic', cascade='all, delete-orphan')
    tags = db.relationship('Tag', secondary='post_tags', backref='posts')
    
    def __repr__(self):
        return f'<Post {self.title}>'

class Category(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(100), unique=True, nullable=False)
    description = db.Column(db.Text)
    posts = db.relationship('Post', backref='category', lazy='dynamic')

class Tag(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(50), unique=True, nullable=False)

# 多对多关系表
post_tags = db.Table('post_tags',
    db.Column('post_id', db.Integer, db.ForeignKey('post.id'), primary_key=True),
    db.Column('tag_id', db.Integer, db.ForeignKey('tag.id'), primary_key=True)
)

class Comment(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    content = db.Column(db.Text, nullable=False)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    
    # 外键
    post_id = db.Column(db.Integer, db.ForeignKey('post.id'), nullable=False)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
    
    # 关系
    user = db.relationship('User', backref='comments')

5.3 数据库操作

python 复制代码
# app/routes.py
from flask import render_template, request, redirect, url_for, flash, jsonify
from app.models import db, User, Post, Category

@app.route('/users')
def users_list():
    page = request.args.get('page', 1, type=int)
    users = User.query.paginate(
        page=page, per_page=10, error_out=False
    )
    return render_template('users.html', users=users)

@app.route('/posts')
def posts_list():
    # 查询所有已发布的文章,按创建时间降序排列
    posts = Post.query.filter_by(published=True)\
                     .order_by(Post.created_at.desc())\
                     .all()
    return render_template('posts.html', posts=posts)

@app.route('/create_post', methods=['GET', 'POST'])
def create_post():
    if request.method == 'POST':
        title = request.form['title']
        content = request.form['content']
        category_id = request.form.get('category_id')
        
        post = Post(
            title=title,
            content=content,
            user_id=current_user.id,
            category_id=category_id if category_id else None
        )
        
        # 处理标签
        tag_names = request.form.get('tags', '').split(',')
        for tag_name in tag_names:
            tag_name = tag_name.strip()
            if tag_name:
                tag = Tag.query.filter_by(name=tag_name).first()
                if not tag:
                    tag = Tag(name=tag_name)
                    db.session.add(tag)
                post.tags.append(tag)
        
        db.session.add(post)
        db.session.commit()
        
        flash('Post created successfully!', 'success')
        return redirect(url_for('posts_list'))
    
    categories = Category.query.all()
    return render_template('create_post.html', categories=categories)

@app.route('/api/users/<int:user_id>', methods=['GET', 'PUT', 'DELETE'])
def user_api(user_id):
    user = User.query.get_or_404(user_id)
    
    if request.method == 'GET':
        return jsonify({
            'id': user.id,
            'username': user.username,
            'email': user.email,
            'created_at': user.created_at.isoformat(),
            'posts_count': user.posts.count()
        })
    
    elif request.method == 'PUT':
        data = request.get_json()
        user.username = data.get('username', user.username)
        user.email = data.get('email', user.email)
        
        db.session.commit()
        return jsonify({'message': 'User updated successfully'})
    
    elif request.method == 'DELETE':
        db.session.delete(user)
        db.session.commit()
        return jsonify({'message': 'User deleted successfully'})

# 复杂查询示例
@app.route('/search')
def search():
    query = request.args.get('q', '')
    category_id = request.args.get('category')
    
    posts_query = Post.query.filter_by(published=True)
    
    if query:
        posts_query = posts_query.filter(
            Post.title.contains(query) | Post.content.contains(query)
        )
    
    if category_id:
        posts_query = posts_query.filter_by(category_id=category_id)
    
    posts = posts_query.order_by(Post.created_at.desc()).all()
    
    return render_template('search_results.html', posts=posts, query=query)

6. 表单处理 (Flask-WTF)

6.1 定义表单

python 复制代码
# app/forms.py
from flask_wtf import FlaskForm
from flask_wtf.file import FileField, FileAllowed
from wtforms import StringField, TextAreaField, PasswordField, SelectField, BooleanField, SubmitField
from wtforms.validators import DataRequired, Length, Email, EqualTo, ValidationError
from app.models import User

class LoginForm(FlaskForm):
    username = StringField('用户名', validators=[DataRequired(), Length(min=4, max=20)])
    password = PasswordField('密码', validators=[DataRequired()])
    remember_me = BooleanField('记住我')
    submit = SubmitField('登录')

class RegistrationForm(FlaskForm):
    username = StringField('用户名', validators=[
        DataRequired(), 
        Length(min=4, max=20)
    ])
    email = StringField('邮箱', validators=[DataRequired(), Email()])
    password = PasswordField('密码', validators=[
        DataRequired(), 
        Length(min=6)
    ])
    password2 = PasswordField('确认密码', validators=[
        DataRequired(), 
        EqualTo('password', message='密码不匹配')
    ])
    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 PostForm(FlaskForm):
    title = StringField('标题', validators=[DataRequired(), Length(max=200)])
    content = TextAreaField('内容', validators=[DataRequired()])
    category = SelectField('分类', coerce=int)
    tags = StringField('标签 (用逗号分隔)')
    published = BooleanField('立即发布')
    featured_image = FileField('特色图片', validators=[
        FileAllowed(['jpg', 'jpeg', 'png', 'gif'], '只允许图片文件!')
    ])
    submit = SubmitField('保存')
    
    def __init__(self, *args, **kwargs):
        super(PostForm, self).__init__(*args, **kwargs)
        from app.models import Category
        self.category.choices = [(0, '选择分类')] + [
            (c.id, c.name) for c in Category.query.all()
        ]

class ProfileForm(FlaskForm):
    first_name = StringField('名字', validators=[Length(max=50)])
    last_name = StringField('姓氏', validators=[Length(max=50)])
    bio = TextAreaField('个人简介', validators=[Length(max=500)])
    avatar = FileField('头像', validators=[
        FileAllowed(['jpg', 'jpeg', 'png'], '只允许图片文件!')
    ])
    submit = SubmitField('更新资料')

6.2 在视图中使用表单

python 复制代码
from app.forms import LoginForm, RegistrationForm, PostForm
from flask_login import login_user, logout_user, login_required, current_user

@app.route('/login', methods=['GET', 'POST'])
def login():
    if current_user.is_authenticated:
        return redirect(url_for('index'))
    
    form = LoginForm()
    if form.validate_on_submit():
        user = User.query.filter_by(username=form.username.data).first()
        
        if user and user.check_password(form.password.data):
            login_user(user, remember=form.remember_me.data)
            
            # 重定向到用户原本想访问的页面
            next_page = request.args.get('next')
            if not next_page or not next_page.startswith('/'):
                next_page = url_for('index')
            
            flash(f'欢迎回来,{user.username}!', 'success')
            return redirect(next_page)
        else:
            flash('用户名或密码错误', 'error')
    
    return render_template('login.html', form=form)

@app.route('/register', methods=['GET', 'POST'])
def register():
    if current_user.is_authenticated:
        return redirect(url_for('index'))
    
    form = RegistrationForm()
    if form.validate_on_submit():
        user = User(
            username=form.username.data,
            email=form.email.data
        )
        user.set_password(form.password.data)
        
        db.session.add(user)
        db.session.commit()
        
        flash('注册成功!请登录', 'success')
        return redirect(url_for('login'))
    
    return render_template('register.html', form=form)

@app.route('/create_post', methods=['GET', 'POST'])
@login_required
def create_post():
    form = PostForm()
    
    if form.validate_on_submit():
        post = Post(
            title=form.title.data,
            content=form.content.data,
            published=form.published.data,
            user_id=current_user.id
        )
        
        if form.category.data:
            post.category_id = form.category.data
        
        # 处理文件上传
        if form.featured_image.data:
            filename = secure_filename(form.featured_image.data.filename)
            form.featured_image.data.save(
                os.path.join(app.config['UPLOAD_FOLDER'], filename)
            )
            post.featured_image = filename
        
        # 处理标签
        if form.tags.data:
            tag_names = [tag.strip() for tag in form.tags.data.split(',')]
            for tag_name in tag_names:
                if tag_name:
                    tag = Tag.query.filter_by(name=tag_name).first()
                    if not tag:
                        tag = Tag(name=tag_name)
                        db.session.add(tag)
                    post.tags.append(tag)
        
        db.session.add(post)
        db.session.commit()
        
        flash('文章创建成功!', 'success')
        return redirect(url_for('post_detail', id=post.id))
    
    return render_template('create_post.html', form=form)

7. 用户认证 (Flask-Login)

7.1 配置用户认证

python 复制代码
# app/__init__.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
from flask_migrate import Migrate

db = SQLAlchemy()
login_manager = LoginManager()
migrate = Migrate()

def create_app(config_name='default'):
    app = Flask(__name__)
    app.config.from_object(config[config_name])
    
    # 初始化扩展
    db.init_app(app)
    login_manager.init_app(app)
    migrate.init_app(app, db)
    
    # 配置登录管理器
    login_manager.login_view = 'login'
    login_manager.login_message = '请先登录以访问此页面'
    login_manager.login_message_category = 'info'
    
    @login_manager.user_loader
    def load_user(user_id):
        return User.query.get(int(user_id))
    
    # 注册蓝图
    from app.main import bp as main_bp
    app.register_blueprint(main_bp)
    
    from app.auth import bp as auth_bp
    app.register_blueprint(auth_bp, url_prefix='/auth')
    
    return app

7.2 权限装饰器

python 复制代码
# app/decorators.py
from functools import wraps
from flask import abort
from flask_login import current_user

def admin_required(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if not current_user.is_authenticated or not current_user.has_role('admin'):
            abort(403)
        return f(*args, **kwargs)
    return decorated_function

def permission_required(permission):
    def decorator(f):
        @wraps(f)
        def decorated_function(*args, **kwargs):
            if not current_user.can(permission):
                abort(403)
            return f(*args, **kwargs)
        return decorated_function
    return decorator

def same_user_required(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        user_id = kwargs.get('user_id') or kwargs.get('id')
        if not current_user.is_authenticated or \
           (current_user.id != user_id and not current_user.has_role('admin')):
            abort(403)
        return f(*args, **kwargs)
    return decorated_function

# 使用示例
@app.route('/admin')
@login_required
@admin_required
def admin_panel():
    return render_template('admin/dashboard.html')

@app.route('/user/<int:user_id>/edit')
@login_required
@same_user_required
def edit_profile(user_id):
    user = User.query.get_or_404(user_id)
    return render_template('edit_profile.html', user=user)

8. API 开发

8.1 RESTful API 设计

python 复制代码
# app/api.py
from flask import Blueprint, request, jsonify
from flask_login import login_required, current_user
from app.models import db, User, Post

api = Blueprint('api', __name__, url_prefix='/api/v1')

# 错误处理
@api.errorhandler(404)
def not_found(error):
    return jsonify({'error': 'Resource not found'}), 404

@api.errorhandler(400)
def bad_request(error):
    return jsonify({'error': 'Bad request'}), 400

@api.errorhandler(403)
def forbidden(error):
    return jsonify({'error': 'Forbidden'}), 403

# 用户 API
@api.route('/users', methods=['GET'])
def get_users():
    page = request.args.get('page', 1, type=int)
    per_page = min(request.args.get('per_page', 10, type=int), 100)
    
    users = User.query.paginate(
        page=page, per_page=per_page, error_out=False
    )
    
    return jsonify({
        'users': [user.to_dict() for user in users.items],
        'pagination': {
            'page': page,
            'pages': users.pages,
            'per_page': per_page,
            'total': users.total,
            'has_next': users.has_next,
            'has_prev': users.has_prev
        }
    })

@api.route('/users/<int:user_id>', methods=['GET'])
def get_user(user_id):
    user = User.query.get_or_404(user_id)
    return jsonify(user.to_dict())

@api.route('/users', methods=['POST'])
def create_user():
    data = request.get_json()
    
    # 验证必需字段
    if not data or not data.get('username') or not data.get('email'):
        return jsonify({'error': 'Missing required fields'}), 400
    
    # 检查用户名和邮箱唯一性
    if User.query.filter_by(username=data['username']).first():
        return jsonify({'error': 'Username already exists'}), 400
    
    if User.query.filter_by(email=data['email']).first():
        return jsonify({'error': 'Email already exists'}), 400
    
    user = User(
        username=data['username'],
        email=data['email']
    )
    
    if data.get('password'):
        user.set_password(data['password'])
    
    db.session.add(user)
    db.session.commit()
    
    return jsonify(user.to_dict()), 201

@api.route('/users/<int:user_id>', methods=['PUT'])
@login_required
def update_user(user_id):
    if current_user.id != user_id and not current_user.has_role('admin'):
        return jsonify({'error': 'Forbidden'}), 403
    
    user = User.query.get_or_404(user_id)
    data = request.get_json()
    
    if 'username' in data:
        if User.query.filter_by(username=data['username']).first() and \
           data['username'] != user.username:
            return jsonify({'error': 'Username already exists'}), 400
        user.username = data['username']
    
    if 'email' in data:
        if User.query.filter_by(email=data['email']).first() and \
           data['email'] != user.email:
            return jsonify({'error': 'Email already exists'}), 400
        user.email = data['email']
    
    db.session.commit()
    return jsonify(user.to_dict())

@api.route('/users/<int:user_id>', methods=['DELETE'])
@login_required
def delete_user(user_id):
    if current_user.id != user_id and not current_user.has_role('admin'):
        return jsonify({'error': 'Forbidden'}), 403
    
    user = User.query.get_or_404(user_id)
    db.session.delete(user)
    db.session.commit()
    
    return jsonify({'message': 'User deleted successfully'})

# 文章 API
@api.route('/posts', methods=['GET'])
def get_posts():
    page = request.args.get('page', 1, type=int)
    per_page = min(request.args.get('per_page', 10, type=int), 100)
    
    query = Post.query.filter_by(published=True)
    
    # 筛选参数
    author_id = request.args.get('author_id', type=int)
    if author_id:
        query = query.filter_by(user_id=author_id)
    
    category_id = request.args.get('category_id', type=int)
    if category_id:
        query = query.filter_by(category_id=category_id)
    
    search = request.args.get('search')
    if search:
        query = query.filter(
            Post.title.contains(search) | Post.content.contains(search)
        )
    
    posts = query.order_by(Post.created_at.desc()).paginate(
        page=page, per_page=per_page, error_out=False
    )
    
    return jsonify({
        'posts': [post.to_dict() for post in posts.items],
        'pagination': {
            'page': page,
            'pages': posts.pages,
            'per_page': per_page,
            'total': posts.total
        }
    })

@api.route('/posts', methods=['POST'])
@login_required
def create_post():
    data = request.get_json()
    
    if not data or not data.get('title') or not data.get('content'):
        return jsonify({'error': 'Missing required fields'}), 400
    
    post = Post(
        title=data['title'],
        content=data['content'],
        published=data.get('published', False),
        user_id=current_user.id
    )
    
    if data.get('category_id'):
        post.category_id = data['category_id']
    
    db.session.add(post)
    db.session.commit()
    
    return jsonify(post.to_dict()), 201

# 添加序列化方法到模型
# 在 models.py 中添加
class User(UserMixin, db.Model):
    # ... 现有代码 ...
    
    def to_dict(self, include_email=False):
        data = {
            'id': self.id,
            'username': self.username,
            'created_at': self.created_at.isoformat(),
            'is_active': self.is_active,
            'posts_count': self.posts.count()
        }
        if include_email:
            data['email'] = self.email
        return data

class Post(db.Model):
    # ... 现有代码 ...
    
    def to_dict(self):
        return {
            'id': self.id,
            'title': self.title,
            'content': self.content,
            'created_at': self.created_at.isoformat(),
            'updated_at': self.updated_at.isoformat(),
            'published': self.published,
            'author': {
                'id': self.author.id,
                'username': self.author.username
            },
            'category': {
                'id': self.category.id,
                'name': self.category.name
            } if self.category else None,
            'tags': [tag.name for tag in self.tags],
            'comments_count': self.comments.count()
        }

8.2 API 认证和限流

python 复制代码
# app/auth.py
from flask import Blueprint, request, jsonify
from flask_login import login_user, logout_user, current_user
import jwt
from datetime import datetime, timedelta
from functools import wraps

auth_bp = Blueprint('auth', __name__)

def token_required(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        token = request.headers.get('Authorization')
        
        if not token:
            return jsonify({'error': 'Token is missing'}), 401
        
        try:
            if token.startswith('Bearer '):
                token = token[7:]
            
            data = jwt.decode(
                token, 
                current_app.config['SECRET_KEY'], 
                algorithms=['HS256']
            )
            current_user_id = data['user_id']
            current_user = User.query.get(current_user_id)
            
            if not current_user:
                return jsonify({'error': 'Token is invalid'}), 401
                
        except jwt.ExpiredSignatureError:
            return jsonify({'error': 'Token has expired'}), 401
        except jwt.InvalidTokenError:
            return jsonify({'error': 'Token is invalid'}), 401
        
        return f(current_user, *args, **kwargs)
    return decorated_function

@auth_bp.route('/api/login', methods=['POST'])
def api_login():
    data = request.get_json()
    
    if not data or not data.get('username') or not data.get('password'):
        return jsonify({'error': 'Missing credentials'}), 400
    
    user = User.query.filter_by(username=data['username']).first()
    
    if user and user.check_password(data['password']):
        # 生成 JWT token
        token = jwt.encode({
            'user_id': user.id,
            'exp': datetime.utcnow() + timedelta(hours=24)
        }, current_app.config['SECRET_KEY'], algorithm='HS256')
        
        return jsonify({
            'token': token,
            'user': user.to_dict(include_email=True),
            'expires_in': 24 * 3600
        })
    
    return jsonify({'error': 'Invalid credentials'}), 401

# 使用 token 认证的 API
@api.route('/profile')
@token_required
def get_profile(current_user):
    return jsonify(current_user.to_dict(include_email=True))

9. 蓝图 (Blueprints) 组织代码

9.1 创建蓝图

python 复制代码
# app/main/__init__.py
from flask import Blueprint

bp = Blueprint('main', __name__)

from app.main import routes

# app/main/routes.py
from flask import render_template, request
from app.main import bp
from app.models import Post, User

@bp.route('/')
def index():
    posts = Post.query.filter_by(published=True)\
                     .order_by(Post.created_at.desc())\
                     .limit(10).all()
    return render_template('index.html', posts=posts)

@bp.route('/about')
def about():
    return render_template('about.html')

# app/auth/__init__.py
from flask import Blueprint

bp = Blueprint('auth', __name__)

from app.auth import routes

# app/auth/routes.py
from flask import render_template, redirect, url_for, flash, request
from flask_login import login_user, logout_user, current_user
from app.auth import bp
from app.auth.forms import LoginForm, RegistrationForm
from app.models import User, db

@bp.route('/login', methods=['GET', 'POST'])
def login():
    if current_user.is_authenticated:
        return redirect(url_for('main.index'))
    
    form = LoginForm()
    if form.validate_on_submit():
        user = User.query.filter_by(username=form.username.data).first()
        if user and user.check_password(form.password.data):
            login_user(user, remember=form.remember_me.data)
            next_page = request.args.get('next')
            if not next_page or not next_page.startswith('/'):
                next_page = url_for('main.index')
            return redirect(next_page)
        flash('Invalid username or password')
    return render_template('auth/login.html', title='Sign In', form=form)

@bp.route('/logout')
def logout():
    logout_user()
    return redirect(url_for('main.index'))

9.2 注册蓝图

python 复制代码
# app/__init__.py
def create_app(config_class=Config):
    app = Flask(__name__)
    app.config.from_object(config_class)
    
    db.init_app(app)
    migrate.init_app(app, db)
    login.init_app(app)
    
    # 注册蓝图
    from app.main import bp as main_bp
    app.register_blueprint(main_bp)
    
    from app.auth import bp as auth_bp
    app.register_blueprint(auth_bp, url_prefix='/auth')
    
    from app.api import bp as api_bp
    app.register_blueprint(api_bp, url_prefix='/api')
    
    return app

10. 配置管理

10.1 环境配置

python 复制代码
# config.py
import os
from dotenv import load_dotenv

basedir = os.path.abspath(os.path.dirname(__file__))
load_dotenv(os.path.join(basedir, '.env'))

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(basedir, 'app.db')
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    
    # 邮件配置
    MAIL_SERVER = os.environ.get('MAIL_SERVER')
    MAIL_PORT = int(os.environ.get('MAIL_PORT') or 587)
    MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS', 'true').lower() in \
        ['true', 'on', '1']
    MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
    MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
    
    # 上传文件配置
    UPLOAD_FOLDER = os.path.join(basedir, 'uploads')
    MAX_CONTENT_LENGTH = 16 * 1024 * 1024  # 16MB
    
    # 分页配置
    POSTS_PER_PAGE = 10
    USERS_PER_PAGE = 20
    
    # Redis 配置
    REDIS_URL = os.environ.get('REDIS_URL') or 'redis://localhost:6379/0'
    
    # 日志配置
    LOG_TO_STDOUT = os.environ.get('LOG_TO_STDOUT')
    
    @staticmethod
    def init_app(app):
        pass

class DevelopmentConfig(Config):
    DEBUG = True
    SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URL') or \
        'sqlite:///' + os.path.join(basedir, 'data-dev.sqlite')

class TestingConfig(Config):
    TESTING = True
    SQLALCHEMY_DATABASE_URI = os.environ.get('TEST_DATABASE_URL') or \
        'sqlite://'
    WTF_CSRF_ENABLED = False

class ProductionConfig(Config):
    SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
        'sqlite:///' + os.path.join(basedir, 'data.sqlite')
    
    @classmethod
    def init_app(cls, app):
        Config.init_app(app)
        
        # 错误邮件通知
        import logging
        from logging.handlers import SMTPHandler
        if app.config['MAIL_SERVER']:
            auth = None
            if app.config['MAIL_USERNAME'] or app.config['MAIL_PASSWORD']:
                auth = (app.config['MAIL_USERNAME'], app.config['MAIL_PASSWORD'])
            secure = None
            if app.config['MAIL_USE_TLS']:
                secure = ()
            mail_handler = SMTPHandler(
                mailhost=(app.config['MAIL_SERVER'], app.config['MAIL_PORT']),
                fromaddr='no-reply@' + app.config['MAIL_SERVER'],
                toaddrs=app.config['ADMINS'], 
                subject='Application Failure',
                credentials=auth, 
                secure=secure
            )
            mail_handler.setLevel(logging.ERROR)
            app.logger.addHandler(mail_handler)

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

10.2 环境变量文件

bash 复制代码
# .env
SECRET_KEY=your-secret-key-here
DATABASE_URL=postgresql://username:password@localhost/mydatabase
MAIL_SERVER=smtp.gmail.com
MAIL_PORT=587
MAIL_USE_TLS=1
MAIL_USERNAME=your-email@gmail.com
MAIL_PASSWORD=your-app-password
REDIS_URL=redis://localhost:6379/0

11. 部署和生产环境

11.1 WSGI 配置

python 复制代码
# wsgi.py
import os
from app import create_app

app = create_app(os.getenv('FLASK_CONFIG') or 'default')

if __name__ == "__main__":
    app.run()

11.2 Gunicorn 配置

python 复制代码
# gunicorn.conf.py
bind = "0.0.0.0:8000"
workers = 4
worker_class = "sync"
worker_connections = 1000
timeout = 30
keepalive = 2
max_requests = 1000
max_requests_jitter = 100
preload_app = True

11.3 Nginx 配置

nginx 复制代码
# /etc/nginx/sites-available/myapp
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;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
    
    location /static {
        alias /path/to/your/app/app/static;
        expires 30d;
    }
    
    location /uploads {
        alias /path/to/your/app/uploads;
        expires 30d;
    }
}

11.4 Docker 部署

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=wsgi.py
ENV FLASK_ENV=production

EXPOSE 8000

CMD ["gunicorn", "--bind", "0.0.0.0:8000", "wsgi:app"]
yaml 复制代码
# docker-compose.yml
version: '3.8'

services:
  web:
    build: .
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=postgresql://user:pass@db:5432/myapp
      - REDIS_URL=redis://redis:6379/0
    depends_on:
      - db
      - redis
    volumes:
      - ./uploads:/app/uploads

  db:
    image: postgres:13
    environment:
      - POSTGRES_DB=myapp
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=pass
    volumes:
      - postgres_data:/var/lib/postgresql/data

  redis:
    image: redis:6-alpine

volumes:
  postgres_data:

12. 测试

12.1 单元测试

python 复制代码
# tests/test_models.py
import unittest
from app import create_app, db
from app.models import User, Post

class UserModelCase(unittest.TestCase):
    def setUp(self):
        self.app = create_app('testing')
        self.app_context = self.app.app_context()
        self.app_context.push()
        db.create_all()

    def tearDown(self):
        db.session.remove()
        db.drop_all()
        self.app_context.pop()

    def test_password_hashing(self):
        u = User(username='susan')
        u.set_password('cat')
        self.assertFalse(u.check_password('dog'))
        self.assertTrue(u.check_password('cat'))

    def test_user_creation(self):
        u = User(username='john', email='john@example.com')
        u.set_password('test')
        db.session.add(u)
        db.session.commit()
        
        self.assertEqual(User.query.count(), 1)
        self.assertEqual(u.username, 'john')
        self.assertTrue(u.check_password('test'))

if __name__ == '__main__':
    unittest.main(verbosity=2)

12.2 集成测试

python 复制代码
# tests/test_routes.py
import unittest
from app import create_app, db
from app.models import User

class RoutesTestCase(unittest.TestCase):
    def setUp(self):
        self.app = create_app('testing')
        self.client = self.app.test_client()
        self.app_context = self.app.app_context()
        self.app_context.push()
        db.create_all()

    def tearDown(self):
        db.session.remove()
        db.drop_all()
        self.app_context.pop()

    def test_home_page(self):
        response = self.client.get('/')
        self.assertEqual(response.status_code, 200)

    def test_login_logout(self):
        # 创建测试用户
        user = User(username='test', email='test@example.com')
        user.set_password('test')
        db.session.add(user)
        db.session.commit()

        # 测试登录
        response = self.client.post('/auth/login', data={
            'username': 'test',
            'password': 'test'
        }, follow_redirects=True)
        self.assertEqual(response.status_code, 200)

        # 测试登出
        response = self.client.get('/auth/logout', follow_redirects=True)
        self.assertEqual(response.status_code, 200)

    def test_api_users(self):
        response = self.client.get('/api/users')
        self.assertEqual(response.status_code, 200)
        data = response.get_json()
        self.assertIn('users', data)

13. 最佳实践

13.1 项目结构最佳实践

bash 复制代码
my_flask_app/
├── app/
│   ├── __init__.py          # 应用工厂
│   ├── models.py            # 数据模型
│   ├── main/                # 主要功能蓝图
│   ├── auth/                # 认证蓝图
│   ├── api/                 # API 蓝图
│   ├── templates/           # 模板文件
│   ├── static/              # 静态文件
│   └── utils/               # 工具函数
├── migrations/              # 数据库迁移
├── tests/                   # 测试文件
├── config.py               # 配置文件
├── requirements.txt        # 依赖列表
├── .env                    # 环境变量
├── .gitignore             # Git 忽略文件
└── run.py                 # 应用入口

13.2 安全最佳实践

python 复制代码
# 1. 使用强密钥
app.config['SECRET_KEY'] = os.urandom(24)

# 2. 防止 SQL 注入 - 使用 ORM
# Bad
query = f"SELECT * FROM users WHERE id = {user_id}"
# Good
user = User.query.get(user_id)

# 3. 防止 XSS 攻击 - 自动转义
# Jinja2 默认启用自动转义

# 4. 防止 CSRF 攻击
from flask_wtf.csrf import CSRFProtect
csrf = CSRFProtect(app)

# 5. 使用 HTTPS
@app.before_request
def force_https():
    if not request.is_secure and app.env != 'development':
        return redirect(request.url.replace('http://', 'https://'))

# 6. 设置安全头
@app.after_request
def set_security_headers(response):
    response.headers['X-Content-Type-Options'] = 'nosniff'
    response.headers['X-Frame-Options'] = 'DENY'
    response.headers['X-XSS-Protection'] = '1; mode=block'
    response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'
    return response

13.3 性能优化

python 复制代码
# 1. 数据库查询优化
# Bad
posts = Post.query.all()
for post in posts:
    print(post.author.username)  # N+1 查询问题

# Good
posts = Post.query.options(db.joinedload(Post.author)).all()

# 2. 分页
posts = Post.query.paginate(
    page=page, per_page=20, error_out=False
)

# 3. 缓存
from flask_caching import Cache
cache = Cache(app)

@cache.cached(timeout=300)
def expensive_function():
    return calculate_something()

# 4. 数据库连接池
app.config['SQLALCHEMY_ENGINE_OPTIONS'] = {
    'pool_pre_ping': True,
    'pool_recycle': 300,
}

Flask 是一个功能强大且灵活的 Web 框架,适合从简单的原型到复杂的企业级应用。通过合理的项目结构、安全实践和性能优化,可以构建出高质量的 Web 应用程序。

Similar code found with 6 license types

相关推荐
运维行者_3 小时前
网络监控与ITSM集成:从告警到工单,实现运维自动化闭环
开发语言·网络·分布式·后端·架构·flask·php
way_hj7 小时前
Flask笔记(2)快速web项目
笔记·flask·vue·web
海盗12348 小时前
微软技术日报——2026-08-07
python·microsoft·flask
weixin_BYSJ19879 小时前
【java项目分享】springboot阅读推荐平台10600
java·javascript·spring boot·python·django·flask·php
Vec‑Jie1 天前
MerchantOps-KBQA 实践(十四):本地运行、性能限制与可验证的交付边界
人工智能·后端·python·flask
mqiqe3 天前
LLM Gateway:企业级大模型应用的“统一控制平面”
平面·flask·gateway
aGdF8E3gQ3 天前
【Application Insights】采样率对Function App日志收集的影响和解决方法
python·flask·wpf
编码者卢布3 天前
【Azure APIM】通过 API Management 公开API为 MCP Server 的试验 (二)
microsoft·flask·azure
编码者卢布4 天前
【Azure APIM】通过 API Management 公开现有 MCP Server 的试验 (一)
microsoft·flask·azure
这个人懒得名字都没写5 天前
Flask + PyArmor Gunicorn启动报错:RuntimeError: unauthorised use of script
python·flask·gunicorn·pyarmor