Flutter + FastAPI 30天速成计划自用并实践-第6天

Day 6 详细学习计划:后端项目结构优化

学习目标

  • 完善后端项目结构
  • 优化 API 接口组织
  • 准备前端对接

知识点详解

1. 项目结构优化

重要性:

  • 提高代码可维护性
  • 便于团队协作
  • 符合最佳实践

推荐的项目结构:

复制代码
project/
├── main.py              # 应用入口
├── database.py          # 数据库配置
├── models/              # 数据模型
│   ├── __init__.py
│   ├── article.py       # 文章模型
│   └── user.py          # 用户模型(未来扩展)
├── schemas/             # Pydantic 模型
│   ├── __init__.py
│   ├── article.py       # 文章 Schema
│   └── user.py          # 用户 Schema
├── crud/                # 数据库操作
│   ├── __init__.py
│   └── article.py       # 文章 CRUD 操作
├── api/                 # API 路由
│   ├── __init__.py
│   ├── api.py           # API 路由整合
│   └── v1/              # API 版本
│       ├── __init__.py
│       ├── articles.py  # 文章相关路由
│       └── users.py     # 用户相关路由
└── utils/               # 工具函数
    ├── __init__.py
    └── markdown_utils.py # Markdown 处理工具

2. CRUD 模块化

好处:

  • 业务逻辑与路由分离
  • 提高代码复用性
  • 便于单元测试

练习代码

重构后的项目结构

main.py
python 复制代码
from contextlib import asynccontextmanager
from fastapi import FastAPI  # 导入FastAPI框架
from fastapi.middleware.cors import CORSMiddleware  # 导入CORS中间件,用于处理跨域请求
from api.v1.api import api_router
from database import create_db_and_tables  # 导入API路由


@asynccontextmanager
async def lifespan(app: FastAPI):
    # 应用启动时创建数据库表
    create_db_and_tables()
    yield
    # 应用关闭时可以执行清理工作

app = FastAPI(title="Tutorial Site API", version="1.0.0", lifespan=lifespan)  # 创建FastAPI应用实例,设置标题和版本
# 添加 CORS 中间件
app.add_middleware(
    CORSMiddleware,  # 使用CORS中间件
    allow_origins=["*"],  # 允许所有来源的跨域请求
    allow_credentials=True,  # 允许携带凭证信息
    allow_methods=["*"],  # 允许所有HTTP方法
    allow_headers=["*"],  # 允许所有请求头
)

# 包含 API 路由
app.include_router(api_router, prefix="/api/v1")  # 包含API路由,并设置路由前缀为/api/v1

@app.get("/")  # 定义根路径的GET请求处理函数
def read_root():  # 根路径处理函数
    return {"message": "Welcome to Tutorial Site API"}  # 返回欢迎信息
database.py
python 复制代码
from sqlmodel import SQLModel, create_engine, Session
from typing import Generator

DATABASE_URL = "sqlite:///./tutorial.db"

engine = create_engine(DATABASE_URL, echo=True)

def create_db_and_tables():
    SQLModel.metadata.create_all(engine)

def get_session() -> Generator[Session, None, None]:
    with Session(engine) as session:
        yield session
models/article.py
python 复制代码
from sqlmodel import SQLModel, Field
from typing import Optional
from datetime import datetime

class Article(SQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    title: str
    content: str
    author: Optional[str] = None
    published: bool = False
    created_at: Optional[datetime] = None
schemas/article.py
python 复制代码
from sqlmodel import SQLModel
from typing import Optional
from datetime import datetime

class ArticleBase(SQLModel):
    title: str
    content: str
    author: Optional[str] = None
    published: bool = False

class ArticleCreate(ArticleBase):
    pass

class ArticleUpdate(ArticleBase):
    title: Optional[str] = None
    content: Optional[str] = None

class ArticleRead(ArticleBase):
    id: int
    created_at: Optional[datetime] = None
crud/article.py
python 复制代码
from sqlmodel import Session, select
from models.article import Article
from schemas.article import ArticleCreate, ArticleUpdate
from typing import List, Optional

def create_article(session: Session, article_create: ArticleCreate) -> Article:
    db_article = Article.from_orm(article_create)
    session.add(db_article)
    session.commit()
    session.refresh(db_article)
    return db_article

def get_articles(session: Session) -> List[Article]:
    articles = session.exec(select(Article)).all()
    return articles

def get_article_by_id(session: Session, article_id: int) -> Optional[Article]:
    article = session.get(Article, article_id)
    return article

def update_article(session: Session, article_id: int, article_update: ArticleUpdate) -> Optional[Article]:
    article = session.get(Article, article_id)
    if not article:
        return None
    
    article_data = article_update.dict(exclude_unset=True)
    for key, value in article_data.items():
        setattr(article, key, value)
    
    session.add(article)
    session.commit()
    session.refresh(article)
    return article

def delete_article(session: Session, article_id: int) -> bool:
    article = session.get(Article, article_id)
    if not article:
        return False
    
    session.delete(article)
    session.commit()
    return True
api/v1/articles.py
python 复制代码
from fastapi import APIRouter, Depends, HTTPException, status
from sqlmodel import Session
from typing import List
from database import get_session
from crud.article import create_article, get_articles, get_article_by_id, update_article, delete_article
from schemas.article import ArticleCreate, ArticleRead, ArticleUpdate

router = APIRouter(prefix="/articles", tags=["articles"])

@router.post("/", response_model=ArticleRead, status_code=status.HTTP_201_CREATED)
def create_new_article(*, session: Session = Depends(get_session), article: ArticleCreate):
    return create_article(session, article)

@router.get("/", response_model=List[ArticleRead])
def read_all_articles(*, session: Session = Depends(get_session)):
    return get_articles(session)

@router.get("/{article_id}", response_model=ArticleRead)
def read_single_article(*, session: Session = Depends(get_session), article_id: int):
    article = get_article_by_id(session, article_id)
    if not article:
        raise HTTPException(status_code=404, detail="Article not found")
    return article

@router.put("/{article_id}", response_model=ArticleRead)
def update_single_article(
    *, 
    session: Session = Depends(get_session), 
    article_id: int, 
    article_update: ArticleUpdate
):
    article = update_article(session, article_id, article_update)
    if not article:
        raise HTTPException(status_code=404, detail="Article not found")
    return article

@router.delete("/{article_id}")
def delete_single_article(*, session: Session = Depends(get_session), article_id: int):
    success = delete_article(session, article_id)
    if not success:
        raise HTTPException(status_code=404, detail="Article not found")
    return {"ok": True}
api/v1/api.py
python 复制代码
from fastapi import APIRouter
from api.v1.articles import router as articles_router

api_router = APIRouter()
api_router.include_router(articles_router)

易错点及解决方案

1. 循环导入问题

问题:

模块之间相互导入导致 ImportError

解决方案:

  • 重新组织项目结构
  • 使用延迟导入
  • 将共享部分提取到单独文件

2. 相对导入错误

问题:

复制代码
ValueError: attempted relative import with no known parent package

解决方案:

确保正确设置 Python 路径或使用绝对导入

3. 数据库连接问题

问题:

多个地方创建数据库连接导致冲突

解决方案:

统一管理数据库引擎和会话

4. API 路由未正确包含

问题:

API 路由无法访问

解决方案:

检查路由是否正确包含,前缀是否正确设置

今天在main.py中少添加了 lifespan=lifespan

  • app = FastAPI(title="Tutorial Site API", version="1.0.0", lifespan=lifespan)
    导致我找了好半天的问题,无法创建表格,真是恶心坏了。

ai找了半天都没有找到问题在哪里?还给我一顿乱改。

这个是由于版本更新,摒弃旧的方法导致的,正确的main.py前面应该这样写,一定要记得添加
lifespan=lifespan

python 复制代码
from contextlib import asynccontextmanager
from fastapi import FastAPI  # 导入FastAPI框架
from fastapi.middleware.cors import CORSMiddleware  # 导入CORS中间件,用于处理跨域请求
from api.v1.api import api_router
from database import create_db_and_tables  # 导入API路由


@asynccontextmanager
async def lifespan(app: FastAPI):
    # 应用启动时创建数据库表
    create_db_and_tables()
    yield
    # 应用关闭时可以执行清理工作

app = FastAPI(title="Tutorial Site API", version="1.0.0", lifespan=lifespan)  # 创建FastAPI应用实例,设置标题和版本
相关推荐
克喵的水银蛇7 小时前
Flutter 通用弹窗组件:CommonDialog 一键实现自定义弹窗
flutter
解局易否结局7 小时前
Flutter:重塑跨平台开发的生态与实践
flutter
Android_Trot8 小时前
Flutter android 多渠道配置,多包名、icon、等配置。
android·flutter
淡写成灰9 小时前
Flutter PopScope 返回拦截完整指南
flutter
ujainu9 小时前
Flutter与DevEco Studio协同开发:HarmonyOS应用实战指南
flutter·华为·harmonyos
赵财猫._.10 小时前
【Flutter x 鸿蒙】第四篇:双向通信——Flutter调用鸿蒙原生能力
flutter·华为·harmonyos
解局易否结局10 小时前
Flutter:跨平台开发的范式革新与实践之道
flutter
解局易否结局11 小时前
Flutter:跨平台开发的革命与实战指南
flutter