diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..5e6525f --- /dev/null +++ b/.env.example @@ -0,0 +1,23 @@ +# ============================================================ +# 使用说明:复制本文件为 .env,然后填入真实的配置值 +# 命令: cp .env.example .env +# ⚠️ .env 文件已被 .gitignore 忽略,不会被提交到 Git +# ============================================================ + +# Flask 会话签名密钥(生产环境必须替换为随机字符串) +# 生成方式: python -c "import secrets; print(secrets.token_hex(32))" +SECRET_KEY=change-me-to-random-string + +# 数据库连接 URI +# 格式: dialect+driver://username:password@host:port/database?params +# 默认使用 SQLite 方便本地开发,零配置即可运行 +DATABASE_URL=sqlite:///app.db + +# MySQL 示例(需安装 PyMySQL): +# DATABASE_URL=mysql+pymysql://root:your_password@localhost:3306/mydb?charset=utf8mb4 + +# PostgreSQL 示例(需安装 psycopg2-binary): +# DATABASE_URL=postgresql+psycopg2://user:your_password@localhost:5432/mydb + +# 数据库连接池大小(仅生产环境生效,根据服务器内存调整) +DB_POOL_SIZE=10 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 36b13f1..0997986 100644 --- a/.gitignore +++ b/.gitignore @@ -1,176 +1,50 @@ -# ---> Python -# Byte-compiled / optimized / DLL files +# ==================== Python 运行时产物 ==================== +# 字节码缓存目录,每次运行自动生成,无需版本控制 __pycache__/ *.py[cod] *$py.class - -# C extensions *.so - -# Distribution / packaging .Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec +# 虚拟环境目录(每个开发者本地创建,不纳入仓库) +venv/ +env/ +.venv/ -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: +# ==================== Flask 框架产物 ==================== +# Flask 默认实例文件夹(存放 SQLite、日志等运行时文件) instance/ .webassets-cache -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# UV -# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -#uv.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/latest/usage/project/#working-with-version-control -.pdm.toml -.pdm-python -.pdm-build/ - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments +# ==================== ⚠️ 安全相关(最重要) ==================== +# 环境变量文件:包含数据库密码、密钥等敏感信息,绝对禁止提交! .env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ +.env.* +# 但保留示例文件,让新成员知道需要配置哪些变量 +!.env.example -# Spyder project settings -.spyderproject -.spyproject +# ==================== IDE 与操作系统 ==================== +.vscode/ +.idea/ +.DS_Store +Thumbs.db -# Rope project settings -.ropeproject +# ==================== 数据库文件 ==================== +# SQLite 数据库文件是运行时生成的,不应纳入版本控制 +*.db +*.sqlite3 -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ - -# Ruff stuff: -.ruff_cache/ - -# PyPI configuration file -.pypirc +# ==================== 测试与覆盖率 ==================== +htmlcov/ +.coverage +.coverage.* +.pytest_cache/ +.tox/ +.nox/ +.cache/ +# ==================== 构建/分发产物 ==================== +build/ +dist/ +*.egg-info/ +*.egg \ No newline at end of file diff --git a/README.md b/README.md index 32461a3..097a302 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,75 @@ # flask_example -flask框架和连接数据库模板 +> 基于 Flask 3.x + SQLAlchemy 2.0 的生产级项目模板 +> 内置应用工厂、数据库迁移、安全配置和 CRUD 示例 + +## 🚀 快速开始 + +```bash +# 1. 克隆项目 +git clone && cd flask-db-template + +# 2. 创建虚拟环境并安装依赖 +python -m venv venv +source venv/bin/activate # macOS/Linux +# venv\Scripts\activate # Windows +pip install -r requirements.txt + +# 3. 配置环境变量(⚠️ 必须执行) +cp .env.example .env +# 编辑 .env,至少修改 SECRET_KEY 和 DATABASE_URL + +# 4. 初始化数据库 +flask db init # 首次执行,生成 migrations/ 目录 +flask db migrate -m "init" # 生成迁移脚本 +flask db upgrade # 应用迁移到数据库 + +# 5. 启动开发服务器 +python run.py ``` -my_flask_app/ + +## 📡 API 接口 + +| 方法 | 路径 | 说明 | 请求体 | +|------|------|------|--------| +| `POST` | `/api/users` | 创建用户 | `{"username":"test","email":"t@example.com"}` | +| `GET` | `/api/users?page=1&per_page=20` | 分页查询用户 | - | + +## ⚠️ 安全须知 + +- **永远不要**将 `.env` 提交到 Git(已在 `.gitignore` 中排除) +- 生产部署使用 Gunicorn:`gunicorn -w 4 -b 0.0.0.0:8000 "run:app"` +- 修改模型后**必须**执行 `flask db migrate` + `flask db upgrade` +- `debug=True` 仅限开发环境,生产环境开启会导致源码泄露 + +## 📂 项目结构说明 + +``` +flask-db-template/ ├── app/ -│ ├── __init__.py # 应用工厂 & 扩展初始化 -│ ├── config.py # 配置文件 -│ ├── models/ # 数据模型 -│ │ ├── __init__.py -│ │ └── user.py -│ └── routes/ # 蓝图/路由 -│ ├── __init__.py -│ └── user.py -├── migrations/ # 数据库迁移文件 (Flask-Migrate) -├── .env # 环境变量(不要提交到Git) -├── requirements.txt -└── run.py # 启动入口 -``` \ No newline at end of file +│ ├── __init__.py # 应用工厂:创建Flask实例并初始化扩展 +│ ├── config.py # 配置管理:集中管理所有环境变量和数据库配置 +│ ├── models/ +│ │ ├── __init__.py # 模型统一导出:确保迁移工具能发现所有表 +│ │ └── user.py # 用户模型:定义数据表结构和序列化方法 +│ └── routes/ +│ ├── __init__.py # 蓝图统一导出 +│ └── user.py # 用户API:包含完整的CRUD和异常处理 +├── migrations/ # 数据库迁移目录(由flask db init生成) +│ └── .gitkeep # Git空目录占位符 +├── .env.example # 环境变量模板(不含真实密码,可安全提交) +├── .gitignore # Git忽略规则(防止敏感文件和缓存被提交) +├── README.md # 项目文档 +├── requirements.txt # Python依赖清单(锁定版本保证环境一致) +└── run.py # 启动入口:开发环境启动脚本 +``` + +| 文件/目录 | 作用 | +|-----------|------| +| `app/__init__.py` | 应用工厂,创建 Flask 实例并初始化扩展 | +| `app/config.py` | 集中配置管理,从环境变量读取敏感信息 | +| `app/models/` | SQLAlchemy 数据模型定义 | +| `app/routes/` | Blueprint 路由模块,按资源拆分 | +| `migrations/` | Alembic 数据库迁移脚本 | +| `.env.example` | 环境变量模板(安全提交到 Git) | +| `run.py` | 开发环境启动入口 | \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..6bc644b --- /dev/null +++ b/app/__init__.py @@ -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 \ No newline at end of file diff --git a/app/config.py b/app/config.py new file mode 100644 index 0000000..60a4617 --- /dev/null +++ b/app/config.py @@ -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, + } \ No newline at end of file diff --git a/app/models/__init__.py b/app/models/__init__.py new file mode 100644 index 0000000..1c13422 --- /dev/null +++ b/app/models/__init__.py @@ -0,0 +1,9 @@ +""" +模型统一导出模块 +- Alembic(Flask-Migrate 底层)通过扫描此文件发现所有模型 +- 新增模型后必须在此处添加 import,否则 `flask db migrate` 无法检测到新表 +- __all__ 明确声明公开接口,避免意外导入内部模块 +""" +from app.models.user import User + +__all__ = ['User'] \ No newline at end of file diff --git a/app/models/user.py b/app/models/user.py new file mode 100644 index 0000000..bcdfeee --- /dev/null +++ b/app/models/user.py @@ -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'' \ No newline at end of file diff --git a/app/routes/__init__.py b/app/routes/__init__.py new file mode 100644 index 0000000..01aa091 --- /dev/null +++ b/app/routes/__init__.py @@ -0,0 +1,8 @@ +""" +蓝图统一导出模块 +- 当蓝图数量增多时,在此集中管理,简化 __init__.py 中的注册逻辑 +- 新增蓝图后只需在此添加一行 import +""" +from app.routes.user import user_bp + +__all__ = ['user_bp'] \ No newline at end of file diff --git a/app/routes/user.py b/app/routes/user.py new file mode 100644 index 0000000..b4e2990 --- /dev/null +++ b/app/routes/user.py @@ -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, # 当前页码 + }) \ No newline at end of file diff --git a/migrations/.gitkeep b/migrations/.gitkeep new file mode 100644 index 0000000..1e48788 --- /dev/null +++ b/migrations/.gitkeep @@ -0,0 +1,3 @@ +# 此文件唯一作用:让 Git 跟踪 migrations/ 空目录 +# Git 不支持空目录,没有此文件则 clone 后缺少 migrations/ 目录 +# 执行 `flask db init` 后此文件会被实际迁移文件替代,可删除 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..1eb737f --- /dev/null +++ b/requirements.txt @@ -0,0 +1,17 @@ +# Flask 核心框架(3.x 支持异步和现代特性) +Flask==3.0.0 + +# ORM 扩展:将数据库操作映射为 Python 对象 +Flask-SQLAlchemy==3.1.1 + +# 数据库迁移工具:基于 Alembic,自动追踪模型变更并生成迁移脚本 +Flask-Migrate==4.0.5 + +# 环境变量加载:从 .env 文件读取配置,避免硬编码敏感信息 +python-dotenv==1.0.0 + +# MySQL 驱动:纯 Python 实现,兼容性好(PostgreSQL 请替换为 psycopg2-binary) +PyMySQL==1.1.0 + +# PyMySQL 加密依赖:支持 MySQL 8.0+ 的 caching_sha2_password 认证 +cryptography==41.0.7 \ No newline at end of file diff --git a/run.py b/run.py new file mode 100644 index 0000000..b76cb59 --- /dev/null +++ b/run.py @@ -0,0 +1,20 @@ +""" +开发环境启动入口 +- 仅在本地开发时使用,生产环境请用 Gunicorn/uWSGI +- 启动前自动建表(仅限开发便利,生产环境必须使用 flask db upgrade) +- debug=True 开启热重载和交互式调试器(⚠️ 生产环境严禁开启) +""" +from app import create_app, db + +app = create_app() + +if __name__ == '__main__': + # 在应用上下文中执行数据库操作 + with app.app_context(): + # ⚠️ create_all() 仅用于开发快速验证 + # 生产环境请使用: flask db migrate && flask db upgrade + db.create_all() + + # host='0.0.0.0' 允许外部访问(Docker/局域网调试需要) + # 仅监听 127.0.0.1 则只允许本机访问 + app.run(debug=True, host='0.0.0.0', port=5000) \ No newline at end of file