2026-09-22 11:42:27 +08:00
|
|
|
|
<?php
|
|
|
|
|
|
// ============================================
|
|
|
|
|
|
// 文件: lib/reviews.php
|
|
|
|
|
|
// 说明: 电影评价 + 评价下的讨论(评论)。
|
|
|
|
|
|
//
|
|
|
|
|
|
// 评价人是「登录账号」(admins),与观影人(persons / viewers)无关:
|
|
|
|
|
|
// 没一起去看这部电影的人,只要有账号,也能打分、写评价、参与讨论。
|
|
|
|
|
|
//
|
|
|
|
|
|
// 数据模型:
|
|
|
|
|
|
// reviews 一部电影 × 一个账号 至多一条(rating 1-10 + 正文)
|
|
|
|
|
|
// review_comments 挂在某条评价下的讨论;可带多个「观后感受」
|
|
|
|
|
|
//
|
|
|
|
|
|
// 观后感受只存 key(如 happy / sad),emoji 与文案统一在本文件的
|
|
|
|
|
|
// feelingCatalog() 里维护,改文案 / 换 emoji 都不需要动数据库。
|
|
|
|
|
|
// ============================================
|
|
|
|
|
|
|
|
|
|
|
|
// 评价正文 / 评论正文的最大字数(UTF-8 字符数)
|
|
|
|
|
|
define('REVIEW_CONTENT_MAX', 5000);
|
|
|
|
|
|
define('REVIEW_COMMENT_MAX', 1000);
|
|
|
|
|
|
// 一条评论最多能选几种感受
|
|
|
|
|
|
define('REVIEW_FEELING_MAX', 6);
|
|
|
|
|
|
|
|
|
|
|
|
// ===== 观后感受目录 =====
|
2026-09-22 15:07:10 +08:00
|
|
|
|
// key => [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)。
|
2026-09-22 11:42:27 +08:00
|
|
|
|
function feelingCatalog() {
|
|
|
|
|
|
static $catalog = null;
|
|
|
|
|
|
if ($catalog === null) {
|
|
|
|
|
|
$catalog = [
|
|
|
|
|
|
'happy' => ['emoji' => '😄', 'label' => '开心'],
|
2026-09-22 15:07:10 +08:00
|
|
|
|
'moved' => ['emoji' => '💖', 'label' => '感动'],
|
2026-09-22 11:42:27 +08:00
|
|
|
|
'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();
|
2026-09-22 13:03:04 +08:00
|
|
|
|
foreach ($reviews as &$review) {
|
|
|
|
|
|
// 评价本身带的观后感受(JSON 字符串 → key 列表)
|
|
|
|
|
|
$review['feelings'] = parseStoredFeelings($review['feelings'] ?? '');
|
|
|
|
|
|
}
|
|
|
|
|
|
unset($review);
|
2026-09-22 11:42:27 +08:00
|
|
|
|
attachCommentsToReviews($reviews);
|
|
|
|
|
|
return $reviews;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-22 16:09:48 +08:00
|
|
|
|
// 给一批评价挂上 comments(一次查询,避免每条评价查一次库)。
|
|
|
|
|
|
//
|
|
|
|
|
|
// 评论是**两级结构**:顶层评论(parent_id 为空)直接挂在评价下;
|
|
|
|
|
|
// 对评论的回复统一平铺在它所属的顶层评论的 replies 里——页面上不再逐层缩进,
|
|
|
|
|
|
// 只用 reply_to_username 标出「回复 @谁」。这样无论来回回复多少轮,
|
|
|
|
|
|
// 视觉上永远只有两层,页面也干净。
|
2026-09-22 11:42:27 +08:00
|
|
|
|
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("
|
2026-09-22 16:09:48 +08:00
|
|
|
|
SELECT c.*, a.username, a.avatar, ra.username AS reply_to_username
|
2026-09-22 11:42:27 +08:00
|
|
|
|
FROM review_comments c
|
2026-09-22 16:09:48 +08:00
|
|
|
|
LEFT JOIN admins a ON c.admin_id = a.id
|
|
|
|
|
|
LEFT JOIN admins ra ON c.reply_to_admin_id = ra.id
|
2026-09-22 11:42:27 +08:00
|
|
|
|
WHERE c.review_id IN ($placeholders)
|
|
|
|
|
|
ORDER BY c.created_at ASC, c.id ASC
|
|
|
|
|
|
");
|
|
|
|
|
|
$stmt->execute($ids);
|
|
|
|
|
|
|
2026-09-22 16:09:48 +08:00
|
|
|
|
$byId = [];
|
2026-09-22 11:42:27 +08:00
|
|
|
|
foreach ($stmt->fetchAll() as $row) {
|
2026-09-22 16:09:48 +08:00
|
|
|
|
$row['feelings'] = parseStoredFeelings($row['feelings'] ?? '');
|
|
|
|
|
|
// 老数据里这两列可能还不存在(库刚迁移)或为 0,统一成「空 = null」
|
|
|
|
|
|
$row['parent_id'] = ($row['parent_id'] === null || (int)$row['parent_id'] <= 0)
|
|
|
|
|
|
? null : (int)$row['parent_id'];
|
|
|
|
|
|
$byId[(int)$row['id']] = $row;
|
2026-09-22 11:42:27 +08:00
|
|
|
|
}
|
2026-09-22 16:09:48 +08:00
|
|
|
|
|
|
|
|
|
|
// 先把顶层评论按评价分桶(保持查询给出的时间顺序)
|
|
|
|
|
|
$grouped = []; // review_id => [ ['comment' => 顶层评论, 'replies' => [回复…]], … ]
|
|
|
|
|
|
foreach ($byId as $row) {
|
|
|
|
|
|
if ($row['parent_id'] !== null) continue;
|
|
|
|
|
|
$grouped[(int)$row['review_id']][] = ['comment' => $row, 'replies' => []];
|
|
|
|
|
|
}
|
|
|
|
|
|
// 顶层评论 id => 它在 $grouped 里的下标,便于把回复挂上去
|
|
|
|
|
|
$index = [];
|
|
|
|
|
|
foreach ($grouped as $rvId => $list) {
|
|
|
|
|
|
foreach ($list as $i => $item) {
|
|
|
|
|
|
$index[$rvId][(int)$item['comment']['id']] = $i;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 再挂回复:沿 parent 链向上找到所属的顶层评论。
|
|
|
|
|
|
// 正常写入时 parent_id 一定就是顶层评论 id(addReviewComment 会规范化),
|
|
|
|
|
|
// 这里多走几步只是容忍被手工改坏的数据,最多 20 步防成环。
|
|
|
|
|
|
foreach ($byId as $row) {
|
|
|
|
|
|
if ($row['parent_id'] === null) continue;
|
|
|
|
|
|
$rvId = (int)$row['review_id'];
|
|
|
|
|
|
$root = (int)$row['parent_id'];
|
|
|
|
|
|
for ($step = 0; $step < 20 && isset($byId[$root]) && $byId[$root]['parent_id'] !== null; $step++) {
|
|
|
|
|
|
$root = $byId[$root]['parent_id'];
|
|
|
|
|
|
}
|
|
|
|
|
|
if (isset($index[$rvId][$root])) {
|
|
|
|
|
|
$grouped[$rvId][$index[$rvId][$root]]['replies'][] = $row;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 找不到所属顶层(顶层已被删)的回复直接丢掉,不让它变成挂在评价上的孤儿
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-22 11:42:27 +08:00
|
|
|
|
foreach ($reviews as &$review) {
|
2026-09-22 16:09:48 +08:00
|
|
|
|
$review['comments'] = $grouped[(int)$review['id']] ?? [];
|
2026-09-22 11:42:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
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();
|
2026-09-22 13:03:04 +08:00
|
|
|
|
if ($row) {
|
|
|
|
|
|
$row['feelings'] = parseStoredFeelings($row['feelings'] ?? '');
|
|
|
|
|
|
}
|
2026-09-22 11:42:27 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-22 16:09:48 +08:00
|
|
|
|
// 取单条评论(用于校验「回复的目标」存在、且属于同一条评价)
|
|
|
|
|
|
function getReviewCommentById($comment_id) {
|
|
|
|
|
|
if ((int)$comment_id <= 0) return null;
|
|
|
|
|
|
$db = getDB();
|
|
|
|
|
|
$stmt = $db->prepare("SELECT * FROM review_comments WHERE id = ?");
|
|
|
|
|
|
$stmt->execute([(int)$comment_id]);
|
|
|
|
|
|
// 用 fetchAll 取:只取一行就放着不读完,游标会一直挂着共享读锁(本项目踩过)
|
|
|
|
|
|
$rows = $stmt->fetchAll();
|
|
|
|
|
|
return $rows ? $rows[0] : null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-22 11:42:27 +08:00
|
|
|
|
// 某部电影的评分概况:['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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ===== 写入 =====
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 发表 / 修改评价。一个账号对一部电影只保留一条:已有则更新,没有则插入。
|
2026-09-22 13:03:04 +08:00
|
|
|
|
* $feelings 是评价人自己选的观后感受 key 数组(可以为空)。
|
2026-09-22 11:42:27 +08:00
|
|
|
|
* 调用前请自行校验 rating 在 1-10、content 非空且未超长。
|
|
|
|
|
|
* 成功返回 true。
|
|
|
|
|
|
*/
|
2026-09-22 13:03:04 +08:00
|
|
|
|
function saveReview($movie_id, $admin_id, $rating, $content, array $feelings = []) {
|
2026-09-22 11:42:27 +08:00
|
|
|
|
$db = getDB();
|
2026-09-22 13:03:04 +08:00
|
|
|
|
$rating = (int)$rating;
|
|
|
|
|
|
$content = trim((string)$content);
|
|
|
|
|
|
$feelings = normalizeFeelings($feelings);
|
2026-09-22 11:42:27 +08:00
|
|
|
|
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("
|
2026-09-22 13:03:04 +08:00
|
|
|
|
UPDATE reviews SET rating = ?, content = ?, feelings = ?, updated_at = CURRENT_TIMESTAMP
|
2026-09-22 11:42:27 +08:00
|
|
|
|
WHERE id = ?
|
|
|
|
|
|
");
|
2026-09-22 13:03:04 +08:00
|
|
|
|
$stmt->execute([$rating, $content, feelingsToJson($feelings), $existing['id']]);
|
2026-09-22 11:42:27 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
$stmt = $db->prepare("
|
2026-09-22 13:03:04 +08:00
|
|
|
|
INSERT INTO reviews (movie_id, admin_id, rating, content, feelings)
|
|
|
|
|
|
VALUES (?, ?, ?, ?, ?)
|
2026-09-22 11:42:27 +08:00
|
|
|
|
");
|
2026-09-22 13:03:04 +08:00
|
|
|
|
$stmt->execute([$movie_id, $admin_id, $rating, $content, feelingsToJson($feelings)]);
|
2026-09-22 11:42:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
$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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-09-22 16:09:48 +08:00
|
|
|
|
* 在一条评价下发表评论,或回复某条评论。$feelings 是感受 key 数组(可为空)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* $parent_id = 0 → 直接评论这条评价(成为顶层评论)
|
|
|
|
|
|
* $parent_id > 0 → 回复该 id 所指的那条评论(顶层评论或某条回复都行)
|
|
|
|
|
|
*
|
|
|
|
|
|
* 为了维持「两级平铺」的结构,回复的回复会被**规范化**到同一条顶层评论之下,
|
|
|
|
|
|
* 所以库里任何一条评论的 parent_id 要么为空、要么指向一条顶层评论,永远不会更深。
|
|
|
|
|
|
*
|
2026-09-22 11:42:27 +08:00
|
|
|
|
* 返回新评论 id,失败返回 false。
|
|
|
|
|
|
*/
|
2026-09-22 16:09:48 +08:00
|
|
|
|
function addReviewComment($review_id, $admin_id, $content, array $feelings = [], $parent_id = 0) {
|
2026-09-22 11:42:27 +08:00
|
|
|
|
$db = getDB();
|
|
|
|
|
|
$content = trim((string)$content);
|
|
|
|
|
|
$review = getReviewById($review_id);
|
|
|
|
|
|
if (!$review || $content === '' || (int)$admin_id <= 0) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-22 16:09:48 +08:00
|
|
|
|
$parent_id = (int)$parent_id;
|
|
|
|
|
|
$replyTo = null;
|
|
|
|
|
|
if ($parent_id > 0) {
|
|
|
|
|
|
$parent = getReviewCommentById($parent_id);
|
|
|
|
|
|
// 只能回复「同一条评价下」的评论,防止拿别的评价的 comment_id 跨过去
|
|
|
|
|
|
if (!$parent || (int)$parent['review_id'] !== (int)$review_id) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 回复的回复:挂到那条顶层评论下,@ 的仍然是真正被回复的那个人
|
|
|
|
|
|
if ($parent['parent_id'] !== null && (int)$parent['parent_id'] > 0) {
|
|
|
|
|
|
$parent_id = (int)$parent['parent_id'];
|
|
|
|
|
|
}
|
|
|
|
|
|
$replyTo = (int)$parent['admin_id'];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-22 11:42:27 +08:00
|
|
|
|
$stmt = $db->prepare("
|
2026-09-22 16:09:48 +08:00
|
|
|
|
INSERT INTO review_comments (review_id, admin_id, parent_id, reply_to_admin_id, content, feelings)
|
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
2026-09-22 11:42:27 +08:00
|
|
|
|
");
|
|
|
|
|
|
$ok = $stmt->execute([
|
|
|
|
|
|
$review_id,
|
|
|
|
|
|
$admin_id,
|
2026-09-22 16:09:48 +08:00
|
|
|
|
$parent_id > 0 ? $parent_id : null,
|
|
|
|
|
|
$replyTo,
|
2026-09-22 11:42:27 +08:00
|
|
|
|
$content,
|
|
|
|
|
|
feelingsToJson(normalizeFeelings($feelings)),
|
|
|
|
|
|
]);
|
|
|
|
|
|
return $ok ? (int)$db->lastInsertId() : false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-22 16:09:48 +08:00
|
|
|
|
// 删除自己的评论;只能删自己的。
|
|
|
|
|
|
// 删的是顶层评论时,连它下面的回复一起删——回复在页面上只存在于某条顶层评论之下,
|
|
|
|
|
|
// 留着就会变成挂在评价上的孤儿。
|
2026-09-22 11:42:27 +08:00
|
|
|
|
function deleteReviewComment($comment_id, $admin_id) {
|
|
|
|
|
|
$db = getDB();
|
|
|
|
|
|
$stmt = $db->prepare("DELETE FROM review_comments WHERE id = ? AND admin_id = ?");
|
2026-09-22 16:09:48 +08:00
|
|
|
|
$stmt->execute([(int)$comment_id, (int)$admin_id]);
|
|
|
|
|
|
if ($stmt->rowCount() <= 0) return false;
|
|
|
|
|
|
|
|
|
|
|
|
$stmt = $db->prepare("DELETE FROM review_comments WHERE parent_id = ?");
|
|
|
|
|
|
$stmt->execute([(int)$comment_id]);
|
|
|
|
|
|
return true;
|
2026-09-22 11:42:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 删除某部电影的全部评价与讨论(删电影时调用;级联未开启,需手动清)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* $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]);
|
|
|
|
|
|
}
|