00 项目目标与练习规则

本练习把 MySQL 官方示例数据库 Sakila 改造成一个小型的“电影租赁管理后台 API”。你将以管理员身份登录,然后通过 Express 接口管理演员、客户、库存、租赁和支付,并使用 Sequelize 与 Raw SQL 访问 MySQL。

本章只确定边界与约定,不要求编写业务查询。

1. 最终请求链路

管理后台前端
  -> HTTP 请求
  -> Express Router
  -> express-validator
  -> JWT 鉴权中间件
  -> Controller
  -> Service
  -> Sequelize Model 或 Raw SQL
  -> MySQL sakila_training

PM2 只负责运行 Node.js 进程,Nginx 只负责接收和转发外部请求;它们都不代替 Express 的认证和业务校验。

2. 数据库使用规则

建议从原始 Sakila 复制一份练习库:

sakila           只读参考库
sakila_training  本练习读写库

本练习会新增 admin_user,并会修改 Actor、Customer、Rental、Payment 等业务数据,因此不要把唯一一份 Sakila 当作练习库。

禁止在现有数据库上使用:

sequelize.sync({ force: true });
sequelize.sync({ alter: true });

它们可能删除表、重建表或作出你没有审核过的 DDL。练习新增表应使用手工审核的 SQL;Sequelize Model 用来映射数据库,不负责猜测数据库结构。

3. 环境变量

建议的 .env 配置:

DB_HOST=127.0.0.1
DB_PORT=3306
DB_NAME=sakila_training
DB_USER=sakila_app
DB_PASSWORD=请使用真实密码

JWT_ACCESS_SECRET=请使用足够长的随机值
JWT_ACCESS_EXPIRES_IN_SECONDS=1800
JWT_ISSUER=nloop-sakila-api
JWT_AUDIENCE=nloop-sakila-admin

要求:

4. HTTP 接口范围

公开接口只有:

GET  /health
POST /api/auth/login

需要管理员 Token 的接口:

GET   /api/auth/me
PATCH /api/auth/password
POST  /api/auth/logout-all

GET   /api/admin/users
POST  /api/admin/users
PATCH /api/admin/users/:adminId/status

GET   /api/sakila/actors
POST  /api/sakila/actors
...

第一阶段只有 manager 一种角色。不要一开始实现菜单权限、动态角色或复杂 RBAC;先把“未登录不能访问业务接口”做正确。

5. 认证约定

客户端通过请求头发送访问令牌:

Authorization: Bearer <access-token>

JWT Payload 只放认证所需的最小信息,例如管理员 ID、角色和 Token 版本。JWT 是签名数据,不是加密数据;任何拿到 Token 的人都能读取 Payload,所以禁止放入:

6. 本项目的响应约定

当前工程约定:Express 已捕获的业务成功与失败都返回 HTTP 200,通过 JSON code 判断结果。

成功:

{
  "code": "OK",
  "message": "success",
  "data": {}
}

失败:

{
  "code": "INVALID_CREDENTIALS",
  "message": "用户名或密码错误",
  "data": null
}

这是一项项目约定,不代表 HTTP 的通用最佳实践。你必须理解它的代价:

7. 分页、筛选与排序规则

所有列表接口必须:

HTTP Query 最初都是字符串:

req.query.page === "2";

Controller 应把经过校验的输入转换成内部类型,Service 接收 number,而不是重复处理 string | undefined

8. 时间与金额规则

日期区间统一使用左闭右开:

startAt <= value < endAt

例如查询 2026-01-01 到 2026-01-02 两天的数据,内部结束时间应是 2026-01-03 00:00:00。前后端必须明确时区。

MySQL DECIMAL 可能由 Sequelize/mysql2 返回字符串。金额 DTO 默认保留字符串,不要未经论证就转为 JavaScript number 并累加。

9. 各层职责

Router

Validator

Controller

Service

Model

10. 安全底线

11. 每章的完成方法

每个练习会区分:

已提供:HTTP 契约、DTO、函数签名、代码骨架
需完成:TODO 中的校验、查询、业务判断和错误映射
进阶项:事务、并发、EXPLAIN、集成测试

建议每完成一个接口都执行:

npx tsc --noEmit

然后用 curl 测试正常、缺少参数、非法参数、未登录、数据不存在等分支。

12. 本章练习

  1. 画出“不带 Token 请求演员列表”时的中间件执行顺序。
  2. 解释为什么 PM2 显示 online 不等于数据库接口一定可用。
  3. 写出三个不能放入 JWT Payload 的字段并说明原因。
  4. 如果所有业务错误都是 HTTP 200,前端 Axios 响应拦截器至少应检查什么?
  5. 为什么不能对现成 Sakila 直接执行 sync({ force: true })
  6. 说明 Validator 和 Service 中数据库存在性检查的边界。
  7. 设计一个不会被用户任意控制 SQL 列名的排序白名单。
  8. 说明为什么分页结果必须有稳定排序。