[emoji, 中文名]。评价时可多选,每种感受一个专属 emoji。 // 注意:emoji 只选 Emoji 11.0(2018)及以前的老码位——更新的字形在较旧的 // Windows / Android 系统 emoji 字体里可能没有,页面上会显示成空白或方框。 // (下面注释一律用码位描述,不直接写 emoji 字符,免得在缺字形的环境里注释也是方框。) // 2026-09:「感动」原本用 U+1F979(含泪微笑,Emoji 14.0 / 2021 新增),在 // Emoji 12 时代的系统字体里缺字形,已换成 U+1F496(闪亮的心,Emoji 1.0 / 2010)。 function feelingCatalog() { static $catalog = null; if ($catalog === null) { $catalog = [ 'happy' => ['emoji' => '😄', 'label' => '开心'], 'moved' => ['emoji' => '💖', 'label' => '感动'], 'warm' => ['emoji' => '🥰', 'label' => '温暖'], 'excited' => ['emoji' => '🤩', 'label' => '激动'], 'thoughtful' => ['emoji' => '🤔', 'label' => '深思'], 'tense' => ['emoji' => '😰', 'label' => '紧张'], 'scared' => ['emoji' => '😨', 'label' => '恐惧'], 'surprised' => ['emoji' => '😲', 'label' => '震惊'], 'sad' => ['emoji' => '😢', 'label' => '难受'], 'angry' => ['emoji' => '😠', 'label' => '气愤'], 'bored' => ['emoji' => '😴', 'label' => '无聊'], 'confused' => ['emoji' => '😵', 'label' => '困惑'], ]; } return $catalog; } // 取某一种感受的 emoji + 文案;key 不存在时返回 null function feelingInfo($key) { $catalog = feelingCatalog(); return isset($catalog[$key]) ? $catalog[$key] : null; } // ===== 文本长度(按 UTF-8 字符计;没有 mbstring 时退化为字节数) ===== function reviewTextLength($str) { return function_exists('mb_strlen') ? mb_strlen((string)$str, 'UTF-8') : strlen((string)$str); } // ===== 感受的校验 / 存取 ===== // 把表单提交(数组)或库里取出的数组,整理成合法的感受 key 列表: // 过滤不在目录里的值、去重、截断到上限,并保持目录里的顺序。 function normalizeFeelings($raw, $max = REVIEW_FEELING_MAX) { $catalog = feelingCatalog(); $picked = []; if (is_array($raw)) { foreach ($raw as $key) { $key = is_string($key) ? trim($key) : ''; if ($key !== '' && isset($catalog[$key]) && !in_array($key, $picked, true)) { $picked[] = $key; } } } // 保持目录顺序,读出来展示时才不会跟着勾选顺序乱跳 $ordered = []; foreach ($catalog as $key => $info) { if (in_array($key, $picked, true)) $ordered[] = $key; } return array_slice($ordered, 0, max(0, (int)$max)); } // 解析库里 feelings 字段(JSON 数组字符串)→ 合法的 key 列表 function parseStoredFeelings($json) { if ($json === null || $json === '') return []; $keys = json_decode((string)$json, true); if (!is_array($keys)) return []; return normalizeFeelings($keys); } // 感受列表 → 入库用的 JSON 字符串 function feelingsToJson(array $keys) { return json_encode(array_values($keys), JSON_UNESCAPED_UNICODE); } // ===== 评分文案 ===== // 1-10 分给一句词,详情页打分时实时显示,评价列表里也用它做 title。 function ratingWord($rating) { $r = (int)$rating; if ($r <= 2) return '很失望'; if ($r <= 4) return '不太行'; if ($r <= 6) return '还行'; if ($r <= 8) return '不错'; return '很棒'; } // ===== 读取 ===== // 某部电影的全部评价(新在前),并把讨论一次性挂到每条评价的 comments 上 function getMovieReviews($movie_id) { $db = getDB(); $stmt = $db->prepare(" SELECT r.*, a.username, a.avatar FROM reviews r LEFT JOIN admins a ON r.admin_id = a.id WHERE r.movie_id = ? ORDER BY r.created_at DESC, r.id DESC "); $stmt->execute([$movie_id]); $reviews = $stmt->fetchAll(); foreach ($reviews as &$review) { // 评价本身带的观后感受(JSON 字符串 → key 列表) $review['feelings'] = parseStoredFeelings($review['feelings'] ?? ''); } unset($review); attachCommentsToReviews($reviews); return $reviews; } // 给一批评价挂上 comments(一次查询,避免每条评价查一次库) function attachCommentsToReviews(array &$reviews) { if (empty($reviews)) return; $ids = array_column($reviews, 'id'); $ids = array_values(array_filter($ids, function ($v) { return (int)$v > 0; })); if (empty($ids)) return; $db = getDB(); $placeholders = implode(',', array_fill(0, count($ids), '?')); $stmt = $db->prepare(" SELECT c.*, a.username, a.avatar FROM review_comments c LEFT JOIN admins a ON c.admin_id = a.id WHERE c.review_id IN ($placeholders) ORDER BY c.created_at ASC, c.id ASC "); $stmt->execute($ids); $map = []; foreach ($stmt->fetchAll() as $row) { $row['feelings'] = parseStoredFeelings($row['feelings'] ?? ''); $map[(int)$row['review_id']][] = $row; } foreach ($reviews as &$review) { $review['comments'] = $map[(int)$review['id']] ?? []; } unset($review); } // 当前账号在这部电影上的评价(没有则 null) function getMyReview($movie_id, $admin_id) { if ((int)$admin_id <= 0) return null; $db = getDB(); $stmt = $db->prepare("SELECT * FROM reviews WHERE movie_id = ? AND admin_id = ?"); $stmt->execute([$movie_id, $admin_id]); $row = $stmt->fetch(); if ($row) { $row['feelings'] = parseStoredFeelings($row['feelings'] ?? ''); } return $row ?: null; } // 取单条评价(用于校验评论挂在的评价确实属于这部电影) function getReviewById($review_id) { $db = getDB(); $stmt = $db->prepare("SELECT * FROM reviews WHERE id = ?"); $stmt->execute([$review_id]); $row = $stmt->fetch(); return $row ?: null; } // 某部电影的评分概况:['count' => n, 'avg' => 7.5] // 没人评价时 avg 为 null,展示时不要显示 0 分。 function reviewStats($movie_id) { $db = getDB(); $stmt = $db->prepare("SELECT COUNT(*) AS c, AVG(rating) AS a FROM reviews WHERE movie_id = ?"); $stmt->execute([$movie_id]); $row = $stmt->fetch(); $count = (int)($row['c'] ?? 0); return [ 'count' => $count, 'avg' => $count > 0 ? round((float)$row['a'], 1) : null, ]; } // 给电影列表批量挂上评分概况(列表页展示均分用,一次查询搞定) function attachReviewStatsToMovies(array &$movies) { if (empty($movies)) return; $ids = array_values(array_unique(array_filter(array_column($movies, 'id'), function ($v) { return (int)$v > 0; }))); if (empty($ids)) return; $db = getDB(); $placeholders = implode(',', array_fill(0, count($ids), '?')); $stmt = $db->prepare(" SELECT movie_id, COUNT(*) AS c, AVG(rating) AS a FROM reviews WHERE movie_id IN ($placeholders) GROUP BY movie_id "); $stmt->execute($ids); $map = []; foreach ($stmt->fetchAll() as $row) { $map[(int)$row['movie_id']] = [ 'count' => (int)$row['c'], 'avg' => round((float)$row['a'], 1), ]; } foreach ($movies as &$movie) { $movie['review_count'] = $map[(int)$movie['id']]['count'] ?? 0; $movie['review_avg'] = $map[(int)$movie['id']]['avg'] ?? null; } unset($movie); } // ===== 写入 ===== /** * 发表 / 修改评价。一个账号对一部电影只保留一条:已有则更新,没有则插入。 * $feelings 是评价人自己选的观后感受 key 数组(可以为空)。 * 调用前请自行校验 rating 在 1-10、content 非空且未超长。 * 成功返回 true。 */ function saveReview($movie_id, $admin_id, $rating, $content, array $feelings = []) { $db = getDB(); $rating = (int)$rating; $content = trim((string)$content); $feelings = normalizeFeelings($feelings); if ($rating < 1 || $rating > 10 || $content === '' || (int)$admin_id <= 0) { return false; } try { $db->beginTransaction(); $stmt = $db->prepare("SELECT id FROM reviews WHERE movie_id = ? AND admin_id = ?"); $stmt->execute([$movie_id, $admin_id]); $existing = $stmt->fetch(); if ($existing) { $stmt = $db->prepare(" UPDATE reviews SET rating = ?, content = ?, feelings = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? "); $stmt->execute([$rating, $content, feelingsToJson($feelings), $existing['id']]); } else { $stmt = $db->prepare(" INSERT INTO reviews (movie_id, admin_id, rating, content, feelings) VALUES (?, ?, ?, ?, ?) "); $stmt->execute([$movie_id, $admin_id, $rating, $content, feelingsToJson($feelings)]); } $db->commit(); return true; } catch (PDOException $e) { if ($db->inTransaction()) $db->rollBack(); return false; } } // 删除自己的评价(连同下面的讨论一起删)。只能删自己的,删别人的返回 false。 function deleteReview($review_id, $admin_id) { $db = getDB(); $stmt = $db->prepare("DELETE FROM reviews WHERE id = ? AND admin_id = ?"); $stmt->execute([$review_id, $admin_id]); if ($stmt->rowCount() <= 0) return false; // 外键级联在本项目里没有开启(SQLite 默认关闭 PRAGMA foreign_keys), // 所以评论要手动清一遍,删掉评价后不能留下孤儿讨论。 $stmt = $db->prepare("DELETE FROM review_comments WHERE review_id = ?"); $stmt->execute([$review_id]); return true; } /** * 在一条评价下发表讨论。$feelings 是感受 key 数组(可为空)。 * 返回新评论 id,失败返回 false。 */ function addReviewComment($review_id, $admin_id, $content, array $feelings = []) { $db = getDB(); $content = trim((string)$content); $review = getReviewById($review_id); if (!$review || $content === '' || (int)$admin_id <= 0) { return false; } $stmt = $db->prepare(" INSERT INTO review_comments (review_id, admin_id, content, feelings) VALUES (?, ?, ?, ?) "); $ok = $stmt->execute([ $review_id, $admin_id, $content, feelingsToJson(normalizeFeelings($feelings)), ]); return $ok ? (int)$db->lastInsertId() : false; } // 删除自己的讨论;只能删自己的 function deleteReviewComment($comment_id, $admin_id) { $db = getDB(); $stmt = $db->prepare("DELETE FROM review_comments WHERE id = ? AND admin_id = ?"); $stmt->execute([$comment_id, $admin_id]); return $stmt->rowCount() > 0; } /** * 删除某部电影的全部评价与讨论(删电影时调用;级联未开启,需手动清)。 * * $db 可以传入调用方已经打开的连接:delete.php 手里那条连接上还有没读完的 * 游标(持有共享读锁),此时另开一条连接去 DELETE 会撞上 PDO 默认的 * 60 秒 busy timeout,表现成「页面卡死」。复用同一条连接就不会互等。 */ function deleteReviewsOfMovie($movie_id, $db = null) { if (!($db instanceof PDO)) { $db = getDB(); } $stmt = $db->prepare("SELECT id FROM reviews WHERE movie_id = ?"); $stmt->execute([$movie_id]); $ids = $stmt->fetchAll(PDO::FETCH_COLUMN); if (empty($ids)) return; $placeholders = implode(',', array_fill(0, count($ids), '?')); $stmt = $db->prepare("DELETE FROM review_comments WHERE review_id IN ($placeholders)"); $stmt->execute($ids); $stmt = $db->prepare("DELETE FROM reviews WHERE movie_id = ?"); $stmt->execute([$movie_id]); }