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
+45 -5
View File
@@ -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/<id>`,那个分支的 `$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/<id>/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();