第 12 章 · 毕业项目:api-demo 服务 MVP
本章目标:在 ch01~ch11 基础上,独立交付 api-demo Flask API 服务最小可行产品(MVP);覆盖用户认证、商品 API、简易管理页与运维文档;按 5 天计划实施;通过 100 分验收表自评;完成答辩演示与项目说明。
学时建议:5 天 × 6~8 小时(合计 30~40 小时)
前置:完成本模块 ch01~ch11;建议复习 python-dev ch06 OOP 与 ch11 requests;前端联调可参考 frontend-framework API 章节。
12.1 项目背景与边界
api-demo 是一家虚构的「轻量商品 API 平台」,面向运营人员提供后台管理能力,同时为 SPA / 移动端提供 JSON 接口。C 端前端部署在独立域名,调用 https://api.example.com(或你本地部署的等价地址)。
┌──────────────────────────────────────────────────┐
│ api-demo 服务 MVP(本章) │
├──────────┬──────────┬──────────┬─────────────────┤
│ 用户认证 │ 商品 API │ 管理页面 │ 运维与测试 │
│ 注册登录 │ CRUD+分页 │ 列表表单 │ pytest/Gunicorn│
└──────────┴──────────┴──────────┴─────────────────┘
│
▼
PostgreSQL / SQLite(教学可用)
Redis(缓存,ch10)+ Celery(选修)
| 模块 | MVP 必须 | 不做(加分扩展) |
|---|---|---|
| 用户 | 注册、登录、退出、/api/v1/me | OAuth、短信验证码 |
| 商品 | 列表、搜索、创建/编辑、封面上传、上下架 | SKU 矩阵、批量导入 |
| 管理页 | 登录后商品列表 + 表单 CRUD | 复杂权限 RBAC |
| API | RESTful + 统一错误码 + 分页 | GraphQL、gRPC |
| 运维 | .env.example、Gunicorn 文档、日志 | K8s 全量(见云原生课) |
严禁将真实公司内部域名、数据库连接串、密钥写入仓库或答辩材料。统一使用 api-demo、user-demo、api.example.com。
12.2 技术栈清单
| 层级 | 技术 | 对应章节 |
|---|---|---|
| 框架 | Flask 3.x + 应用工厂 | ch01 |
| 路由 | Blueprint(web + api) | ch02 |
| 模板 | Jinja2 管理页 | ch03 |
| 表单 | WTForms + CSRF | ch04 |
| ORM | Flask-SQLAlchemy | ch05 |
| 认证 | Flask-Login(见 12.4 方案对比) | ch06 |
| API | jsonify + 统一 envelope | ch07 |
| 上传 | UPLOAD_FOLDER + 分环境 Config | ch08 |
| 横切 | 请求 ID、日志、errorhandler | ch09 |
| 性能 | Redis 缓存热门列表(选修) | ch10 |
| 异步 | Celery 注册通知(选修) | ch10 |
| 测试 | pytest + test client | ch11 |
| 部署 | Gunicorn + Nginx + .env | ch11 |
cd ~/python-learn
mkdir api-demo && cd api-demo
python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install "Flask>=3.0" Flask-SQLAlchemy Flask-Login Flask-WTF Flask-Caching \
flask-cors python-dotenv Pillow celery redis \
pytest pytest-cov gunicorn psycopg2-binary
12.3 推荐目录结构
api-demo/
├── .env.example
├── .gitignore
├── docker-compose.dev.yml # Redis
├── gunicorn.conf.py
├── wsgi.py
├── pytest.ini
├── requirements.txt
├── docs/
│ ├── API.md # 接口文档
│ ├── DEPLOY.md # 部署说明
│ └── SELF_REVIEW.md # 100 分自评表
├── api_demo/
│ ├── __init__.py # create_app
│ ├── config.py
│ ├── extensions.py
│ ├── celery_app.py
│ ├── logging_config.py
│ ├── api/
│ │ ├── __init__.py
│ │ ├── products.py
│ │ ├── auth.py
│ │ └── errors.py
│ ├── web/
│ │ ├── __init__.py
│ │ ├── views.py
│ │ └── forms.py
│ ├── models/
│ │ ├── user.py
│ │ └── product.py
│ ├── middleware/
│ └── tasks/
├── templates/
├── uploads/
└── tests/
├── conftest.py
└── test_api_products.py
12.4 认证方案:Session 与 JWT 二选一
毕业项目必须实现一种完整认证链路;答辩时说明选型理由。
方案 A:Session + Cookie(推荐入门)
| 优点 | 缺点 |
|---|---|
| 与 ch06 Flask-Login 一致 | 跨域需 supports_credentials |
| 管理页与 API 共享登录态 | 移动端原生需 Cookie 管理 |
| CSRF 由 Flask-WTF 保护 Web 表单 | 水平扩展需 Redis Session(选修) |
API 写操作:@login_required + unauthorized_handler 返回 JSON 401。
方案 B:JWT Bearer Token
pip install PyJWT
| 优点 | 缺点 |
|---|---|
| 无状态,适合 SPA/移动端 | 注销需黑名单或短过期 |
| 跨域简单,Header 传 Token | 需自行实现刷新 Token |
| 与前后端完全分离 | XSS 泄露 Token 风险更高 |
登录返回:
{
"code": 0,
"data": {
"access_token": "eyJ...",
"expires_in": 3600,
"token_type": "Bearer"
}
}
请求头:Authorization: Bearer eyJ...
| 验收 | Session | JWT |
|---|---|---|
登录后调 /api/v1/me 成功 | Cookie 自动携带 | Header 带 Token |
| 未登录写操作 401 | ✓ | ✓ |
| 管理页登录 | ✓ | 可仅 API 登录 |
答辩话术:「MVP 选用 Session 降低复杂度;若 C 端为纯 SPA 且多端复用,生产可迁移 JWT。」
12.5 API 契约(最小集)
基址:https://api.example.com/api/v1(本地 http://127.0.0.1:5000/api/v1)
| 方法 | 路径 | 认证 | 说明 |
|---|---|---|---|
| POST | /auth/register | 否 | 注册 |
| POST | /auth/login | 否 | 登录 |
| POST | /auth/logout | 是 | 退出 |
| GET | /me | 是 | 当前用户 |
| GET | /products | 否 | 列表 + page per_page q |
| GET | /products/{id} | 否 | 详情 |
| POST | /products | 是 | 创建 |
| PATCH | /products/{id} | 是 | 更新 |
| DELETE | /products/{id} | 是 | 删除 |
| POST | /products/{id}/cover | 是 | 封面上传 |
统一响应:{ "code": 0, "message": "ok", "data": ... };错误见 ch07 错误码表。
12.5.1 curl 冒烟脚本(答辩备用)
将下列脚本保存为 scripts/smoke_api.sh,答辩前一键验证核心链路:
BASE="http://127.0.0.1:5000/api/v1"
COOKIE_JAR="/tmp/api-demo-cookie.txt"
# 1. 列表(匿名)
curl -s "$BASE/products?page=1&per_page=5" | python -m json.tool
# 2. 注册 + 登录(Session)
curl -s -c "$COOKIE_JAR" -X POST "$BASE/auth/register" \
-H "Content-Type: application/json" \
-d '{"email":"demo@user-demo.example.com","password":"DemoPass123!"}'
curl -s -b "$COOKIE_JAR" -c "$COOKIE_JAR" -X POST "$BASE/auth/login" \
-H "Content-Type: application/json" \
-d '{"email":"demo@user-demo.example.com","password":"DemoPass123!"}'
# 3. 当前用户
curl -s -b "$COOKIE_JAR" "$BASE/me" | python -m json.tool
# 4. 创建商品
curl -s -b "$COOKIE_JAR" -X POST "$BASE/products" \
-H "Content-Type: application/json" \
-d '{"name":"答辩演示商品","slug":"demo-item","price":"88.00","stock":50}'
# 5. 未登录写操作应 401
curl -s -o /dev/null -w "%{http_code}" -X POST "$BASE/products" \
-H "Content-Type: application/json" -d '{"name":"x"}'
# 期望输出 401
| 步骤 | 期望 |
|---|---|
| 列表 | code: 0,含 pagination |
/me | 返回当前用户 email |
| 创建 | status 201,data.slug 正确 |
| 匿名 POST | HTTP 401,code: 40101 |