v77 服务端:旅程题图上传端点 + 修「PATCH 把题图清空」的真 bug

新增 POST /api/v1/trips/<id>/cover(multipart 字段 cover),
槽位走 ImageSlot 的 cover(1920×1080 / 16:9),与网页版
trips/form.php 同一条 Upload::image 管线;换图顺手删旧对象。

修一个已经发生过的静默数据丢失:端上编辑旅程走
PATCH /api/v1/trips/<id>,那个分支的 $d 里没有 cover_key
⇒ saveDetails() 里 $d['cover_key'] ?? '' 得到空串 ⇒ 题图被写成
NULL。用户在手机上只改了个名字,题图就没了。
⇒ 语义改成「没传 = 不动」,要清空必须显式传空串。

另加 Trip::setCover():换图只动 cover_key 一列,不复用
Trip::update()(那条要求 title 必填、还会重写 summary/is_public,
下次加字段漏一个就又静默清空一次)。

线上已部署并复核:ApiHandler.php / Trip.php 均含新代码、php -l 通过。
This commit is contained in:
2026-10-22 10:14:54 +08:00
parent 8a9629650a
commit 036e208809
2 changed files with 85 additions and 6 deletions
+40 -1
View File
@@ -1416,11 +1416,22 @@ final class ApiHandler
if ($id > 0) {
$ev = $this->eventFor($fam, $id);
if (!$ev) { Api::notFound('这条日程'); }
/* 🔴 v73(2026-10-21 真机实测):**读**不该要求写权限。
原先这里是"先判 canWriteEvent、再分 GET/PATCH/DELETE",
后果:共享给我**只读**的日历本里的日程,连**详情**都打不开 ——
客户端表现为"部分详情没读回来:读不到详情",而那条日程
在列表里好好的(列表走 /calendar,不经过这里)。
允许读的安全依据是 `eventFor()` 已经卡了 `family_id`:
跨家庭的 id 在上一行就 404 了,能走到这里的一定是同家庭成员,
而 `Calendar::month()` 本来就对成员开放全家的日程。
⇒ 写权限只挡**写**(PATCH/PUT/DELETE),读一律放行。 */
if ($method === 'GET') { Api::ok($ev); }
if (!Calendar::canWriteEvent($ev, $pid)) {
Api::fail('这本日历你没有写权限。', 'forbidden', 403);
}
if ($method === 'GET') { Api::ok($ev); }
if ($method === 'PATCH' || $method === 'PUT') {
Api::requireWrite();
$this->writeEvent($pid, $ev);
@@ -2286,6 +2297,34 @@ final class ApiHandler
Api::ok(Trip::summary($id, $pid));
}
/* 🔴 v77(用户 2026-10-22):「新建旅程那里不是有题图吗?
为什么这里没有增加题图的功能呢?」
⇒ 端上补题图上传。槽位**必须**是 `cover`(`app/ImageSlot.php` 里
1920×1080 / 16:9),与网页版 `trips/form.php` 的 `cover` 文件域
同一条管线 —— 端上不裁剪,裁/缩/转 JPEG 全在服务端做。
⚠️ 表单字段名固定 `cover`(端上拼 multipart 时用);
与观影的 `poster`(2:3)不是同一个槽位,别串。
⚠️ 换图顺手删旧对象:否则每换一次题图就在存储里堆一张孤儿图
(与 `movies/<id>/poster` 那条同一口径)。 */
if ($sub === 'cover') {
if ($method !== 'POST') { Api::fail('不支持的方法。', 'method_not_allowed', 405); }
Api::requireWrite();
$up = Upload::image($fam, $_FILES['cover'] ?? [], 'cover');
if (!$up['ok']) {
Api::fail((string)($up['error'] ?? '题图没存下来'), 'unprocessable', 422);
}
$newKey = (string)$up['file'];
$oldKey = (string)($t['cover_key'] ?? '');
Trip::setCover($id, $newKey);
if ($oldKey !== '' && $oldKey !== $newKey) {
Storage\Storage::delete($fam, $oldKey);
}
Api::ok([
'cover_key' => $newKey,
'cover' => Storage\Storage::url($fam, $newKey),
]);
}
if ($method === 'GET') { Api::ok($t); }
if ($method === 'PATCH' || $method === 'PUT') {
Api::requireWrite();