【Web全栈进阶】早报站重构:需求重审、数据模型与API设计
发布时间:2026/10/17 10:38:45 锦皓数字建站

之前前后端各自装修完毕但合体还需要一份契约接口长什么样、返回什么形状、错了怎么报——先定契约再写代码前端后端才能并行。本篇产出早报站v3的需求重审、ER关系确认、全接口契约表以及response_model欠账的正式偿还schemas.py。含代码约90行。 太长不看版给想快速上手的你项目信息一句话说明本篇目标给早报站API定契约、补响应模型、统一错误形状代码行数~90行含注释依赖无新增核心功能/api前缀 response_model 统一错误跑起来的命令uvicorn app.main:app --reload→/docs核心知识点契约先行、读写分离、from_attributes、列表信封做完你能得到一份前后端可并行开发的接口契约一、需求重审回看早报站重构前先考古。盘点早报站API的现状问题#现状问题症状v3的解法①裸ORM对象直接返回响应字段模型字段改模型前端就崩response_model统一出口形状②列表没有总数前端不知道有几页列表统一信封{items, total}③错误格式不统一有时{detail:...}有时裸文本全站统一{detail: ...}④路由无版本无前缀以后加管理端接口会撞车统一挂/api前缀⑤收藏只有“加”没有“查”用户看不到自己收藏了什么补GET /favorites v3非目标第28篇纪律❌ 不做评论 ❌ 不做管理后台 ❌ 不做全文搜索 ——先让契约稳功能稳步加二、数据模型重审ER关系确认四张表的关系建模的成果画成ER图文字版users 1 ──── ∞ rss_sources 一个用户提交多个订阅源 users ∞ ──── ∞ articles 经favorites关联表多对多收藏 favorites: user_id article_id 联合主键 created_at模型层不动models.py已就位——本篇动的是“接口层”给这些数据设计规范的出口。三、API契约全接口清单本篇核心交付物契约表是前后端的“合同”后端照表实现前端照表对接。路由统一挂/api前缀为将来管理端/开放接口留空间方法路径认证入参出参200错误POST/api/auth/register无{username, password}{id, username}400 用户名占用POST/api/auth/login无表单username/password{access_token, refresh_token, token_type}401POST/api/auth/refreshrefresh令牌form: refresh_token{access_token, ...}401GET/api/auth/meJWT—{id, username}401GET/api/articles无?keyword page size{items: [ArticleOut], total}—GET/api/articles/{id}无—ArticleOut404GET/api/favoritesJWT—{items: [ArticleOut], total}401POST/api/favorites/{id}JWT—{article_id, favorited}401 / 404ArticleOut{id, url, title, date, summary}。契约定了就不轻易改要改先改这张表、再改代码、最后通知前端——接口的“宪法”。四、第1步schemas.py——读写分离的响应模型app/schemas.py新文件app/schemas.py —— 接口的入参与出参模型读写分离frompydanticimportBaseModel,ConfigDictclassArticleOut(BaseModel):文章的对外形状比模型少敏感字段、多业务字段model_configConfigDict(from_attributesTrue)# 允许从ORM对象读取id:inturl:strtitle:strdate:str|NoneNonesummary:strclassArticlePage(BaseModel):列表统一信封items totalitems:list[ArticleOut]total:intclassUserOut(BaseModel):model_configConfigDict(from_attributesTrue)id:intusername:strclassRegisterBody(BaseModel):注册入参第06篇的注册接口用username:strpassword:strclassTokenOut(BaseModel):登录出参第06篇的登录接口用access_token:strrefresh_token:strtoken_type:strbearer 三个要点#要点说明①ConfigDict(from_attributesTrue)允许直接从ORM对象构造ArticleOut.model_validate(article_obj)——这就是response_model欠账的第一笔还款②读写分离入参模型如RegisterBody管“进来什么”出参模型ArticleOut管“出去什么”——改内部模型不再影响对外形状前端契约从此稳定③列表信封{items, total}全站列表接口统一形状前端写一次“解信封”逻辑全站复用五、第2步路由改造——挂/api前缀 套响应模型 app/main.pyappFastAPI(title早报站 API,version0.3.0)app.include_router(auth.router,prefix/api)app.include_router(articles.router,prefix/api)app.include_router(favorites.router,prefix/api) app/routers/articles.py改造后fromfastapiimportAPIRouter,Depends,HTTPExceptionfromsqlalchemyimportfunc,selectfromsqlalchemy.ormimportSessionfromapp.depsimportget_sessionfromapp.schemasimportArticleOut,ArticlePagefromcore.modelsimportArticle routerAPIRouter(prefix/articles,tags[articles])router.get(,response_modelArticlePage)deflist_articles(keyword:str,page:int1,size:int20,session:SessionDepends(get_session),):offset(page-1)*size stmtselect(Article).order_by(Article.id.desc())ifkeyword:stmtstmt.where(Article.title.contains(keyword))itemssession.scalars(stmt.offset(offset).limit(size)).all()totalsession.scalar(select(func.count()).select_from(Article))return{items:items,total:total}router.get(/{article_id},response_modelArticleOut)defget_article(article_id:int,session:SessionDepends(get_session)):articlesession.get(Article,article_id)ifarticleisNone:raiseHTTPException(status_code404,detail文章不存在)returnarticleresponse_modelArticlePage的三重作用#作用说明①自动把ORM对象转换成ArticleOut形状不用手动构造字典②过滤掉不该出去的字段比如password_hash这类敏感列③/docs的文档自动更新Schemas区自动列出ArticleOut/ArticlePage你声明形状框架执行转换。 app/routers/favorites.py 补GET接口第12篇预告的收藏查询——本篇正式补上fromsqlalchemyimportselectfromapp.schemasimportArticlePagefromcore.modelsimportArticle,favoritesrouter.get(,response_modelArticlePage)deflist_favorites(session:SessionDepends(get_session),user:UserDepends(get_current_user),):返回当前用户收藏的文章列表stmt(select(Article).join(favorites,favorites.c.article_idArticle.id).where(favorites.c.user_iduser.id).order_by(Article.id.desc()))itemssession.scalars(stmt).all()return{items:items,total:len(items)}favorites.c.article_id的.c是访问Table对象列的语法——关联表在这里正式被查询用到。六、第3步统一错误形状全站错误统一为{detail: ...}。FastAPI的HTTPException默认就是它再补一个全局兜底让“没想到的错”也长一样fromfastapi.responsesimportJSONResponseapp.exception_handler(Exception)defunexpected_error_handler(request,exc:Exception):returnJSONResponse(status_code500,content{detail:服务器内部错误请稍后重试},) 服务端日志里照样记录完整堆栈——对用户说人话、对自己留案底。七、验收清单1. /docs打开 → Schemas区出现ArticleOut / ArticlePage / UserOut / RegisterBody / TokenOut2. GET /api/articles?keyword重构 → 返回{items:[...],total:n}3. GET /api/articles/999 →{detail:文章不存在}4. 带令牌GET /api/favorites → 只返回当前用户收藏的文章5. 前端页面暂时报错 → 正常契约切换的阵痛下一篇前端整体对接6. 全程无报错后提交Gitgitadd.gitcommit-mAPI v0.3/api前缀 响应模型 统一错误八、常见报错这6个response_model的标配重点①fastapi.exceptions.ResponseValidationError: ... Input should be a valid dictionary 原因response_model期望字典/对象但你返回了裸ORM对象且模型没开from_attributes。✅ 解法出参模型加model_config ConfigDict(from_attributesTrue)。② 模型字段与响应对不上某字段永远缺失 原因ArticleOut里写的字段名和ORM属性名不一致或模型里没有该属性。✅ 解法from_attributes按属性名读取——名字必须一字不差需要别名用alias。③ 列表接口返回{items: [...]}但total永远0或报类型错 原因count的写法不对——select(func.count())必须指定from表。✅ 解法select(func.count()).select_from(Article)# 方式一select(func.count(Article.id))# 方式二④ 挂了/api前缀后前端全404 原因前端baseURL还是旧的契约切换的正常阵痛。✅ 解法下一篇前端统一对接本篇验收只看/docs与curl。⑤ 404的detail与你的统一错误形状不一致 原因HTTPException默认形状就是{detail: ...}但你自定义handler时覆盖了它。✅ 解法自定义异常处理器返回同样的{detail: ...}形状——全站错误“一种长相”。⑥ response_model加了新字段响应里却没有 原因ORM对象上没有这个属性from_attributes按属性名取取不到就是None或缺失。✅ 解法确认模型真的有该属性、名字一致新列记得跑alembic迁移第04篇。九、课后练习#练习难度提示1SourceOut给订阅源出参建模id/url/user_idsources接口套上response_model⭐⭐复习from_attributes2统一错误处理器给404/401分别定制响应体保留{detail: ...}形状对比默认页⭐⭐全站错误一种长相3信封泛型研究Pydantic泛型模型Generic[TypeVar]把ArticlePage泛化成Page[T]⭐⭐⭐任何列表都能复用4选做契约评审把契约表发给一位同行或AI请它以“前端开发者”身份挑刺⭐⭐契约要经得起外人挑 配套代码重构后的API工程已上传Gitpython_daily/【gitee仓库地址】
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。