From 1f2bb12178849aa4d94fb23d761d07221d6fe86c Mon Sep 17 00:00:00 2001 From: yangyu <929216764@qq.com> Date: Sat, 27 Sep 2025 23:23:25 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0MySQL=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=BA=93=E6=94=AF=E6=8C=81=E5=92=8C=E5=AE=8C=E6=95=B4?= =?UTF-8?q?=E7=9A=84=E6=95=B0=E6=8D=AE=E5=BA=93=E7=AE=A1=E7=90=86=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加MySQL数据库连接配置 - 创建SQLAlchemy数据库模型(用户表和产品表) - 添加Alembic数据库迁移工具 - 创建数据库管理脚本(database_manager.py) - 添加SQL文件目录结构(migrations, seeds, schemas) - 更新应用配置以支持数据库连接 - 添加完整的数据库设置文档 - 配置开发环境数据库连接(localhost:3306, root/123456, fast_demo) --- CODEBUDDY.md | 86 ++++++++++++ DATABASE_SETUP.md | 199 +++++++++++++++++++++++++++ DEV_CONFIG.md | 110 +++++++++++++++ alembic.ini | 112 +++++++++++++++ alembic/env.py | 93 +++++++++++++ alembic/script.py.mako | 24 ++++ app/core/application.py | 6 + app/core/config.py | 17 ++- app/database/__init__.py | 7 + app/database/base.py | 6 + app/database/connection.py | 35 +++++ app/database/models.py | 39 ++++++ database_manager.py | 139 +++++++++++++++++++ requirements.txt | 8 +- sql/migrations/001_create_tables.sql | 35 +++++ sql/schemas/init_database.sql | 39 ++++++ sql/seeds/sample_data.sql | 15 ++ 17 files changed, 968 insertions(+), 2 deletions(-) create mode 100644 CODEBUDDY.md create mode 100644 DATABASE_SETUP.md create mode 100644 DEV_CONFIG.md create mode 100644 alembic.ini create mode 100644 alembic/env.py create mode 100644 alembic/script.py.mako create mode 100644 app/database/__init__.py create mode 100644 app/database/base.py create mode 100644 app/database/connection.py create mode 100644 app/database/models.py create mode 100644 database_manager.py create mode 100644 sql/migrations/001_create_tables.sql create mode 100644 sql/schemas/init_database.sql create mode 100644 sql/seeds/sample_data.sql diff --git a/CODEBUDDY.md b/CODEBUDDY.md new file mode 100644 index 0000000..072ebe3 --- /dev/null +++ b/CODEBUDDY.md @@ -0,0 +1,86 @@ +# CODEBUDDY.md + +此文件为 CodeBuddy Code 在此代码库中工作时提供指导。 + +## 常用命令 + +### 应用启动 +```bash +# 安装依赖 +pip install -r requirements.txt + +# 使用便捷启动脚本(推荐) +python start.py --env dev # 开发环境 +python start.py --env prod # 生产环境 +python start.py --port 9000 # 指定端口 + +# 直接运行主文件 +python main.py +``` + +### 环境配置 +```bash +# 环境变量优先级(从高到低): +# 1. 环境变量 +# 2. .env.{environment} 文件 +# 3. .env 基础配置文件 +# 4. 代码中的默认值 + +# 配置验证 +python -c "from app.core.config import settings; print(f'Environment: {settings.ENVIRONMENT}'); print(f'Debug: {settings.DEBUG}')" +``` + +## 架构设计 + +### 应用工厂模式 +项目采用应用工厂模式,核心文件: +- **应用创建**:`app/core/application.py` - 使用 `create_application()` 工厂函数 +- **配置管理**:`app/core/config.py` - 基于 pydantic-settings 的多环境配置 +- **路由聚合**:`app/api/v1/router.py` - 集中管理 API 路由注册 + +### 分层架构 +1. **API层** (`app/api/v1/endpoints/`) - FastAPI 路由端点,处理 HTTP 请求/响应 +2. **服务层** (`app/services/`) - 业务逻辑封装,使用单例服务实例 +3. **模型层** (`app/models/`) - 核心领域模型,Pydantic BaseModel +4. **Schema层** (`app/schemas/`) - 请求/响应验证和序列化 + +### 路由结构 +- 所有 API 以 `/api/v1/` 为前缀 +- 路由通过 `app/api/v1/router.py` 统一注册 +- 每个资源对应一个独立的路由器文件 + +### 配置管理 +- 支持多环境:development、testing、production +- 自动加载对应配置文件:`.env.dev.{env}` +- 类型安全验证和转换 +- 便利属性:`settings.is_development`、`settings.is_production` + +### 服务模式 +- 使用静态服务类 (`UserService`、`ProductService`) +- 当前使用内存数据库进行演示 +- 服务层负责所有 CRUD 操作和业务规则 + +### 关键特性 +- CORS 中间件配置 +- 应用生命周期事件处理器 +- 模块化路由设计 +- 版本化的 API 结构 + +## 开发规范 + +### 添加新资源 +1. 模型层:在 `app/models/` 创建数据模型 +2. Schema层:在 `app/schemas/` 创建 Base/Create/Update/Response 模式 +3. 服务层:在 `app/services/` 创建服务类 +4. API层:在 `app/api/v1/endpoints/` 创建路由端点 +5. 路由注册:在 `app/api/v1/router.py` 注册新路由 + +### 配置扩展 +- 新配置项添加到 `app/core/config.py` 中的 Settings 类 +- 遵循现有配置验证模式 +- 支持环境变量覆盖 + +### 数据存储 +- 当前使用内存数据存储用于演示 +- 实际实现将在服务层集成数据库 +- 服务接口设计支持无缝替换存储后端 \ No newline at end of file diff --git a/DATABASE_SETUP.md b/DATABASE_SETUP.md new file mode 100644 index 0000000..12f3b9a --- /dev/null +++ b/DATABASE_SETUP.md @@ -0,0 +1,199 @@ +# 数据库设置指南 + +本项目已配置支持MySQL数据库,包含完整的数据库模型、迁移脚本和管理工具。 + +## 目录结构 + +``` +sql/ +├── migrations/ # 数据库迁移文件 +│ └── 001_create_tables.sql +├── seeds/ # 种子数据文件 +│ └── sample_data.sql +└── schemas/ # 数据库架构文件 + └── init_database.sql + +alembic/ # Alembic迁移工具 +├── versions/ # 自动生成的迁移版本 +├── env.py # Alembic环境配置 +└── script.py.mako # 迁移模板 + +app/database/ # 数据库相关代码 +├── __init__.py +├── connection.py # 数据库连接管理 +├── base.py # 基础模型类 +└── models.py # SQLAlchemy模型 +``` + +## 快速开始 + +### 1. 安装依赖 + +```bash +pip install -r requirements.txt +``` + +### 2. 配置数据库 + +创建 `.env.dev` 文件(参考 `.env.example`): + +```env +# MySQL 数据库配置 +DB_HOST=localhost +DB_PORT=3306 +DB_USER=root +DB_PASSWORD=your_password +DB_NAME=fastapi_demo +DB_CHARSET=utf8mb4 +``` + +### 3. 初始化数据库 + +使用数据库管理脚本: + +```bash +# 初始化数据库(创建数据库和表,插入示例数据) +python database_manager.py init + +# 或者分步执行 +python database_manager.py create-db # 创建数据库 +python database_manager.py create-tables # 创建表 +python database_manager.py seed # 插入示例数据 +``` + +### 4. 启动应用 + +```bash +python main.py +``` + +## 数据库管理 + +### 使用数据库管理脚本 + +```bash +python database_manager.py init # 初始化数据库 +python database_manager.py reset # 重置数据库 +python database_manager.py create-db # 仅创建数据库 +python database_manager.py create-tables # 仅创建表 +python database_manager.py seed # 仅插入示例数据 +python database_manager.py help # 显示帮助 +``` + +### 使用Alembic进行迁移 + +```bash +# 初始化Alembic(首次使用) +alembic init alembic + +# 创建新的迁移 +alembic revision --autogenerate -m "描述信息" + +# 执行迁移 +alembic upgrade head + +# 回滚迁移 +alembic downgrade -1 + +# 查看迁移历史 +alembic history + +# 查看当前版本 +alembic current +``` + +## 数据库模型 + +### 用户表 (users) + +| 字段 | 类型 | 说明 | +|------|------|------| +| id | int(11) | 主键,自增 | +| username | varchar(50) | 用户名,唯一 | +| email | varchar(100) | 邮箱,唯一 | +| full_name | varchar(100) | 全名,可空 | +| is_active | tinyint(1) | 是否激活 | +| created_at | datetime | 创建时间 | +| updated_at | datetime | 更新时间 | + +### 产品表 (products) + +| 字段 | 类型 | 说明 | +|------|------|------| +| id | int(11) | 主键,自增 | +| name | varchar(200) | 产品名称 | +| description | text | 产品描述,可空 | +| price | decimal(10,2) | 价格 | +| stock | int(11) | 库存数量 | +| is_available | tinyint(1) | 是否可用 | +| created_at | datetime | 创建时间 | +| updated_at | datetime | 更新时间 | + +## 配置说明 + +### 数据库连接配置 + +在 `app/core/config.py` 中配置: + +```python +# MySQL 数据库配置 +DB_HOST: str = "localhost" # 数据库主机 +DB_PORT: int = 3306 # 数据库端口 +DB_USER: str = "root" # 数据库用户名 +DB_PASSWORD: str = "" # 数据库密码 +DB_NAME: str = "fastapi_demo" # 数据库名称 +DB_CHARSET: str = "utf8mb4" # 字符集 + +# 数据库连接池配置 +DB_POOL_SIZE: int = 5 # 连接池大小 +DB_MAX_OVERFLOW: int = 10 # 最大溢出连接数 +DB_POOL_TIMEOUT: int = 30 # 连接超时时间(秒) +DB_POOL_RECYCLE: int = 3600 # 连接回收时间(秒) +``` + +### 环境变量 + +支持通过环境变量覆盖配置: + +```bash +export DB_HOST=localhost +export DB_PORT=3306 +export DB_USER=root +export DB_PASSWORD=your_password +export DB_NAME=fastapi_demo +``` + +## 故障排除 + +### 常见问题 + +1. **连接失败** + - 检查MySQL服务是否启动 + - 验证数据库配置信息 + - 确认用户权限 + +2. **字符编码问题** + - 确保数据库使用utf8mb4字符集 + - 检查连接字符串中的charset参数 + +3. **迁移失败** + - 检查Alembic配置 + - 确认数据库连接正常 + - 查看迁移文件语法 + +### 日志调试 + +启用SQL日志: + +```env +DB_ECHO=true +LOG_LEVEL=debug +``` + +## 开发建议 + +1. **使用迁移管理数据库结构变更** +2. **在开发环境中使用示例数据** +3. **定期备份生产数据库** +4. **使用连接池优化性能** +5. **监控数据库连接状态** diff --git a/DEV_CONFIG.md b/DEV_CONFIG.md new file mode 100644 index 0000000..7b6c0fa --- /dev/null +++ b/DEV_CONFIG.md @@ -0,0 +1,110 @@ +# 开发环境配置总结 + +## 🎯 数据库配置 + +### 连接信息 +- **主机**: localhost +- **端口**: 3306 +- **用户名**: root +- **密码**: 123456 +- **数据库名**: fast_demo +- **字符集**: utf8mb4 + +### 连接URL +``` +mysql+pymysql://root:123456@localhost:3306/fast_demo?charset=utf8mb4 +``` + +## 📁 配置文件 + +### 1. 应用配置 (`app/core/config.py`) +已更新默认数据库配置: +```python +DB_HOST: str = "localhost" +DB_PORT: int = 3306 +DB_USER: str = "root" +DB_PASSWORD: str = "123456" +DB_NAME: str = "fast_demo" +DB_CHARSET: str = "utf8mb4" +``` + +### 2. Alembic配置 (`alembic.ini`) +已更新数据库URL: +```ini +sqlalchemy.url = mysql+pymysql://root:123456@localhost:3306/fast_demo?charset=utf8mb4 +``` + +## 🗄️ 数据库表 + +### 用户表 (users) +| 字段 | 类型 | 说明 | +|------|------|------| +| id | int(11) | 主键,自增 | +| username | varchar(50) | 用户名,唯一 | +| email | varchar(100) | 邮箱,唯一 | +| full_name | varchar(100) | 全名,可空 | +| is_active | tinyint(1) | 是否激活 | +| created_at | datetime | 创建时间 | +| updated_at | datetime | 更新时间 | + +### 产品表 (products) +| 字段 | 类型 | 说明 | +|------|------|------| +| id | int(11) | 主键,自增 | +| name | varchar(200) | 产品名称 | +| description | text | 产品描述,可空 | +| price | decimal(10,2) | 价格 | +| stock | int(11) | 库存数量 | +| is_available | tinyint(1) | 是否可用 | +| created_at | datetime | 创建时间 | +| updated_at | datetime | 更新时间 | + +## 🚀 快速开始 + +### 1. 启动应用 +```bash +python main.py +``` + +### 2. 访问API文档 +- Swagger UI: http://127.0.0.1:8000/docs +- ReDoc: http://127.0.0.1:8000/redoc + +### 3. 数据库管理 +```bash +# 初始化数据库 +python database_manager.py init + +# 重置数据库 +python database_manager.py reset + +# 查看帮助 +python database_manager.py help +``` + +## 🔧 环境变量 + +可以通过环境变量覆盖配置: + +```bash +export DB_HOST=localhost +export DB_PORT=3306 +export DB_USER=root +export DB_PASSWORD=123456 +export DB_NAME=fast_demo +``` + +## ✅ 验证配置 + +数据库连接已成功验证: +- ✅ 数据库 'fast_demo' 已存在 +- ✅ 数据库连接成功 +- ✅ 表 'users' 和 'products' 已创建 +- ✅ 示例数据已插入 + +## 📝 注意事项 + +1. 确保MySQL服务正在运行 +2. 确保用户 'root' 有访问数据库的权限 +3. 开发环境配置已优化,包含热重载等功能 +4. 生产环境请修改默认密码和配置 diff --git a/alembic.ini b/alembic.ini new file mode 100644 index 0000000..d5c33ad --- /dev/null +++ b/alembic.ini @@ -0,0 +1,112 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = alembic + +# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s +# Uncomment the line below if you want the files to be prepended with date and time +# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s + +# sys.path path, will be prepended to sys.path if present. +# defaults to the current working directory. +prepend_sys_path = . + +# timezone to use when rendering the date within the migration file +# as well as the filename. +# If specified, requires the python-dateutil library that can be +# installed by adding `alembic[tz]` to the pip requirements +# string value is passed to dateutil.tz.gettz() +# leave blank for localtime +# timezone = + +# max length of characters to apply to the +# "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version number format +version_num_format = %04d + +# version path separator; As mentioned above, this is the character used to split +# version_locations. The default within new alembic.ini files is "os", which uses +# os.pathsep. If this key is omitted entirely, it falls back to the legacy +# behavior of splitting on spaces and/or commas. +# Valid values for version_path_separator are: +# +# version_path_separator = : +# version_path_separator = ; +# version_path_separator = space +version_path_separator = os + +# set to 'true' to search source files recursively +# in each "version_locations" directory +# new in Alembic version 1.10 +# recursive_version_locations = false + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +sqlalchemy.url = mysql+pymysql://root:123456@localhost:3306/fast_demo?charset=utf8mb4 + + +[post_write_hooks] +# post_write_hooks defines scripts or Python functions that are run +# on newly generated revision scripts. See the documentation for further +# detail and examples + +# format using "black" - use the console_scripts runner, against the "black" entrypoint +# hooks = black +# black.type = console_scripts +# black.entrypoint = black +# black.options = -l 79 REVISION_SCRIPT_FILENAME + +# lint with attempts to fix using "ruff" - use the exec runner, execute a binary +# hooks = ruff +# ruff.type = exec +# ruff.executable = %(here)s/.venv/bin/ruff +# ruff.options = --fix REVISION_SCRIPT_FILENAME + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/alembic/env.py b/alembic/env.py new file mode 100644 index 0000000..bf1fc42 --- /dev/null +++ b/alembic/env.py @@ -0,0 +1,93 @@ +""" +Alembic 环境配置 +""" +from logging.config import fileConfig +from sqlalchemy import engine_from_config +from sqlalchemy import pool +from alembic import context +import os +import sys + +# 添加项目根目录到 Python 路径 +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from app.database.models import Base +from app.core.config import settings + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +# add your model's MetaData object here +# for 'autogenerate' support +target_metadata = Base.metadata + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def get_url(): + """获取数据库连接URL""" + return settings.DATABASE_URL + + +def run_migrations_offline() -> None: + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = get_url() + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online() -> None: + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + configuration = config.get_section(config.config_ini_section) + configuration["sqlalchemy.url"] = get_url() + + connectable = engine_from_config( + configuration, + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + context.configure( + connection=connection, target_metadata=target_metadata + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/alembic/script.py.mako b/alembic/script.py.mako new file mode 100644 index 0000000..55df286 --- /dev/null +++ b/alembic/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade() -> None: + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + ${downgrades if downgrades else "pass"} diff --git a/app/core/application.py b/app/core/application.py index 67d73cb..dafefda 100644 --- a/app/core/application.py +++ b/app/core/application.py @@ -95,6 +95,12 @@ def register_events(app: FastAPI) -> None: print(f"[INFO] {settings.PROJECT_NAME} v{settings.VERSION} is starting up...") # 可以在这里添加启动时的初始化操作 # 如:数据库连接、缓存初始化等 + + # 创建数据库表(如果不存在) + from app.database.connection import engine + from app.database.models import Base + Base.metadata.create_all(bind=engine) + print("[INFO] Database tables created/verified successfully") @app.on_event("shutdown") async def shutdown_event(): diff --git a/app/core/config.py b/app/core/config.py index 37ffd45..ac75eb5 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -30,10 +30,25 @@ class Settings(BaseSettings): REDOC_URL: Optional[str] = "/redoc" # === 数据库配置 === - DATABASE_URL: Optional[str] = None + # MySQL 数据库配置 + DB_HOST: str = "localhost" + DB_PORT: int = 3306 + DB_USER: str = "root" + DB_PASSWORD: str = "123456" + DB_NAME: str = "fast_demo" + DB_CHARSET: str = "utf8mb4" + + # 数据库连接池配置 DB_ECHO: bool = False DB_POOL_SIZE: int = 5 DB_MAX_OVERFLOW: int = 10 + DB_POOL_TIMEOUT: int = 30 + DB_POOL_RECYCLE: int = 3600 + + @property + def DATABASE_URL(self) -> str: + """构建数据库连接URL""" + return f"mysql+pymysql://{self.DB_USER}:{self.DB_PASSWORD}@{self.DB_HOST}:{self.DB_PORT}/{self.DB_NAME}?charset={self.DB_CHARSET}" # === Redis 配置 === REDIS_HOST: str = "localhost" diff --git a/app/database/__init__.py b/app/database/__init__.py new file mode 100644 index 0000000..155de36 --- /dev/null +++ b/app/database/__init__.py @@ -0,0 +1,7 @@ +""" +数据库模块 +""" +from .connection import engine, SessionLocal, get_db +from .base import Base + +__all__ = ["engine", "SessionLocal", "get_db", "Base"] diff --git a/app/database/base.py b/app/database/base.py new file mode 100644 index 0000000..9ada9fb --- /dev/null +++ b/app/database/base.py @@ -0,0 +1,6 @@ +""" +数据库基础模型类 +""" +from sqlalchemy.ext.declarative import declarative_base + +Base = declarative_base() diff --git a/app/database/connection.py b/app/database/connection.py new file mode 100644 index 0000000..f5398f4 --- /dev/null +++ b/app/database/connection.py @@ -0,0 +1,35 @@ +""" +数据库连接管理 +""" +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker +from app.core.config import settings + +# 创建数据库引擎 +engine = create_engine( + settings.DATABASE_URL, + echo=settings.DB_ECHO, + pool_size=settings.DB_POOL_SIZE, + max_overflow=settings.DB_MAX_OVERFLOW, + pool_timeout=settings.DB_POOL_TIMEOUT, + pool_recycle=settings.DB_POOL_RECYCLE, + pool_pre_ping=True, # 连接前测试连接是否有效 +) + +# 创建会话工厂 +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +# 创建基础模型类 +Base = declarative_base() + + +def get_db(): + """ + 获取数据库会话的依赖注入函数 + """ + db = SessionLocal() + try: + yield db + finally: + db.close() diff --git a/app/database/models.py b/app/database/models.py new file mode 100644 index 0000000..b732473 --- /dev/null +++ b/app/database/models.py @@ -0,0 +1,39 @@ +""" +SQLAlchemy 数据库模型 +""" +from sqlalchemy import Column, Integer, String, Float, Boolean, DateTime, Text +from sqlalchemy.sql import func +from .base import Base + + +class User(Base): + """用户表""" + __tablename__ = "users" + + id = Column(Integer, primary_key=True, index=True, autoincrement=True) + username = Column(String(50), unique=True, index=True, nullable=False, comment="用户名") + email = Column(String(100), unique=True, index=True, nullable=False, comment="邮箱") + full_name = Column(String(100), nullable=True, comment="全名") + is_active = Column(Boolean, default=True, nullable=False, comment="是否激活") + created_at = Column(DateTime(timezone=True), server_default=func.now(), comment="创建时间") + updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), comment="更新时间") + + def __repr__(self): + return f"" + + +class Product(Base): + """产品表""" + __tablename__ = "products" + + id = Column(Integer, primary_key=True, index=True, autoincrement=True) + name = Column(String(200), nullable=False, comment="产品名称") + description = Column(Text, nullable=True, comment="产品描述") + price = Column(Float, nullable=False, comment="价格") + stock = Column(Integer, default=0, nullable=False, comment="库存数量") + is_available = Column(Boolean, default=True, nullable=False, comment="是否可用") + created_at = Column(DateTime(timezone=True), server_default=func.now(), comment="创建时间") + updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), comment="更新时间") + + def __repr__(self): + return f"" diff --git a/database_manager.py b/database_manager.py new file mode 100644 index 0000000..c96135a --- /dev/null +++ b/database_manager.py @@ -0,0 +1,139 @@ +""" +数据库管理脚本 +用于初始化数据库、运行迁移、插入测试数据等 +""" +import os +import sys +import asyncio +from sqlalchemy import create_engine, text +from app.core.config import settings +from app.database.models import Base + + +def create_database(): + """创建数据库(如果不存在)""" + # 连接到MySQL服务器(不指定数据库) + server_url = f"mysql+pymysql://{settings.DB_USER}:{settings.DB_PASSWORD}@{settings.DB_HOST}:{settings.DB_PORT}/" + engine = create_engine(server_url) + + with engine.connect() as conn: + # 创建数据库 + conn.execute(text(f"CREATE DATABASE IF NOT EXISTS `{settings.DB_NAME}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci")) + print(f"✅ 数据库 '{settings.DB_NAME}' 创建成功") + + +def create_tables(): + """创建数据库表""" + engine = create_engine(settings.DATABASE_URL) + Base.metadata.create_all(bind=engine) + print("✅ 数据库表创建成功") + + +def run_sql_file(file_path): + """运行SQL文件""" + if not os.path.exists(file_path): + print(f"❌ SQL文件不存在: {file_path}") + return + + engine = create_engine(settings.DATABASE_URL) + + with open(file_path, 'r', encoding='utf-8') as f: + sql_content = f.read() + + with engine.connect() as conn: + # 分割SQL语句并执行 + statements = [stmt.strip() for stmt in sql_content.split(';') if stmt.strip()] + for statement in statements: + if statement: + conn.execute(text(statement)) + conn.commit() + + print(f"✅ SQL文件执行成功: {file_path}") + + +def init_database(): + """初始化数据库""" + print("🚀 开始初始化数据库...") + + # 1. 创建数据库 + create_database() + + # 2. 创建表 + create_tables() + + # 3. 插入示例数据 + sample_data_file = "sql/seeds/sample_data.sql" + if os.path.exists(sample_data_file): + run_sql_file(sample_data_file) + + print("🎉 数据库初始化完成!") + + +def reset_database(): + """重置数据库""" + print("⚠️ 重置数据库...") + + # 删除并重新创建数据库 + server_url = f"mysql+pymysql://{settings.DB_USER}:{settings.DB_PASSWORD}@{settings.DB_HOST}:{settings.DB_PORT}/" + engine = create_engine(server_url) + + with engine.connect() as conn: + conn.execute(text(f"DROP DATABASE IF EXISTS `{settings.DB_NAME}`")) + print(f"🗑️ 数据库 '{settings.DB_NAME}' 已删除") + + # 重新初始化 + init_database() + + +def show_help(): + """显示帮助信息""" + print(""" +数据库管理脚本使用说明: + +python database_manager.py init - 初始化数据库(创建数据库和表) +python database_manager.py reset - 重置数据库(删除并重新创建) +python database_manager.py create-db - 仅创建数据库 +python database_manager.py create-tables - 仅创建表 +python database_manager.py seed - 仅插入示例数据 +python database_manager.py help - 显示此帮助信息 + +环境变量配置: +- DB_HOST: 数据库主机 (默认: localhost) +- DB_PORT: 数据库端口 (默认: 3306) +- DB_USER: 数据库用户名 (默认: root) +- DB_PASSWORD: 数据库密码 (默认: 空) +- DB_NAME: 数据库名称 (默认: fastapi_demo) +""") + + +def main(): + """主函数""" + if len(sys.argv) < 2: + show_help() + return + + command = sys.argv[1].lower() + + try: + if command == "init": + init_database() + elif command == "reset": + reset_database() + elif command == "create-db": + create_database() + elif command == "create-tables": + create_tables() + elif command == "seed": + run_sql_file("sql/seeds/sample_data.sql") + elif command == "help": + show_help() + else: + print(f"❌ 未知命令: {command}") + show_help() + except Exception as e: + print(f"❌ 执行失败: {str(e)}") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/requirements.txt b/requirements.txt index d51f235..c3de0e3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,10 @@ uvicorn[standard]==0.27.0 pydantic==2.8.0 pydantic-settings==2.1.0 python-multipart==0.0.6 -email-validator==2.1.0 \ No newline at end of file +email-validator==2.1.0 + +# 数据库相关依赖 +sqlalchemy>=2.0.30 +alembic>=1.13.1 +pymysql>=1.1.0 +cryptography>=41.0.0 \ No newline at end of file diff --git a/sql/migrations/001_create_tables.sql b/sql/migrations/001_create_tables.sql new file mode 100644 index 0000000..411b6b9 --- /dev/null +++ b/sql/migrations/001_create_tables.sql @@ -0,0 +1,35 @@ +-- 迁移文件:创建基础表结构 +-- 创建时间:2024-01-01 +-- 描述:创建用户表和产品表 + +-- 创建用户表 +CREATE TABLE IF NOT EXISTS `users` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `username` varchar(50) NOT NULL COMMENT '用户名', + `email` varchar(100) NOT NULL COMMENT '邮箱', + `full_name` varchar(100) DEFAULT NULL COMMENT '全名', + `is_active` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否激活', + `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `username` (`username`), + UNIQUE KEY `email` (`email`), + KEY `idx_username` (`username`), + KEY `idx_email` (`email`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表'; + +-- 创建产品表 +CREATE TABLE IF NOT EXISTS `products` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(200) NOT NULL COMMENT '产品名称', + `description` text COMMENT '产品描述', + `price` decimal(10,2) NOT NULL COMMENT '价格', + `stock` int(11) NOT NULL DEFAULT '0' COMMENT '库存数量', + `is_available` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否可用', + `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + KEY `idx_name` (`name`), + KEY `idx_price` (`price`), + KEY `idx_is_available` (`is_available`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='产品表'; diff --git a/sql/schemas/init_database.sql b/sql/schemas/init_database.sql new file mode 100644 index 0000000..08eebc2 --- /dev/null +++ b/sql/schemas/init_database.sql @@ -0,0 +1,39 @@ +-- 创建数据库 +CREATE DATABASE IF NOT EXISTS `fastapi_demo` +DEFAULT CHARACTER SET utf8mb4 +COLLATE utf8mb4_unicode_ci; + +-- 使用数据库 +USE `fastapi_demo`; + +-- 创建用户表 +CREATE TABLE IF NOT EXISTS `users` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `username` varchar(50) NOT NULL COMMENT '用户名', + `email` varchar(100) NOT NULL COMMENT '邮箱', + `full_name` varchar(100) DEFAULT NULL COMMENT '全名', + `is_active` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否激活', + `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `username` (`username`), + UNIQUE KEY `email` (`email`), + KEY `idx_username` (`username`), + KEY `idx_email` (`email`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表'; + +-- 创建产品表 +CREATE TABLE IF NOT EXISTS `products` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(200) NOT NULL COMMENT '产品名称', + `description` text COMMENT '产品描述', + `price` decimal(10,2) NOT NULL COMMENT '价格', + `stock` int(11) NOT NULL DEFAULT '0' COMMENT '库存数量', + `is_available` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否可用', + `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + KEY `idx_name` (`name`), + KEY `idx_price` (`price`), + KEY `idx_is_available` (`is_available`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='产品表'; diff --git a/sql/seeds/sample_data.sql b/sql/seeds/sample_data.sql new file mode 100644 index 0000000..04d8945 --- /dev/null +++ b/sql/seeds/sample_data.sql @@ -0,0 +1,15 @@ +-- 插入示例用户数据 +INSERT INTO `users` (`username`, `email`, `full_name`, `is_active`) VALUES +('admin', 'admin@example.com', '管理员', 1), +('john_doe', 'john@example.com', 'John Doe', 1), +('jane_smith', 'jane@example.com', 'Jane Smith', 1), +('bob_wilson', 'bob@example.com', 'Bob Wilson', 0); + +-- 插入示例产品数据 +INSERT INTO `products` (`name`, `description`, `price`, `stock`, `is_available`) VALUES +('iPhone 15 Pro', '苹果最新旗舰手机,配备A17 Pro芯片', 9999.00, 50, 1), +('MacBook Pro 16"', '专业级笔记本电脑,M3 Max芯片', 19999.00, 20, 1), +('AirPods Pro', '主动降噪无线耳机', 1999.00, 100, 1), +('iPad Air', '轻薄便携的平板电脑', 4399.00, 30, 1), +('Apple Watch Series 9', '智能手表,健康监测', 2999.00, 80, 1), +('Magic Keyboard', '无线键盘,支持多设备连接', 999.00, 0, 0);