观影照片支持视频:转 H.264 + 抽首帧封面 (#84)

用户:确认一下观影照片是不是允许上传视频,如果不允许需要修改,
要求能上传视频、视频需要转码,参考旅程中的行程做法。

事实核查:原来不允许 —— POST /movies/<id>/photos 走 Upload::image(),
那条管线只收 JPG/PNG/GIF/WebP(要裁剪缩转运 JPEG),传视频直接拒。
旅程走 Attachment::save(),内部对 video/* 调 Video::toH264()。

Schema VERSION 35→36:
- event_photos 加 kind / thumb_key / mime 三列;
  kind 默认 image —— 老数据全是照片,默认值即正确,不需要回填 UPDATE;
- 建表语句同步改(新库直接是新结构)。

Video.php:
- 新增 poster() 抽首帧。取第 1 秒(手机视频开头常是黑场/对焦),
  宽 480 足够列表缩略;失败返回空 path 且**不阻断上传**。

ApiHandler.php 上传分支按 MIME 分流:
- 视频 → toH264 转码 + poster 抽封面 + Storage 落盘 videos/ 与 thumbs/;
  转码失败不阻断(存原视频,至少能下载后用本地播放器看);
- 图片 → 仍走 Upload::image(),裁剪压缩行为完全不变。
- 返回值加了 kind / thumb / mime,端上靠它画播放按钮。

Movie.php:
- photos() 带出 kind/thumb_key/mime;
- addPhotos() 兼容裸键数组与结构化数组,只有一条 INSERT 路径;
- deletePhoto() / destroy() 补删 thumb_key,否则删视频留孤儿封面。

验证(服务器实测):造 H.265 视频 → needsTranscode=true,toH264 成功
转成 h264/yuv420p,poster 抽出 13KB 封面;完整上传后接口返回 5 条
(4 image + 1 video 带 thumb)。迁移已跑(35→36),字段与数据核实无误,
测试视频及封面已清理,videos/ 与 thumbs/ 目录确认为空。
This commit is contained in:
2026-11-06 10:53:02 +08:00
parent 3d745b65b2
commit c6e4191bb3
4 changed files with 218 additions and 19 deletions
+31 -2
View File
@@ -188,7 +188,7 @@ final class Schema
* ⚠️ 它**不再**作为"要不要跑升级"的判据 —— 见 fingerprint() 的说明。
* 它仍然要手工维护,因为它给人看(排查时一眼知道代码到哪一版了)。
*/
public const VERSION = '35';
public const VERSION = '36';
/**
* 「源码指纹」:拿**本文件自身的内容**算一个哈希,作为"要不要跑升级"的判据。
@@ -539,12 +539,15 @@ CREATE TABLE IF NOT EXISTS `event_photos` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`event_id` INT UNSIGNED NOT NULL,
`key` VARCHAR(255) NOT NULL COMMENT '照片(存储相对键)',
`kind` ENUM('image','video') NOT NULL DEFAULT 'image' COMMENT '图片还是视频(老数据全是图片,默认值即正确)',
`thumb_key` VARCHAR(255) DEFAULT NULL COMMENT '视频首帧封面(存储相对键;图片为空)',
`mime` VARCHAR(100) DEFAULT NULL COMMENT 'MIME(视频转码后是 video/mp4;图片可空)',
`position` INT NOT NULL DEFAULT 100 COMMENT '展示先后',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_ep_event` (`event_id`, `position`),
CONSTRAINT `fk_ep_event` FOREIGN KEY (`event_id`) REFERENCES `events` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='事件的现场照片(可多张)'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='事件的现场照片/视频(可多张)'
SQL,
// ---- 统一附件(v12)----
// 旅程照片 / 资源附件 / 任务附件 / 评论附件,全都走这一张表:
@@ -1858,6 +1861,32 @@ SQL);
self::addColumn($db, 'trip_details', 'is_public',
"ADD COLUMN `is_public` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否公开显示(用户 2026-10-03 第 4 条):0=不显示在旅程列表与日历' AFTER `summary`");
// ---- 观影照片支持视频(版本 36,2026-11-06)----
/* 🔴 用户:「确认一下观影照片是不是允许上传视频,如果不允许,需要修改,
要求能够上传视频,视频需要转码,参考旅程中的行程做法。」
事实核查:**原来不允许**。`POST /movies/<id>/photos` 走的是
`Upload::image()`,那条管线只收 JPG/PNG/GIF/WebP(要裁剪缩放转 JPEG),
传视频会被直接拒("只支持 JPG / PNG / GIF / WebP 图片")。
旅程那边走的是 `Attachment::save()`,它内部对视频调 `Video::toH264()`
转成 H.264 再存 —— 所以观影要支持视频,得把照片也改走那条路。
改走 Attachment 后有三件事要落到 `event_photos` 上:
· `kind` —— 是图片还是视频。端上要靠它决定"显示图"还是"显示播放按钮";
(不靠扩展名猜:`.mp4` 也可能是图片的误命名,MIME 才靠谱)
· `thumb_key` —— 视频首帧封面。列表里不可能为每个视频解一次码
(一屏 9 个视频 × 解码 = 卡死),必须服务端抽一帧存成图;
· `mime` —— 冗余存一份,免得每次都要回查 Storage 或猜。
⚠️ 三列都给默认值:`kind` 默认 `'image'`,老数据(全是照片)自动正确,
不需要回填 UPDATE —— 这是最安全的一种迁移默认值。 */
self::addColumn($db, 'event_photos', 'kind',
"ADD COLUMN `kind` ENUM('image','video') NOT NULL DEFAULT 'image' COMMENT '图片还是视频(老数据全是图片,默认值即正确)' AFTER `key`");
self::addColumn($db, 'event_photos', 'thumb_key',
"ADD COLUMN `thumb_key` VARCHAR(255) DEFAULT NULL COMMENT '视频首帧封面(存储相对键;图片为空)' AFTER `kind`");
self::addColumn($db, 'event_photos', 'mime',
"ADD COLUMN `mime` VARCHAR(100) DEFAULT NULL COMMENT 'MIME(视频转码后是 video/mp4;图片可空)' AFTER `thumb_key`");
// ---- 纪念日(版本 13,用户第 13 条)----
// ⚠️ 名册里每个人的生日**不入这张表** —— 它们是实时从 persons 派生的
// (见 Anniversary::birthdays),改了生日纪念日就跟着变。