首次提交

This commit is contained in:
2026-09-23 13:56:49 +08:00
parent 7c235aab6e
commit 0e47b1bba4
12 changed files with 364 additions and 176 deletions
+35
View File
@@ -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
+37
View File
@@ -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,
}
+9
View File
@@ -0,0 +1,9 @@
"""
模型统一导出模块
- Alembic(Flask-Migrate 底层)通过扫描此文件发现所有模型
- 新增模型后必须在此处添加 import,否则 `flask db migrate` 无法检测到新表
- __all__ 明确声明公开接口,避免意外导入内部模块
"""
from app.models.user import User
__all__ = ['User']
+42
View File
@@ -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}>'
+8
View File
@@ -0,0 +1,8 @@
"""
蓝图统一导出模块
- 当蓝图数量增多时,在此集中管理,简化 __init__.py 中的注册逻辑
- 新增蓝图后只需在此添加一行 import
"""
from app.routes.user import user_bp
__all__ = ['user_bp']
+64
View File
@@ -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, # 当前页码
})