v10: 修影院编辑回填布尔化 bug;评分改 10 星制;观影人改弹窗+拼音首字母联想;豆瓣自动补充迁移(movie_details.douban_meta,schema v10)

This commit is contained in:
2026-10-01 10:21:45 +08:00
parent 746f02581e
commit 2c36335c3e
8 changed files with 1050 additions and 39 deletions
+133
View File
@@ -16,6 +16,7 @@ use App\Birthday;
use App\Cinema;
use App\Crypto;
use App\Database;
use App\Douban;
use App\Family;
use App\Kinship;
use App\Movie;
@@ -1437,6 +1438,26 @@ switch ($r) {
// 不能凭它把别人家的成员挂进自己家的观影记录里(会顺着详情页漏人名)。
'attendees' => array_map('intval', (array)($_POST['attendees'] ?? [])),
];
// 豆瓣元数据(v10):表单隐藏字段带 JSON,解码后只认白名单键、逐字段限长,
// 防手搓请求往库里塞垃圾。整体一个数组交给模型存 douban_meta。
$doubanMeta = [];
$metaRaw = trim((string)($_POST['douban_meta'] ?? ''));
if ($metaRaw !== '') {
$metaDecoded = json_decode($metaRaw, true);
if (is_array($metaDecoded)) {
foreach (['douban_id', 'douban_url', 'year', 'genre', 'runtime', 'director',
'actors', 'plot', 'rating', 'country', 'alias'] as $mk) {
if (isset($metaDecoded[$mk]) && is_scalar($metaDecoded[$mk])) {
$mv = trim((string)$metaDecoded[$mk]);
if ($mv !== '') {
$doubanMeta[$mk] = mb_substr($mv, 0, 2000, 'UTF-8');
}
}
}
}
}
$d['douban_meta'] = $doubanMeta;
$_familyPeople = Person::map($familyId);
$d['attendees'] = array_values(array_filter($d['attendees'],
static fn($pid) => isset($_familyPeople[$pid])));
@@ -1467,6 +1488,22 @@ switch ($r) {
$d[$slot === 'poster' ? 'poster_key' : 'ticket_key'] = (string)($m[$slot === 'poster' ? 'poster_key' : 'ticket_key'] ?? '');
}
}
// 豆瓣抓取的海报(v10):前端抓取成功时把存储键放进了隐藏 poster_key。
// 用户**自己选的文件优先**($_FILES['poster'] 真有文件就不覆盖);
// 键必须是 posters/ 下的本站存储键,编辑时换用新键就顺带删掉旧对象。
$dbPosterKey = trim((string)($_POST['poster_key'] ?? ''));
$posterFileGiven = isset($_FILES['poster']['error']) && !is_array($_FILES['poster']['error'])
&& (int)$_FILES['poster']['error'] === UPLOAD_ERR_OK;
if (!$posterFileGiven && $dbPosterKey !== ''
&& preg_match('#^posters/[A-Za-z0-9._-]+$#', $dbPosterKey)) {
if (($d['poster_key'] ?? '') !== $dbPosterKey) {
$oldPosterKey = $isEdit ? (string)($m['poster_key'] ?? '') : '';
$d['poster_key'] = $dbPosterKey;
if ($oldPosterKey !== '' && $oldPosterKey !== $dbPosterKey) {
\App\Storage\Storage::delete($familyId, $oldPosterKey);
}
}
}
// 现场照片(多选,仅新增表单提供;编辑页在详情页里逐张补/删)
if (!$isEdit) {
$photoFiles = [];
@@ -1570,6 +1607,102 @@ switch ($r) {
redirect('movies');
break;
// ---------------- 豆瓣自动补充(v10,MovieLog fetch_movie 同源迁移) ----------------
/** 片名联想 / 缩略图代理 / 抓取详情+海报入库。全部回 JSON(thumb 回图片字节)。 */
case 'movies/douban':
$familyId = (int)Auth::familyId();
$act = (string)($_GET['action'] ?? '');
if (!Auth::check()) {
http_response_code(401);
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['error' => '未登录或登录已过期,请重新登录后再试'], JSON_UNESCAPED_UNICODE);
break;
}
if ($act === 'suggest') {
header('Content-Type: application/json; charset=utf-8');
$res = Douban::suggest((string)($_GET['q'] ?? ''));
echo json_encode($res, JSON_UNESCAPED_UNICODE);
break;
}
if ($act === 'thumb') {
// 浏览器不能直连豆瓣图片(防盗链),由服务端代取;只读、不落盘
$img = Douban::thumb((string)($_GET['u'] ?? ''));
if ($img === null) {
http_response_code(404);
break;
}
header('Content-Type: ' . $img['mime']);
header('Content-Length: ' . strlen($img['body']));
header('Cache-Control: public, max-age=86400');
echo $img['body'];
break;
}
if ($act !== '') {
http_response_code(400);
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['error' => '不支持的 action'], JSON_UNESCAPED_UNICODE);
break;
}
// 默认:抓取详情 + 海报入库(写存储,访客不行)。JSON 错误而非 302,
// 前端才能在会话过期/无权限时给出明确提示。
header('Content-Type: application/json; charset=utf-8');
if (!Auth::canWrite()) {
http_response_code(403);
echo json_encode(['error' => '访客账号没有写权限'], JSON_UNESCAPED_UNICODE);
break;
}
$idDb = trim((string)($_GET['id'] ?? ''));
$qDb = trim((string)($_GET['q'] ?? ''));
if ($idDb !== '') {
$resDb = Douban::fetchById($idDb);
} elseif ($qDb !== '') {
$resDb = Douban::fetchByTitle($qDb);
} else {
$resDb = ['ok' => false, 'error' => '请输入电影名称'];
}
if (!$resDb['ok']) {
echo json_encode(['error' => $resDb['error']], JSON_UNESCAPED_UNICODE);
break;
}
// 海报:下载字节 → poster 槽位管线(裁 2:3、缩放、转 JPEG、Storage 统一出口)
$posterKey = '';
$posterShow = '';
if (($resDb['poster_url'] ?? '') !== '') {
$bytes = Douban::downloadPoster((string)$resDb['poster_url']);
if ($bytes !== '') {
$tmp = tempnam(sys_get_temp_dir(), 'fbdb');
if ($tmp !== false) {
file_put_contents($tmp, $bytes);
$up = Upload::image($familyId, ['error' => UPLOAD_ERR_OK, 'tmp_name' => $tmp], 'poster');
@unlink($tmp);
if ($up['ok']) {
$posterKey = (string)$up['file'];
$posterShow = \App\Storage\Storage::url($familyId, $posterKey);
}
}
}
}
// 清掉上一次抓取但没被采用的海报(每次「自动获取」都下一张,不清就堆孤儿)
$oldPoster = trim((string)($_GET['old_poster'] ?? ''));
if ($oldPoster !== '' && $oldPoster !== $posterKey
&& preg_match('#^posters/[A-Za-z0-9._-]+$#', $oldPoster)) {
$stRef = Database::get()->prepare('SELECT COUNT(*) FROM movie_details WHERE poster_key = ?');
$stRef->execute([$oldPoster]);
if ((int)$stRef->fetchColumn() === 0) {
\App\Storage\Storage::delete($familyId, $oldPoster);
}
}
echo json_encode([
'ok' => true,
'source' => 'douban',
'title' => (string)($resDb['title'] ?? ''),
'meta' => $resDb['meta'],
'poster_key' => $posterKey,
'poster' => $posterShow,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
break;
// ---------------- 影院管理(v9,MovieLog 同款) ----------------
/** 影院管理单页:上面加/编表单,下面列表。GET 带参数时进入编辑态。 */