commit 3a5cc50d026b68fc061eb3d10f8fa39a2164e895 Author: chenpanliang <3245129380@qq.com> Date: Mon Dec 15 11:34:24 2025 +0800 feat: 初始化FastAPI项目基础框架 添加项目基础结构,包括: - 核心模块(src/main.py) - 路由模块(users/items) - 数据库配置和模型 - 日志工具 - 测试用例 - 项目文档和依赖配置 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..ce9074f --- /dev/null +++ b/.env.example @@ -0,0 +1,11 @@ +# Database configuration +DATABASE_URL=sqlite:///./test.db + +# API configuration +API_HOST=localhost +API_PORT=8000 + +# Security +SECRET_KEY=your-secret-key-here +ALGORITHM=HS256 +ACCESS_TOKEN_EXPIRE_MINUTES=30 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..83cd9c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,234 @@ +# 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/ +*.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 + +# 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 +# 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 + +# 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 + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Project specific +test.db + +# Additions for better Python project management + +# Environment variables file +.env.local +.env.*.local + +# IDE and editor files +.vscode/settings.json +.vscode/tasks.json +.vscode/launch.json +.idea/ +*.sublime-project +*.sublime-workspace + +# Operating System files +.DS_Store* +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Icon? +Thumbs.db + +# Logs +*.log +logs/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Dependency directories +node_modules/ + +# Distribution / packaging +.py[cod] +*.so +.Python +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +*.manifest +*.spec + +# Coverage reports +htmlcov/ +.coverage +.coverage.* +.cache + +# Jupyter Notebook +.ipynb_checkpoints + +# Database +*.sqlite3 +*.db +*.dump + +# Temporary files +*.tmp +*.temp +~* +*~ + +# Compiled files +*.com +*.class +*.dll +*.exe +*.o +*.so + +# Packages +*.7z +*.dmg +*.gz +*.iso +*.jar +*.rar +*.tar +*.zip \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..ddaf66c --- /dev/null +++ b/README.md @@ -0,0 +1,171 @@ +# FastAPI项目模板 及 快速开发范式 + + +## 1\. 核心原则 + + * **简洁至上**:避免过度设计。优先选择简单直接的方案。 + * **一致性**:团队成员遵循相同的规范,降低沟通和维护成本。 + * **自动化**:利用工具自动完成格式化、检查等重复性工作。 + +----- + +## 2\. 虚拟环境与依赖管理 + +**目标**:隔离项目依赖,保证环境一致性。 + +**规范**: + + * **工具**:使用 Python 内置的 `venv` 模块。 + + * **创建虚拟环境**:在项目根目录执行 `python -m venv venv`。`.gitignore` 文件应包含 `venv/` 目录。 + * **激活环境**: + * macOS/Linux: `source venv/bin/activate` + * Windows: `.\venv\Scripts\activate` + + * **依赖管理**: + + * 使用 `pip` 和 `requirements.txt` 文件。 + * **安装依赖**:`pip install -r requirements.txt` + * **更新依赖文件**:完成开发或添加新包后,运行 `pip freeze > requirements.txt` 更新依赖列表。或手动修改依赖列表。 + * **(可选) 开发依赖**:可以创建一个 `requirements-dev.txt` 文件,存放仅在开发时使用的工具(如 `pytest`, `black`),并将其 `include` 到主 `requirements.txt` 中或分开安装。 + +----- + +## 3\. 项目结构 + +**目标**:清晰、模块化,便于快速定位代码。 + +推荐一个可扩展的结构: + +``` +your_project_name/ +├── src/                      # 主要应用代码 +│   ├── __init__.py +│   ├── main.py               # FastAPI 应用实例、全局配置、根路由 +│   ├── routers/              # 业务逻辑路由 +│   │   ├── __init__.py +│   │   ├── users.py          # 用户相关的路由 +│   │   └── items.py          # 物品相关的路由 +│   ├── services/             # 业务逻辑层 (当业务复杂时,将复杂逻辑拆分到此处) +│   │   ├── __init__.py +│   │   ├── users.py +│   │   └── items.py +│   ├── models/               # 数据模型 (如 Pydantic 模型) +│   │   ├── __init__.py +│   │   └── user.py           # 用户数据模型 +│   ├── db/                   # 数据库相关配置与连接 +│   │   ├── __init__.py +│   │   └── database.py +│   └── utils/                # 常用工具函数 +│       ├── __init__.py +│       └── tools.py +│ +├── tests/                    # 测试代码 +│   ├── __init__.py +│   └── test_users.py         # 用户模块的测试 +│ +├── .gitignore                # Git 忽略文件 +├── README.md                 # 项目说明文档 +├── requirements.txt          # 项目依赖列表 +├── Dockerfile                # Docker 配置文件 +├── .env.example              # 环境变量文件模板 +└── .env                      # 环境变量文件 (不提交到Git) + +``` + +**说明**: + + * `main.py`: 只做最核心的初始化和包含各个 `router`。 + * `.env`: 存放环境变量,如数据库连接信息、密钥等。 + * `.env.example`: 示例环境变量文件,用于创建 `.env` 文件。 + * `Dockerfile`: Dockerfile 文件,用于构建镜像。 + * `requirements.txt`: 项目依赖文件,用于安装依赖。 + * `README.md`: 项目说明文档。 + * `routers`: 路由模块,存放各个接口的实现。 + * `models`: 模型模块,存放数据库模型定义。 + * `utils`: 工具模块,存放工具函数。 + +----- + +#### 4\. 命名规范 + +**目标**:遵循 PEP 8,保持代码风格统一。 + + * **文件和目录**:使用小写蛇形命名法 (snake\_case),例如 `user_router.py`。 + * **变量和函数**:使用小写蛇形命名法,例如 `get_user_by_id`, `db_session`。 + * **类和 Pydantic 模型**:使用大驼峰命名法 (PascalCase),例如 `UserCreate`, `ItemSchema`。 + * **常量**:使用全大写蛇形命名法,例如 `DATABASE_URL`, `API_PREFIX`。 + * **FastAPI 路由函数**:建议以 `动作_资源` 的方式命名,例如 `read_users`, `create_item`。 + +----- + +#### 5\. 编码习惯与最佳实践 + + * **代码格式化**: + + * **强制使用 `black`**。`black` 是一个无妥协的代码格式化工具,能消除所有关于格式的争论。 + * **使用 `isort`** 对 `import` 语句进行排序。 + * 在 CI/CD 或 Git pre-commit hook 中加入自动格式化检查。 + + * **代码质量检查 (Linter)**: + + * 使用 `flake8` 或更现代的 `ruff` 进行代码风格和潜在错误的检查。 + + * **FastAPI 实践**: + + * **善用依赖注入 (`Depends`)**:将数据库会话、用户认证等逻辑通过 `Depends` 注入到路由函数中,实现逻辑复用和解耦。 + * **当router逻辑繁重时,考虑分离功能**:如果路由逻辑过于复杂,考虑将复杂逻辑提取到单独的函数中,放置在service层,并调用。 + * **异步优先**:对于 I/O 密集型操作(如数据库查询、外部 API 请求),始终使用 `async def` 和 `await`。 + * **统一日志输出**:使用 uvcrion 的 `from uvicorn.server import logger` 来捕获特定异常,并返回统一格式的错误响应。例子:`logger.error(f"Error: {e}")` + * **配置管理**:使用 os.getenv("ENV_VAR_NAME", "default_value") 从配置文件和环境变量中加载配置。 + + * **类型提示 (Type Hinting)**: + + * **强制要求**:为所有函数参数和返回值添加类型提示。这是 FastAPI 的核心特性之一,能提供强大的编辑器支持和自动文档生成。 + +----- + +#### 6\. 版本控制 (Git) + + * **分支模型**: + + * `main` (或 `master`) 分支:用于部署的稳定版本。**只接受**来自 `develop` 的合并。 + * `develop` 分支:用于集成各个功能开发的分支。 + * `faet/xxx` 分支:从 `develop` 创建,用于开发新功能。开发完成后合并回 `develop`。 + * `fix/xxx` 分支:用于修复 `main` 分支上的紧急 bug。 + * `dev/xxx` 分支:从 `develop` 创建,用于开发新功能。开发完成后合并回 `develop`。 + + * **Commit Message**: + + * **约定格式**:推荐使用 "Conventional Commits" 规范。 + * `feat: add new user registration endpoint` (新功能) + * `fix: correct password hashing algorithm` (修复 Bug) + * `docs: update README with setup instructions` (文档) + * `style: format code with black` (格式化) + * `refactor: restructure database session handling` (重构) + * `test: add tests for user creation` (测试) + * 这样做的好处是提交历史清晰,便于追溯和自动生成更新日志。 + +----- + +#### 7\. 测试 + + * **框架**:使用 `pytest`。 + * **工具**:结合 FastAPI 的 `TestClient` 和 `HTTPX` 来发送模拟请求。 + * **要求**:核心业务逻辑和重要的 API 端点必须有单元测试或集成测试覆盖。 + * **数据库**:测试时应使用一个独立的测试数据库,并在每次测试前后清理数据。 + +----- + +### 总结 + +这份范式旨在为小型 FastAPI 团队提供一个简单、易于上手的起点。最重要的不是规则本身,而是**团队达成共识并持续遵守**。随着项目和团队的发展,可以随时对这些规范进行调整和补充。 + +**工具链建议**: + + * 虚拟环境: `venv` + * 依赖管理: `pip` + `requirements.txt` + * 代码格式化: `black`, `isort` + * 代码检查: `ruff` (推荐) 或 `flake8` + * 测试: `pytest` + * 版本控制: `Git` \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f4e4b1e --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +fastapi==0.104.1 +uvicorn[standard]==0.24.0 +sqlalchemy==2.0.23 +python-dotenv==1.0.0 +pydantic==2.5.0 +pydantic-settings==2.1.0 \ No newline at end of file diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/db/__init__.py b/src/db/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/db/database.py b/src/db/database.py new file mode 100644 index 0000000..da9ee17 --- /dev/null +++ b/src/db/database.py @@ -0,0 +1,22 @@ +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker +import os +from dotenv import load_dotenv + +load_dotenv() + +DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./test.db") + +engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False}) + +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +Base = declarative_base() + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() \ No newline at end of file diff --git a/src/db/models.py b/src/db/models.py new file mode 100644 index 0000000..2a55c8c --- /dev/null +++ b/src/db/models.py @@ -0,0 +1,58 @@ +""" +数据库模型模块 + +该模块定义了 SQLAlchemy ORM 模型,用于与数据库进行交互。 +当前包含日志表的模型定义。 +""" + +from sqlalchemy import Column, String, Text, DateTime, BIGINT +from sqlalchemy.ext.declarative import declarative_base +from datetime import datetime + +# 创建基类,所有模型都需要继承此类 +Base = declarative_base() + +class Log(Base): + """ + 系统日志数据库模型 + + 该模型定义了日志表的结构,用于存储系统的各种日志信息, + 包括普通日志和异常日志。 + """ + __tablename__ = "logs" # 数据库表名 + + # 日志ID,主键,自动递增 + id = Column(BIGINT, primary_key=True, autoincrement=True, comment="日志ID,主键") + + # 日志时间戳,默认为当前UTC时间 + timestamp = Column(DateTime, default=datetime.utcnow, nullable=False, comment="日志时间戳") + + # 日志级别 (INFO, WARNING, ERROR, DEBUG, CRITICAL) + level = Column(String(20), nullable=False, comment="日志级别 (INFO, WARNING, ERROR, DEBUG, CRITICAL)") + + # 产生日志的模块名 + module = Column(String(100), nullable=False, comment="产生日志的模块") + + # 产生日志的函数名(可选) + function = Column(String(100), nullable=True, comment="产生日志的函数") + + # 日志消息内容 + message = Column(Text, nullable=False, comment="日志消息") + + # 错误堆栈信息(可选,主要用于异常日志) + traceback = Column(Text, nullable=True, comment="错误堆栈信息") + + # 请求URL(可选,用于记录HTTP请求相关信息) + request_url = Column(String(500), nullable=True, comment="请求URL") + + # 请求方法(可选,如 GET, POST 等) + request_method = Column(String(10), nullable=True, comment="请求方法 (GET, POST等)") + + # 用户代理信息(可选) + user_agent = Column(String(500), nullable=True, comment="用户代理") + + # IP地址(可选) + ip_address = Column(String(45), nullable=True, comment="IP地址") + + # 关联的用户ID(可选) + user_id = Column(BIGINT, nullable=True, comment="关联的用户ID") \ No newline at end of file diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..ed29d0a --- /dev/null +++ b/src/main.py @@ -0,0 +1,59 @@ +""" +FastAPI 应用主入口文件 + +该文件负责初始化 FastAPI 应用实例,配置中间件, +注册路由以及定义根路径和健康检查端点。 +""" + +import sys +import os + +# 将项目根目录添加到 Python 路径中,确保可以正确导入项目模块 +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from src.routers import users, items + +# 初始化 FastAPI 应用实例 +app = FastAPI( + title="规范FastApi 开发基础框架", + description="规范的FastApi 开发基础框架", + version="1.0.0" +) + +# 配置 CORS 中间件,允许所有来源、凭证、方法和头部 +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + allow_origin_regex=None, +) + +# 注册路由模块 +# 用户相关路由,前缀为 /users,标签为 users +app.include_router(users.router, prefix="/users", tags=["users"]) +# 物品相关路由,前缀为 /items,标签为 items +app.include_router(items.router, prefix="/items", tags=["items"]) + +# 根路径端点,返回欢迎信息 +@app.get("/") +async def root(): + return {"message": "Welcome to 规范FastApi 开发基础框架"} + +# 健康检查端点,用于检查应用是否正常运行 +@app.get("/health") +async def health_check(): + return {"status": "healthy"} + +# 当直接运行此文件时启动应用服务器 +if __name__ == "__main__": + import uvicorn + uvicorn.run( + "src.main:app", + host="0.0.0.0", + port=8000, + reload=True + ) \ No newline at end of file diff --git a/src/models/__init__.py b/src/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/models/item.py b/src/models/item.py new file mode 100644 index 0000000..86bfb24 --- /dev/null +++ b/src/models/item.py @@ -0,0 +1,36 @@ +""" +物品数据模型模块 + +该模块定义了物品相关的数据模型,使用 Pydantic 进行数据验证和序列化。 +包括基础物品模型、创建物品模型和完整物品模型。 +""" + +from pydantic import BaseModel +from typing import Optional +from datetime import datetime + +class ItemBase(BaseModel): + """ + 物品基础模型,定义了物品的基本信息字段 + """ + name: str # 物品名称,必需字段 + description: Optional[str] = None # 物品描述,可选字段 + price: float # 物品价格,必需字段 + +class ItemCreate(ItemBase): + """ + 物品创建模型,继承自 ItemBase + 当前与 ItemBase 相同,但保留独立的类以便未来扩展 + """ + pass + +class Item(ItemBase): + """ + 完整物品模型,继承自 ItemBase,增加了数据库相关字段 + """ + id: int # 物品唯一标识符 + created_at: datetime = None # 物品创建时间,可选字段 + + class Config: + # 允许从 ORM 模型转换为 Pydantic 模型 + from_attributes = True \ No newline at end of file diff --git a/src/models/user.py b/src/models/user.py new file mode 100644 index 0000000..5c3ce41 --- /dev/null +++ b/src/models/user.py @@ -0,0 +1,35 @@ +""" +用户数据模型模块 + +该模块定义了用户相关的数据模型,使用 Pydantic 进行数据验证和序列化。 +包括基础用户模型、创建用户模型和完整用户模型。 +""" + +from pydantic import BaseModel +from typing import Optional +from datetime import datetime + +class UserBase(BaseModel): + """ + 用户基础模型,定义了用户的基本信息字段 + """ + email: str # 用户邮箱,必需字段 + first_name: str # 用户名字,必需字段 + last_name: str # 用户姓氏,必需字段 + +class UserCreate(UserBase): + """ + 用户创建模型,继承自 UserBase,增加了密码字段 + """ + password: str # 用户密码,必需字段 + +class User(UserBase): + """ + 完整用户模型,继承自 UserBase,增加了数据库相关字段 + """ + id: int # 用户唯一标识符 + created_at: datetime = None # 用户创建时间,可选字段 + + class Config: + # 允许从 ORM 模型转换为 Pydantic 模型 + from_attributes = True \ No newline at end of file diff --git a/src/routers/__init__.py b/src/routers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/routers/items.py b/src/routers/items.py new file mode 100644 index 0000000..74a29fa --- /dev/null +++ b/src/routers/items.py @@ -0,0 +1,123 @@ +""" +物品路由器模块 + +该模块定义了物品相关的 RESTful API 端点, +包括创建、读取、更新和删除物品等功能。 +注意:当前实现使用内存存储,实际应用中应替换为数据库存储。 +""" + +import sys +import os + +# 将项目根目录添加到 Python 路径中,确保可以正确导入项目模块 +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from fastapi import APIRouter, HTTPException +from typing import List +from src.models.item import Item, ItemCreate + +# 创建 API 路由器实例 +router = APIRouter() + +# 模拟数据库存储,实际应用中应使用真实数据库 +items_db = [] + +# 创建物品端点 +# 接收 ItemCreate 模型数据,返回创建的 Item 对象 +@router.post("/", response_model=Item, status_code=201) +async def create_item(item: ItemCreate): + """ + 创建新物品 + + 参数: + - item: ItemCreate 模型,包含物品创建所需信息 + + 返回: + - Item: 创建成功的物品对象 + """ + # 创建新物品对象并添加到数据库 + new_item = Item(id=len(items_db) + 1, **item.dict()) + items_db.append(new_item) + return new_item + +# 根据物品ID获取物品信息端点 +@router.get("/{item_id}", response_model=Item) +async def read_item(item_id: int): + """ + 根据物品ID获取物品信息 + + 参数: + - item_id: 物品ID + + 返回: + - Item: 找到的物品对象 + + 异常: + - HTTPException: 当物品不存在时返回 404 错误 + """ + # 查找指定ID的物品 + for item in items_db: + if item.id == item_id: + return item + raise HTTPException(status_code=404, detail="Item not found") + +# 获取物品列表端点,支持分页 +@router.get("/", response_model=List[Item]) +async def read_items(skip: int = 0, limit: int = 100): + """ + 获取物品列表,支持分页 + + 参数: + - skip: 跳过的记录数,默认为 0 + - limit: 返回的记录数,默认为 100 + + 返回: + - List[Item]: 物品对象列表 + """ + return items_db[skip : skip + limit] + +# 更新物品信息端点 +@router.put("/{item_id}", response_model=Item) +async def update_item(item_id: int, item_update: ItemCreate): + """ + 更新物品信息 + + 参数: + - item_id: 要更新的物品ID + - item_update: ItemCreate 模型,包含更新后的物品信息 + + 返回: + - Item: 更新后的物品对象 + + 异常: + - HTTPException: 当物品不存在时返回 404 错误 + """ + # 查找并更新指定ID的物品 + for index, item in enumerate(items_db): + if item.id == item_id: + updated_item = Item(id=item_id, **item_update.dict()) + items_db[index] = updated_item + return updated_item + raise HTTPException(status_code=404, detail="Item not found") + +# 删除物品端点 +@router.delete("/{item_id}", status_code=204) +async def delete_item(item_id: int): + """ + 删除物品 + + 参数: + - item_id: 要删除的物品ID + + 返回: + - 无内容,成功时返回 204 状态码 + + 异常: + - HTTPException: 当物品不存在时返回 404 错误 + """ + # 查找并删除指定ID的物品 + for index, item in enumerate(items_db): + if item.id == item_id: + items_db.pop(index) + return + raise HTTPException(status_code=404, detail="Item not found") \ No newline at end of file diff --git a/src/routers/users.py b/src/routers/users.py new file mode 100644 index 0000000..c38fb87 --- /dev/null +++ b/src/routers/users.py @@ -0,0 +1,131 @@ +""" +用户路由器模块 + +该模块定义了用户相关的 RESTful API 端点, +包括创建、读取、更新和删除用户等功能。 +注意:当前实现使用内存存储,实际应用中应替换为数据库存储。 +""" + +import sys +import os + +# 将项目根目录添加到 Python 路径中,确保可以正确导入项目模块 +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from fastapi import APIRouter, HTTPException +from typing import List +from src.models.user import User, UserCreate + +# 创建 API 路由器实例 +router = APIRouter() + +# 模拟数据库存储,实际应用中应使用真实数据库 +users_db = [] + +# 创建用户端点 +# 接收 UserCreate 模型数据,返回创建的 User 对象 +@router.post("/", response_model=User, status_code=201) +async def create_user(user: UserCreate): + """ + 创建新用户 + + 参数: + - user: UserCreate 模型,包含用户创建所需信息 + + 返回: + - User: 创建成功的用户对象 + + 异常: + - HTTPException: 当邮箱已被注册时返回 400 错误 + """ + # 检查邮箱是否已存在 + for existing_user in users_db: + if existing_user.email == user.email: + raise HTTPException(status_code=400, detail="Email already registered") + + # 创建新用户对象并添加到数据库 + new_user = User(id=len(users_db) + 1, **user.dict()) + users_db.append(new_user) + return new_user + +# 根据用户ID获取用户信息端点 +@router.get("/{user_id}", response_model=User) +async def read_user(user_id: int): + """ + 根据用户ID获取用户信息 + + 参数: + - user_id: 用户ID + + 返回: + - User: 找到的用户对象 + + 异常: + - HTTPException: 当用户不存在时返回 404 错误 + """ + # 查找指定ID的用户 + for user in users_db: + if user.id == user_id: + return user + raise HTTPException(status_code=404, detail="User not found") + +# 获取用户列表端点,支持分页 +@router.get("/", response_model=List[User]) +async def read_users(skip: int = 0, limit: int = 100): + """ + 获取用户列表,支持分页 + + 参数: + - skip: 跳过的记录数,默认为 0 + - limit: 返回的记录数,默认为 100 + + 返回: + - List[User]: 用户对象列表 + """ + return users_db[skip : skip + limit] + +# 更新用户信息端点 +@router.put("/{user_id}", response_model=User) +async def update_user(user_id: int, user_update: UserCreate): + """ + 更新用户信息 + + 参数: + - user_id: 要更新的用户ID + - user_update: UserCreate 模型,包含更新后的用户信息 + + 返回: + - User: 更新后的用户对象 + + 异常: + - HTTPException: 当用户不存在时返回 404 错误 + """ + # 查找并更新指定ID的用户 + for index, user in enumerate(users_db): + if user.id == user_id: + updated_user = User(id=user_id, **user_update.dict()) + users_db[index] = updated_user + return updated_user + raise HTTPException(status_code=404, detail="User not found") + +# 删除用户端点 +@router.delete("/{user_id}", status_code=204) +async def delete_user(user_id: int): + """ + 删除用户 + + 参数: + - user_id: 要删除的用户ID + + 返回: + - 无内容,成功时返回 204 状态码 + + 异常: + - HTTPException: 当用户不存在时返回 404 错误 + """ + # 查找并删除指定ID的用户 + for index, user in enumerate(users_db): + if user.id == user_id: + users_db.pop(index) + return + raise HTTPException(status_code=404, detail="User not found") \ No newline at end of file diff --git a/src/services/__init__.py b/src/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/utils/__init__.py b/src/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/utils/logger.py b/src/utils/logger.py new file mode 100644 index 0000000..6b30cea --- /dev/null +++ b/src/utils/logger.py @@ -0,0 +1,179 @@ +""" +日志工具模块 + +该模块提供了完整的日志记录功能,包括: +1. 控制台日志输出 +2. 数据库日志存储 +3. 异常信息捕获和记录 +4. 不同日志级别的记录函数 + +日志信息会被同时输出到控制台和存储到数据库中,便于问题排查和系统监控。 +""" + +import sys +import os +import logging +import traceback +from typing import Optional +from datetime import datetime + +# 将项目根目录添加到 Python 路径中,确保可以正确导入项目模块 +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) + +from src.db.database import get_db +from src.db.models import Log + +# 配置基础日志设置 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) + +# 创建模块级日志记录器 +logger = logging.getLogger(__name__) + +def log_to_database( + level: str, + message: str, + module: str, + function: Optional[str] = None, + traceback_info: Optional[str] = None, + request_url: Optional[str] = None, + request_method: Optional[str] = None, + user_agent: Optional[str] = None, + ip_address: Optional[str] = None, + user_id: Optional[int] = None +): + """ + 将日志信息保存到数据库 + + 参数: + - level: 日志级别 (INFO, WARNING, ERROR, DEBUG) + - message: 日志消息内容 + - module: 产生日志的模块名 + - function: 产生日志的函数名(可选) + - traceback_info: 异常堆栈信息(可选) + - request_url: 请求URL(可选) + - request_method: 请求方法(可选) + - user_agent: 用户代理信息(可选) + - ip_address: IP地址(可选) + - user_id: 用户ID(可选) + """ + try: + # 获取数据库会话 + db_generator = get_db() + db = next(db_generator) + + # 创建日志条目对象 + log_entry = Log( + level=level, + message=message, + module=module, + function=function, + traceback=traceback_info, + request_url=request_url, + request_method=request_method, + user_agent=user_agent, + ip_address=ip_address, + user_id=user_id + ) + + # 保存到数据库 + db.add(log_entry) + db.commit() + db.refresh(log_entry) + db.close() + except Exception as e: + # 如果数据库记录失败,至少打印到控制台 + logger.error(f"Failed to log to database: {str(e)}") + +def capture_exception( + exception: Exception, + module: str, + function: Optional[str] = None, + request_url: Optional[str] = None, + request_method: Optional[str] = None, + user_agent: Optional[str] = None, + ip_address: Optional[str] = None, + user_id: Optional[int] = None +): + """ + 捕获并记录异常信息 + + 参数: + - exception: 捕获到的异常对象 + - module: 产生异常的模块名 + - function: 产生异常的函数名(可选) + - request_url: 请求URL(可选) + - request_method: 请求方法(可选) + - user_agent: 用户代理信息(可选) + - ip_address: IP地址(可选) + - user_id: 用户ID(可选) + """ + # 获取异常堆栈信息 + tb_str = ''.join(traceback.format_exception(type(exception), exception, exception.__traceback__)) + + # 记录错误日志到数据库和控制台 + log_to_database( + level="ERROR", + message=str(exception), + module=module, + function=function, + traceback_info=tb_str, + request_url=request_url, + request_method=request_method, + user_agent=user_agent, + ip_address=ip_address, + user_id=user_id + ) + + # 同时打印到控制台 + logger.error(f"[{module}] {str(exception)}", exc_info=True) + +def info(message: str, module: str, function: Optional[str] = None): + """ + 记录INFO级别日志 + + 参数: + - message: 日志消息内容 + - module: 产生日志的模块名 + - function: 产生日志的函数名(可选) + """ + logger.info(f"[{module}] {message}") + log_to_database("INFO", message, module, function) + +def warning(message: str, module: str, function: Optional[str] = None): + """ + 记录WARNING级别日志 + + 参数: + - message: 日志消息内容 + - module: 产生日志的模块名 + - function: 产生日志的函数名(可选) + """ + logger.warning(f"[{module}] {message}") + log_to_database("WARNING", message, module, function) + +def error(message: str, module: str, function: Optional[str] = None): + """ + 记录ERROR级别日志 + + 参数: + - message: 日志消息内容 + - module: 产生日志的模块名 + - function: 产生日志的函数名(可选) + """ + logger.error(f"[{module}] {message}") + log_to_database("ERROR", message, module, function) + +def debug(message: str, module: str, function: Optional[str] = None): + """ + 记录DEBUG级别日志 + + 参数: + - message: 日志消息内容 + - module: 产生日志的模块名 + - function: 产生日志的函数名(可选) + """ + logger.debug(f"[{module}] {message}") + log_to_database("DEBUG", message, module, function) \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_users.py b/tests/test_users.py new file mode 100644 index 0000000..b67cfb3 --- /dev/null +++ b/tests/test_users.py @@ -0,0 +1,92 @@ +import pytest +from fastapi.testclient import TestClient +from src.main import app + +client = TestClient(app) + +def test_create_user(): + response = client.post( + "/users/", + json={ + "email": "test@example.com", + "first_name": "Test", + "last_name": "User", + "password": "testpassword" + } + ) + assert response.status_code == 201 + data = response.json() + assert data["email"] == "test@example.com" + assert "id" in data + +def test_read_users(): + response = client.get("/users/") + assert response.status_code == 200 + data = response.json() + assert isinstance(data, list) + +def test_read_user(): + # First create a user + response = client.post( + "/users/", + json={ + "email": "test2@example.com", + "first_name": "Test2", + "last_name": "User2", + "password": "testpassword" + } + ) + assert response.status_code == 201 + user_id = response.json()["id"] + + # Then read the user + response = client.get(f"/users/{user_id}") + assert response.status_code == 200 + data = response.json() + assert data["id"] == user_id + +def test_update_user(): + # First create a user + response = client.post( + "/users/", + json={ + "email": "test3@example.com", + "first_name": "Test3", + "last_name": "User3", + "password": "testpassword" + } + ) + assert response.status_code == 201 + user_id = response.json()["id"] + + # Then update the user + response = client.put( + f"/users/{user_id}", + json={ + "email": "updated@example.com", + "first_name": "Updated", + "last_name": "User", + "password": "updatedpassword" + } + ) + assert response.status_code == 200 + data = response.json() + assert data["email"] == "updated@example.com" + +def test_delete_user(): + # First create a user + response = client.post( + "/users/", + json={ + "email": "test4@example.com", + "first_name": "Test4", + "last_name": "User4", + "password": "testpassword" + } + ) + assert response.status_code == 201 + user_id = response.json()["id"] + + # Then delete the user + response = client.delete(f"/users/{user_id}") + assert response.status_code == 204 \ No newline at end of file