# T-WEB-003 /api/songs CRUD - **状态**: done - **依赖**: T-WEB-002 ## 目标 曲目的增删改查与分页检索,字段语义符合 `contracts/data-models.md` 2。 ## load ``` DSH.md contracts/rest-api.yaml # 锚点:#/paths/~1api~1songs contracts/data-models.md # 锚点:1 通用约定、2 Song web-server/src/songs/* web-server/src/common/source.dto.ts web-server/src/common/pagination.ts ``` ## 端点 | 方法 | 路径 | 说明 | |--|--|--| | GET | `/api/songs` | 分页 + `q`/`artist`/`status` 过滤 | | POST | `/api/songs` | 新建(来源五元组必填) | | GET | `/api/songs/:id` | 详情 | | PATCH | `/api/songs/:id` | 更新(来源需五元组同时提交) | | DELETE | `/api/songs/:id` | **软删除** | | POST | `/api/songs/:id/audio` | 上传音频(multipart,字段名 `file`) | | POST | `/api/songs/:id/cover` | 上传封面 | ## 关键实现 - `likePattern()` 转义 `%` `_` `\`,用户输入不会被当作 LIKE 模式。 - `assertSourceTupleComplete()` 实现「全有或全无」:PATCH 只给部分来源字段 -> 400。 - PATCH 直接改 `status` 时校验状态机;非法流转 -> 409 并提示改用 `/api/reviews`。 - `toDtos()` 批量解析 asset -> URL,避免 N+1。 ## 验收 ```bash npm run test:smoke ``` 冒烟脚本 [1] 段落覆盖:创建 201、id 为 number、来源回显、tags 往返、bpm 为 number、 分页结构、模糊查询命中、音频上传后 `audioUrl` 以 `/static/` 开头、PATCH 生效、 部分来源字段 400、软删除后不再出现在列表。 ## 结果 全部通过(见 `web-server/test/smoke.mjs`)。