30. FastAPI 划分代码功能
路由接请求,业务层处理规则
“一条函数从头干到尾”在小例子里方便,业务增长后会难以复用和测试。通常可以拆成:Schema 定义输入输出、路由处理 HTTP、Service 执行业务、数据库层读取写入。
下面延续第 29 篇骨架,增加一个暂不依赖数据库的报价功能,重点观察职责边界。
输入输出:app/schemas/quote.py
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
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
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 两个边界写报价验证;再加入不可与折扣同时使用的优惠券规则,决定该放字段验证还是业务层。