diff --git a/app/ApiHandler.php b/app/ApiHandler.php index e9ecee5..3612ba3 100644 --- a/app/ApiHandler.php +++ b/app/ApiHandler.php @@ -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//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(); diff --git a/app/Trip.php b/app/Trip.php index fe7b7b4..16f7c2b 100644 --- a/app/Trip.php +++ b/app/Trip.php @@ -679,9 +679,24 @@ final class Trip private static function saveDetails(int $eventId, array $d): void { - $db = Database::get(); + $db = Database::get(); + /* 先读一次现有值 —— `cover_key` **没传时要保留原值**。 + 🔴 v77 修的真实 bug(2026-10-22 查出来的): + 端上编辑旅程走 `PATCH /api/v1/trips/`,那个分支的 `$d` 里 + 只有 title / note / summary / is_public —— 于是这里 + `$d['cover_key'] ?? ''` 得到空串 ⇒ `keyOrNull('')` ⇒ **null** + ⇒ 用户在手机上只改了个名字,题图就被**静默清空**了。 + (网页版没这个病:`index.php` 的保存路径每次都显式带上旧值。) + ⚠️ 语义按"没传 = 不动":要清空题图必须显式传 `cover_key = ''`。 */ + $sel = $db->prepare('SELECT cover_key FROM trip_details WHERE event_id = ?'); + $sel->execute([$eventId]); + $cur = $sel->fetchColumn(); + $has = $cur !== false; + $cover = array_key_exists('cover_key', $d) + ? self::keyOrNull((string)$d['cover_key']) + : ($has ? self::keyOrNull((string)$cur) : null); $row = [ - self::keyOrNull($d['cover_key'] ?? ''), + $cover, self::textOrNull($d['summary'] ?? ''), // 用户第 4 条:是否公开。 // ⚠️ 必须区分「这个键没传」与「用户明确取消勾选」: @@ -691,9 +706,7 @@ final class Trip // · 传了 0(表单里取消勾选)→ 真的不公开。 array_key_exists('is_public', $d) ? (!empty($d['is_public']) ? 1 : 0) : 1, ]; - $has = $db->prepare('SELECT COUNT(*) FROM trip_details WHERE event_id = ?'); - $has->execute([$eventId]); - if ((int)$has->fetchColumn() > 0) { + if ($has) { $db->prepare('UPDATE trip_details SET cover_key=?, summary=?, is_public=? WHERE event_id=?') ->execute([...$row, $eventId]); } else { @@ -702,6 +715,33 @@ final class Trip } } + /** + * 单独换题图(端上 `POST /api/v1/trips//cover` 用)。 + * + * ⚠️ 为什么不复用 `update()`:那条要求 `title` 必填、而且会把 + * note / summary / is_public 一起重写 —— 只想换张图却要先把 + * 整趟旅程的其它字段原样传一遍,最容易在下次加字段时漏掉一个 + * (漏了的后果就是静默清空)。换图就只动 cover_key 一列。 + * + * @param string $key 新的存储键(`covers/xxx.jpg`);空串 = 清掉题图 + */ + public static function setCover(int $eventId, string $key): void + { + $db = Database::get(); + $val = self::keyOrNull($key); + $sel = $db->prepare('SELECT COUNT(*) FROM trip_details WHERE event_id = ?'); + $sel->execute([$eventId]); + if ((int)$sel->fetchColumn() > 0) { + $db->prepare('UPDATE trip_details SET cover_key = ? WHERE event_id = ?') + ->execute([$val, $eventId]); + } else { + // 老旅程可能压根没有 trip_details 行(早于这张表的年份)⇒ 补一行 + $db->prepare( + 'INSERT INTO trip_details (cover_key, summary, is_public, event_id) VALUES (?, NULL, 1, ?)' + )->execute([$val, $eventId]); + } + } + public static function saveAttendees(int $eventId, array $personIds): void { $db = Database::get();