首次提交
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
"""
|
||||
应用工厂模块(Application Factory Pattern)
|
||||
- 延迟创建 Flask 实例,支持测试时传入不同配置
|
||||
- 扩展对象在模块顶层创建但不绑定 app,通过 init_app() 延迟绑定
|
||||
- 这种模式是 Flask 官方推荐的最佳实践,避免循环导入
|
||||
"""
|
||||
from flask import Flask
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_migrate import Migrate
|
||||
from app.config import Config
|
||||
|
||||
# ✅ 扩展实例在工厂外部创建,不与任何 app 绑定
|
||||
# 这样可以在测试中创建多个 app 实例共享同一套扩展
|
||||
db = SQLAlchemy()
|
||||
migrate = Migrate()
|
||||
|
||||
|
||||
def create_app(config_class=Config):
|
||||
"""
|
||||
应用工厂函数
|
||||
:param config_class: 配置类,默认使用 Config,测试时可替换为 TestingConfig
|
||||
:return: 配置完成的 Flask 应用实例
|
||||
"""
|
||||
app = Flask(__name__)
|
||||
app.config.from_object(config_class)
|
||||
|
||||
# 将扩展绑定到当前 app 实例
|
||||
db.init_app(app)
|
||||
migrate.init_app(app, db)
|
||||
|
||||
# 注册蓝图(放在这里避免循环导入:蓝图引用 db → db 在工厂中初始化)
|
||||
from app.routes.user import user_bp
|
||||
app.register_blueprint(user_bp, url_prefix='/api/users')
|
||||
|
||||
return app
|
||||
@@ -0,0 +1,37 @@
|
||||
"""
|
||||
应用配置模块
|
||||
- 集中管理所有配置项,支持多环境切换
|
||||
- 通过 python-dotenv 自动加载 .env 文件中的环境变量
|
||||
- 敏感信息全部从环境变量读取,绝不硬编码
|
||||
"""
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# 自动加载项目根目录下的 .env 文件
|
||||
load_dotenv()
|
||||
|
||||
|
||||
class Config:
|
||||
"""基础配置类(开发/生产通用)"""
|
||||
|
||||
# Flask 会话加密密钥,用于 cookie 签名和 CSRF 保护
|
||||
SECRET_KEY = os.environ.get('SECRET_KEY', 'dev-secret-key-change-in-prod')
|
||||
|
||||
# 数据库连接字符串,优先读取环境变量,回退到本地 SQLite
|
||||
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL', 'sqlite:///app.db')
|
||||
|
||||
# 关闭 SQLAlchemy 事件追踪,避免不必要的内存开销和警告
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||
|
||||
# 是否打印 SQL 语句(开发调试时可设为 True,生产环境务必关闭)
|
||||
SQLALCHEMY_ECHO = False
|
||||
|
||||
# 数据库引擎高级选项(主要针对 MySQL/PostgreSQL 连接池)
|
||||
SQLALCHEMY_ENGINE_OPTIONS = {
|
||||
# 连接池常驻连接数
|
||||
'pool_size': int(os.environ.get('DB_POOL_SIZE', 10)),
|
||||
# 连接最大存活时间(秒),防止数据库端主动断开空闲连接
|
||||
'pool_recycle': 3600,
|
||||
# 每次从池中取出连接前先 ping 一下,避免拿到已断开的死连接
|
||||
'pool_pre_ping': True,
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
"""
|
||||
模型统一导出模块
|
||||
- Alembic(Flask-Migrate 底层)通过扫描此文件发现所有模型
|
||||
- 新增模型后必须在此处添加 import,否则 `flask db migrate` 无法检测到新表
|
||||
- __all__ 明确声明公开接口,避免意外导入内部模块
|
||||
"""
|
||||
from app.models.user import User
|
||||
|
||||
__all__ = ['User']
|
||||
@@ -0,0 +1,42 @@
|
||||
"""
|
||||
用户数据模型
|
||||
- 定义 users 表结构、字段约束和索引
|
||||
- 提供 to_dict() 方法用于 API 响应序列化
|
||||
- 注意:不要在模型中直接 import Blueprint 或路由,避免循环导入
|
||||
"""
|
||||
from datetime import datetime
|
||||
from app import db
|
||||
|
||||
|
||||
class User(db.Model):
|
||||
"""用户表模型"""
|
||||
__tablename__ = 'users'
|
||||
|
||||
# 主键:自增整数,InnoDB 聚簇索引默认按主键排序
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
|
||||
# 用户名:唯一约束 + B-tree 索引,加速查询和去重校验
|
||||
username = db.Column(db.String(80), unique=True, nullable=False, index=True)
|
||||
|
||||
# 邮箱:唯一约束,业务层应额外做格式校验
|
||||
email = db.Column(db.String(120), unique=True, nullable=False)
|
||||
|
||||
# 创建时间:默认值为记录插入时的 UTC 时间
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
def to_dict(self):
|
||||
"""
|
||||
将模型实例序列化为字典
|
||||
- 用于 API JSON 响应,避免直接暴露内部字段(如密码哈希)
|
||||
- datetime 转为 ISO 8601 字符串,前端可直接解析
|
||||
"""
|
||||
return {
|
||||
'id': self.id,
|
||||
'username': self.username,
|
||||
'email': self.email,
|
||||
'created_at': self.created_at.isoformat(),
|
||||
}
|
||||
|
||||
def __repr__(self):
|
||||
"""调试友好的字符串表示,在 Flask shell 和日志中显示"""
|
||||
return f'<User {self.username}>'
|
||||
@@ -0,0 +1,8 @@
|
||||
"""
|
||||
蓝图统一导出模块
|
||||
- 当蓝图数量增多时,在此集中管理,简化 __init__.py 中的注册逻辑
|
||||
- 新增蓝图后只需在此添加一行 import
|
||||
"""
|
||||
from app.routes.user import user_bp
|
||||
|
||||
__all__ = ['user_bp']
|
||||
@@ -0,0 +1,64 @@
|
||||
"""
|
||||
用户 API 路由模块
|
||||
- 使用 Blueprint 实现模块化路由,避免单文件膨胀
|
||||
- 包含参数校验、异常处理和分页查询的生产级实现
|
||||
- 所有数据库写操作均包裹在 try/except 中,失败时自动回滚
|
||||
"""
|
||||
from flask import Blueprint, request, jsonify
|
||||
from app import db
|
||||
from app.models.user import User
|
||||
|
||||
# 创建蓝图,名称用于 url_for() 反向解析,__name__ 用于定位模板/静态文件
|
||||
user_bp = Blueprint('user', __name__)
|
||||
|
||||
|
||||
@user_bp.route('', methods=['POST'])
|
||||
def create_user():
|
||||
"""
|
||||
创建新用户
|
||||
- POST /api/users
|
||||
- Body: {"username": "string", "email": "string"}
|
||||
- Returns: 201 + 用户JSON / 400 + 错误信息 / 500 + 服务器错误
|
||||
"""
|
||||
# silent=True 使得非 JSON 请求体返回 None 而非抛出 400 异常
|
||||
data = request.get_json(silent=True)
|
||||
|
||||
# ✅ 入参校验:永远不要信任客户端输入
|
||||
if not data or 'username' not in data or 'email' not in data:
|
||||
return jsonify({'error': 'Missing required fields: username, email'}), 400
|
||||
|
||||
try:
|
||||
user = User(username=data['username'], email=data['email'])
|
||||
db.session.add(user)
|
||||
# commit 同时完成 flush(发送SQL)和事务提交
|
||||
db.session.commit()
|
||||
return jsonify(user.to_dict()), 201
|
||||
except Exception as e:
|
||||
# ✅ 异常时必须回滚,否则后续请求复用该 session 会持续报错
|
||||
db.session.rollback()
|
||||
# 生产环境应记录日志而非直接返回异常详情,此处为模板演示
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@user_bp.route('', methods=['GET'])
|
||||
def get_users():
|
||||
"""
|
||||
分页获取用户列表
|
||||
- GET /api/users?page=1&per_page=20
|
||||
- Returns: 分页结果 JSON,包含 items、total、pages、current_page
|
||||
"""
|
||||
# type=int 确保参数被转换为整数,非法值回退到默认值
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = request.args.get('per_page', 20, type=int)
|
||||
|
||||
# paginate() 自动处理 LIMIT/OFFSET,error_out=False 避免页码越界时抛异常
|
||||
pagination = User.query.order_by(User.created_at.desc()).paginate(
|
||||
page=page, per_page=per_page, error_out=False
|
||||
)
|
||||
|
||||
return jsonify({
|
||||
'items': [u.to_dict() for u in pagination.items],
|
||||
'total': pagination.total, # 总记录数
|
||||
'pages': pagination.pages, # 总页数
|
||||
'current_page': page, # 当前页码
|
||||
})
|
||||
Reference in New Issue
Block a user