v64:观影豆瓣自动获取 + 海报/多图上传(移动端 API)

端上「记一场」要与网页端同步实现(用户 2026-10-19 六条),
服务端这一半只加**移动端入口**,网页端(index.php 那条路由)一行没动。

- ApiHandler:新增 `/movies/douban` 子路由(action=suggest / action=thumb /
  默认抓详情+海报落盘),逻辑与网页端 `movies/douban` 逐条对齐;
  新增 `/movies/<id>/poster` 与 `/movies/<id>/photos`(multipart 上传),
  字段名与端上一致(poster / photo);
  新增 `normalizeFiles()` 把 `photo[]` 的多文件从"按字段竖切"转成"一行一个";
  `movieData()` 认 `poster_key` 并**校验形态**(只接受 posters/xxx 形式);
  features 声明 movies_douban / movie_media(端上可探测老服务端)。

- Douban:新增 `apiThumbUrl()` —— 端上打不到 `index.php?r=...`,
  把豆瓣图片代理换成 `/api/v1/...` 形式(拼法只此一处);
  联想候选带上**过白名单后**的原始图片地址(未过白名单给空串,不下发 SSRF 跳板)。

- Movie:新增 `setPoster()`(单字段更新 —— 端上是"先建记录再传图"两段式,
  用 update() 要拼一份完整草稿,漏一个字段就静默覆盖用户刚填的内容)与
  `addPhotos()`(**追加**语义,与网页表单用的 savePhotos() 替换语义并存不混用)。

验证:接口 e2e 57/0(自带豆瓣 mock,离线可跑)· 反向验证 18/0 ·
本地全量 e2e 1521/0 · 线上部署 + 站点自检 19 条全过 +
`/api/v1/movies/douban` 401(路由通)· ping.features 已含两条新能力
This commit is contained in:
2026-10-19 23:41:15 +08:00
parent 2f7dc6ca23
commit 3459eea8f3
3 changed files with 361 additions and 0 deletions
+33
View File
@@ -327,6 +327,14 @@ final class Douban
'episode' => $episode,
'kind' => self::kindLabel($type, $episode),
'thumb' => $img !== '' ? self::thumbProxyUrl($img) : '',
/* 🔴 v64:**原始**豆瓣图片地址(已过白名单)也带上。
⚠️ 为什么需要它:`thumb` 是**网页路由**的代理地址
(`index.php?r=movies/douban&action=thumb&u=…`),
手机端拿不到 `index.php?r=` 这种入口 —— 它只会打 `/api/v1/…`。
端上要用 `apiThumbUrl()` 把这个原始地址重新拼成 **API 形式**。
⚠️ 只有过白名单的才给(没过的一律空串)——
否则等于把"服务端代取任意 URL"的 SSRF 跳板下发给客户端。 */
'img' => self::imageUrlAllowed($img) ? $img : '',
];
if (count($items) >= $limit) {
break;
@@ -381,6 +389,31 @@ final class Douban
return 'index.php?r=movies/douban&action=thumb&u=' . rawurlencode($img);
}
/**
* 豆瓣图片地址 → **移动端 API** 代理地址(`/api/v1/movies/douban?action=thumb&u=…`)。
*
* 🔴 v64:端上只能打 `/api/v1/…`,拿不到 `index.php?r=` 那种网页入口
* (见 `buildSuggestItems` 里 `img` 字段的说明)。
* ⇒ 拼法在这里**定义一次**,端上不自己拼串 ——
* 免得以后接口路径一改,两端各改一份还对不齐。
*
* ⚠️ 与网页路由**同构**(那边是 `movies/douban&action=thumb`):
* `Api::dispatch()` 只解析到「资源 / id / 子动作」两层,
* `/movies/douban/thumb` 这种三段路径它认不出来(会把 thumb 当子动作丢掉)。
* 用 query 传 action 是最省事、也不会跟现有路由打架的形态。
*
* ⚠️ 返回值是**相对路径**,端上要自己补服务器基址并追加 `&token=`
* (`Image()` 加不了请求头,见 `media.php` 的说明)。
* 这样同一个地址在"用户换服务器"后仍然有效。
*/
public static function apiThumbUrl(string $img): string
{
if (!self::imageUrlAllowed($img)) {
return '';
}
return '/api/v1/movies/douban?action=thumb&u=' . rawurlencode($img);
}
// ---------------- HTTP 与格式识别 ----------------
private static function searchEndpoint(string $q): string