From 227bade97ca48fcfbcf9f1550b142cc215a8551e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=A8=E7=85=9C?= Date: Fri, 26 Sep 2025 17:54:05 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96=20FastAPI=20?= =?UTF-8?q?=E6=BC=94=E7=A4=BA=E9=A1=B9=E7=9B=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现分层架构:API层、服务层、模式层、模型层 - 应用工厂模式和路由模块化设计 - 完整的配置管理系统,支持多环境配置 - 类似 Spring Boot 的配置文件体系 - 提供便捷启动脚本和详细文档 主要功能: - 用户和产品管理 API - 多环境配置支持 (.env.dev/.env.test/.env.prod) - 配置优先级管理 - 热重载和生产环境支持 - CORS、日志、限流等中间件配置 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .claude/settings.local.json | 13 +++ .env.example | 73 +++++++++++++++ .gitignore | 156 +++++++++++++++++++++++++++++++ CLAUDE.md | 133 ++++++++++++++++++++++++++ app/__init__.py | 0 app/api/__init__.py | 0 app/api/v1/__init__.py | 0 app/api/v1/endpoints/__init__.py | 0 app/api/v1/endpoints/products.py | 20 ++++ app/api/v1/endpoints/users.py | 20 ++++ app/api/v1/router.py | 21 +++++ app/core/__init__.py | 0 app/core/application.py | 104 +++++++++++++++++++++ app/core/config.py | 144 ++++++++++++++++++++++++++++ app/models/__init__.py | 0 app/models/product.py | 15 +++ app/models/user.py | 14 +++ app/schemas/__init__.py | 0 app/schemas/product.py | 25 +++++ app/schemas/user.py | 23 +++++ app/services/__init__.py | 0 app/services/product_service.py | 46 +++++++++ app/services/user_service.py | 45 +++++++++ main.py | 23 +++++ requirements.txt | 6 ++ start.py | 109 +++++++++++++++++++++ 26 files changed, 990 insertions(+) create mode 100644 .claude/settings.local.json create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 app/__init__.py create mode 100644 app/api/__init__.py create mode 100644 app/api/v1/__init__.py create mode 100644 app/api/v1/endpoints/__init__.py create mode 100644 app/api/v1/endpoints/products.py create mode 100644 app/api/v1/endpoints/users.py create mode 100644 app/api/v1/router.py create mode 100644 app/core/__init__.py create mode 100644 app/core/application.py create mode 100644 app/core/config.py create mode 100644 app/models/__init__.py create mode 100644 app/models/product.py create mode 100644 app/models/user.py create mode 100644 app/schemas/__init__.py create mode 100644 app/schemas/product.py create mode 100644 app/schemas/user.py create mode 100644 app/services/__init__.py create mode 100644 app/services/product_service.py create mode 100644 app/services/user_service.py create mode 100644 main.py create mode 100644 requirements.txt create mode 100644 start.py diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..a974f46 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,13 @@ +{ + "permissions": { + "allow": [ + "Bash(python:*)", + "Bash(curl:*)", + "Bash(git init:*)", + "Bash(git remote:*)", + "Bash(git add:*)" + ], + "deny": [], + "ask": [] + } +} \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..e5df0fb --- /dev/null +++ b/.env.example @@ -0,0 +1,73 @@ +# 应用基础配置 +PROJECT_NAME=FastAPI Demo +VERSION=1.0.0 +DESCRIPTION=A simple FastAPI learning project +DEBUG=True +ENVIRONMENT=development + +# 服务器配置 +HOST=0.0.0.0 +PORT=8000 +WORKERS=1 +RELOAD=True +LOG_LEVEL=info + +# API 配置 +API_V1_STR=/api/v1 +DOCS_URL=/docs +REDOC_URL=/redoc + +# 数据库配置 +DATABASE_URL=postgresql://user:password@localhost/dbname +# DATABASE_URL=mysql://user:password@localhost/dbname +# DATABASE_URL=sqlite:///./app.db +DB_ECHO=True +DB_POOL_SIZE=5 +DB_MAX_OVERFLOW=10 + +# Redis 配置 +REDIS_HOST=localhost +REDIS_PORT=6379 +REDIS_DB=0 +REDIS_PASSWORD= + +# CORS 配置 +CORS_ORIGINS=["http://localhost:3000","http://localhost:8080"] +CORS_ALLOW_CREDENTIALS=True +CORS_ALLOW_METHODS=["*"] +CORS_ALLOW_HEADERS=["*"] + +# JWT 认证配置 +SECRET_KEY=your-secret-key-here-change-in-production +ALGORITHM=HS256 +ACCESS_TOKEN_EXPIRE_MINUTES=30 +REFRESH_TOKEN_EXPIRE_DAYS=7 + +# 邮件配置 +SMTP_HOST=smtp.gmail.com +SMTP_PORT=587 +SMTP_USER=your-email@gmail.com +SMTP_PASSWORD=your-password +SMTP_FROM=noreply@example.com + +# 文件上传配置 +UPLOAD_DIR=./uploads +MAX_UPLOAD_SIZE=10485760 # 10MB in bytes +ALLOWED_EXTENSIONS=["jpg","jpeg","png","pdf","doc","docx"] + +# 第三方 API 配置 +OPENAI_API_KEY= +STRIPE_API_KEY= +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_REGION=us-east-1 + +# 监控配置 +SENTRY_DSN= +PROMETHEUS_ENABLED=False +METRICS_PATH=/metrics + +# 限流配置 +RATE_LIMIT_ENABLED=True +RATE_LIMIT_REQUESTS=100 +RATE_LIMIT_PERIOD=60 # seconds \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d5313e4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,156 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +*.manifest +*.spec + +# 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/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +Pipfile.lock + +# PEP 582 +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.env.local +.env.dev +.env.test +.env.prod +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# IDEs +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# FastAPI specific +uploads/ +logs/ +*.db +.coverage +htmlcov/ + +# Database files +*.sqlite +*.sqlite3 +dev.db +test.db \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..ff14ba5 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,133 @@ +# CLAUDE.md + +此文件为 Claude Code (claude.ai/code) 在此代码库中工作时提供指导。 + +## 常用命令 + +### 运行应用程序 +```bash +# 安装依赖 +pip install -r requirements.txt + +# 1. 直接运行(使用 .env 或默认配置) +python main.py + +# 2. 指定环境运行(推荐) +ENVIRONMENT=development python main.py # 使用 .env.dev 配置 +ENVIRONMENT=testing python main.py # 使用 .env.test 配置 +ENVIRONMENT=production python main.py # 使用 .env.prod 配置 + +# 3. Windows 系统设置环境变量 +set ENVIRONMENT=development && python main.py +set ENVIRONMENT=production && python main.py + +# 4. 手动指定 uvicorn 参数(不推荐,应优先使用配置文件) +uvicorn main:app --host 0.0.0.0 --port 8000 --reload + +# 5. 临时覆盖配置(环境变量优先级最高) +PORT=9000 HOST=127.0.0.1 python main.py + +# 6. 使用便捷启动脚本(推荐) +python start.py --env dev # 开发环境 +python start.py --env prod # 生产环境 +python start.py --port 9000 # 指定端口 +python start.py --env dev --reload # 开发环境+强制热重载 +``` + +### 配置文件启动说明 +应用会自动读取配置并使用对应的启动参数: + +- **开发环境** (`.env.dev`):自动启用热重载、调试模式、允许所有CORS +- **测试环境** (`.env.test`):关闭API文档、严格CORS限制 +- **生产环境** (`.env.prod`):多worker、关闭热重载、严格安全设置 + +### 配置管理 +```bash +# 复制环境配置模板 +cp .env.example .env + +# 查看当前配置 +python -c "from app.core.config import settings; print(f'Environment: {settings.ENVIRONMENT}'); print(f'Debug: {settings.DEBUG}')" +``` + +### 测试 API +```bash +# 访问交互式 API 文档 +# 在浏览器中打开 http://localhost:8000/docs (Swagger UI) +# 或 http://localhost:8000/redoc (ReDoc) + +# 健康检查端点 +curl http://localhost:8000/health +``` + +## 架构说明 + +### 项目结构模式 +这个 FastAPI 演示项目采用分层架构和应用工厂模式,具有清晰的关注点分离: + +- **应用工厂** (`app/core/application.py`):使用工厂模式创建和配置应用实例,集中管理中间件、路由注册和事件处理器。 + +- **路由聚合** (`app/api/v1/router.py`):集中管理 v1 版本的所有路由注册,保持主文件精简。 + +- **API 层** (`app/api/v1/endpoints/`):处理 HTTP 请求和响应的 FastAPI 路由器。每个路由器对应一个资源(用户、产品)并定义 API 端点。 + +- **服务层** (`app/services/`):使用静态服务类实现业务逻辑。目前使用内存中的模拟数据库进行演示。服务处理 CRUD 操作和业务规则。 + +- **模式层** (`app/schemas/`):用于请求/响应验证和序列化的 Pydantic 模型。为每个实体遵循 Base、Create、Update 和 Response 模型的模式。 + +- **模型层** (`app/models/`):表示核心实体的领域模型。这些是配置了 `from_attributes = True` 的 Pydantic BaseModel,以兼容 ORM。 + +- **核心配置** (`app/core/config.py`):使用 pydantic-settings 的集中式设置管理,支持多环境配置文件和环境变量覆盖。 + +### 关键架构决策 + +1. **应用工厂模式**:使用 `create_application()` 函数创建应用,便于测试和配置管理,支持多环境部署。 + +2. **路由模块化**:通过 `router.py` 集中管理路由注册,`main.py` 保持精简,只负责应用启动。 + +3. **版本化的 API 路由**:所有端点都以 `/api/v1/` 为前缀,便于 API 版本迭代和兼容性管理。 + +4. **服务模式**:使用单例服务实例(`user_service`、`product_service`)封装所有业务逻辑,保持路由器精简。 + +5. **模式分离**:为不同操作(Create、Update、Response)设置不同的模式,在 API 设计中提供灵活性,而不暴露内部模型细节。 + +6. **内存数据存储**:目前使用内存中的模拟列表进行数据存储。这是有意为演示而设计的 - 实际实现将在服务层中集成数据库。 + +7. **CORS 中间件**:配置了宽松的设置(`allow_origins=["*"]`)适合开发环境。生产环境中应该限制。 + +8. **事件处理器**:在 `application.py` 中注册启动和关闭事件,便于管理应用生命周期(如数据库连接、缓存初始化等)。 + +### 配置管理详情 + +项目采用类似 Spring Boot 的分层配置管理: + +1. **配置文件优先级**(从高到低): + - 环境变量 + - `.env.{environment}` (如 `.env.dev`, `.env.prod`) + - `.env` 基础配置文件 + - 代码中的默认值 + +2. **多环境配置**: + - `.env.dev` - 开发环境:调试开启、允许所有CORS、使用SQLite + - `.env.test` - 测试环境:关闭API文档、严格限流、测试数据库 + - `.env.prod` - 生产环境:关闭调试、使用PostgreSQL、启用监控 + +3. **配置特性**: + - 类型验证和转换 + - 环境变量解析(如CORS_ORIGINS支持逗号分隔) + - 配置验证器(如环境名称校验) + - 便利属性(如 `settings.is_development`) + +4. **敏感信息管理**: + - 密钥、密码等敏感信息通过环境变量管理 + - `.env` 文件已添加到 `.gitignore` + - 提供 `.env.example` 作为配置模板 + +### 添加新功能 + +添加新资源/实体时: +1. 在 `app/models/` 中创建模型 +2. 在 `app/schemas/` 中创建模式(Base、Create、Update、Response) +3. 在 `app/services/` 中创建包含 CRUD 操作的服务类 +4. 在 `app/api/v1/endpoints/` 中创建包含端点的路由器 +5. 在 `app/api/v1/router.py` 中注册新的路由器,设置适当的前缀和标签 \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/api/__init__.py b/app/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/api/v1/__init__.py b/app/api/v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/api/v1/endpoints/__init__.py b/app/api/v1/endpoints/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/api/v1/endpoints/products.py b/app/api/v1/endpoints/products.py new file mode 100644 index 0000000..3de4699 --- /dev/null +++ b/app/api/v1/endpoints/products.py @@ -0,0 +1,20 @@ +from fastapi import APIRouter, HTTPException +from typing import List +from app.schemas.product import ProductResponse, ProductCreate +from app.services.product_service import product_service + +router = APIRouter() + +@router.get("/", response_model=List[ProductResponse]) +async def get_products(): + """获取所有商品列表""" + products = product_service.get_all_products() + return products + +@router.get("/{product_id}", response_model=ProductResponse) +async def get_product(product_id: int): + """根据ID获取单个商品""" + product = product_service.get_product_by_id(product_id) + if not product: + raise HTTPException(status_code=404, detail="Product not found") + return product \ No newline at end of file diff --git a/app/api/v1/endpoints/users.py b/app/api/v1/endpoints/users.py new file mode 100644 index 0000000..816c65c --- /dev/null +++ b/app/api/v1/endpoints/users.py @@ -0,0 +1,20 @@ +from fastapi import APIRouter, HTTPException +from typing import List +from app.schemas.user import UserResponse, UserCreate +from app.services.user_service import user_service + +router = APIRouter() + +@router.get("/", response_model=List[UserResponse]) +async def get_users(): + """获取所有用户列表""" + users = user_service.get_all_users() + return users + +@router.get("/{user_id}", response_model=UserResponse) +async def get_user(user_id: int): + """根据ID获取单个用户""" + user = user_service.get_user_by_id(user_id) + if not user: + raise HTTPException(status_code=404, detail="User not found") + return user \ No newline at end of file diff --git a/app/api/v1/router.py b/app/api/v1/router.py new file mode 100644 index 0000000..a4f54d6 --- /dev/null +++ b/app/api/v1/router.py @@ -0,0 +1,21 @@ +""" +API v1 路由聚合 +""" +from fastapi import APIRouter +from app.api.v1.endpoints import users, products + +# 创建 v1 版本的主路由 +api_router = APIRouter() + +# 注册所有端点路由 +api_router.include_router( + users.router, + prefix="/users", + tags=["用户管理"] +) + +api_router.include_router( + products.router, + prefix="/products", + tags=["产品管理"] +) \ No newline at end of file diff --git a/app/core/__init__.py b/app/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/core/application.py b/app/core/application.py new file mode 100644 index 0000000..67d73cb --- /dev/null +++ b/app/core/application.py @@ -0,0 +1,104 @@ +""" +应用工厂模式 - 创建和配置 FastAPI 应用实例 +""" +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from app.core.config import settings + + +def create_application() -> FastAPI: + """ + 创建 FastAPI 应用实例 + + Returns: + FastAPI: 配置完成的应用实例 + """ + app = FastAPI( + title=settings.PROJECT_NAME, + version=settings.VERSION, + description=settings.DESCRIPTION, + debug=settings.DEBUG, + docs_url=settings.DOCS_URL, + redoc_url=settings.REDOC_URL + ) + + # 设置中间件 + setup_middleware(app) + + # 注册路由 + register_routers(app) + + # 注册事件处理器 + register_events(app) + + return app + + +def setup_middleware(app: FastAPI) -> None: + """ + 配置应用中间件 + + Args: + app: FastAPI 应用实例 + """ + # CORS 中间件配置 + app.add_middleware( + CORSMiddleware, + allow_origins=settings.CORS_ORIGINS, + allow_credentials=settings.CORS_ALLOW_CREDENTIALS, + allow_methods=settings.CORS_ALLOW_METHODS, + allow_headers=settings.CORS_ALLOW_HEADERS, + ) + + # 可以在这里添加其他中间件 + # app.add_middleware(...) + + +def register_routers(app: FastAPI) -> None: + """ + 注册所有路由 + + Args: + app: FastAPI 应用实例 + """ + from app.api.v1.router import api_router + + # 注册 API v1 路由 + app.include_router(api_router, prefix="/api/v1") + + # 根路径路由 + @app.get("/") + async def root(): + return { + "message": "Welcome to FastAPI Demo", + "version": settings.VERSION, + "docs": "/docs", + "redoc": "/redoc" + } + + # 健康检查路由 + @app.get("/health") + async def health_check(): + return {"status": "healthy", "service": settings.PROJECT_NAME} + + +def register_events(app: FastAPI) -> None: + """ + 注册应用事件处理器 + + Args: + app: FastAPI 应用实例 + """ + @app.on_event("startup") + async def startup_event(): + """应用启动时执行""" + print(f"[INFO] {settings.PROJECT_NAME} v{settings.VERSION} is starting up...") + # 可以在这里添加启动时的初始化操作 + # 如:数据库连接、缓存初始化等 + + @app.on_event("shutdown") + async def shutdown_event(): + """应用关闭时执行""" + print(f"[INFO] {settings.PROJECT_NAME} is shutting down...") + # 可以在这里添加关闭时的清理操作 + # 如:关闭数据库连接、保存缓存等 \ No newline at end of file diff --git a/app/core/config.py b/app/core/config.py new file mode 100644 index 0000000..bbb462e --- /dev/null +++ b/app/core/config.py @@ -0,0 +1,144 @@ +""" +应用配置管理 +支持从环境变量和 .env 文件加载配置 +""" +from typing import List, Optional +from pydantic import Field, validator +from pydantic_settings import BaseSettings + + +class Settings(BaseSettings): + """应用配置类""" + + # === 应用基础配置 === + PROJECT_NAME: str = "FastAPI Demo" + VERSION: str = "1.0.0" + DESCRIPTION: str = "A simple FastAPI learning project" + DEBUG: bool = False + ENVIRONMENT: str = Field(default="production", description="运行环境: development, testing, production") + + # === 服务器配置 === + HOST: str = "127.0.0.1" + PORT: int = 8000 + WORKERS: int = 1 + RELOAD: bool = False + LOG_LEVEL: str = "info" + + # === API 配置 === + API_V1_STR: str = "/api/v1" + DOCS_URL: Optional[str] = "/docs" + REDOC_URL: Optional[str] = "/redoc" + + # === 数据库配置 === + DATABASE_URL: Optional[str] = None + DB_ECHO: bool = False + DB_POOL_SIZE: int = 5 + DB_MAX_OVERFLOW: int = 10 + + # === Redis 配置 === + REDIS_HOST: str = "localhost" + REDIS_PORT: int = 6379 + REDIS_DB: int = 0 + REDIS_PASSWORD: Optional[str] = None + + # === CORS 配置 === + CORS_ORIGINS: List[str] = ["*"] + CORS_ALLOW_CREDENTIALS: bool = True + CORS_ALLOW_METHODS: List[str] = ["*"] + CORS_ALLOW_HEADERS: List[str] = ["*"] + + # === JWT 认证配置 === + SECRET_KEY: str = Field(default="your-secret-key-change-in-production", min_length=32) + ALGORITHM: str = "HS256" + ACCESS_TOKEN_EXPIRE_MINUTES: int = 30 + REFRESH_TOKEN_EXPIRE_DAYS: int = 7 + + # === 邮件配置 === + SMTP_HOST: Optional[str] = None + SMTP_PORT: int = 587 + SMTP_USER: Optional[str] = None + SMTP_PASSWORD: Optional[str] = None + SMTP_FROM: Optional[str] = None + + # === 文件上传配置 === + UPLOAD_DIR: str = "./uploads" + MAX_UPLOAD_SIZE: int = 10485760 # 10MB + ALLOWED_EXTENSIONS: List[str] = ["jpg", "jpeg", "png", "pdf", "doc", "docx"] + + # === 第三方 API 配置 === + OPENAI_API_KEY: Optional[str] = None + STRIPE_API_KEY: Optional[str] = None + AWS_ACCESS_KEY_ID: Optional[str] = None + AWS_SECRET_ACCESS_KEY: Optional[str] = None + AWS_REGION: str = "us-east-1" + + # === 监控配置 === + SENTRY_DSN: Optional[str] = None + PROMETHEUS_ENABLED: bool = False + METRICS_PATH: str = "/metrics" + + # === 限流配置 === + RATE_LIMIT_ENABLED: bool = True + RATE_LIMIT_REQUESTS: int = 100 + RATE_LIMIT_PERIOD: int = 60 # seconds + + @validator("ENVIRONMENT") + def validate_environment(cls, v): + """验证运行环境""" + allowed = ["development", "testing", "production"] + if v not in allowed: + raise ValueError(f"ENVIRONMENT must be one of {allowed}") + return v + + @validator("CORS_ORIGINS", pre=True) + def parse_cors_origins(cls, v): + """解析 CORS 来源配置""" + if isinstance(v, str): + return [origin.strip() for origin in v.split(",")] + return v + + @property + def is_development(self) -> bool: + """是否为开发环境""" + return self.ENVIRONMENT == "development" + + @property + def is_production(self) -> bool: + """是否为生产环境""" + return self.ENVIRONMENT == "production" + + @property + def is_testing(self) -> bool: + """是否为测试环境""" + return self.ENVIRONMENT == "testing" + + class Config: + env_file = ".env" + env_file_encoding = "utf-8" + case_sensitive = True + + +# 根据环境加载不同的配置文件 +def get_settings() -> Settings: + """获取配置实例""" + import os + env = os.getenv("ENVIRONMENT", "development") + + env_files = [".env"] + if env == "development": + env_files.append(".env.dev") + elif env == "testing": + env_files.append(".env.test") + elif env == "production": + env_files.append(".env.prod") + + # 尝试加载对应的环境配置文件 + for env_file in reversed(env_files): + if os.path.exists(env_file): + return Settings(_env_file=env_file) + + return Settings() + + +# 全局配置实例 +settings = get_settings() \ No newline at end of file diff --git a/app/models/__init__.py b/app/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/models/product.py b/app/models/product.py new file mode 100644 index 0000000..b76b84f --- /dev/null +++ b/app/models/product.py @@ -0,0 +1,15 @@ +from pydantic import BaseModel +from typing import Optional +from datetime import datetime + +class Product(BaseModel): + id: int + name: str + description: Optional[str] = None + price: float + stock: int = 0 + is_available: bool = True + created_at: datetime = datetime.now() + + class Config: + from_attributes = True \ No newline at end of file diff --git a/app/models/user.py b/app/models/user.py new file mode 100644 index 0000000..9681e0b --- /dev/null +++ b/app/models/user.py @@ -0,0 +1,14 @@ +from pydantic import BaseModel +from typing import Optional +from datetime import datetime + +class User(BaseModel): + id: int + username: str + email: str + full_name: Optional[str] = None + is_active: bool = True + created_at: datetime = datetime.now() + + class Config: + from_attributes = True \ No newline at end of file diff --git a/app/schemas/__init__.py b/app/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/schemas/product.py b/app/schemas/product.py new file mode 100644 index 0000000..8c9384b --- /dev/null +++ b/app/schemas/product.py @@ -0,0 +1,25 @@ +from pydantic import BaseModel +from typing import Optional + +class ProductBase(BaseModel): + name: str + description: Optional[str] = None + price: float + stock: int = 0 + +class ProductCreate(ProductBase): + pass + +class ProductUpdate(BaseModel): + name: Optional[str] = None + description: Optional[str] = None + price: Optional[float] = None + stock: Optional[int] = None + is_available: Optional[bool] = None + +class ProductResponse(ProductBase): + id: int + is_available: bool + + class Config: + from_attributes = True \ No newline at end of file diff --git a/app/schemas/user.py b/app/schemas/user.py new file mode 100644 index 0000000..6e10532 --- /dev/null +++ b/app/schemas/user.py @@ -0,0 +1,23 @@ +from pydantic import BaseModel, EmailStr +from typing import Optional + +class UserBase(BaseModel): + username: str + email: EmailStr + full_name: Optional[str] = None + +class UserCreate(UserBase): + pass + +class UserUpdate(BaseModel): + username: Optional[str] = None + email: Optional[EmailStr] = None + full_name: Optional[str] = None + is_active: Optional[bool] = None + +class UserResponse(UserBase): + id: int + is_active: bool + + class Config: + from_attributes = True \ No newline at end of file diff --git a/app/services/__init__.py b/app/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/services/product_service.py b/app/services/product_service.py new file mode 100644 index 0000000..3331955 --- /dev/null +++ b/app/services/product_service.py @@ -0,0 +1,46 @@ +from typing import List, Optional +from app.models.product import Product +from app.schemas.product import ProductCreate, ProductUpdate + +fake_products_db = [ + Product(id=1, name="Laptop", description="High performance laptop", price=999.99, stock=10), + Product(id=2, name="Mouse", description="Wireless mouse", price=29.99, stock=50), + Product(id=3, name="Keyboard", description="Mechanical keyboard", price=89.99, stock=25), +] + +class ProductService: + @staticmethod + def get_all_products() -> List[Product]: + return fake_products_db + + @staticmethod + def get_product_by_id(product_id: int) -> Optional[Product]: + for product in fake_products_db: + if product.id == product_id: + return product + return None + + @staticmethod + def create_product(product_data: ProductCreate) -> Product: + new_id = max([p.id for p in fake_products_db]) + 1 if fake_products_db else 1 + new_product = Product( + id=new_id, + name=product_data.name, + description=product_data.description, + price=product_data.price, + stock=product_data.stock + ) + fake_products_db.append(new_product) + return new_product + + @staticmethod + def update_product(product_id: int, product_data: ProductUpdate) -> Optional[Product]: + for i, product in enumerate(fake_products_db): + if product.id == product_id: + update_data = product_data.model_dump(exclude_unset=True) + for field, value in update_data.items(): + setattr(product, field, value) + return product + return None + +product_service = ProductService() \ No newline at end of file diff --git a/app/services/user_service.py b/app/services/user_service.py new file mode 100644 index 0000000..d9362ca --- /dev/null +++ b/app/services/user_service.py @@ -0,0 +1,45 @@ +from typing import List, Optional +from app.models.user import User +from app.schemas.user import UserCreate, UserUpdate + +fake_users_db = [ + User(id=1, username="admin", email="admin@example.com", full_name="Admin User"), + User(id=2, username="john", email="john@example.com", full_name="John Doe"), + User(id=3, username="jane", email="jane@example.com", full_name="Jane Smith"), +] + +class UserService: + @staticmethod + def get_all_users() -> List[User]: + return fake_users_db + + @staticmethod + def get_user_by_id(user_id: int) -> Optional[User]: + for user in fake_users_db: + if user.id == user_id: + return user + return None + + @staticmethod + def create_user(user_data: UserCreate) -> User: + new_id = max([u.id for u in fake_users_db]) + 1 if fake_users_db else 1 + new_user = User( + id=new_id, + username=user_data.username, + email=user_data.email, + full_name=user_data.full_name + ) + fake_users_db.append(new_user) + return new_user + + @staticmethod + def update_user(user_id: int, user_data: UserUpdate) -> Optional[User]: + for i, user in enumerate(fake_users_db): + if user.id == user_id: + update_data = user_data.model_dump(exclude_unset=True) + for field, value in update_data.items(): + setattr(user, field, value) + return user + return None + +user_service = UserService() \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..aa2c545 --- /dev/null +++ b/main.py @@ -0,0 +1,23 @@ +""" +FastAPI 应用主入口 +""" +from app.core.application import create_application +from app.core.config import settings + +# 创建应用实例 +app = create_application() + +if __name__ == "__main__": + import uvicorn + + # 根据环境决定是否使用字符串形式的 app(支持热重载) + app_str = "main:app" if settings.RELOAD else app + + uvicorn.run( + app_str, + host=settings.HOST, + port=settings.PORT, + reload=settings.RELOAD, + log_level=settings.LOG_LEVEL.lower(), + workers=1 if settings.RELOAD else settings.WORKERS, # 热重载模式下只能用1个worker + ) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b3265a0 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +fastapi==0.109.0 +uvicorn[standard]==0.27.0 +pydantic==2.5.3 +pydantic-settings==2.1.0 +python-multipart==0.0.6 +email-validator==2.1.0 \ No newline at end of file diff --git a/start.py b/start.py new file mode 100644 index 0000000..76a4406 --- /dev/null +++ b/start.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +""" +便捷启动脚本 - 支持命令行参数指定环境 +用法: + python start.py # 使用默认配置 + python start.py --env dev # 开发环境 + python start.py --env test # 测试环境 + python start.py --env prod # 生产环境 + python start.py --port 9000 # 临时指定端口 +""" +import argparse +import os +import sys +from pathlib import Path + +def main(): + parser = argparse.ArgumentParser(description="启动 FastAPI 应用") + parser.add_argument( + "--env", + choices=["dev", "test", "prod"], + help="指定运行环境 (dev/test/prod)" + ) + parser.add_argument( + "--host", + help="指定主机地址" + ) + parser.add_argument( + "--port", + type=int, + help="指定端口号" + ) + parser.add_argument( + "--workers", + type=int, + help="指定 worker 数量(生产环境)" + ) + parser.add_argument( + "--reload", + action="store_true", + help="启用热重载" + ) + parser.add_argument( + "--log-level", + choices=["critical", "error", "warning", "info", "debug"], + help="日志级别" + ) + + args = parser.parse_args() + + # 设置环境变量 + if args.env: + env_map = {"dev": "development", "test": "testing", "prod": "production"} + os.environ["ENVIRONMENT"] = env_map[args.env] + print(f"[INFO] 启动环境: {env_map[args.env]} (配置文件: .env.{args.env})") + + # 临时覆盖配置 + if args.host: + os.environ["HOST"] = args.host + if args.port: + os.environ["PORT"] = str(args.port) + if args.workers: + os.environ["WORKERS"] = str(args.workers) + if args.reload: + os.environ["RELOAD"] = "True" + if args.log_level: + os.environ["LOG_LEVEL"] = args.log_level + + # 导入并启动应用 + try: + from main import app + from app.core.config import settings + import uvicorn + + print(f"[CONFIG] 当前配置:") + print(f" - 环境: {settings.ENVIRONMENT}") + print(f" - 主机: {settings.HOST}") + print(f" - 端口: {settings.PORT}") + print(f" - 调试: {settings.DEBUG}") + print(f" - 热重载: {settings.RELOAD}") + print(f" - Workers: {settings.WORKERS}") + print(f" - 日志级别: {settings.LOG_LEVEL}") + + if settings.DOCS_URL: + print(f"[DOCS] API 文档: http://{settings.HOST}:{settings.PORT}{settings.DOCS_URL}") + + print("-" * 50) + + # 根据环境决定是否使用字符串形式的 app(支持热重载) + app_str = "main:app" if settings.RELOAD else app + + uvicorn.run( + app_str, + host=settings.HOST, + port=settings.PORT, + reload=settings.RELOAD, + log_level=settings.LOG_LEVEL.lower(), + workers=1 if settings.RELOAD else settings.WORKERS, + ) + + except ImportError as e: + print(f"[ERROR] 导入错误: {e}") + print("请确保在正确的目录中运行此脚本") + sys.exit(1) + except Exception as e: + print(f"[ERROR] 启动失败: {e}") + sys.exit(1) + +if __name__ == "__main__": + main() \ No newline at end of file