- Add archives/ directory for immutable external skill snapshots - Strict immutability rule: only README/CHANGELOG can be modified - First import: dev-pipeline-universal v1.0.0 (Hermes Agent) - Import dev-pipeline-universal into hermes-agent/skills/ with CHANGELOG - Update hermes-agent/publish/registry.json with imported skill - Rewrite meta-skills/create-hermes-agent-skill/ with full spec: - SKILL.md frontmatter fields and validator constraints - Recommended body structure (Overview → When to Use → Pitfalls → Verification) - Progressive disclosure (3-level loading) - 8 writing quality principles - Official reference skills from GitHub repo - Tool chain docs (skill_manage, /learn, skill_view) - Update README.md with archives/ in directory structure
8.9 KiB
FastAPI 实战陷阱与最佳实践
**适用于 dev-pipeline ④编码实现阶段。程序员(主程序员/结对程序员)在编写 FastAPI 代码时必须注意以下陷阱。
1. 路由注册顺序
问题
FastAPI 按注册顺序匹配路由。具体路由必须在参数化路由之前注册,否则会被错误匹配。
# ❌ 错误:/{personnel_id} 先注册,/workload 被匹配为 personnel_id="workload"
@router.get("/{personnel_id}") # 第63行
def get_personnel(...)
@router.get("/{personnel_id}/workload") # 第151行 → 永远匹配不到
def get_personnel_workload(...)
# ✅ 正确:具体路由先注册
@router.get("/{personnel_id}/workload") # 先注册
def get_personnel_workload(...)
@router.get("/{personnel_id}") # 后注册
def get_personnel(...)
排查方法
当某个路由返回 404 但确信路径正确时:
- 检查路由注册顺序(
grep -n "@router\." file.py) - 确认具体路由(含固定路径段)在参数化路由之前
同类陷阱
/milestones/all必须在/{project_id}/milestones之前/tasks/all必须在/{project_id}/tasks之前/export/projects必须在/{project_id}之前
2. Excel 导出中文文件名编码
问题
Content-Disposition: attachment; filename=中文.xlsx 中的中文字符在 Starlette TestClient 中触发 UnicodeEncodeError,某些浏览器也无法正确解析。
修复
使用 RFC 5987 编码:
from urllib.parse import quote
def _excel_response(wb, filename):
output = io.BytesIO()
wb.save(output)
output.seek(0)
encoded_filename = quote(filename)
return StreamingResponse(
output,
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={"Content-Disposition": f"attachment; filename*=UTF-8''{encoded_filename}"},
)
测试注意事项
- Starlette TestClient 的
UnicodeEncodeError是已知问题,测试中需 try/except 捕获 - 验证 Excel 内容时检查
content[:2] == b"PK"(ZIP 签名)即可 - 用
openpyxl.load_workbook(io.BytesIO(resp.content))验证内容正确性
3. 依赖声明
问题
新增 Python 依赖(如 openpyxl)后忘记更新 requirements.txt,导致部署时崩溃。
规则
每次④编码实现阶段新增依赖后,必须:
- 确认依赖已安装(
pip list | grep <pkg>) - 更新
requirements.txt(pip freeze | grep <pkg> >> requirements.txt或手动添加) - 结对程序员 review 时检查 requirements.txt 变更
4. SQLAlchemy N+1 查询
问题
遍历 ORM 对象时访问关联属性会触发额外 SQL 查询。
# ❌ N+1:对每个任务查一次 project
for task in tasks:
project_name = task.project.project_name # 触发额外 SQL
# ✅ 修复:使用 joinedload 批量加载
from sqlalchemy.orm import joinedload
tasks = db.query(ProjectTask).options(joinedload(ProjectTask.project)).all()
批量查询替代方案
当无法使用 joinedload 时,用 IN 查询替代循环查询:
# ❌ N+1
for task in tasks:
progress = db.query(TaskMonthlyProgress).filter(
TaskMonthlyProgress.task_id == task.id
).first()
# ✅ 批量
all_progress = db.query(TaskMonthlyProgress).filter(
TaskMonthlyProgress.task_id.in_(task_ids)
).all()
task_latest = {p.task_id: p for p in all_progress}
5. SQLAlchemy NULL 处理
问题
SQLAlchemy 中 column < 100 遇到 NULL 时返回 UNKNOWN(不匹配),导致 NULL 值被遗漏。
# ❌ 遗漏 overall_progress IS NULL 的任务
tasks = db.query(ProjectTask).filter(ProjectTask.overall_progress < 100).all()
# ✅ 正确:显式处理 NULL
from sqlalchemy import or_
tasks = db.query(ProjectTask).filter(
or_(ProjectTask.overall_progress < 100, ProjectTask.overall_progress.is_(None))
).all()
# 或使用 | 运算符(注意括号)
tasks = db.query(ProjectTask).filter(
(ProjectTask.overall_progress < 100) | (ProjectTask.overall_progress.is_(None))
).all()
6. 前端导出绕过认证
问题
用 <a> 标签直接访问 API 路径会跳过 axios 的 Authorization: Bearer 拦截器,导致 401。
// ❌ 错误:绕过认证
const link = document.createElement('a')
link.href = '/api/reports/export/projects'
link.click()
// ✅ 正确:通过 axios 获取 blob 后下载
api.get('/reports/export/projects', { responseType: 'blob' }).then(res => {
const url = URL.createObjectURL(new Blob([res.data]))
const link = document.createElement('a')
link.href = url
link.download = 'filename.xlsx'
link.click()
URL.revokeObjectURL(url)
})
DRY 原则
多个导出函数应提取为通用函数:
function downloadExcel(url, filename) {
exporting.value = true
api.get(url, { responseType: 'blob' }).then(res => {
const blobUrl = URL.createObjectURL(new Blob([res.data]))
const link = document.createElement('a')
link.href = blobUrl
link.download = filename
link.click()
URL.revokeObjectURL(blobUrl)
}).finally(() => { exporting.value = false })
}
7. ECharts 生命周期管理
问题
Vue 组件销毁时未释放 ECharts 实例导致内存泄漏;路由参数变化(如 /personnel/1 → /personnel/2)时图表不刷新。
修复
import { onBeforeUnmount, onBeforeRouteUpdate } from 'vue-router'
// 销毁时释放
onBeforeUnmount(() => {
chart?.dispose()
})
// 路由参数变化时重新加载
onBeforeRouteUpdate(() => {
chart?.dispose()
loadData()
})
8. 浮点精度
问题
Python 浮点累加可能产生尾数误差(如 49.9999999 而非 50.0)。
修复
# 在最终输出时统一 round
total = round(sum(values), 2)
10. Pydantic Schema 字段同步
问题
后端 API 返回字典中新增了字段,但 Pydantic response_model schema 中没有声明,导致字段被静默丢弃。前端收到 undefined,页面渲染异常。
# ❌ 后端返回了 project_count,但 PersonnelOut schema 没有这个字段
class PersonnelOut(BaseModel):
id: int
name: str
# ... 没有 project_count → 被 Pydantic 过滤掉
# ✅ 必须在 schema 中声明
class PersonnelOut(BaseModel):
id: int
name: str
project_count: Optional[int] = 0
total_allocation: Optional[float] = 0.0
排查方法
当前端收到 undefined 但确信后端有返回时:
- 去掉
response_model参数,看原始返回是否包含该字段 - 检查 schema 定义是否包含新字段
- 注意
from_attributes = True只影响 ORM→Pydantic 转换,不影响 dict→Pydantic 过滤
关联陷阱
- 修改
_build_personnel_dict等 helper 函数后,必须同步更新对应的 schema - 结对程序员 review 时重点检查:helper 返回字段 ↔ schema 字段 的一致性
11. 部署验证脚本模式
问题
部署验证时使用 shell 管道(curl | python3 -c)容易触发安全防护(命令超时/阻断),尤其是涉及变量引用和 URL 编码时。
推荐方案
用 Python 脚本替代 shell 管道,一个文件完成全部验证:
#!/usr/bin/env python3
import urllib.request, json
BASE = "http://127.0.0.1:8001"
def api(method, path, data=None, token=None):
url = BASE + path
headers = {"Content-Type": "application/json"}
if token:
headers["Authorization"] = f"Bearer {token}"
body = json.dumps(data).encode() if data else None
req = urllib.request.Request(url, data=body, headers=headers, method=method)
resp = urllib.request.urlopen(req, timeout=10)
return resp.status, resp.read()
# 1. 登录
status, data = api("POST", "/api/auth/login", {"username": "admin", "password": "admin123"})
token = json.loads(data)["access_token"]
# 2. 验证端点
status, data = api("GET", "/api/projects?project_type=创新项目", token=token)
projects = json.loads(data)
print(f"类型筛选: {len(projects)}个")
# 3. 验证 Excel 导出
status, data = api("GET", "/api/reports/export/projects", token=token)
assert data[:2] == b"PK" # ZIP 签名
import openpyxl, io
wb = openpyxl.load_workbook(io.BytesIO(data))
print(f"导出: {wb.active.title}, {wb.active.max_row-1}行")
优势
- 避免 shell 变量注入和管道超时
- 可在虚拟环境中直接运行
- 断言清晰,失败时立即知道哪个端点出问题
- 可复用(保存为
scripts/verify_deploy.py)
9. 时区一致性
问题
项目中混用 timezone-naive 和 timezone-aware 的 datetime 导致比较偏差。
规范
from datetime import datetime, timezone
# 统一使用 UTC aware datetime
now = datetime.now(timezone.utc)
# 解析字符串时也设为 aware
from calendar import monthrange
parts = month_str.split("-")
y, m = int(parts[0]), int(parts[1])
last_day = monthrange(y, m)[1]
progress_date = datetime(y, m, last_day, tzinfo=timezone.utc)