观影照片支持视频:转 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:
+62
-12
@@ -133,8 +133,16 @@ final class Movie
|
||||
|
||||
public static function photos(int $eventId): array
|
||||
{
|
||||
/* 🔴 2026-11-06:带上 `kind` / `thumb_key` / `mime`(观影照片支持视频)。
|
||||
⚠️ 用 `COALESCE` 兜底 `kind`:极老的库(迁移没跑过)可能没这列,
|
||||
但这种情形会直接报 1054 —— 迁移一定会先跑,这里不加 COALESCE。
|
||||
真正需要兜底的是**值为 NULL** 的历史行(迁移加列时默认 'image',
|
||||
但手写过 INSERT 的行可能是 NULL)。 */
|
||||
$st = Database::get()->prepare(
|
||||
'SELECT id, `key` FROM event_photos WHERE event_id = ? ORDER BY position, id'
|
||||
"SELECT id, `key`,
|
||||
COALESCE(`kind`, 'image') AS kind,
|
||||
`thumb_key`, `mime`
|
||||
FROM event_photos WHERE event_id = ? ORDER BY position, id"
|
||||
);
|
||||
$st->execute([$eventId]);
|
||||
return $st->fetchAll();
|
||||
@@ -294,14 +302,34 @@ final class Movie
|
||||
*/
|
||||
public static function addPhotos(int $eventId, array $keys): void
|
||||
{
|
||||
$clean = [];
|
||||
foreach ($keys as $k) {
|
||||
$k = trim((string)$k);
|
||||
if ($k !== '') {
|
||||
$clean[] = $k;
|
||||
/* ⚠️ 兼容两种入参(2026-11-06 起):
|
||||
· `['a.jpg', 'b.jpg']` —— 老调用方(网页表单、端上旧版)
|
||||
· `[['key'=>'a.mp4','kind'=>'video','thumb_key'=>'…','mime'=>'video/mp4']]`
|
||||
—— 新调用方(带类型信息)
|
||||
统一归一成"每项一个数组",下面只有一条 INSERT 路径,
|
||||
不会出现"有的走老分支有的走新分支"那种分叉。 */
|
||||
$rows = [];
|
||||
foreach ($keys as $item) {
|
||||
if (is_array($item)) {
|
||||
$k = trim((string)($item['key'] ?? ''));
|
||||
if ($k === '') {
|
||||
continue;
|
||||
}
|
||||
$rows[] = [
|
||||
'key' => $k,
|
||||
'kind' => ((string)($item['kind'] ?? 'image')) === 'video' ? 'video' : 'image',
|
||||
'thumb_key' => (string)($item['thumb_key'] ?? ''),
|
||||
'mime' => (string)($item['mime'] ?? ''),
|
||||
];
|
||||
} else {
|
||||
$k = trim((string)$item);
|
||||
if ($k === '') {
|
||||
continue;
|
||||
}
|
||||
$rows[] = ['key' => $k, 'kind' => 'image', 'thumb_key' => '', 'mime' => ''];
|
||||
}
|
||||
}
|
||||
if ($clean === []) {
|
||||
if ($rows === []) {
|
||||
return;
|
||||
}
|
||||
$db = Database::get();
|
||||
@@ -309,24 +337,41 @@ final class Movie
|
||||
$st = $db->prepare('SELECT COALESCE(MAX(position), 0) FROM event_photos WHERE event_id = ?');
|
||||
$st->execute([$eventId]);
|
||||
$pos = (int)$st->fetchColumn();
|
||||
$ins = $db->prepare('INSERT INTO event_photos (event_id, `key`, position) VALUES (?, ?, ?)');
|
||||
foreach ($clean as $k) {
|
||||
$ins = $db->prepare(
|
||||
'INSERT INTO event_photos (event_id, `key`, kind, thumb_key, mime, position)
|
||||
VALUES (?, ?, ?, ?, ?, ?)'
|
||||
);
|
||||
foreach ($rows as $r) {
|
||||
$pos += 10;
|
||||
$ins->execute([$eventId, $k, $pos]);
|
||||
$ins->execute([
|
||||
$eventId, $r['key'], $r['kind'],
|
||||
$r['thumb_key'] !== '' ? $r['thumb_key'] : null,
|
||||
$r['mime'] !== '' ? $r['mime'] : null,
|
||||
$pos,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/** 删一张现场照片(连带删存储对象) */
|
||||
public static function deletePhoto(int $familyId, int $eventId, int $photoId): void
|
||||
{
|
||||
$st = Database::get()->prepare('SELECT `key` FROM event_photos WHERE id = ? AND event_id = ?');
|
||||
/* 🔴 2026-11-06:视频的**封面**也要一起删(`thumb_key`),
|
||||
否则每删一个视频就在存储里留一张孤儿封面。 */
|
||||
$st = Database::get()->prepare(
|
||||
'SELECT `key`, `thumb_key` FROM event_photos WHERE id = ? AND event_id = ?'
|
||||
);
|
||||
$st->execute([$photoId, $eventId]);
|
||||
$key = (string)($st->fetchColumn() ?: '');
|
||||
$row = $st->fetch();
|
||||
$key = $row ? (string)($row['key'] ?? '') : '';
|
||||
$thumb = $row ? (string)($row['thumb_key'] ?? '') : '';
|
||||
Database::get()->prepare('DELETE FROM event_photos WHERE id = ? AND event_id = ?')
|
||||
->execute([$photoId, $eventId]);
|
||||
if ($key !== '') {
|
||||
Storage\Storage::delete($familyId, $key);
|
||||
}
|
||||
if ($thumb !== '' && $thumb !== $key) {
|
||||
Storage\Storage::delete($familyId, $thumb);
|
||||
}
|
||||
}
|
||||
|
||||
/** 删除整条观影记录(events 级联带走卫星/同行人/照片行;存储对象手动清) */
|
||||
@@ -335,6 +380,11 @@ final class Movie
|
||||
$keys = [];
|
||||
foreach (self::photos($id) as $p) {
|
||||
$keys[] = (string)$p['key'];
|
||||
/* 视频的封面也要清(2026-11-06) */
|
||||
$tk = (string)($p['thumb_key'] ?? '');
|
||||
if ($tk !== '') {
|
||||
$keys[] = $tk;
|
||||
}
|
||||
}
|
||||
$st = Database::get()->prepare(
|
||||
'SELECT poster_key, ticket_key FROM movie_details WHERE event_id = ?'
|
||||
|
||||
Reference in New Issue
Block a user