v8:观影落地(MovieLog 整体迁入统一事件原语)+ 三处 UI 撤除
- Schema v8:events / movie_details / event_attendees / event_photos 四张表, uk_event_source(family_id, source_ref) 作迁移幂等锚 - Movie 模型:时间线/详情/增删改、同行人整组替换、照片逐张管理、感受英→中映射 - ImageSlot 新增 poster(2:3) / ticket(free) / photo(free) 三槽位; free 槽位不裁剪、整图等比缩进框(只缩不放大) - OssDriver::copyObject:同桶 CopyObject(手写 V1 签名,x-oss-* 头按字典序进签名串) - 路由:movies 时间线/详情/新增/编辑/删除/补照片/删照片;同行人按家庭过滤; 访客只读(requireWrite) - UI 撤除:首页「我的一句话」、家谱页「补充我的亲属关系 / 看整个名册」两个按钮; 顶栏加「观影」入口 - 修潜伏 bug:ImageSlot::parseRatio 的正则用 / 作分隔符,字符类里的 / 把模式 提前截断(Unknown modifier ']')—— ratio() 只在上传 POST 里被调到,一直没被观察到 - 线上数据迁移(临时 token 门禁页,用完即删):MovieLog 219 条观影 → events, 14 位观影人新增进名册(带 MovieLog 头像),431 个 OSS 对象同桶拷贝 (movielog/uploads/<dir>/x → family/<dir>/x),review/feelings/影院三段/照片全量搬
This commit is contained in:
@@ -17,6 +17,7 @@ use App\Crypto;
|
||||
use App\Database;
|
||||
use App\Family;
|
||||
use App\Kinship;
|
||||
use App\Movie;
|
||||
use App\Nickname;
|
||||
use App\Person;
|
||||
use App\PersonGroup;
|
||||
@@ -1329,6 +1330,228 @@ switch ($r) {
|
||||
redirect('settings');
|
||||
break;
|
||||
|
||||
// ==================== 观影(v8) ====================
|
||||
//
|
||||
// MovieLog 整体迁入后的新家。数据落在统一事件原语上:
|
||||
// events(骨架)+ movie_details(卫星)+ event_attendees + event_photos,
|
||||
// 见 App\Movie。可见性与名册同一口径:能登录就能看,能写才能改;
|
||||
// 迁进来的历史记录 source_ref = 'movielog:movie:<id>'(重跑迁移的幂等锚点)。
|
||||
|
||||
/** 观影时间线 —— 按年分组倒序。 */
|
||||
case 'movies':
|
||||
$familyId = (int)Auth::familyId();
|
||||
$rows = Movie::all($familyId);
|
||||
$years = [];
|
||||
foreach ($rows as $r) {
|
||||
$y = $r['starts_at'] !== null ? (int)substr((string)$r['starts_at'], 0, 4) : 0;
|
||||
$years[$y][] = $r;
|
||||
}
|
||||
unset($r);
|
||||
krsort($years);
|
||||
View::render('movies/index', [
|
||||
'title' => '观影记录',
|
||||
'years' => $years,
|
||||
'total' => count($rows),
|
||||
], 'movies');
|
||||
break;
|
||||
|
||||
case 'movies/show':
|
||||
$familyId = (int)Auth::familyId();
|
||||
$m = Movie::find($familyId, (int)($_GET['id'] ?? 0));
|
||||
if (!$m) {
|
||||
flash('err', '找不到这条观影记录。');
|
||||
redirect('movies');
|
||||
}
|
||||
View::render('movies/show', [
|
||||
'title' => (string)$m['title'],
|
||||
'm' => $m,
|
||||
], 'movies');
|
||||
break;
|
||||
|
||||
case 'movies/new':
|
||||
Auth::requireWrite();
|
||||
View::render('movies/form', [
|
||||
'title' => '记一场电影',
|
||||
'm' => null,
|
||||
'people' => Person::roster((int)Auth::familyId(), (int)Auth::personId(), 'member', ''),
|
||||
'errors' => [],
|
||||
], 'movies');
|
||||
break;
|
||||
|
||||
case 'movies/edit':
|
||||
Auth::requireWrite();
|
||||
$familyId = (int)Auth::familyId();
|
||||
$m = Movie::find($familyId, (int)($_GET['id'] ?? 0));
|
||||
if (!$m) {
|
||||
flash('err', '找不到这条观影记录。');
|
||||
redirect('movies');
|
||||
}
|
||||
View::render('movies/form', [
|
||||
'title' => '编辑观影记录',
|
||||
'm' => $m,
|
||||
'people' => Person::roster($familyId, (int)Auth::personId(), 'member', ''),
|
||||
'errors' => [],
|
||||
], 'movies');
|
||||
break;
|
||||
|
||||
/** 收集表单里和观影有关的字段(create / update 共用一份)。 */
|
||||
case 'movies/create':
|
||||
case 'movies/update':
|
||||
$familyId = (int)Auth::familyId();
|
||||
$me = (int)Auth::personId();
|
||||
Auth::requireWrite();
|
||||
csrf_check();
|
||||
|
||||
$isEdit = $r === 'movies/update';
|
||||
$id = $isEdit ? (int)($_POST['id'] ?? 0) : 0;
|
||||
$m = $isEdit ? Movie::find($familyId, $id) : null;
|
||||
if ($isEdit && !$m) {
|
||||
flash('err', '找不到这条观影记录。');
|
||||
redirect('movies');
|
||||
}
|
||||
|
||||
$d = [
|
||||
'title' => trim((string)($_POST['title'] ?? '')),
|
||||
'starts_at' => trim((string)($_POST['starts_at'] ?? '')),
|
||||
'location' => trim((string)($_POST['location'] ?? '')),
|
||||
'note' => trim((string)($_POST['note'] ?? '')),
|
||||
'info' => trim((string)($_POST['info'] ?? '')),
|
||||
'rating' => trim((string)($_POST['rating'] ?? '')),
|
||||
'review' => trim((string)($_POST['review'] ?? '')),
|
||||
'feelings' => array_values((array)($_POST['feelings'] ?? [])),
|
||||
'cinema_place' => trim((string)($_POST['cinema_place'] ?? '')),
|
||||
'cinema_name' => trim((string)($_POST['cinema_name'] ?? '')),
|
||||
'cinema_address' => trim((string)($_POST['cinema_address'] ?? '')),
|
||||
// 同行的人:只认「确实存在且属于这个家」的 id —— 表单可以改,
|
||||
// 不能凭它把别人家的成员挂进自己家的观影记录里(会顺着详情页漏人名)。
|
||||
'attendees' => array_map('intval', (array)($_POST['attendees'] ?? [])),
|
||||
];
|
||||
$_familyPeople = Person::map($familyId);
|
||||
$d['attendees'] = array_values(array_filter($d['attendees'],
|
||||
static fn($pid) => isset($_familyPeople[$pid])));
|
||||
$errors = $d['title'] === '' ? ['title' => '片名不能为空'] : [];
|
||||
|
||||
// ⚠️ 图片上传失败**不挡保存**(与头像同一思路:一条记录因为一张 5MB 原图
|
||||
// 存不进去,看起来像功能坏了)。现在是先存记录,图片的事单独提示。
|
||||
// 海报 / 票根:free 槽位,不需要裁剪框;编辑时换新图就顺带删掉旧对象。
|
||||
$warns = [];
|
||||
foreach (['poster', 'ticket'] as $slot) {
|
||||
$up = Upload::image($familyId, $_FILES[$slot] ?? [], $slot);
|
||||
if ($up['ok']) {
|
||||
$oldKey = $isEdit ? (string)($m[$slot === 'poster' ? 'poster_key' : 'ticket_key'] ?? '') : '';
|
||||
$d[$slot === 'poster' ? 'poster_key' : 'ticket_key'] = (string)$up['file'];
|
||||
if ($oldKey !== '' && $oldKey !== (string)$up['file']) {
|
||||
\App\Storage\Storage::delete($familyId, $oldKey);
|
||||
}
|
||||
} elseif ((string)($up['error'] ?? '') !== '') {
|
||||
$warns[] = ($slot === 'poster' ? '海报' : '票根') . '没有保存:' . $up['error'];
|
||||
} elseif ($isEdit) {
|
||||
// 没传新图 → 保留库里的旧值
|
||||
$d[$slot === 'poster' ? 'poster_key' : 'ticket_key'] = (string)($m[$slot === 'poster' ? 'poster_key' : 'ticket_key'] ?? '');
|
||||
}
|
||||
}
|
||||
// 现场照片(多选,仅新增表单提供;编辑页在详情页里逐张补/删)
|
||||
if (!$isEdit) {
|
||||
$photoFiles = [];
|
||||
if (!empty($_FILES['photo']) && is_array($_FILES['photo']['name'])) {
|
||||
foreach ($_FILES['photo']['name'] as $i => $_n) {
|
||||
$photoFiles[] = [
|
||||
'name' => $_FILES['photo']['name'][$i],
|
||||
'type' => $_FILES['photo']['type'][$i],
|
||||
'tmp_name' => $_FILES['photo']['tmp_name'][$i],
|
||||
'error' => $_FILES['photo']['error'][$i],
|
||||
'size' => $_FILES['photo']['size'][$i],
|
||||
];
|
||||
}
|
||||
}
|
||||
$photoKeys = [];
|
||||
foreach ($photoFiles as $f) {
|
||||
$up = Upload::image($familyId, $f, 'photo');
|
||||
if ($up['ok']) {
|
||||
$photoKeys[] = (string)$up['file'];
|
||||
} elseif ((string)($up['error'] ?? '') !== '') {
|
||||
$warns[] = '有一张照片没有保存:' . $up['error'];
|
||||
}
|
||||
}
|
||||
$d['photos'] = $photoKeys;
|
||||
}
|
||||
|
||||
if ($errors) {
|
||||
set_old($_POST);
|
||||
View::render('movies/form', [
|
||||
'title' => $isEdit ? '编辑观影记录' : '记一场电影',
|
||||
'm' => $m,
|
||||
'people' => Person::roster($familyId, $me, 'member', ''),
|
||||
'errors' => $errors,
|
||||
], 'movies');
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($isEdit) {
|
||||
Movie::update($familyId, $id, $d);
|
||||
clear_old();
|
||||
flash('ok', '已保存。' . ($warns ? '(' . implode(';', $warns) . ')' : ''));
|
||||
redirect('movies/show', ['id' => $id]);
|
||||
} else {
|
||||
$newId = Movie::create($familyId, $me, $d);
|
||||
clear_old();
|
||||
flash('ok', '已记下这场电影。' . ($warns ? '(' . implode(';', $warns) . ')' : ''));
|
||||
redirect('movies/show', ['id' => $newId]);
|
||||
}
|
||||
break;
|
||||
|
||||
/** 编辑页补传一张现场照片(详情页里的小表单,一次一张)。 */
|
||||
case 'movies/photo_add':
|
||||
$familyId = (int)Auth::familyId();
|
||||
Auth::requireWrite();
|
||||
csrf_check();
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
if (!Movie::find($familyId, $id)) {
|
||||
flash('err', '找不到这条观影记录。');
|
||||
redirect('movies');
|
||||
}
|
||||
$up = Upload::image($familyId, $_FILES['photo'] ?? [], 'photo');
|
||||
if ($up['ok']) {
|
||||
// savePhotos 是整组替换 —— 现有键取出来再追加一张,位置接着排
|
||||
$keys = array_map('strval', array_column(Movie::photos($id), 'key'));
|
||||
$keys[] = (string)$up['file'];
|
||||
Movie::savePhotos($id, $keys);
|
||||
flash('ok', '照片已加上。');
|
||||
} else {
|
||||
flash('err', '照片没有保存:' . (string)($up['error'] ?: '没选文件'));
|
||||
}
|
||||
redirect('movies/show', ['id' => $id]);
|
||||
break;
|
||||
|
||||
case 'movies/photo_del':
|
||||
$familyId = (int)Auth::familyId();
|
||||
Auth::requireWrite();
|
||||
csrf_check();
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$pid = (int)($_POST['photo_id'] ?? 0);
|
||||
if (!Movie::find($familyId, $id)) {
|
||||
flash('err', '找不到这条观影记录。');
|
||||
redirect('movies');
|
||||
}
|
||||
Movie::deletePhoto($familyId, $id, $pid);
|
||||
flash('ok', '照片已删除。');
|
||||
redirect('movies/show', ['id' => $id]);
|
||||
break;
|
||||
|
||||
case 'movies/destroy':
|
||||
$familyId = (int)Auth::familyId();
|
||||
Auth::requireWrite();
|
||||
csrf_check();
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
if (!Movie::find($familyId, $id)) {
|
||||
flash('err', '找不到这条观影记录。');
|
||||
redirect('movies');
|
||||
}
|
||||
Movie::destroy($familyId, $id);
|
||||
flash('ok', '这条观影记录已删除。');
|
||||
redirect('movies');
|
||||
break;
|
||||
|
||||
default:
|
||||
http_response_code(404);
|
||||
View::render('error', [
|
||||
|
||||
Reference in New Issue
Block a user