v9: 观影对齐 MovieLog —— 瀑布流/影院管理/评价评论/起止时间
- events 起止时间精确到秒(starts_at DATE→DATETIME + ends_at),_datetime.php 复用 - 影院独立成表(cinemas,地方/名称/地址/主题色 + 50 色板),movie_details 归一为 cinema_id - 评价模型 MovieLog 同款:movie_reviews 每账号每片一条(1-10 分+正文+感受 key), movie_review_comments 两级平铺评论(回复的回复规范化到顶层、跨评价回复拦截) - 感受目录迁到 MovieReview::CATALOG(12 种,Emoji 11.0 老码位) - 观影列表改瀑布流(CSS grid 海报墙),详情页五大块 + 灯箱 + 评价区 - 升级路径:v8 老数据自动归一(影院三段按 (family,place,name) 去重建行、 评价回填、中文感受反查英文 key),回填全部幂等;老库补 cinema_id 列+索引+外键 - 修三 bug:建表顺序(cinemas 必须在 movie_details 前,否则全新安装 1215)、 layout 导航用键名当路由(影院链接指向 r=cinemas)、 _datetime.php 分钟循环变量 $m 覆盖宿主模板的观影记录(编辑页简介/备注回填被清空)
This commit is contained in:
+90
-63
@@ -3,16 +3,18 @@
|
||||
* 观影记录(统一事件原语的第一个落地种类)。
|
||||
*
|
||||
* 归一模型(见 familybook.md 的核心设计):
|
||||
* events = 一切记录的共同骨架(标题 / 日期 / 地点 / 来源引用)
|
||||
* movie_details = 观影特有的卫星表(海报 / 票根 / 评分 / 影评 / 影院三段)
|
||||
* events = 一切记录的共同骨架(标题 / 起止时刻 / 地点 / 来源引用)
|
||||
* movie_details = 观影特有的卫星表(海报 / 票根 / 关联影院)
|
||||
* attendees = 同行观影的人(persons.id)
|
||||
* photos = 现场照片(可多张)
|
||||
* reviews = 评价与讨论(movie_reviews / movie_review_comments,见 MovieReview)
|
||||
* cinemas = 影院独立管理(见 Cinema)
|
||||
*
|
||||
* 图片一律存**相对键**(如 posters/xxx.jpg),地址由 media.php 代理 ——
|
||||
* 本地与 OSS 的差别被 Storage 吃掉,这里永远不碰完整 URL。
|
||||
*
|
||||
* source_ref 是迁移幂等的锚点:MovieLog 整体迁入时写成 'movielog:movie:<id>',
|
||||
* 重跑迁移按它先删后插,不会出现一半新一半旧的中间态。
|
||||
* 重跑迁移按它先查再插,不会出现一半新一半旧的中间态。
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
@@ -21,49 +23,48 @@ namespace App;
|
||||
|
||||
final class Movie
|
||||
{
|
||||
/** 观后感受标签(MovieLog 迁移时把英文键映射成中文,这里就是唯一清单) */
|
||||
public const FEELINGS = ['温暖', '激动', '欢乐', '深思', '伤感', '震撼', '治愈', '无聊'];
|
||||
|
||||
/** MovieLog 的英文感受键 → 中文标签(没见过的键原样保留) */
|
||||
public const FEELING_MAP = [
|
||||
'warm' => '温暖',
|
||||
'excited' => '激动',
|
||||
'thoughtful' => '深思',
|
||||
'sad' => '伤感',
|
||||
'funny' => '欢乐',
|
||||
'happy' => '欢乐',
|
||||
'boring' => '无聊',
|
||||
'moved' => '治愈',
|
||||
'shocked' => '震撼',
|
||||
];
|
||||
|
||||
/** @return array<int,array> 按年份倒序的全部观影记录(时间线用) */
|
||||
public static function all(int $familyId): array
|
||||
/** @return array<int,array> 观影记录,按开场时刻倒序(瀑布流用)。
|
||||
* $keyword 非空时搜:片名 / 影院名 / 同行人名(LIKE)。 */
|
||||
public static function all(int $familyId, string $keyword = ''): array
|
||||
{
|
||||
$st = Database::get()->prepare(
|
||||
'SELECT e.id, e.title, e.starts_at, e.location, e.note,
|
||||
d.poster_key, d.ticket_key, d.rating, d.cinema_name, d.cinema_place,
|
||||
$sql =
|
||||
'SELECT e.id, e.title, e.starts_at, e.ends_at, e.note,
|
||||
d.poster_key, c.name AS cinema_name, c.color AS cinema_color,
|
||||
(SELECT COUNT(*) FROM event_attendees a WHERE a.event_id = e.id) AS attendee_count,
|
||||
(SELECT GROUP_CONCAT(p.name ORDER BY a.position, p.id SEPARATOR "、")
|
||||
FROM event_attendees a JOIN persons p ON p.id = a.person_id
|
||||
WHERE a.event_id = e.id) AS attendee_names
|
||||
FROM events e
|
||||
LEFT JOIN movie_details d ON d.event_id = e.id
|
||||
WHERE e.family_id = ? AND e.kind = "movie"
|
||||
ORDER BY e.starts_at DESC, e.id DESC'
|
||||
);
|
||||
$st->execute([$familyId]);
|
||||
return $st->fetchAll();
|
||||
LEFT JOIN cinemas c ON c.id = d.cinema_id
|
||||
WHERE e.family_id = ? AND e.kind = "movie"';
|
||||
$bind = [$familyId];
|
||||
$kw = trim($keyword);
|
||||
if ($kw !== '') {
|
||||
$sql .= ' AND (e.title LIKE ? OR c.name LIKE ? OR e.location LIKE ?
|
||||
OR EXISTS (SELECT 1 FROM event_attendees a JOIN persons p ON p.id = a.person_id
|
||||
WHERE a.event_id = e.id AND p.name LIKE ?))';
|
||||
$like = '%' . $kw . '%';
|
||||
array_push($bind, $like, $like, $like, $like);
|
||||
}
|
||||
$sql .= ' ORDER BY e.starts_at DESC, e.id DESC';
|
||||
$st = Database::get()->prepare($sql);
|
||||
$st->execute($bind);
|
||||
$rows = $st->fetchAll();
|
||||
MovieReview::attachStats($rows);
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/** 一条完整记录(详情/编辑页用):事件 + 卫星 + 同行人 + 照片 */
|
||||
/** 一条完整记录(详情/编辑页用):事件 + 卫星 + 影院 + 同行人 + 照片 */
|
||||
public static function find(int $familyId, int $id): ?array
|
||||
{
|
||||
$st = Database::get()->prepare(
|
||||
'SELECT e.*, d.info, d.poster_key, d.ticket_key, d.rating, d.review, d.feelings,
|
||||
d.cinema_place, d.cinema_name, d.cinema_address
|
||||
'SELECT e.*, d.info, d.poster_key, d.ticket_key, d.cinema_id,
|
||||
c.place AS cinema_place, c.name AS cinema_name,
|
||||
c.address AS cinema_address, c.color AS cinema_color
|
||||
FROM events e
|
||||
LEFT JOIN movie_details d ON d.event_id = e.id
|
||||
LEFT JOIN cinemas c ON c.id = d.cinema_id
|
||||
WHERE e.family_id = ? AND e.id = ? AND e.kind = "movie"'
|
||||
);
|
||||
$st->execute([$familyId, $id]);
|
||||
@@ -100,18 +101,18 @@ final class Movie
|
||||
|
||||
/**
|
||||
* 新建一条观影记录,返回事件 id。
|
||||
* $d 字段:title, starts_at(Y-m-d), location, note, info, rating, review,
|
||||
* feelings(array), cinema_place, cinema_name, cinema_address,
|
||||
* poster_key, ticket_key, attendees(int[]), photos(string[])
|
||||
* $d 字段:title, starts_at(Y-m-d H:i:s), ends_at, location, note, info,
|
||||
* cinema_id(int|null), poster_key, ticket_key, attendees(int[]), photos(string[])
|
||||
*/
|
||||
public static function create(int $familyId, ?int $creatorId, array $d): int
|
||||
{
|
||||
$db = Database::get();
|
||||
$db->prepare(
|
||||
'INSERT INTO events (family_id, creator_id, kind, title, starts_at, location, note, source_ref)
|
||||
VALUES (?, ?, "movie", ?, ?, ?, ?, ?)'
|
||||
'INSERT INTO events (family_id, creator_id, kind, title, starts_at, ends_at, location, note, source_ref)
|
||||
VALUES (?, ?, "movie", ?, ?, ?, ?, ?, ?)'
|
||||
)->execute([
|
||||
$familyId, $creatorId, (string)$d['title'], self::dateOrNull($d['starts_at'] ?? ''),
|
||||
$familyId, $creatorId, (string)$d['title'],
|
||||
self::dtOrNull($d['starts_at'] ?? ''), self::dtOrNull($d['ends_at'] ?? ''),
|
||||
self::locOrNull($d['location'] ?? ''), self::textOrNull($d['note'] ?? ''),
|
||||
$d['source_ref'] ?? null,
|
||||
]);
|
||||
@@ -126,9 +127,11 @@ final class Movie
|
||||
{
|
||||
$db = Database::get();
|
||||
$db->prepare(
|
||||
'UPDATE events SET title = ?, starts_at = ?, location = ?, note = ? WHERE id = ? AND family_id = ?'
|
||||
'UPDATE events SET title = ?, starts_at = ?, ends_at = ?, location = ?, note = ?
|
||||
WHERE id = ? AND family_id = ?'
|
||||
)->execute([
|
||||
(string)$d['title'], self::dateOrNull($d['starts_at'] ?? ''),
|
||||
(string)$d['title'],
|
||||
self::dtOrNull($d['starts_at'] ?? ''), self::dtOrNull($d['ends_at'] ?? ''),
|
||||
self::locOrNull($d['location'] ?? ''), self::textOrNull($d['note'] ?? ''),
|
||||
$id, $familyId,
|
||||
]);
|
||||
@@ -140,35 +143,22 @@ final class Movie
|
||||
/** 卫星表 upsert(编辑时可能还没建过这行) */
|
||||
private static function saveDetails(int $eventId, array $d): void
|
||||
{
|
||||
$db = Database::get();
|
||||
$feel = array_values(array_filter(array_map(
|
||||
static fn($f) => mb_substr(trim((string)$f), 0, 20, 'UTF-8'),
|
||||
(array)($d['feelings'] ?? [])
|
||||
), static fn($f) => $f !== ''));
|
||||
$row = [
|
||||
$db = Database::get();
|
||||
$cid = (int)($d['cinema_id'] ?? 0);
|
||||
$row = [
|
||||
self::textOrNull($d['info'] ?? ''),
|
||||
self::keyOrNull($d['poster_key'] ?? ''),
|
||||
self::keyOrNull($d['ticket_key'] ?? ''),
|
||||
isset($d['rating']) && $d['rating'] !== '' ? max(0, min(10, (int)$d['rating'])) : null,
|
||||
self::textOrNull($d['review'] ?? ''),
|
||||
$feel ? json_encode($feel, JSON_UNESCAPED_UNICODE) : null,
|
||||
self::locOrNull($d['cinema_place'] ?? ''),
|
||||
self::locOrNull($d['cinema_name'] ?? ''),
|
||||
self::textOrNull($d['cinema_address'] ?? ''),
|
||||
$cid > 0 ? $cid : null,
|
||||
];
|
||||
$has = $db->prepare('SELECT COUNT(*) FROM movie_details WHERE event_id = ?');
|
||||
$has->execute([$eventId]);
|
||||
if ((int)$has->fetchColumn() > 0) {
|
||||
$db->prepare(
|
||||
'UPDATE movie_details SET info=?, poster_key=?, ticket_key=?, rating=?, review=?,
|
||||
feelings=?, cinema_place=?, cinema_name=?, cinema_address=? WHERE event_id=?'
|
||||
)->execute([...$row, $eventId]);
|
||||
$db->prepare('UPDATE movie_details SET info=?, poster_key=?, ticket_key=?, cinema_id=? WHERE event_id=?')
|
||||
->execute([...$row, $eventId]);
|
||||
} else {
|
||||
$db->prepare(
|
||||
'INSERT INTO movie_details (info, poster_key, ticket_key, rating, review,
|
||||
feelings, cinema_place, cinema_name, cinema_address, event_id)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?)'
|
||||
)->execute([...$row, $eventId]);
|
||||
$db->prepare('INSERT INTO movie_details (info, poster_key, ticket_key, cinema_id, event_id) VALUES (?,?,?,?,?)')
|
||||
->execute([...$row, $eventId]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,12 +236,49 @@ final class Movie
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------- 展示格式化 ----------------
|
||||
|
||||
/**
|
||||
* 观影时刻的人话展示:
|
||||
* 只有日期(时间恰好 00:00:00 且无散场)→「2026年09月30日」;
|
||||
* 有开场时刻 →「2026年09月30日 19:30:00」;有散场 → 追加「 – 21:45:12」。
|
||||
* 两个都空 →「日期待补」。
|
||||
*/
|
||||
public static function formatWatch(?string $start, ?string $end): string
|
||||
{
|
||||
if ($start === null || $start === '') {
|
||||
return '日期待补';
|
||||
}
|
||||
$ts = strtotime($start);
|
||||
if ($ts === false) {
|
||||
return '日期待补';
|
||||
}
|
||||
$out = date('Y年m月d日', $ts);
|
||||
$midnight = substr((string)$start, 11) === '00:00:00';
|
||||
if (!$midnight) {
|
||||
$out .= ' ' . date('H:i:s', $ts);
|
||||
}
|
||||
if ($end !== null && $end !== '') {
|
||||
$ets = strtotime($end);
|
||||
if ($ets !== false) {
|
||||
if (date('Y-m-d', $ets) !== date('Y-m-d', $ts)) {
|
||||
$out = date('Y年m月d日', $ts) . ($midnight ? '' : ' ' . date('H:i:s', $ts))
|
||||
. ' – ' . date('Y年m月d日 H:i:s', $ets);
|
||||
} else {
|
||||
$out .= ' – ' . date('H:i:s', $ets);
|
||||
}
|
||||
}
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
// ---------------- 小工具 ----------------
|
||||
|
||||
private static function dateOrNull(string $v): ?string
|
||||
/** 'Y-m-d H:i:s' 校验(秒级精确);空串/不合法回 NULL */
|
||||
private static function dtOrNull(string $v): ?string
|
||||
{
|
||||
$v = trim($v);
|
||||
if ($v === '' || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $v)) {
|
||||
if ($v === '' || !preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', $v)) {
|
||||
return null;
|
||||
}
|
||||
return $v;
|
||||
|
||||
Reference in New Issue
Block a user