需求列表接口 422 Unprocessable Entity 排查与修复全记录

需求列表接口 422 Unprocessable Entity 排查与修复全记录

环境:Vue 3 + Element Plus + Vite(前端)/ FastAPI + SQLAlchemy(后端)

日期:2026-09-07

一、问题现象

需求列表页面请求报错:

复制代码
GET http://localhost:5173/api/requirements?page=1&page_size=100&keyword=&priority=&status=&project_id=:projectId
422 Unprocessable Entity

后端接口定义:

python 复制代码
def list_requirements(project_id: int, page: int = 1, page_size: int = 50,
                      status: str = "", priority: str = "", keyword: str = "",
                      db: Session = Depends(get_db), _: User = Depends(get_current_user)):

二、排查过程(四层排查)

第 1 层:看请求参数,怀疑占位符未替换

请求里 project_id=:projectId 带着字面量冒号写法,第一反应是前端模板字符串没替换占位符。

第 2 层:看组件代码,排除组件问题

js 复制代码
const projectId = route.params.projectId || route.query.projectId || ''
...
const params = { page: 1, page_size: 100, keyword: '', priority: '', status: '' }
if (projectId) params.project_id = projectId   // 有值才带,axios 会自动丢弃空值

组件层没问题。注意:axios 序列化时会静默丢弃 undefined 参数project_id: undefined 不会出现在 URL 里。

第 3 层:看 api 封装,排除 api 层

js 复制代码
list: (params) => request.get('/api/requirements', { params }),   // 干净

api 层也没问题。

第 4 层:看路由和菜单 ------ 找到根因

关键线索:URL 里出现 :projectId 字面量,说明拼 URL 的代码里写了 :projectId 。组件和 api 层都没有这串字符,那它只可能来自浏览器地址栏本身

路由配置:

js 复制代码
{
  path: 'projects/:projectId/requirements',   // 项目跳转专用路由
  name: 'Requirements',
  component: () => import('@/views/requirement/index.vue'),
  meta: { title: '需求管理', icon: 'Document', hidden: false },   // ← 显示在左侧菜单!
},
{
  path: 'requirements',                        // 全局入口
  name: 'RequirementList',
  ...
  meta: { title: '需求管理', icon: 'Document', hidden: false },
},

根因实锤 :菜单生成逻辑会遍历所有 meta.hidden !== true 的子路由生成菜单项。路由里有两个"需求管理"入口,点击 projects/:projectId/requirements 那个时,Vue Router 把路径段 :projectId 原样解析成参数值

复制代码
点击菜单 → 地址栏 /projects/:projectId/requirements
→ route.params.projectId === ':projectId'
→ 请求带出 project_id=:projectId
→ 后端 int 转换失败 → 422

三、修复方案

前端(2 个文件)

1. router/index.js:项目跳转路由从菜单隐藏,全局入口保留

js 复制代码
// projects/:projectId/requirements 改为 hidden: true(项目页跳转不受影响,路由仍可匹配)
meta: { title: '需求管理', icon: 'Document', hidden: true },

// requirements(全局入口)保持 hidden: false,成为菜单唯一入口

2. views/requirement/index.vue:兼容两种进入方式

js 复制代码
const projectId = route.params.projectId || route.query.projectId || ''

// fetchData:有项目上下文才带 project_id
const params = { page: 1, page_size: 100, keyword: keyword.value, priority: filterPriority.value, status: filterStatus.value }
if (projectId) params.project_id = projectId

返回项目按钮加 v-if="projectId"(从菜单进入不显示)。

后端(1 个文件)

3. routers/requirement.py:project_id 改可选 + 修复 query 初始化 bug

python 复制代码
def list_requirements(project_id: int | None = None, page: int = 1, page_size: int = 50, ...):
    query = db.query(Requirement)              # 无条件初始化(修复前:只在传了 project_id 时才创建 → 不传就 NameError 500)
    if project_id is not None:
        query = query.filter(Requirement.project_id == project_id)

四、知识点总结(踩坑记录)

1. FastAPI 422 两种报错,看 detail 区分根因

detail 报错 含义
"msg": "field required", "type": "value_error.missing" 参数缺失(必填没传)
"msg": "value is not a valid integer", "type": "type_error.integer" 类型错误(传了非数字)

2. FastAPI 参数可选的关键:必须有默认值

写法 必填?
project_id: int 必填,不传 422
`project_id: int None = None`
`project_id: int None`

int | None 只是类型注解;= None 才让参数变可选。想让 FastAPI 参数可选,必须给默认值

3. SQLAlchemy:db.query 无条件初始化

python 复制代码
query = db.query(Requirement)   # 先建"查全表"的查询对象
if project_id is not None:      # 再按需叠加过滤
    query = query.filter(Requirement.project_id == project_id)

模型里的 project_id 列只是表结构声明,不会自动按它过滤db.query 是"查整张表",filter 才是加 WHERE 条件。条件分支里创建的变量,分支不执行时变量不存在(NameError → 500)。

4. Pydantic model_validate:展示用字段必须给默认值

python 复制代码
class RequirementOut(BaseModel):
    project_id: int
    project_name: str = ""   # ❗ 必须带默认值!
    model_config = {"from_attributes": True}

model_validate(orm_obj) 从 ORM 对象的属性 取值,ORM 上只有 project 关系(req.project.name),没有扁平的 project_name 属性。不加默认值 = 必填字段 = ValidationError

补充:RequirementUpdate 里加 project_id 也要用 int | None = None,否则编辑时只改标题不带 project_id 会 422。

5. Pydantic v2 API 对应关系

Pydantic v1 Pydantic v2
parse_obj(x) model_validate(x)
from_orm(x) model_validate(x) + from_attributes: True
instance.dict() instance.model_dump()

6. Vue Router:useRoute vs useRouterparams vs query

  • useRoute():读当前路由状态( );useRouter():执行跳转(
  • route.params:URL 路径 中的动态段(/projects/123/requirementsparams.projectId = "123"),必须路由声明 :xxx
  • route.query:URL 问号后 的查询参数(?projectId=123),无需声明,适合筛选条件
  • 两者值都是字符串

7. Vue 3 其他小知识点

  • @ 是 vite 路径别名(vite.config.jsalias: { '@': path.resolve(__dirname, 'src') }),等价于 src/
  • Object.assign(form, data) 原地修改 reactive 对象(保持响应性);{ ...form } 生成新对象(提交用)
  • route.params.projectId || route.query.projectId || '' 可兼容"路由参数 / query / 都不传"三种进入方式

五、延伸:需求新增/编辑/列表显示项目名称(改造清单)

后端

schemas/requirement.py

python 复制代码
class RequirementOut(BaseModel):
    ...
    project_name: str = ""          # 新增
    ...

class RequirementUpdate(BaseModel):
    project_id: int | None = None   # 新增(编辑可改项目)

routers/requirement.py:列表批量查项目名避免 N+1:

python 复制代码
project_ids = {req.project_id for req in requirements}
project_map = {}
if project_ids:
    project_map = {p.id: p.name for p in db.query(Project).filter(Project.id.in_(project_ids)).all()}
...
item["project_name"] = project_map.get(req.project_id, "")

create / update / get / tree 四个接口同样补 project_name

前端(views/requirement/index.vue

  1. 表格加列:<el-table-column prop="project_name" label="所属项目" width="120" />
  2. 弹窗加下拉(有项目上下文锁定,无则可选):
html 复制代码
<el-form-item label="所属项目" required>
  <el-select v-model="form.project_id" style="width: 100%;" :disabled="!!projectId">
    <el-option v-for="p in projects" :key="p.id" :label="p.name" :value="p.id" />
  </el-select>
</el-form-item>
  1. formproject_idopenDialog 编辑回填、新建默认路由项目;handleSubmit 校验 form.project_id 并提交

六、验证清单

入口 期望
左侧菜单"需求管理" URL /requirements,请求无 project_id,返回全部需求
项目页点项目名/需求按钮 URL /projects/1/requirements,请求带 project_id=1,只返回该项目需求
接口返回 每条需求带 project_name 字段
相关推荐
Json____1 小时前
基于 FastAPI + Vue3 的在线拍卖系统技术解析
spring boot·后端·fastapi·wwwoop.com
创新技术阁1 小时前
FastapiAdmin 实战:二次开发前的准备(环境配置与项目启动)
前端·后端·fastapi
神秘的猪头19 小时前
新版 LangChain Agent 核心架构:State、Context、ToolRuntime 与 Middleware
langchain·llm·fastapi
神秘的猪头19 小时前
新版 LangChain Agent 入门:从 `bind_tools + while` 到 `create_agent`
langchain·fastapi
神秘的猪头19 小时前
把新版 LangChain Agent 做成真正应用:Memory、Streaming、SSE 与 Structured Output
langchain·fastapi
两点王爷19 小时前
Java 与前端加载 MVT 数据:从服务端切片到浏览器渲染
java·前端·状态模式
minhuan21 小时前
搭建本地人脸识别系统:解析FastAPI+InsightFace+FAISS全栈实现原理与优化方案26.6
fastapi·faiss·insightface·大模型应用·人脸识别模型·本地化人脸识别系统
码匠许师傅1 天前
【设计模式精讲】25.状态模式(State)
c++·ui·设计模式·状态模式·uml
tryCbest1 天前
FastAPI中passlib包的作用
python·fastapi·passlib