16 最终综合项目:Sakila 管理后台 API

这一章不再逐行引导。你需要把前面各章组合为一套能够登录、鉴权、操作 Sakila、输出报表并留下日志的后端服务。

项目目标

管理员账号初始化
  ↓
登录签发 JWT
  ↓
鉴权保护业务 Router
  ↓
Actor / Customer / Film
  ↓
Rental / Payment 事务
  ↓
Raw SQL 报表
  ↓
日志、审计和安全验证

1. 最终目录建议

errors/
  app-error.ts

models/sakila/
  admin-user.ts
  actor.ts
  customer.ts
  address.ts
  city.ts
  country.ts
  film.ts
  actor-film.ts
  category.ts
  film-category.ts
  inventory.ts
  rental.ts
  payment.ts
  staff.ts
  store.ts
  associations.ts
  index.ts

routes/sakila/
controllers/sakila/
services/sakila/
validations/sakila/
types/sakila/

middlewares/
  authenticate-admin.ts
  error-handler.ts
  validate-request.ts

scripts/
  create-admin.ts

不要求机械照搬。重点是职责明确,不要让一个 routes/sakila.ts 同时容纳所有查询和业务逻辑。

2. 必做:管理员身份系统

数据库

[ ] admin_user 表存在于 sakila_training
[ ] username 唯一
[ ] password_hash NOT NULL
[ ] account_status 有稳定范围
[ ] token_version 有默认值
[ ] 时间字段和时区策略明确

初始化

[ ] 没有公开管理员注册接口
[ ] 可以通过脚本创建第一个管理员
[ ] 密码使用 Argon2id
[ ] 脚本不打印密码和 Hash

登录接口

POST /api/auth/login

验收场景:

正确账号密码
不存在的用户名
错误密码
禁用账号
临时锁定账号
缺少字段
超长输入

鉴权接口

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

必须验证:

3. 必做:管理员账号管理

GET   /api/admin/users
POST  /api/admin/users
PATCH /api/admin/users/:adminId/status
POST  /api/admin/users/:adminId/reset-password

业务规则至少包含:

用户名不能重复
密码不能明文保存
管理员列表不返回 passwordHash
禁用账号后旧 JWT 不能继续操作
不能意外禁用最后一个启用管理员
重置密码后 tokenVersion 递增

4. 必做:Actor CRUD

GET    /api/sakila/actors
GET    /api/sakila/actors/:actorId
POST   /api/sakila/actors
PATCH  /api/sakila/actors/:actorId
DELETE /api/sakila/actors/:actorId

验收:

5. 必做:Customer 管理

GET   /api/sakila/customers
GET   /api/sakila/customers/:customerId
POST  /api/sakila/customers
PATCH /api/sakila/customers/:customerId
PATCH /api/sakila/customers/:customerId/status

列表支持:

active
lastName
countryId
storeId
page
pageSize

详情至少包含:

客户基础信息
地址、城市和国家
最近一次租赁
累计支付金额
当前未归还数量

验收:

6. 必做:Film 查询

GET /api/sakila/films
GET /api/sakila/films/:filmId
GET /api/sakila/actors/:actorId/films

电影列表支持:

title
rating
categoryId
minRentalRate
maxRentalRate
minLength
maxLength
page
pageSize

详情至少包含:

语言
分类
演员
各门店库存数
各门店可出租数量
累计出租次数

验收:

7. 必做:Rental 流程

GET   /api/sakila/stores/:storeId/available-inventory
POST  /api/sakila/rentals
PATCH /api/sakila/rentals/:rentalId/return
GET   /api/sakila/rentals

创建租赁时至少检查:

客户存在且启用
库存存在
员工存在
员工与库存门店关系正确
库存当前可出租
写入位于事务中
两个并发请求不能成功出租同一库存

归还接口必须有明确幂等策略,不能重复覆盖 return_date

8. 必做:Payment 与复合事务

POST /api/sakila/rentals/checkout
GET  /api/sakila/payments
GET  /api/sakila/customers/:customerId/payment-summary

checkout 事务:

锁定并检查库存
  ↓
创建 rental
  ↓
创建 payment
  ↓
任一步失败则整体回滚

验收:

9. 必做:Raw SQL 报表

至少完成两个,推荐完成四个:

GET /api/sakila/reports/daily-revenue
GET /api/sakila/reports/top-films
GET /api/sakila/reports/overdue-rentals
GET /api/sakila/reports/customer-spending
GET /api/sakila/reports/category-revenue

要求:

[ ] 使用 sequelize.query<T>()
[ ] 使用 QueryTypes.SELECT
[ ] 用户值通过 replacements 或 bind
[ ] 动态排序使用白名单
[ ] list 与 total 含义明确
[ ] 返回类型没有 any
[ ] 至少对一条 SQL 执行 EXPLAIN

10. 必做:校验和错误

所有外部输入必须先校验:

params
query
body
Authorization

错误响应统一:

ApiResponse<T>

必须覆盖:

VALIDATION_ERROR
AUTH_REQUIRED
INVALID_ACCESS_TOKEN
INVALID_CREDENTIALS
NOT_FOUND 类错误
CONFLICT 类错误
INTERNAL_SERVER_ERROR

未知异常不得向客户端返回:

SQL
stack
服务器路径
passwordHash
JWT Secret

11. 必做:日志

Pino 请求日志至少包含:

requestId
adminId(登录后)
method
path
businessCode
duration

因为 HTTP 始终为 200,必须能通过 businessCode 找到业务失败。

禁止记录:

明文密码
passwordHash
完整 JWT
Authorization
数据库密码

12. 选做:审计日志

使用 admin_audit_log 记录:

创建/修改/删除 Actor
停用 Customer
创建和归还 Rental
创建 Payment
禁用管理员
重置密码

能够根据:

adminUserId
resourceType + resourceId
actionCode
createdAt

查询历史操作。

13. Router 顺序验收

推荐结构:

app.use("/health", healthRouter);
app.use("/api/auth", authRouter);

app.use("/api/admin", authenticateAdmin, adminRouter);
app.use("/api/sakila", authenticateAdmin, sakilaRouter);

app.use(notFoundHandler);
app.use(errorHandler);

验证故意放错顺序时会发生什么:

14. 完整 curl 验收流程

登录前访问

curl -i http://127.0.0.1:8080/api/sakila/actors

预期业务码:

AUTH_REQUIRED

登录

curl -i \
  -H 'Content-Type: application/json' \
  -d '{"username":"admin","password":"你的密码"}' \
  http://127.0.0.1:8080/api/auth/login

保存返回的 accessToken。不要把真实 Token 提交到 Git 或练习文档。

带 Token 查询

curl -i \
  -H 'Authorization: Bearer <access-token>' \
  'http://127.0.0.1:8080/api/sakila/actors?page=1&pageSize=20'

创建演员

curl -i \
  -X POST \
  -H 'Authorization: Bearer <access-token>' \
  -H 'Content-Type: application/json' \
  -d '{"firstName":"TEST","lastName":"ACTOR"}' \
  http://127.0.0.1:8080/api/sakila/actors

让旧 Token 失效

curl -i \
  -X POST \
  -H 'Authorization: Bearer <access-token>' \
  http://127.0.0.1:8080/api/auth/logout-all

再次使用旧 Token 请求,预期失败。

15. 故障注入验收

[ ] 密码错误
[ ] Token 缺失
[ ] Token 过期
[ ] Token 签名错误
[ ] 管理员被禁用后使用旧 Token
[ ] pageSize 超上限
[ ] Actor 删除遇到外键约束
[ ] Customer 有未归还租赁时停用
[ ] 两个请求同时租同一库存
[ ] checkout 创建 payment 失败并回滚
[ ] Raw SQL 注入输入
[ ] MySQL 连接中断
[ ] 未知异常不泄漏内部信息

16. TypeScript 验收

执行:

npx tsc --noEmit

检查:

[ ] 没有为逃避类型错误随意使用 any
[ ] Request 泛型顺序正确
[ ] req.admin 类型来自 declaration merging
[ ] JWT Payload 完成运行时校验,不能只信任类型断言
[ ] Raw Query 返回类型明确
[ ] BIGINT 和 DECIMAL 的 API 类型策略一致

17. 最终自评

基础完成

能登录
能鉴权
能完成 Actor CRUD
能查询 Customer 和 Film
能返回统一 JSON

中级完成

能完成 Rental/Payment 事务
能处理 Association 和分页 count
能编写参数化 Raw SQL
能记录业务错误日志

进阶完成

能解释并发租赁方案
能使旧 JWT 失效
能使用 EXPLAIN 分析查询
能完成故障注入和集成测试
能记录并查询审计日志

如果一个接口只能在正常输入下运行,还不能算完成。能够解释错误路径、数据库约束、并发行为和日志证据,才是真正从前端调用者走向后端实现者的关键一步。