Skip to content

30. FastAPI 划分代码功能 ​

路由接请求,业务层处理规则 ​

“一条函数从头干到尾”在小例子里方便,业务增长后会难以复用和测试。通常可以拆成:Schema 定义输入输出、路由处理 HTTP、Service 执行业务、数据库层读取写入。

下面延续第 29 篇骨架,增加一个暂不依赖数据库的报价功能,重点观察职责边界。

输入输出:app/schemas/quote.py ​

python
from decimal import Decimal
from pydantic import BaseModel, Field

class QuoteIn(BaseModel):
    unit_price: Decimal = Field(gt=0, max_digits=10, decimal_places=2)
    quantity: int = Field(ge=1, le=10000)

class QuoteOut(BaseModel):
    subtotal: Decimal
    discount: Decimal
    payable: Decimal

规则:app/services/quote_service.py ​

python
from decimal import Decimal, ROUND_HALF_UP
from app.schemas.quote import QuoteIn, QuoteOut

def calculate_quote(data: QuoteIn) -> QuoteOut:
    subtotal = data.unit_price * data.quantity
    discount = Decimal("0.10") if data.quantity >= 10 else Decimal("0")
    payable = (subtotal * (1 - discount)).quantize(
        Decimal("0.01"), rounding=ROUND_HALF_UP,
    )
    return QuoteOut(subtotal=subtotal, discount=discount, payable=payable)

此处折扣表示折扣比例,0.10 表示减免 10%。金额舍入规则要显式约定,不能依赖二进制浮点恰好得到两位小数。

HTTP:app/api/quotes.py ​

python
from fastapi import APIRouter
from app.schemas.quote import QuoteIn, QuoteOut
from app.services.quote_service import calculate_quote

router = APIRouter(prefix="/quotes", tags=["报价"])

@router.post("", response_model=QuoteOut)
def quote(data: QuoteIn):
    return calculate_quote(data)

在 app/api/router.py 导入 quotes.router 并 router.include_router(...)。启动后调用 POST /api/v1/quotes,正文 {"unit_price":"100.00","quantity":10},预期应付 900.00。

接入数据库后怎么保持边界 ​

路由通过依赖获得 Session,再调用业务函数。一次下单需要扣库存、建订单、写明细,这些操作应在一个清楚的事务边界内完成,而不是每个底层函数自己提交一次。

可让 Service 管事务,让数据访问函数只执行 SQL;也可由更外层工作单元管理事务,但整个项目要约定一致。避免先查询触发自动事务,再随意进入同一个 Session 的 begin(),否则可能报“事务已开始”。

业务异常可定义为 StockNotEnough 等类型,由路由或统一异常处理映射为 HTTP 状态。Service 若还要供脚本和后台任务调用,就尽量避免依赖 Request、Cookie 或 HTTP 响应对象。

不必过早抽象 ​

只有一个简单查询时可以先直接写在业务函数里;大量重复查询再考虑 Repository。不要为了套结构新增一层只原样转发参数的函数。依赖方向清晰和事务正确比层数更重要。

练习:为数量 9、10 两个边界写报价验证;再加入不可与折扣同时使用的优惠券规则,决定该放字段验证还是业务层。