P19第 1 / 1 课时未开始内容版本 1.0.0 · 核验于 2026-10-26

学习记录 API 实战

学习记录 API 实战 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。

学习目标

这是 Python track 的收尾实战:把 P08-P18 的零件组装成完整的学习记录服务——记录、笔记、统计三个模块,接上本站前端跑契约测试,最后交付一份能直接照做的启动说明和故障排查清单。

问题场景

前面每一课都在解决一个点,现在要一个面:你的 Next.js 前端需要三个能力——写入学习记录、管理笔记、看统计(总时长、每门课的完成数)。所有零件你都造过:Pydantic 模型、Depends、SQLite、统一错误、测试。缺的是把它们装进一个结构能长大的服务里。

核心解释

FastAPI 的官方答案是 APIRouter:按业务域拆文件,每个模块自带前缀和标签,main 只负责组装——对应本站 app/api/ 下按领域分目录的组织方式。

study-api/
├── main.py          # 组装:create_app(),只做 include_router 和中间件
├── records.py       # 学习记录:POST /records、GET /records
├── notes.py         # 笔记:POST /notes、GET /notes、DELETE /notes/{id}
├── stats.py         # 统计:GET /stats/summary
├── db.py            # 连接、schema、迁移(P11)
└── tests/           # pytest(P17)
# records.py —— 一个模块的样子
router = APIRouter(prefix="/records", tags=["records"])

class RecordIn(BaseModel):
    course_code: str = Field(min_length=2, max_length=8)
    minutes: int = Field(ge=0, le=600)

@router.post("", status_code=201)
def add_record(r: RecordIn, conn=Depends(get_db)):
    ...  # 校验靠模型、存储靠 db.py、错误走统一结构

# main.py —— 组装处
app = FastAPI()
app.add_middleware(CORSMiddleware, allow_origins=["http://localhost:3000"], ...)
app.include_router(records.router)
app.include_router(notes.router)
app.include_router(stats.router)

统计接口是三个模块里最考验 SQL 的:

@router.get("/summary")
def summary(conn=Depends(get_db)):
    row = conn.execute("""
        SELECT COALESCE(SUM(minutes), 0) AS total_minutes,
               COUNT(DISTINCT course_code) AS courses,
               COUNT(*) AS records
        FROM study_record
    """).fetchone()
    per_course = conn.execute(
        "SELECT course_code, SUM(minutes) AS minutes FROM study_record GROUP BY course_code ORDER BY minutes DESC"
    ).fetchall()
    return {"total_minutes": row["total_minutes"], "courses": row["courses"],
            "per_course": [dict(r) for r in per_course]}

契约测试是这一课的验收关卡:从真实接口请求出发,断言字段名、外壳结构、状态码,前端不管内部实现怎么改,契约测试绿就敢接。写法直接用 P17 的 TestClient,但断言对象换成「前端依赖的字段是否存在」:

def test_summary_contract(client):
    res = client.get("/stats/summary")
    assert res.status_code == 200
    body = res.json()
    assert {"total_minutes", "courses", "per_course"} <= set(body)   # 契约字段齐全

示例拆解

全链路验收清单照这个顺序走:

pip install -r requirements.txt
uvicorn main:app --host 127.0.0.1 --port 8100    # 1. 启动说明第一条
pytest                                            # 2. 服务端契约测试先绿
curl -X POST localhost:8100/records -H "Content-Type: application/json" \
  -d '{"course_code":"P19","minutes":30}'         # 3. 写入真实形态的数据
curl localhost:8100/stats/summary                 # 4. 统计能查到刚写的记录

前端侧把 http://127.0.0.1:8100 填进一个 API_BASE 常量接上统计卡片,和本站 app/api 的调用方式同构。启动说明按「装依赖 → 起服务 → 跑测试 → 验证统计」四步写,每步有预期输出;故障排查列三行:端口占用(netstat -ano | findstr 8100)、依赖缺失(ModuleNotFoundError → 重装 requirements)、数据库锁(database is locked → 检查是否有未关连接)。

动手任务

  1. 把 P08-P18 的笔记和记录接口合并进 records.py/notes.py 两个 router,删除原单文件里的重复代码。
  2. 给统计接口加「连续学习天数」字段(按日期去重后最长连续段),并补一个契约测试。
  3. 写完整 README:启动四步、预期输出、三条故障排查——写完交给「三个月后的自己」验证能否照做跑通。

常见错误

  • main.py 变成垃圾场:所有路由堆在组装文件里,改一个模块读全文件;路由必须各回各的 router。
  • 统计逻辑写在 SQL 之外:拉全表到 Python 里循环求和,数据量一大就慢;聚合交给 SQL 的 SUM/GROUP BY。
  • 契约只存在于口头:前端等字段名 totalMinutes,后端返回 total_minutes;契约测试是唯一可信的交接物。
  • 启动说明缺预期输出:只写命令不写「应该看到什么」,排错时无从判断哪步出了问题。

检查与复盘

自查清单:

  • 三个模块各自一个 router 文件,main.py 只组装不写业务
  • pytest 全绿,其中至少两个测试是「前端依赖字段的契约断言」
  • 启动说明每一步都有可对照的预期输出

参考资料

读完这一课了?

标记完成后,工作台会安排接下来的学习。

Command Palette

Search for a command to run...