一,sqlalchemy orm的核心概念

参考这里

二,创建项目目录和虚拟环境

# 1. 创建项目目录
mkdir test_project
cd test_project

# 2. 创建虚拟环境
python -m venv venv

# 3. 激活虚拟环境
# Windows:
venv\Scripts\activate
# Linux/Mac:
source venv/bin/activate

# 4. 创建基础目录结构
mkdir -p app/{api,core,crud,db,models,schemas}
mkdir alembic tests

三,创建 requirements.txt

# requirements.txt

alembic==1.17.2
annotated-doc==0.0.4
annotated-types==0.7.0
anyio==4.12.1
click==8.3.1
colorama==0.4.6
fastapi==0.128.0
greenlet==3.3.0
h11==0.16.0
httptools==0.7.1
idna==3.11
Mako==1.3.10
MarkupSafe==3.0.3
pydantic==2.12.5
pydantic-settings==2.12.0
pydantic_core==2.41.5
python-dotenv==1.2.1
PyYAML==6.0.3
SQLAlchemy==2.0.45
starlette==0.50.0
typing-inspection==0.4.2
typing_extensions==4.15.0
uvicorn==0.40.0
watchfiles==1.1.1
websockets==15.0.1

安装依赖:

pip install -r requirements.txt

四,创建环境配置文件

# .env

DATABASE_URL=sqlite:///./cookies.db
API_V1_STR=/api/v1
PROJECT_NAME=Test FastAPI Project

五,创建 .gitignore 文件

# .gitignore

# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg

# Virtual Environment
venv/
env/
ENV/

# IDE
.vscode/
.idea/
*.swp
*.swo

# Database
*.db
*.sqlite3

# OS
.DS_Store
Thumbs.db

# Logs
*.log

# Alembic
alembic.ini

六,创建核心配置

# app/core/config.py
from typing import Optional
from pydantic_settings import BaseSettings
from pydantic import AnyHttpUrl, PostgresDsn, validator
from typing import List, Union


class Settings(BaseSettings):
    # 基础配置
    API_V1_STR: str = "/api/v1"
    PROJECT_NAME: str = "Test FastAPI Project"
    
    # 数据库配置
    DATABASE_URL: str = "sqlite:///./cookies.db"
    
    # 安全配置
    SECRET_KEY: str = "your-secret-key-change-in-production"
    ALGORITHM: str = "HS256"
    ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
    
    # CORS 配置
    BACKEND_CORS_ORIGINS: List[AnyHttpUrl] = []
    
    @validator("BACKEND_CORS_ORIGINS", pre=True)
    def assemble_cors_origins(cls, v: Union[str, List[str]]) -> Union[List[str], str]:
        if isinstance(v, str) and not v.startswith("["):
            return [i.strip() for i in v.split(",")]
        elif isinstance(v, (list, str)):
            return v
        raise ValueError(v)
    
    # 模型配置
    class Config:
        case_sensitive = True
        env_file = ".env"


settings = Settings()
# app/core/__init__.py
# 空文件,用于标识包

七,创建数据库连接

# app/db/base.py
from sqlalchemy.ext.declarative import declarative_base

# 创建声明性基类
Base = declarative_base()
# app/db/session.py
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, Session
from app.core.config import settings
from app.db.base import Base


# 创建数据库引擎
engine = create_engine(
    settings.DATABASE_URL,
    connect_args={"check_same_thread": False}  # SQLite 需要这个参数
)

# 创建会话工厂
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

# 依赖注入函数
def get_db():
    """
    获取数据库会话的依赖函数
    
    Yields:
        Session: SQLAlchemy 数据库会话
    
    使用示例:
        @app.get("/items")
        def read_items(db: Session = Depends(get_db)):
            return db.query(Item).all()
    """
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()


# 辅助函数:创建所有表
def create_tables():
    """创建数据库中所有定义的表"""
    from app.models.cookie import Cookie  # 导入模型以确保表被注册
    Base.metadata.create_all(bind=engine)
# app/db/__init__.py
# 导出重要的数据库组件
from app.db.base import Base
from app.db.session import engine, SessionLocal, get_db, create_tables

__all__ = ["Base", "engine", "SessionLocal", "get_db", "create_tables"]

八,创建模型

# app/models/cookie.py
from datetime import datetime
from sqlalchemy import Column, Integer, Numeric, String, DateTime, Text
from sqlalchemy.sql import func

from app.db.base import Base


class Cookie(Base):
    """
    Cookie 数据模型
    
    对应数据库中的 cookies 表
    用于存储饼干产品信息
    """
    __tablename__ = "cookies"
    
    # 主键字段
    cookie_id = Column(
        Integer, 
        primary_key=True, 
        index=True,
        comment="饼干ID,主键"
    )
    
    # 基本字段
    cookie_name = Column(
        String(100), 
        nullable=False, 
        index=True,
        comment="饼干名称"
    )
    
    cookie_recipe_url = Column(
        String(500), 
        nullable=True,
        comment="饼干食谱URL"
    )
    
    cookie_sku = Column(
        String(100), 
        nullable=False, 
        unique=True,
        comment="饼干SKU(库存单位),唯一标识"
    )
    
    quantity = Column(
        Integer, 
        nullable=False, 
        default=0,
        comment="库存数量"
    )
    
    unit_cost = Column(
        Numeric(10, 2), 
        nullable=False,
        comment="单位成本(两位小数)"
    )
    
    description = Column(
        Text, 
        nullable=True,
        comment="饼干详细描述"
    )
    
    # 时间戳字段
    created_on = Column(
        DateTime(timezone=True), 
        server_default=func.now(),
        comment="创建时间"
    )
    
    updated_on = Column(
        DateTime(timezone=True), 
        server_default=func.now(), 
        onupdate=func.now(),
        comment="更新时间"
    )
    
    def __repr__(self):
        """对象的字符串表示"""
        return f"<Cookie(id={self.cookie_id}, name='{self.cookie_name}', sku='{self.cookie_sku}')>"
    
    def to_dict(self):
        """将模型转换为字典(便于JSON序列化)"""
        return {
            "cookie_id": self.cookie_id,
            "cookie_name": self.cookie_name,
            "cookie_recipe_url": self.cookie_recipe_url,
            "cookie_sku": self.cookie_sku,
            "quantity": self.quantity,
            "unit_cost": float(self.unit_cost) if self.unit_cost else None,
            "description": self.description,
            "created_on": self.created_on.isoformat() if self.created_on else None,
            "updated_on": self.updated_on.isoformat() if self.updated_on else None,
        }
# app/models/__init__.py
# 导出所有模型
from app.models.cookie import Cookie

__all__ = ["Cookie"]

九,创建 Pydantic 模式

# app/schemas/cookie.py
from typing import Optional
from datetime import datetime
from pydantic import BaseModel, Field, HttpUrl


class CookieBase(BaseModel):
    """
    Cookie 基础模式
    
    包含创建和更新 Cookie 时的共享字段
    """
    cookie_name: str = Field(
        ...,
        min_length=1,
        max_length=100,
        description="饼干名称,必填,1-100字符",
        example="巧克力曲奇"
    )
    
    cookie_recipe_url: Optional[HttpUrl] = Field(
        None,
        description="饼干食谱URL,可选",
        example="https://example.com/recipe/chocolate-cookie"
    )
    
    cookie_sku: str = Field(
        ...,
        min_length=1,
        max_length=100,
        description="饼干SKU,必填,唯一标识,1-100字符",
        example="COOKIE-CHOCO-001"
    )
    
    quantity: int = Field(
        0,
        ge=0,
        description="库存数量,默认为0,必须大于等于0",
        example=100
    )
    
    unit_cost: float = Field(
        ...,
        ge=0,
        description="单位成本,必填,必须大于等于0,两位小数",
        example=2.50
    )
    
    description: Optional[str] = Field(
        None,
        max_length=1000,
        description="饼干描述,可选,最多1000字符",
        example="美味的巧克力曲奇,外酥内软"
    )


class CookieCreate(CookieBase):
    """
    创建 Cookie 请求模式
    
    继承自 CookieBase,用于创建新 Cookie
    """
    pass


class CookieUpdate(BaseModel):
    """
    更新 Cookie 请求模式
    
    用于更新现有 Cookie,所有字段都是可选的
    """
    cookie_name: Optional[str] = Field(
        None,
        min_length=1,
        max_length=100,
        description="饼干名称,可选,1-100字符"
    )
    
    cookie_recipe_url: Optional[HttpUrl] = Field(
        None,
        description="饼干食谱URL,可选"
    )
    
    cookie_sku: Optional[str] = Field(
        None,
        min_length=1,
        max_length=100,
        description="饼干SKU,可选,唯一标识,1-100字符"
    )
    
    quantity: Optional[int] = Field(
        None,
        ge=0,
        description="库存数量,可选,必须大于等于0"
    )
    
    unit_cost: Optional[float] = Field(
        None,
        ge=0,
        description="单位成本,可选,必须大于等于0,两位小数"
    )
    
    description: Optional[str] = Field(
        None,
        max_length=1000,
        description="饼干描述,可选,最多1000字符"
    )


class CookieInDB(CookieBase):
    """
    数据库中的 Cookie 模式
    
    包含从数据库读取的所有字段
    """
    cookie_id: int = Field(..., description="饼干ID")
    created_on: datetime = Field(..., description="创建时间")
    updated_on: datetime = Field(..., description="更新时间")
    
    class Config:
        from_attributes = True  # 支持从ORM对象转换


class CookieResponse(BaseModel):
    """
    Cookie 响应模式
    
    标准的API响应格式
    """
    code: int = Field(default=200, description="状态码")
    msg: str = Field(default="success", description="消息")
    data: CookieInDB = Field(..., description="饼干数据")


# 列表响应相关模式
class CookieListData(BaseModel):
    """Cookie 列表数据模式"""
    items: list[CookieInDB] = Field(..., description="饼干列表")
    total: int = Field(..., description="总记录数")
    page: int = Field(..., description="当前页码")
    page_size: int = Field(..., description="每页数量")
    total_pages: Optional[int] = Field(None, description="总页数")


class CookieListResponse(BaseModel):
    """Cookie 列表响应模式"""
    code: int = Field(default=200, description="状态码")
    msg: str = Field(default="success", description="消息")
    data: CookieListData = Field(..., description="分页数据")
# app/schemas/__init__.py
# 导出所有模式
from app.schemas.cookie import (
    CookieBase, CookieCreate, CookieUpdate, 
    CookieInDB, CookieResponse, CookieListResponse, CookieListData
)

__all__ = [
    "CookieBase", "CookieCreate", "CookieUpdate",
    "CookieInDB", "CookieResponse", "CookieListResponse", "CookieListData"
]

十,创建 CRUD 操作

# app/crud/cookie.py
from typing import Optional, List, Dict, Any
from sqlalchemy.orm import Session
from sqlalchemy import desc, asc

from app.models.cookie import Cookie
from app.schemas.cookie import CookieCreate, CookieUpdate


class CRUDCookie:
    """
    Cookie 的 CRUD 操作类
    
    提供对 cookies 表的增删改查操作
    """
    
    def get(self, db: Session, cookie_id: int) -> Optional[Cookie]:
        """
        根据ID获取单个 Cookie
        
        Args:
            db: 数据库会话
            cookie_id: 饼干ID
        
        Returns:
            Cookie 对象或 None
        """
        return db.query(Cookie).filter(Cookie.cookie_id == cookie_id).first()
    
    def get_by_sku(self, db: Session, sku: str) -> Optional[Cookie]:
        """
        根据SKU获取 Cookie
        
        Args:
            db: 数据库会话
            sku: 饼干SKU
        
        Returns:
            Cookie 对象或 None
        """
        return db.query(Cookie).filter(Cookie.cookie_sku == sku).first()
    
    def get_multi(
        self, 
        db: Session, 
        skip: int = 0, 
        limit: int = 100,
        sort_by: str = "cookie_id",
        sort_order: str = "asc"
    ) -> List[Cookie]:
        """
        获取多个 Cookie(支持分页和排序)
        
        Args:
            db: 数据库会话
            skip: 跳过的记录数
            limit: 返回的最大记录数
            sort_by: 排序字段
            sort_order: 排序顺序(asc 或 desc)
        
        Returns:
            Cookie 对象列表
        """
        # 构建查询
        query = db.query(Cookie)
        
        # 应用排序
        if hasattr(Cookie, sort_by):
            column = getattr(Cookie, sort_by)
            if sort_order.lower() == "desc":
                query = query.order_by(desc(column))
            else:
                query = query.order_by(asc(column))
        else:
            # 默认按ID升序排序
            query = query.order_by(Cookie.cookie_id)
        
        # 应用分页
        return query.offset(skip).limit(limit).all()
    
    def get_multi_by_filter(
        self,
        db: Session,
        skip: int = 0,
        limit: int = 100,
        **filters: Dict[str, Any]
    ) -> List[Cookie]:
        """
        根据过滤条件获取多个 Cookie
        
        Args:
            db: 数据库会话
            skip: 跳过的记录数
            limit: 返回的最大记录数
            **filters: 过滤条件,如 cookie_name="xxx"
        
        Returns:
            过滤后的 Cookie 对象列表
        """
        query = db.query(Cookie)
        
        # 应用过滤条件
        for field, value in filters.items():
            if hasattr(Cookie, field) and value is not None:
                column = getattr(Cookie, field)
                if isinstance(value, str):
                    # 字符串字段使用模糊搜索
                    query = query.filter(column.ilike(f"%{value}%"))
                else:
                    query = query.filter(column == value)
        
        return query.offset(skip).limit(limit).all()
    
    def create(self, db: Session, obj_in: CookieCreate) -> Cookie:
        """
        创建新的 Cookie
        
        Args:
            db: 数据库会话
            obj_in: Cookie 创建数据
        
        Returns:
            创建的 Cookie 对象
        
        Raises:
            ValueError: 如果SKU已存在
        """
        # 检查SKU是否已存在
        existing = self.get_by_sku(db, obj_in.cookie_sku)
        if existing:
            raise ValueError(f"SKU '{obj_in.cookie_sku}' 已存在")
        
        # 创建模型实例
        db_obj = Cookie(
            cookie_name=obj_in.cookie_name,
            cookie_recipe_url=str(obj_in.cookie_recipe_url) if obj_in.cookie_recipe_url else None,
            cookie_sku=obj_in.cookie_sku,
            quantity=obj_in.quantity,
            unit_cost=obj_in.unit_cost,
            description=obj_in.description
        )
        
        # 保存到数据库
        db.add(db_obj)
        db.commit()
        db.refresh(db_obj)
        return db_obj
    
    def update(
        self, 
        db: Session, 
        db_obj: Cookie, 
        obj_in: CookieUpdate
    ) -> Cookie:
        """
        更新现有的 Cookie
        
        Args:
            db: 数据库会话
            db_obj: 要更新的 Cookie 对象
            obj_in: 更新数据
        
        Returns:
            更新后的 Cookie 对象
        """
        update_data = obj_in.model_dump(exclude_unset=True)
        
        # 如果要更新SKU,检查是否重复
        if "cookie_sku" in update_data and update_data["cookie_sku"] != db_obj.cookie_sku:
            existing = self.get_by_sku(db, update_data["cookie_sku"])
            if existing and existing.cookie_id != db_obj.cookie_id:
                raise ValueError(f"SKU '{update_data['cookie_sku']}' 已存在")
        
        # 更新字段
        for field, value in update_data.items():
            if hasattr(db_obj, field):
                if field == "cookie_recipe_url" and value:
                    setattr(db_obj, field, str(value))
                else:
                    setattr(db_obj, field, value)
        
        # 保存更改
        db.add(db_obj)
        db.commit()
        db.refresh(db_obj)
        return db_obj
    
    def remove(self, db: Session, cookie_id: int) -> Cookie:
        """
        删除 Cookie
        
        Args:
            db: 数据库会话
            cookie_id: 要删除的饼干ID
        
        Returns:
            被删除的 Cookie 对象
        
        Raises:
            ValueError: 如果找不到指定的 Cookie
        """
        obj = self.get(db, cookie_id)
        if not obj:
            raise ValueError(f"Cookie ID {cookie_id} 不存在")
        
        db.delete(obj)
        db.commit()
        return obj
    
    def get_total_count(self, db: Session) -> int:
        """
        获取 Cookie 总数
        
        Args:
            db: 数据库会话
        
        Returns:
            总记录数
        """
        return db.query(Cookie).count()


# 创建 CRUDCookie 实例
cookie = CRUDCookie()
# app/crud/__init__.py
# 导出所有 CRUD 实例
from app.crud.cookie import cookie

__all__ = ["cookie"]

十一,创建 API 路由

# app/api/api_v1/api.py
from fastapi import APIRouter

# 导入各个端点的路由
from app.api.api_v1.endpoints import cookies

# 创建 API v1 路由器
api_router = APIRouter()

# 注册各个端点的路由
api_router.include_router(cookies.router, prefix="/cookies", tags=["cookies"])
# app/api/api_v1/endpoints/cookies.py
from typing import Optional, List
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy.orm import Session

from app.db.session import get_db
from app.crud import cookie as crud_cookie
from app.schemas.cookie import (
    CookieCreate, CookieUpdate, CookieResponse, 
    CookieListResponse, CookieInDB
)

# 创建路由器
router = APIRouter()


@router.get("/", response_model=CookieListResponse)
def read_cookies(
    db: Session = Depends(get_db),
    page: int = Query(1, ge=1, description="页码,从1开始"),
    page_size: int = Query(10, ge=1, le=100, description="每页数量,最大100"),
    sort_by: str = Query("cookie_id", description="排序字段"),
    sort_order: str = Query("asc", description="排序顺序,asc或desc"),
    name: Optional[str] = Query(None, description="按名称搜索"),
    sku: Optional[str] = Query(None, description="按SKU搜索"),
):
    """
    获取饼干列表(支持分页、排序和搜索)
    
    - **page**: 页码,默认为1
    - **page_size**: 每页数量,默认为10,最大100
    - **sort_by**: 排序字段,默认为cookie_id
    - **sort_order**: 排序顺序,默认为asc(升序)
    - **name**: 按饼干名称搜索(模糊匹配)
    - **sku**: 按饼干SKU搜索(精确匹配)
    """
    try:
        # 计算跳过的记录数
        skip = (page - 1) * page_size
        
        # 获取数据
        if name or sku:
            # 使用过滤查询
            filters = {}
            if name:
                filters["cookie_name"] = name
            if sku:
                filters["cookie_sku"] = sku
            
            cookies = crud_cookie.get_multi_by_filter(
                db, skip=skip, limit=page_size, **filters
            )
            total = len(cookies)  # 简化处理,实际应查询总数
        else:
            # 使用普通分页查询
            cookies = crud_cookie.get_multi(
                db, skip=skip, limit=page_size, 
                sort_by=sort_by, sort_order=sort_order
            )
            total = crud_cookie.get_total_count(db)
        
        # 计算总页数
        total_pages = (total + page_size - 1) // page_size if page_size > 0 else 0
        
        return CookieListResponse(
            code=200,
            msg="success",
            data={
                "items": cookies,
                "total": total,
                "page": page,
                "page_size": page_size,
                "total_pages": total_pages
            }
        )
        
    except Exception as e:
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=f"获取数据失败: {str(e)}"
        )


@router.get("/{cookie_id}", response_model=CookieResponse)
def read_cookie(
    cookie_id: int, 
    db: Session = Depends(get_db)
):
    """
    根据ID获取单个饼干信息
    
    - **cookie_id**: 饼干ID(路径参数)
    """
    db_cookie = crud_cookie.get(db, cookie_id=cookie_id)
    if db_cookie is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"饼干ID {cookie_id} 不存在"
        )
    
    return CookieResponse(
        code=200,
        msg="success",
        data=db_cookie
    )


@router.post("/", response_model=CookieResponse, status_code=status.HTTP_201_CREATED)
def create_cookie(
    cookie_in: CookieCreate, 
    db: Session = Depends(get_db)
):
    """
    创建新的饼干
    
    - **cookie_name**: 饼干名称(必填)
    - **cookie_recipe_url**: 食谱URL(可选)
    - **cookie_sku**: 饼干SKU(必填,唯一)
    - **quantity**: 库存数量(可选,默认0)
    - **unit_cost**: 单位成本(必填)
    - **description**: 描述(可选)
    """
    try:
        db_cookie = crud_cookie.create(db, obj_in=cookie_in)
        
        return CookieResponse(
            code=201,
            msg="创建成功",
            data=db_cookie
        )
        
    except ValueError as e:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=str(e)
        )
    except Exception as e:
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=f"创建失败: {str(e)}"
        )


@router.put("/{cookie_id}", response_model=CookieResponse)
def update_cookie(
    cookie_id: int, 
    cookie_in: CookieUpdate, 
    db: Session = Depends(get_db)
):
    """
    更新饼干信息
    
    - **cookie_id**: 要更新的饼干ID(路径参数)
    - 其他字段:要更新的数据(所有字段都是可选的)
    """
    db_cookie = crud_cookie.get(db, cookie_id=cookie_id)
    if db_cookie is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"饼干ID {cookie_id} 不存在"
        )
    
    try:
        db_cookie = crud_cookie.update(db, db_obj=db_cookie, obj_in=cookie_in)
        
        return CookieResponse(
            code=200,
            msg="更新成功",
            data=db_cookie
        )
        
    except ValueError as e:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=str(e)
        )
    except Exception as e:
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=f"更新失败: {str(e)}"
        )


@router.delete("/{cookie_id}", response_model=dict)
def delete_cookie(
    cookie_id: int, 
    db: Session = Depends(get_db)
):
    """
    删除饼干
    
    - **cookie_id**: 要删除的饼干ID(路径参数)
    """
    db_cookie = crud_cookie.get(db, cookie_id=cookie_id)
    if db_cookie is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"饼干ID {cookie_id} 不存在"
        )
    
    try:
        crud_cookie.remove(db, cookie_id=cookie_id)
        
        return {
            "code": 200,
            "msg": "删除成功",
            "data": None
        }
        
    except Exception as e:
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=f"删除失败: {str(e)}"
        )


@router.get("/sku/{sku}", response_model=CookieResponse)
def read_cookie_by_sku(
    sku: str, 
    db: Session = Depends(get_db)
):
    """
    根据SKU获取饼干信息
    
    - **sku**: 饼干SKU(路径参数)
    """
    db_cookie = crud_cookie.get_by_sku(db, sku=sku)
    if db_cookie is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"饼干SKU '{sku}' 不存在"
        )
    
    return CookieResponse(
        code=200,
        msg="success",
        data=db_cookie
    )
# app/api/__init__.py
# 空文件,用于标识包
# app/api/api_v1/__init__.py
# 空文件,用于标识包
# app/api/api_v1/endpoints/__init__.py
# 导出所有端点路由
from app.api.api_v1.endpoints.cookies import router as cookies_router

__all__ = ["cookies_router"]

十二,创建 FastAPI 应用主文件

# app/main.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from contextlib import asynccontextmanager

from app.core.config import settings
from app.api.api_v1.api import api_router
from app.db.session import create_tables


@asynccontextmanager
async def lifespan(app: FastAPI):
    """
    应用生命周期管理
    
    - 启动时:创建数据库表
    - 关闭时:清理资源
    """
    # 启动时执行
    print("正在启动应用...")
    create_tables()
    print("数据库表已创建/检查完成")
    
    yield  # 应用运行期间
    
    # 关闭时执行
    print("正在关闭应用...")


# 创建 FastAPI 应用实例
app = FastAPI(
    title=settings.PROJECT_NAME,
    openapi_url=f"{settings.API_V1_STR}/openapi.json",
    version="1.0.0",
    description="""
    ## Cookie 管理 API
    
    一个用于管理饼干产品的 RESTful API,提供完整的 CRUD 操作。
    
    ### 功能特性
    
    - 饼干的增删改查(CRUD)
    - 分页和排序支持
    - 搜索和过滤功能
    - 数据验证和错误处理
    - OpenAPI 文档自动生成
    
    ### 使用说明
    
    1. 访问 `/docs` 查看交互式 API 文档
    2. 使用 `/api/v1/cookies/` 端点管理饼干数据
    3. 所有请求和响应都使用 JSON 格式
    """,
    contact={
        "name": "开发者",
        "email": "developer@example.com",
    },
    license_info={
        "name": "MIT",
        "url": "https://opensource.org/licenses/MIT",
    },
    lifespan=lifespan,
)

# 设置 CORS
if settings.BACKEND_CORS_ORIGINS:
    app.add_middleware(
        CORSMiddleware,
        allow_origins=[str(origin) for origin in settings.BACKEND_CORS_ORIGINS],
        allow_credentials=True,
        allow_methods=["*"],
        allow_headers=["*"],
    )

# 注册 API 路由
app.include_router(api_router, prefix=settings.API_V1_STR)


# 根路由
@app.get("/", tags=["根"])
async def root():
    """
    根端点
    
    返回应用信息和可用端点
    """
    return {
        "app": settings.PROJECT_NAME,
        "version": "1.0.0",
        "docs": "/docs",
        "redoc": "/redoc",
        "api_v1": settings.API_V1_STR,
        "endpoints": {
            "cookies": f"{settings.API_V1_STR}/cookies",
        }
    }


# 健康检查端点
@app.get("/health", tags=["健康检查"])
async def health_check():
    """
    健康检查端点
    
    用于监控应用运行状态
    """
    return {
        "status": "healthy",
        "timestamp": "2024-01-01T00:00:00Z"  # 实际应该使用 datetime.now()
    }


if __name__ == "__main__":
    import uvicorn
    
    # 开发服务器配置
    uvicorn.run(
        "app.main:app",
        host="0.0.0.0",
        port=8000,
        reload=True,
        log_level="info"
    )

十三,创建 Alembic 迁移配置

# 初始化 Alembic
alembic init alembic

修改 alembic.ini:

sqlalchemy.url = sqlite:///./cookies.db

修改 alembic/env.py:

import os
import sys
from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from alembic import context

# 添加项目根目录到 Python 路径
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

# 导入模型
from app.db.base import Base
target_metadata = Base.metadata

# 其余配置保持不变,但在 configure 中添加 SQLite 支持
def run_migrations_online():
    connectable = engine_from_config(
        config.get_section(config.config_ini_section),
        prefix="sqlalchemy.",
        poolclass=pool.NullPool,
    )

    with connectable.connect() as connection:
        context.configure(
            connection=connection,
            target_metadata=target_metadata,
            render_as_batch=True,  # SQLite 需要这个
            compare_type=True,
        )

        with context.begin_transaction():
            context.run_migrations()

创建初始迁移:

alembic revision --autogenerate -m "initial migration"
alembic upgrade head

十四,创建测试数据脚本

# scripts/create_test_data.py
#!/usr/bin/env python3
"""
测试数据生成脚本

用于向数据库中添加测试用的饼干数据
"""
import sys
import os

# 添加项目根目录到 Python 路径
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from sqlalchemy.orm import Session
from app.db.session import SessionLocal
from app.crud import cookie as crud_cookie
from app.schemas.cookie import CookieCreate


def create_test_cookies(db: Session):
    """创建测试饼干数据"""
    test_cookies = [
        CookieCreate(
            cookie_name="巧克力曲奇",
            cookie_recipe_url="https://example.com/recipes/chocolate-chip",
            cookie_sku="COOKIE-CHOCO-001",
            quantity=100,
            unit_cost=2.50,
            description="香浓巧克力曲奇,外酥内软"
        ),
        CookieCreate(
            cookie_name="燕麦葡萄干曲奇",
            cookie_recipe_url="https://example.com/recipes/oatmeal-raisin",
            cookie_sku="COOKIE-OAT-002",
            quantity=75,
            unit_cost=2.00,
            description="健康燕麦与甜葡萄干的完美结合"
        ),
        CookieCreate(
            cookie_name="花生酱曲奇",
            cookie_recipe_url="https://example.com/recipes/peanut-butter",
            cookie_sku="COOKIE-PB-003",
            quantity=120,
            unit_cost=2.25,
            description="浓郁花生酱风味,口感丰富"
        ),
        CookieCreate(
            cookie_name="奶油曲奇",
            cookie_sku="COOKIE-BUTTER-004",
            quantity=90,
            unit_cost=1.80,
            description="经典奶油曲奇,入口即化"
        ),
        CookieCreate(
            cookie_name="抹茶曲奇",
            cookie_recipe_url="https://example.com/recipes/matcha",
            cookie_sku="COOKIE-MATCHA-005",
            quantity=60,
            unit_cost=3.00,
            description="日式抹茶风味,清新不腻"
        ),
    ]
    
    created_count = 0
    for cookie_data in test_cookies:
        try:
            crud_cookie.create(db, obj_in=cookie_data)
            created_count += 1
            print(f"创建饼干: {cookie_data.cookie_name}")
        except ValueError as e:
            print(f"跳过已存在的饼干: {cookie_data.cookie_name} ({str(e)})")
    
    return created_count


def main():
    """主函数"""
    print("开始创建测试数据...")
    
    db = SessionLocal()
    try:
        created = create_test_cookies(db)
        print(f"\n成功创建 {created} 个测试饼干")
        
        # 显示总数
        total = crud_cookie.get_total_count(db)
        print(f"数据库中共有 {total} 个饼干")
        
    except Exception as e:
        print(f"创建测试数据时出错: {str(e)}")
        db.rollback()
    finally:
        db.close()
    
    print("\n测试数据创建完成!")


if __name__ == "__main__":
    main()

十五,项目结构

test_project/
├── alembic/
│   ├── versions/
│   ├── env.py
│   ├── README
│   └── script.py.mako
├── app/
│   ├── api/
│   │   └── api_v1/
│   │       ├── endpoints/
│   │       │   ├── __init__.py
│   │       │   └── cookies.py
│   │       ├── __init__.py
│   │       └── api.py
│   ├── core/
│   │   ├── __init__.py
│   │   └── config.py
│   ├── crud/
│   │   ├── __init__.py
│   │   └── cookie.py
│   ├── db/
│   │   ├── __init__.py
│   │   ├── base.py
│   │   └── session.py
│   ├── models/
│   │   ├── __init__.py
│   │   └── cookie.py
│   ├── schemas/
│   │   ├── __init__.py
│   │   └── cookie.py
│   └── main.py
├── scripts/
│   └── create_test_data.py
├── .env
├── .env.example
├── .gitignore
├── alembic.ini
├── cookies.db
├── requirements.txt
├── README.md
├── run.sh
└── run.bat

十五,总结与问题

(一)app/api/api_v1/endpoints/cookies.py的区别app/crud/cookie.py

✅ 应该放在 endpoints/cookies.py:

  • HTTP 相关的一切:路由定义、请求/响应处理
  • 输入验证:路径参数、查询参数、请求体
  • 错误处理:HTTP 异常、状态码
  • 响应构建:格式化响应数据
  • 认证授权:权限检查
  • 调用 CRUD 层:不直接操作数据库

✅ 应该放在 crud/cookie.py:

  • 数据库操作:所有 SQL 查询
  • ORM 操作:SQLAlchemy 相关代码
  • 事务管理:commit、rollback
  • 查询构建:复杂的查询逻辑
  • 业务逻辑:数据层面的验证和逻辑
  • 返回 ORM 对象:不处理 HTTP 响应

这种分层架构的好处:

  • 关注点分离:API 层只关心 HTTP,CRUD 层只关心数据库
  • 可测试性:可以单独测试 API 逻辑和数据库逻辑
  • 可维护性:代码职责清晰,易于维护和扩展
  • 可重用性:CRUD 层可以被不同的 API 端点复用
Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐