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
必须验证:
- Authorization Bearer 格式;
- JWT 签名、算法、issuer、audience 和过期时间;
- admin_user 存在且启用;
- tokenVersion 一致;
req.admin有明确 TypeScript 类型。
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
验收:
- 分页有默认值和上限;
- 排序字段使用白名单;
- 详情不存在返回稳定 code;
- PATCH 至少包含一个允许字段;
- 不把整个 req.body 交给 update;
- 删除有关联电影的演员时正确处理外键约束。
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
详情至少包含:
客户基础信息
地址、城市和国家
最近一次租赁
累计支付金额
当前未归还数量
验收:
findAndCountAll的 total 是客户数;- 没有无意义
SELECT *; - 多表关系没有造成金额重复;
- 有未归还电影时不能停用客户;
- 使用停用状态而不是强制物理删除历史客户。
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
详情至少包含:
语言
分类
演员
各门店库存数
各门店可出租数量
累计出租次数
验收:
- 没有每部电影单独查询演员的 N+1;
- 没有把演员、分类、库存和租赁同时巨大 JOIN 后错误聚合;
- DTO 不直接暴露中间表无关字段;
- 多对多 Association alias 一致。
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
↓
任一步失败则整体回滚
验收:
- transaction 传递给事务内所有查询;
- payment 失败后没有残留 rental;
- DECIMAL 在 DTO 中使用明确策略;
- 日期范围左闭右开;
- 不使用 JavaScript 浮点数累计财务金额。
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);
验证故意放错顺序时会发生什么:
- authenticateAdmin 放在业务路由之后;
- errorHandler 放在 Router 之前;
- 404 放在业务 Router 之前;
- login Router 被 authenticateAdmin 保护。
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 分析查询
能完成故障注入和集成测试
能记录并查询审计日志
如果一个接口只能在正常输入下运行,还不能算完成。能够解释错误路径、数据库约束、并发行为和日志证据,才是真正从前端调用者走向后端实现者的关键一步。