新增评价与讨论:打分 1-10 + 观后感受 emoji
- 规则:只要有登录账号就能评价,与「是不是观影人」无关;评价归属 admins - 数据层 lib/reviews.php: · reviews(movie_id + admin_id 唯一索引,一个账号一部电影只一条) · review_comments(feelings 存感受 key 的 JSON 数组) · 感受目录 feelingCatalog():12 种,每种一个专属 emoji,文案与 emoji 不入库 - detail.php 新增「💬 评价与讨论」:1-10 打分(原生 radio,无 JS 也能用)、 评价正文、评价列表、每条评价下的讨论与多选感受、均分概况 - index.php:有评价的电影卡片显示 ⭐ 均分 - delete.php:删电影时显式清理评价与讨论(SQLite 外键级联默认未开启) - 顺带修一个隐患:delete.php 取一行后没关游标,读锁一直挂着, 后续同一连接上的 DELETE 拿不到写锁,页面会卡满 60 秒
This commit is contained in:
@@ -50,6 +50,16 @@
|
|||||||
- **安全约束**:不允许删除最后一个管理员,也不允许删除自己。
|
- **安全约束**:不允许删除最后一个管理员,也不允许删除自己。
|
||||||
- **个人资料**:修改自己的用户名与密码(需验证旧密码)。
|
- **个人资料**:修改自己的用户名与密码(需验证旧密码)。
|
||||||
|
|
||||||
|
### 评价与讨论
|
||||||
|
- **谁可以评价**:**只要有登录账号就能评价**,与「是不是观影人」无关——没一起去看过的人也能打分、写评价、参与讨论。评价的归属是账号(`admins`),不是观影人(`persons`)。
|
||||||
|
- **打分 1-10**:详情页「💬 评价与讨论」区块里点数字即可打分,右侧实时显示「7 分 · 不错」;打分用的是原生单选框,禁用 JS 也能照常提交。
|
||||||
|
- **写评价**:一段正文(最多 5000 字)。**每个账号对每部电影只有一条评价**,再次提交即修改,页面上会标注「已编辑」。
|
||||||
|
- **讨论**:任意账号都能在任意一条评价下面参与讨论(正文最多 1000 字)。
|
||||||
|
- **观后感受(可多选)**:发讨论时可以勾选自己的观后感受,每种感受一个专属 emoji:😄 开心、🥹 感动、🥰 温暖、🤩 激动、🤔 深思、😰 紧张、😨 恐惧、😲 震惊、😢 难受、😠 气愤、😴 无聊、😵 困惑。勾选多少个都行,展示时按固定顺序排列。
|
||||||
|
- 感受只把 key(如 `happy`)存进数据库,emoji 与文案统一在 `lib/reviews.php` 的 `feelingCatalog()` 里维护,**以后换 emoji / 改文案都不用动数据**。
|
||||||
|
- **评分概况**:详情页标题栏显示「N 条评价 · 均分 X」;首页列表卡片上有评价的电影会显示 `⭐ 均分`。
|
||||||
|
- **删除**:只能删除**自己**的评价与讨论;删除评价时其下的讨论一并删除。删除电影时也会清掉对应的评价与讨论(SQLite 默认未开启外键级联,由程序手动清理)。
|
||||||
|
|
||||||
### 基础数据维护
|
### 基础数据维护
|
||||||
- **影院管理**:增删改查,支持「所在地方 / 影院名称 / 详细地址」三段式信息,内置 50 种预设配色,每个影院可用专属颜色区分;删除已被引用的影院时会给出醒目提示并要求二次确认(关联电影的影院字段将被置空)。
|
- **影院管理**:增删改查,支持「所在地方 / 影院名称 / 详细地址」三段式信息,内置 50 种预设配色,每个影院可用专属颜色区分;删除已被引用的影院时会给出醒目提示并要求二次确认(关联电影的影院字段将被置空)。
|
||||||
- **观影人管理**:增删改查,支持头像裁剪上传与专属配色;已被电影引用的观影人不允许直接删除。
|
- **观影人管理**:增删改查,支持头像裁剪上传与专属配色;已被电影引用的观影人不允许直接删除。
|
||||||
@@ -96,6 +106,7 @@ MovieLog_Server/
|
|||||||
│ │ └── meta.php # 实例元数据鉴权(ECS RAM 角色 / CVM CAM 角色)与本机地域查询
|
│ │ └── meta.php # 实例元数据鉴权(ECS RAM 角色 / CVM CAM 角色)与本机地域查询
|
||||||
│ ├── auth.php # 登录校验、管理员账号增删改
|
│ ├── auth.php # 登录校验、管理员账号增删改
|
||||||
│ ├── movies.php # 电影/影院读取、搜索,以及表单关联数据的解析
|
│ ├── movies.php # 电影/影院读取、搜索,以及表单关联数据的解析
|
||||||
|
│ ├── reviews.php # 评价与讨论:打分 1-10、正文、讨论、观后感受目录(emoji)
|
||||||
│ └── stats.php # 统计仪表板与年度报告查询
|
│ └── stats.php # 统计仪表板与年度报告查询
|
||||||
├── index.php # 首页:观影列表(瀑布流)+ 快捷搜索
|
├── index.php # 首页:观影列表(瀑布流)+ 快捷搜索
|
||||||
├── add.php # 添加电影
|
├── add.php # 添加电影
|
||||||
@@ -130,7 +141,7 @@ MovieLog_Server/
|
|||||||
└── avatars/ # 观影人头像
|
└── avatars/ # 观影人头像
|
||||||
```
|
```
|
||||||
|
|
||||||
> 页面只需 `require_once 'config.php'`,`lib/` 下的模块会由引导文件统一加载,并保证依赖顺序(db → helpers → auth → movies → stats)。
|
> 页面只需 `require_once 'config.php'`,`lib/` 下的模块会由引导文件统一加载,并保证依赖顺序(db → settings → storage → helpers → auth → movies → reviews → stats)。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -146,6 +157,8 @@ MovieLog_Server/
|
|||||||
| `persons` | 观影人 | `name`(唯一)、`color`、`avatar` |
|
| `persons` | 观影人 | `name`(唯一)、`color`、`avatar` |
|
||||||
| `viewers` | 电影 ↔ 观影人关联 | `movie_id`、`person_id`、`name`(历史遗留字段) |
|
| `viewers` | 电影 ↔ 观影人关联 | `movie_id`、`person_id`、`name`(历史遗留字段) |
|
||||||
| `movie_photos` | 电影多图 | `movie_id`、`photo_path`、`created_at` |
|
| `movie_photos` | 电影多图 | `movie_id`、`photo_path`、`created_at` |
|
||||||
|
| `reviews` | 电影评价 | `movie_id`、`admin_id`(评价人是账号,不是观影人)、`rating`(1-10)、`content`、`created_at`、`updated_at`;**`(movie_id, admin_id)` 上有唯一索引**,保证一个账号一部电影只一条 |
|
||||||
|
| `review_comments` | 评价下的讨论 | `review_id`、`admin_id`、`content`、`feelings`(感受 key 的 JSON 数组,如 `["happy","sad"]`)、`created_at` |
|
||||||
| `settings` | 键值配置与缓存 | `name`(唯一)、`value`。存存储驱动选择、各云厂商配置、图片访问方式等;**云厂商密钥以 `enc:v1:gcm:` 前缀密文落库**(密钥文件 `db/.settings.key`,首次保存时自动生成,**丢失后密文不可恢复**) |
|
| `settings` | 键值配置与缓存 | `name`(唯一)、`value`。存存储驱动选择、各云厂商配置、图片访问方式等;**云厂商密钥以 `enc:v1:gcm:` 前缀密文落库**(密钥文件 `db/.settings.key`,首次保存时自动生成,**丢失后密文不可恢复**) |
|
||||||
|
|
||||||
`settings` 中除配置外的运行时缓存键(都可安全清空,程序会自动重建):
|
`settings` 中除配置外的运行时缓存键(都可安全清空,程序会自动重建):
|
||||||
@@ -156,7 +169,9 @@ MovieLog_Server/
|
|||||||
| `cache_ecs_region` | 本机 ECS 所在地域(用于判断内网 endpoint 是否可用) | 1 小时 |
|
| `cache_ecs_region` | 本机 ECS 所在地域(用于判断内网 endpoint 是否可用) | 1 小时 |
|
||||||
| `cache_oss_internal` | 内网 endpoint 可用性判定结论 | 可用 1 小时 / 不可用 30 分钟 |
|
| `cache_oss_internal` | 内网 endpoint 可用性判定结论 | 可用 1 小时 / 不可用 30 分钟 |
|
||||||
|
|
||||||
外键约束:`movies.cinema_id → cinemas.id`(`ON DELETE SET NULL`)、`viewers.movie_id → movies.id`(`ON DELETE CASCADE`)、`movie_photos.movie_id → movies.id`(`ON DELETE CASCADE`)。
|
外键约束:`movies.cinema_id → cinemas.id`(`ON DELETE SET NULL`)、`viewers.movie_id → movies.id`(`ON DELETE CASCADE`)、`movie_photos.movie_id → movies.id`(`ON DELETE CASCADE`)、`reviews.movie_id → movies.id`(`ON DELETE CASCADE`)、`review_comments.review_id → reviews.id`(`ON DELETE CASCADE`)。
|
||||||
|
|
||||||
|
> SQLite 默认是**关闭**外键约束的(`PRAGMA foreign_keys` 未开启),上面这些 `ON DELETE CASCADE` 实际不会生效。因此删除电影、删除评价时,程序都会**显式地把子表数据一并清掉**(见 `delete.php` 与 `lib/reviews.php`),不要依赖级联。
|
||||||
|
|
||||||
**历史数据迁移**:`initDB()` 会自动把早期版本存在 `viewers.name` 里的姓名去重后写入 `persons`,并回填 `viewers.person_id`,老库可直接升级。
|
**历史数据迁移**:`initDB()` 会自动把早期版本存在 `viewers.name` 里的姓名去重后写入 `persons`,并回填 `viewers.person_id`,老库可直接升级。
|
||||||
|
|
||||||
@@ -249,6 +264,18 @@ server {
|
|||||||
2. 进入「年度报告」,页面顶部切换年份即可查看。
|
2. 进入「年度报告」,页面顶部切换年份即可查看。
|
||||||
3. 未绑定观影人时报告为空并给出提示。
|
3. 未绑定观影人时报告为空并给出提示。
|
||||||
|
|
||||||
|
### 打分、写评价与参与讨论
|
||||||
|
|
||||||
|
1. 打开任意一部电影的详情页,滚到最下面的「💬 评价与讨论」。
|
||||||
|
2. **打分**:点「我的评分」后面的 1-10 任意一个数字(选中会变橙色),旁边实时显示「几分 · 怎么样」。
|
||||||
|
3. **写评价**:在下面文本框里写正文,点「📝 发表评价」。
|
||||||
|
- 已经评价过时,这一块会变成「✏️ 修改我的评价」,按钮是「💾 保存修改」——同一部电影不会留下两条你的评价。
|
||||||
|
4. **参与讨论**:在别人的(或自己的)任意一条评价下面,写一句话,顺手勾几个**观后感受**(😄 🥹 🥰 🤩 🤔 😰 😨 😲 😢 😠 😴 😵,可多选),点「💬 发表讨论」。
|
||||||
|
- 讨论会带上评价人的头像、昵称,以及你选的感受 emoji。
|
||||||
|
5. **删除**:自己发的评价和讨论,右侧有删除按钮(别人的没有);删评价时下面的讨论会一起删掉。
|
||||||
|
|
||||||
|
> 评价人是**登录账号**。家里几个人各有一个账号时,同一部电影可以各自打分,标题栏会显示「N 条评价 · 均分 X」。
|
||||||
|
|
||||||
### CSV 批量导入
|
### CSV 批量导入
|
||||||
|
|
||||||
CSV **共 9 列,顺序固定**:
|
CSV **共 9 列,顺序固定**:
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ require_once ROOT_DIR . '/lib/storage.php'; // 对象存储门面(含各云
|
|||||||
require_once ROOT_DIR . '/lib/helpers.php'; // 通用工具函数
|
require_once ROOT_DIR . '/lib/helpers.php'; // 通用工具函数
|
||||||
require_once ROOT_DIR . '/lib/auth.php'; // 登录校验与账号管理
|
require_once ROOT_DIR . '/lib/auth.php'; // 登录校验与账号管理
|
||||||
require_once ROOT_DIR . '/lib/movies.php'; // 电影 / 影院 / 搜索
|
require_once ROOT_DIR . '/lib/movies.php'; // 电影 / 影院 / 搜索
|
||||||
|
require_once ROOT_DIR . '/lib/reviews.php'; // 评价与评价下的讨论
|
||||||
require_once ROOT_DIR . '/lib/stats.php'; // 统计与年度报告
|
require_once ROOT_DIR . '/lib/stats.php'; // 统计与年度报告
|
||||||
|
|
||||||
// 初始化数据库
|
// 初始化数据库
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ $db = getDB();
|
|||||||
$stmt = $db->prepare("SELECT poster_path, ticket_path, photo_path FROM movies WHERE id = ?");
|
$stmt = $db->prepare("SELECT poster_path, ticket_path, photo_path FROM movies WHERE id = ?");
|
||||||
$stmt->execute([$id]);
|
$stmt->execute([$id]);
|
||||||
$movie = $stmt->fetch();
|
$movie = $stmt->fetch();
|
||||||
|
// 取一行就够,但游标不关的话这条连接会一直挂着共享读锁,
|
||||||
|
// 后面删评价 / 删电影要升级成写锁时就会被自己挡住(PDO 默认等 60 秒,表现成页面卡死)。
|
||||||
|
$stmt->closeCursor();
|
||||||
|
|
||||||
if ($movie) {
|
if ($movie) {
|
||||||
// 删除海报、票根、旧单张照片
|
// 删除海报、票根、旧单张照片
|
||||||
@@ -29,6 +32,11 @@ if ($movie) {
|
|||||||
deleteStoredFile($p['photo_path']);
|
deleteStoredFile($p['photo_path']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 删除这部电影的评价与评价下的讨论
|
||||||
|
// (SQLite 默认没开 PRAGMA foreign_keys,reviews 上的 ON DELETE CASCADE 不会生效,必须手动清)
|
||||||
|
// 传 $db 进去复用本连接:读写都在一条连接上,不会互相等锁。
|
||||||
|
deleteReviewsOfMovie($id, $db);
|
||||||
|
|
||||||
// 删除电影(viewers 和 movie_photos 会自动级联删除)
|
// 删除电影(viewers 和 movie_photos 会自动级联删除)
|
||||||
$stmt = $db->prepare("DELETE FROM movies WHERE id = ?");
|
$stmt = $db->prepare("DELETE FROM movies WHERE id = ?");
|
||||||
$stmt->execute([$id]);
|
$stmt->execute([$id]);
|
||||||
|
|||||||
+671
@@ -19,6 +19,121 @@ if (!$movie) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$admin = getCurrentAdmin();
|
$admin = getCurrentAdmin();
|
||||||
|
$adminId = (int)($admin['id'] ?? 0);
|
||||||
|
|
||||||
|
// ---------- 评价与讨论 ----------
|
||||||
|
// 规则:只要有登录账号就能对任意一部电影打分(1-10)+ 写评价,并在别人的评价
|
||||||
|
// 下面参与讨论;评价人是「账号」(admins),与「观影人」(persons / viewers)无关。
|
||||||
|
// 提交后一律重定向回本页(POST → 重定向 → GET),刷新不会重复提交,
|
||||||
|
// 一次性提示走 session。
|
||||||
|
$rvFlash = null;
|
||||||
|
if (isset($_SESSION['rv_flash'])) {
|
||||||
|
$rvFlash = $_SESSION['rv_flash'];
|
||||||
|
unset($_SESSION['rv_flash']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$rvError = ''; // 评价表单的错误
|
||||||
|
$rvFormRating = 0;
|
||||||
|
$rvFormContent = '';
|
||||||
|
$rvCommentErrId = 0; // 哪一条评价的讨论表单报错
|
||||||
|
$rvCommentErr = '';
|
||||||
|
|
||||||
|
// 当前账号已有的评价(有则表单变成「修改」)
|
||||||
|
$myReview = getMyReview($id, $adminId);
|
||||||
|
if ($myReview) {
|
||||||
|
$rvFormRating = (int)$myReview['rating'];
|
||||||
|
$rvFormContent = $myReview['content'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $adminId > 0) {
|
||||||
|
$rvAction = $_POST['action'] ?? '';
|
||||||
|
$postedMovie = (int)($_POST['movie_id'] ?? 0);
|
||||||
|
|
||||||
|
// movie_id 必须对得上:表单里的 id 是客户端可控的,防止写到别的电影上
|
||||||
|
if ($postedMovie === $id) {
|
||||||
|
if ($rvAction === 'save_review') {
|
||||||
|
$rating = (int)($_POST['rating'] ?? 0);
|
||||||
|
$content = trim((string)($_POST['content'] ?? ''));
|
||||||
|
$rvFormRating = $rating;
|
||||||
|
$rvFormContent = $content;
|
||||||
|
|
||||||
|
if ($rating < 1 || $rating > 10) {
|
||||||
|
$rvError = '请先选一个 1-10 的评分';
|
||||||
|
} elseif ($content === '') {
|
||||||
|
$rvError = '评价内容不能为空';
|
||||||
|
} elseif (reviewTextLength($content) > REVIEW_CONTENT_MAX) {
|
||||||
|
$rvError = '评价内容太长了(最多 ' . REVIEW_CONTENT_MAX . ' 字)';
|
||||||
|
} elseif (saveReview($id, $adminId, $rating, $content)) {
|
||||||
|
$_SESSION['rv_flash'] = ['type' => 'success', 'msg' => $myReview ? '评价已更新' : '评价已发布'];
|
||||||
|
header('Location: detail.php?id=' . $id . '#reviews');
|
||||||
|
exit;
|
||||||
|
} else {
|
||||||
|
$rvError = '保存失败,请稍后再试';
|
||||||
|
}
|
||||||
|
|
||||||
|
} elseif ($rvAction === 'delete_review') {
|
||||||
|
$reviewId = (int)($_POST['review_id'] ?? 0);
|
||||||
|
if (deleteReview($reviewId, $adminId)) {
|
||||||
|
$_SESSION['rv_flash'] = ['type' => 'success', 'msg' => '评价已删除'];
|
||||||
|
} else {
|
||||||
|
$_SESSION['rv_flash'] = ['type' => 'error', 'msg' => '只能删除自己的评价'];
|
||||||
|
}
|
||||||
|
header('Location: detail.php?id=' . $id . '#reviews');
|
||||||
|
exit;
|
||||||
|
|
||||||
|
} elseif ($rvAction === 'add_comment') {
|
||||||
|
$reviewId = (int)($_POST['review_id'] ?? 0);
|
||||||
|
$content = trim((string)($_POST['content'] ?? ''));
|
||||||
|
$feelings = normalizeFeelings($_POST['feelings'] ?? []);
|
||||||
|
$review = getReviewById($reviewId);
|
||||||
|
|
||||||
|
if ($content === '') {
|
||||||
|
$rvCommentErrId = $reviewId;
|
||||||
|
$rvCommentErr = '讨论内容不能为空';
|
||||||
|
} elseif (reviewTextLength($content) > REVIEW_COMMENT_MAX) {
|
||||||
|
$rvCommentErrId = $reviewId;
|
||||||
|
$rvCommentErr = '讨论内容太长了(最多 ' . REVIEW_COMMENT_MAX . ' 字)';
|
||||||
|
} elseif (!$review || (int)$review['movie_id'] !== $id) {
|
||||||
|
$rvCommentErrId = $reviewId;
|
||||||
|
$rvCommentErr = '这条评价不存在';
|
||||||
|
} else {
|
||||||
|
$newId = addReviewComment($reviewId, $adminId, $content, $feelings);
|
||||||
|
if ($newId) {
|
||||||
|
$_SESSION['rv_flash'] = ['type' => 'success', 'msg' => '讨论已发表'];
|
||||||
|
header('Location: detail.php?id=' . $id . '#reviews');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
$rvCommentErrId = $reviewId;
|
||||||
|
$rvCommentErr = '保存失败,请稍后再试';
|
||||||
|
}
|
||||||
|
|
||||||
|
} elseif ($rvAction === 'delete_comment') {
|
||||||
|
$commentId = (int)($_POST['comment_id'] ?? 0);
|
||||||
|
if (deleteReviewComment($commentId, $adminId)) {
|
||||||
|
$_SESSION['rv_flash'] = ['type' => 'success', 'msg' => '讨论已删除'];
|
||||||
|
} else {
|
||||||
|
$_SESSION['rv_flash'] = ['type' => 'error', 'msg' => '只能删除自己的讨论'];
|
||||||
|
}
|
||||||
|
header('Location: detail.php?id=' . $id . '#reviews');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$reviews = getMovieReviews($id);
|
||||||
|
$rvStats = reviewStats($id);
|
||||||
|
$feelings = feelingCatalog();
|
||||||
|
// 讨论的错误提示默认贴在那条评价自己的表单上;但如果出错的评价根本不在这部电影
|
||||||
|
// 下(比如被改过的 review_id),页面上找不到对应的表单,就退化成整段顶部的提示,
|
||||||
|
// 否则错误会被静默吞掉。
|
||||||
|
$rvCommentErrShown = false;
|
||||||
|
if ($rvCommentErrId > 0) {
|
||||||
|
foreach ($reviews as $rv) {
|
||||||
|
if ((int)$rv['id'] === $rvCommentErrId) { $rvCommentErrShown = true; break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 1-10 分对应的文案,交给前端实时显示(下标 = 分值 - 1)
|
||||||
|
$rvRatingWords = array_map('ratingWord', range(1, 10));
|
||||||
|
|
||||||
// ---------- 灯箱图片序列:海报 → 票根 → 观影照片 ----------
|
// ---------- 灯箱图片序列:海报 → 票根 → 观影照片 ----------
|
||||||
// 以前每张图都是用 JS 的 window.open 弹到新窗口,没有关闭按钮、体验很差。
|
// 以前每张图都是用 JS 的 window.open 弹到新窗口,没有关闭按钮、体验很差。
|
||||||
@@ -286,6 +401,369 @@ $hasGallery = !empty($gallery);
|
|||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------- 评价与讨论 ---------- */
|
||||||
|
.rv-flash {
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.rv-flash-success {
|
||||||
|
background: rgba(46, 204, 113, 0.10);
|
||||||
|
border: 1px solid rgba(46, 204, 113, 0.28);
|
||||||
|
color: #1e8449;
|
||||||
|
}
|
||||||
|
.rv-flash-error {
|
||||||
|
background: rgba(231, 76, 60, 0.08);
|
||||||
|
border: 1px solid rgba(231, 76, 60, 0.25);
|
||||||
|
color: #c0392b;
|
||||||
|
}
|
||||||
|
.rv-error {
|
||||||
|
background: rgba(231, 76, 60, 0.08);
|
||||||
|
border: 1px solid rgba(231, 76, 60, 0.25);
|
||||||
|
color: #c0392b;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.rv-form {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px 18px;
|
||||||
|
margin-bottom: 22px;
|
||||||
|
}
|
||||||
|
.rv-form-head {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.rv-rating-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px 14px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.rv-rating-label {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
/* 1-10 打分:用原生 radio,没 JS 也能选;外观全在 + 后面的 span 上 */
|
||||||
|
.rv-scale {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.rv-num {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.rv-num input {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
.rv-num span {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
padding: 0 6px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
/* --input-bg 在部分页面未定义,给个兜底 */
|
||||||
|
background: var(--input-bg, var(--card-bg));
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
transition: var(--transition);
|
||||||
|
}
|
||||||
|
.rv-num:hover span {
|
||||||
|
border-color: var(--primary);
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
.rv-num input:checked + span {
|
||||||
|
background: linear-gradient(135deg, var(--primary), var(--primary-light));
|
||||||
|
border-color: transparent;
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 3px 10px rgba(230, 126, 34, 0.30);
|
||||||
|
}
|
||||||
|
.rv-num:focus-within span {
|
||||||
|
outline: 2px solid var(--primary);
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
.rv-word {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
.rv-word.is-set {
|
||||||
|
color: var(--primary);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.rv-textarea {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 92px;
|
||||||
|
resize: vertical;
|
||||||
|
padding: 12px 14px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-family: inherit;
|
||||||
|
line-height: 1.7;
|
||||||
|
background: var(--input-bg, var(--card-bg));
|
||||||
|
color: var(--text-primary);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.rv-textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--primary);
|
||||||
|
box-shadow: 0 0 0 4px rgba(230, 126, 34, 0.12);
|
||||||
|
}
|
||||||
|
.rv-textarea-sm {
|
||||||
|
min-height: 62px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.rv-form-foot {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.rv-form-foot .rv-hint {
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
.rv-hint {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
.rv-empty {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.rv-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 18px;
|
||||||
|
}
|
||||||
|
.rv-item {
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
padding-top: 16px;
|
||||||
|
}
|
||||||
|
.rv-item:first-child {
|
||||||
|
border-top: none;
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
.rv-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.rv-avatar {
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
.rv-avatar-sm {
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
}
|
||||||
|
.rv-who {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.rv-name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
.rv-mine {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-left: 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 16px;
|
||||||
|
background: var(--primary);
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0 6px;
|
||||||
|
}
|
||||||
|
.rv-time {
|
||||||
|
display: block;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
.rv-score {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--primary);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
.rv-score-max {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
.rv-del {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
.rv-del-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 4px 6px;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
.rv-del-btn:hover {
|
||||||
|
background: rgba(231, 76, 60, 0.12);
|
||||||
|
color: #e74c3c;
|
||||||
|
}
|
||||||
|
/* 正文用 pre-wrap 保留换行,就不必再 nl2br 了(也避免转义后再插标签) */
|
||||||
|
.rv-content {
|
||||||
|
margin: 8px 0 2px 44px;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.75;
|
||||||
|
color: var(--text-primary);
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
.rv-updated {
|
||||||
|
margin-left: 44px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
.rv-comments {
|
||||||
|
margin: 10px 0 10px 44px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.rv-comment {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
}
|
||||||
|
.rv-c-body {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.rv-c-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.rv-c-head .rv-name {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.rv-c-text {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.7;
|
||||||
|
color: var(--text-primary);
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
.rv-feelings {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
margin: 4px 0 2px;
|
||||||
|
}
|
||||||
|
.rv-comment-form {
|
||||||
|
margin-left: 44px;
|
||||||
|
}
|
||||||
|
.rv-feeling-pick {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
.rv-pick-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.feel-chips {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.feel-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
/* 可勾选的感受:原生 checkbox,外观在 .feel-inner 上 */
|
||||||
|
.feel-pick {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.feel-pick input {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
.feel-inner {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 20px;
|
||||||
|
background: var(--input-bg, var(--card-bg));
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
transition: var(--transition);
|
||||||
|
}
|
||||||
|
.feel-pick:hover .feel-inner {
|
||||||
|
border-color: var(--primary);
|
||||||
|
}
|
||||||
|
.feel-pick input:checked + .feel-inner {
|
||||||
|
border-color: var(--primary);
|
||||||
|
background: rgba(230, 126, 34, 0.12);
|
||||||
|
color: var(--primary);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.feel-pick:focus-within .feel-inner {
|
||||||
|
outline: 2px solid var(--primary);
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
.feel-emoji {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
/* 已保存的感受:只读展示 */
|
||||||
|
.feel-chip.is-static {
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 2px 10px;
|
||||||
|
border-radius: 20px;
|
||||||
|
background: rgba(230, 126, 34, 0.10);
|
||||||
|
color: var(--primary);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.rv-content,
|
||||||
|
.rv-updated,
|
||||||
|
.rv-comments,
|
||||||
|
.rv-comment-form {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------- 图片灯箱(替代原来的 window.open 弹窗) ----------
|
/* ---------- 图片灯箱(替代原来的 window.open 弹窗) ----------
|
||||||
z-index 要压过导航栏(导航栏是 100),这里用 300。 */
|
z-index 要压过导航栏(导航栏是 100),这里用 300。 */
|
||||||
.lb-openable {
|
.lb-openable {
|
||||||
@@ -551,6 +1029,168 @@ $hasGallery = !empty($gallery);
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<!-- 评价与讨论
|
||||||
|
只要有登录账号就能打分(1-10)+ 写评价,并可以在任意一条评价下面参与讨论;
|
||||||
|
评价人是「账号」,与「观影人」无关——没去看过的人也能评。 -->
|
||||||
|
<div class="detail-section" id="reviews">
|
||||||
|
<h3>💬 评价与讨论
|
||||||
|
<span class="badge">
|
||||||
|
<?php if ($rvStats['count'] > 0): ?>
|
||||||
|
<?= $rvStats['count'] ?> 条评价 · 均分 <?= $rvStats['avg'] ?>
|
||||||
|
<?php else: ?>
|
||||||
|
还没有评价
|
||||||
|
<?php endif; ?>
|
||||||
|
</span>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<?php if ($rvFlash): ?>
|
||||||
|
<div class="rv-flash rv-flash-<?= $rvFlash['type'] === 'success' ? 'success' : 'error' ?>">
|
||||||
|
<?= h($rvFlash['msg']) ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<!-- 我的评分 + 评价(已评过时这里就是「修改」) -->
|
||||||
|
<form method="post" action="#reviews" class="rv-form" id="reviewForm">
|
||||||
|
<input type="hidden" name="action" value="save_review">
|
||||||
|
<input type="hidden" name="movie_id" value="<?= $id ?>">
|
||||||
|
<div class="rv-form-head"><?= $myReview ? '✏️ 修改我的评价' : '⭐ 写下我的评价' ?></div>
|
||||||
|
<div class="rv-rating-row">
|
||||||
|
<span class="rv-rating-label">我的评分</span>
|
||||||
|
<div class="rv-scale" role="radiogroup" aria-label="评分,1 分到 10 分">
|
||||||
|
<?php for ($i = 1; $i <= 10; $i++): ?>
|
||||||
|
<label class="rv-num" title="<?= $i ?> 分 · <?= h(ratingWord($i)) ?>">
|
||||||
|
<input type="radio" name="rating" value="<?= $i ?>"<?= $rvFormRating === $i ? ' checked' : '' ?>>
|
||||||
|
<span><?= $i ?></span>
|
||||||
|
</label>
|
||||||
|
<?php endfor; ?>
|
||||||
|
</div>
|
||||||
|
<span class="rv-word<?= $rvFormRating > 0 ? ' is-set' : '' ?>" id="rvWord">
|
||||||
|
<?= $rvFormRating > 0 ? $rvFormRating . ' 分 · ' . h(ratingWord($rvFormRating)) : '未评分' ?>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<?php if ($rvError): ?>
|
||||||
|
<div class="rv-error"><?= h($rvError) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<textarea name="content" class="rv-textarea" maxlength="<?= REVIEW_CONTENT_MAX ?>"
|
||||||
|
placeholder="这部电影怎么样?剧情、表演、画面、音效……随便聊聊"><?= h($rvFormContent) ?></textarea>
|
||||||
|
<div class="rv-form-foot">
|
||||||
|
<span class="rv-hint">每个账号对每部电影只有一条评价,再次提交即为修改。</span>
|
||||||
|
<button type="submit" class="btn btn-primary btn-sm"><?= $myReview ? '💾 保存修改' : '📝 发表评价' ?></button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<?php if (empty($reviews)): ?>
|
||||||
|
<p class="rv-empty">还没有人评价这部电影,你可以是第一个 👆</p>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="rv-list">
|
||||||
|
<?php foreach ($reviews as $rv): ?>
|
||||||
|
<?php
|
||||||
|
$rvMine = ((int)$rv['admin_id'] === $adminId);
|
||||||
|
$rvName = ($rv['username'] !== null && $rv['username'] !== '') ? $rv['username'] : '已注销账号';
|
||||||
|
$rvRating = (int)$rv['rating'];
|
||||||
|
?>
|
||||||
|
<div class="rv-item">
|
||||||
|
<div class="rv-head">
|
||||||
|
<img class="rv-avatar" src="<?= getAvatarUrl($rv['avatar'] ?? null, $rvName) ?>" alt="">
|
||||||
|
<div class="rv-who">
|
||||||
|
<span class="rv-name"><?= h($rvName) ?></span>
|
||||||
|
<?php if ($rvMine): ?><span class="rv-mine">我</span><?php endif; ?>
|
||||||
|
<span class="rv-time"><?= h(substr((string)$rv['created_at'], 0, 16)) ?></span>
|
||||||
|
</div>
|
||||||
|
<span class="rv-score" title="<?= h(ratingWord($rvRating)) ?>"><?= $rvRating ?><span class="rv-score-max">/10</span></span>
|
||||||
|
<?php if ($rvMine): ?>
|
||||||
|
<form method="post" action="#reviews" class="rv-del"
|
||||||
|
onsubmit="return confirm('删除我的这条评价?下面的讨论也会一起删除,不可恢复。')">
|
||||||
|
<input type="hidden" name="action" value="delete_review">
|
||||||
|
<input type="hidden" name="movie_id" value="<?= $id ?>">
|
||||||
|
<input type="hidden" name="review_id" value="<?= $rv['id'] ?>">
|
||||||
|
<button type="submit" class="rv-del-btn" title="删除我的评价" aria-label="删除我的评价">🗑️</button>
|
||||||
|
</form>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<div class="rv-content"><?= h($rv['content']) ?></div>
|
||||||
|
<?php if (!empty($rv['updated_at']) && $rv['updated_at'] !== $rv['created_at']): ?>
|
||||||
|
<div class="rv-updated">已编辑 <?= h(substr((string)$rv['updated_at'], 0, 16)) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (!empty($rv['comments'])): ?>
|
||||||
|
<div class="rv-comments">
|
||||||
|
<?php foreach ($rv['comments'] as $c): ?>
|
||||||
|
<?php
|
||||||
|
$cMine = ((int)$c['admin_id'] === $adminId);
|
||||||
|
$cName = ($c['username'] !== null && $c['username'] !== '') ? $c['username'] : '已注销账号';
|
||||||
|
?>
|
||||||
|
<div class="rv-comment">
|
||||||
|
<img class="rv-avatar rv-avatar-sm" src="<?= getAvatarUrl($c['avatar'] ?? null, $cName) ?>" alt="">
|
||||||
|
<div class="rv-c-body">
|
||||||
|
<div class="rv-c-head">
|
||||||
|
<span class="rv-name"><?= h($cName) ?></span>
|
||||||
|
<?php if ($cMine): ?><span class="rv-mine">我</span><?php endif; ?>
|
||||||
|
<span class="rv-time"><?= h(substr((string)$c['created_at'], 0, 16)) ?></span>
|
||||||
|
<?php if ($cMine): ?>
|
||||||
|
<form method="post" action="#reviews" class="rv-del"
|
||||||
|
onsubmit="return confirm('删除我的这条讨论?')">
|
||||||
|
<input type="hidden" name="action" value="delete_comment">
|
||||||
|
<input type="hidden" name="movie_id" value="<?= $id ?>">
|
||||||
|
<input type="hidden" name="comment_id" value="<?= $c['id'] ?>">
|
||||||
|
<button type="submit" class="rv-del-btn" title="删除我的讨论" aria-label="删除我的讨论">✕</button>
|
||||||
|
</form>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php if (!empty($c['feelings'])): ?>
|
||||||
|
<div class="rv-feelings">
|
||||||
|
<?php foreach ($c['feelings'] as $fkey): ?>
|
||||||
|
<?php $f = feelingInfo($fkey); if (!$f) continue; ?>
|
||||||
|
<span class="feel-chip is-static" title="观后感受:<?= h($f['label']) ?>">
|
||||||
|
<span class="feel-emoji"><?= $f['emoji'] ?></span><span class="feel-label"><?= h($f['label']) ?></span>
|
||||||
|
</span>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<div class="rv-c-text"><?= h($c['content']) ?></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<!-- 在这条评价下参与讨论:可多选观后感受 -->
|
||||||
|
<form method="post" action="#reviews" class="rv-comment-form">
|
||||||
|
<input type="hidden" name="action" value="add_comment">
|
||||||
|
<input type="hidden" name="movie_id" value="<?= $id ?>">
|
||||||
|
<input type="hidden" name="review_id" value="<?= $rv['id'] ?>">
|
||||||
|
<?php if ($rvCommentErrId === (int)$rv['id']): ?>
|
||||||
|
<div class="rv-error"><?= h($rvCommentErr) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<textarea name="content" class="rv-textarea rv-textarea-sm" maxlength="<?= REVIEW_COMMENT_MAX ?>"
|
||||||
|
placeholder="说说你的看法…"></textarea>
|
||||||
|
<div class="rv-feeling-pick">
|
||||||
|
<span class="rv-pick-label">看完的感受(可多选)</span>
|
||||||
|
<div class="feel-chips">
|
||||||
|
<?php foreach ($feelings as $fkey => $f): ?>
|
||||||
|
<label class="feel-chip feel-pick" title="<?= h($f['label']) ?>">
|
||||||
|
<input type="checkbox" name="feelings[]" value="<?= h($fkey) ?>">
|
||||||
|
<span class="feel-inner">
|
||||||
|
<span class="feel-emoji"><?= $f['emoji'] ?></span><span class="feel-label"><?= h($f['label']) ?></span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="rv-form-foot">
|
||||||
|
<button type="submit" class="btn btn-outline btn-sm">💬 发表讨论</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($rvCommentErr && !$rvCommentErrShown): ?>
|
||||||
|
<div class="rv-error" style="margin-top: 14px;"><?= h($rvCommentErr) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
@@ -679,6 +1319,37 @@ $hasGallery = !empty($gallery);
|
|||||||
</script>
|
</script>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<!-- 打分时实时提示「几分 · 怎么样」。
|
||||||
|
纯增强:评分用的是原生 radio,没有 JS 也能正常选中并提交。 -->
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var WORDS = <?= json_encode(
|
||||||
|
$rvRatingWords,
|
||||||
|
JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT
|
||||||
|
) ?>;
|
||||||
|
var form = document.getElementById('reviewForm');
|
||||||
|
var word = document.getElementById('rvWord');
|
||||||
|
if (!form || !word) return;
|
||||||
|
|
||||||
|
function sync() {
|
||||||
|
var picked = form.querySelector('input[name="rating"]:checked');
|
||||||
|
if (!picked) {
|
||||||
|
word.textContent = '未评分';
|
||||||
|
word.classList.remove('is-set');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var v = parseInt(picked.value, 10);
|
||||||
|
word.textContent = v + ' 分 · ' + (WORDS[v - 1] || '');
|
||||||
|
word.classList.add('is-set');
|
||||||
|
}
|
||||||
|
|
||||||
|
Array.prototype.forEach.call(form.querySelectorAll('input[name="rating"]'), function (el) {
|
||||||
|
el.addEventListener('change', sync);
|
||||||
|
});
|
||||||
|
sync();
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
<script src="js/main.js"></script>
|
<script src="js/main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -16,6 +16,9 @@ if (!empty($keyword)) {
|
|||||||
} else {
|
} else {
|
||||||
$movies = getMovies();
|
$movies = getMovies();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 挂上评价概况(几条评价 / 平均分),一次查询,列表里只展示有评价的电影
|
||||||
|
attachReviewStatsToMovies($movies);
|
||||||
?>
|
?>
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
@@ -97,6 +100,11 @@ if (!empty($keyword)) {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 4px 8px;
|
gap: 4px 8px;
|
||||||
}
|
}
|
||||||
|
/* 评价均分(有人评价过才显示) */
|
||||||
|
.meta-rating {
|
||||||
|
color: var(--primary);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
.search-bar {
|
.search-bar {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -176,6 +184,11 @@ if (!empty($keyword)) {
|
|||||||
🏛️ <?= h($movie['cinema_name']) ?>
|
🏛️ <?= h($movie['cinema_name']) ?>
|
||||||
</span>
|
</span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
<?php if (!empty($movie['review_count'])): ?>
|
||||||
|
<span class="meta-rating" title="共 <?= (int)$movie['review_count'] ?> 条评价">
|
||||||
|
⭐ <?= $movie['review_avg'] ?><span style="color: var(--text-secondary); font-weight: 400;">/10</span>
|
||||||
|
</span>
|
||||||
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
<!-- 观影人 -->
|
<!-- 观影人 -->
|
||||||
<?php if (!empty($movie['viewers'])): ?>
|
<?php if (!empty($movie['viewers'])): ?>
|
||||||
|
|||||||
+38
@@ -100,6 +100,44 @@ function initDB() {
|
|||||||
)
|
)
|
||||||
");
|
");
|
||||||
|
|
||||||
|
// 电影评价表
|
||||||
|
// 评价人是「登录账号」(admins)——不是观影人(persons / viewers):
|
||||||
|
// 没一起去看的人只要有账号,也能打分、写评价并参与讨论。
|
||||||
|
$db->exec("
|
||||||
|
CREATE TABLE IF NOT EXISTS reviews (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
movie_id INTEGER NOT NULL,
|
||||||
|
admin_id INTEGER NOT NULL,
|
||||||
|
rating INTEGER NOT NULL,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (movie_id) REFERENCES movies(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (admin_id) REFERENCES admins(id) ON DELETE CASCADE
|
||||||
|
)
|
||||||
|
");
|
||||||
|
|
||||||
|
// 评价下的讨论(评论)。
|
||||||
|
// feelings 存的是感受 key 的 JSON 数组(如 [\"happy\",\"sad\"]),
|
||||||
|
// emoji 与文案不入库——统一在 lib/reviews.php 的目录里维护,改文案不用动数据。
|
||||||
|
$db->exec("
|
||||||
|
CREATE TABLE IF NOT EXISTS review_comments (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
review_id INTEGER NOT NULL,
|
||||||
|
admin_id INTEGER NOT NULL,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
feelings TEXT,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (review_id) REFERENCES reviews(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (admin_id) REFERENCES admins(id) ON DELETE CASCADE
|
||||||
|
)
|
||||||
|
");
|
||||||
|
|
||||||
|
// 一部电影每个账号只保留一条评价:再次提交就是修改(见 saveReview)
|
||||||
|
$db->exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_reviews_movie_admin ON reviews(movie_id, admin_id)");
|
||||||
|
$db->exec("CREATE INDEX IF NOT EXISTS idx_reviews_movie ON reviews(movie_id)");
|
||||||
|
$db->exec("CREATE INDEX IF NOT EXISTS idx_review_comments_review ON review_comments(review_id)");
|
||||||
|
|
||||||
// ===== 迁移字段 =====
|
// ===== 迁移字段 =====
|
||||||
try { $db->exec("ALTER TABLE cinemas ADD COLUMN color TEXT DEFAULT '#888888'"); } catch (PDOException $e) {}
|
try { $db->exec("ALTER TABLE cinemas ADD COLUMN color TEXT DEFAULT '#888888'"); } catch (PDOException $e) {}
|
||||||
try { $db->exec("ALTER TABLE admins ADD COLUMN avatar TEXT"); } catch (PDOException $e) {}
|
try { $db->exec("ALTER TABLE admins ADD COLUMN avatar TEXT"); } catch (PDOException $e) {}
|
||||||
|
|||||||
+321
@@ -0,0 +1,321 @@
|
|||||||
|
<?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);
|
||||||
|
|
||||||
|
// ===== 观后感受目录 =====
|
||||||
|
// key => [emoji, 中文名]。评论时可多选,每种感受一个专属 emoji。
|
||||||
|
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();
|
||||||
|
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();
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 写入 =====
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发表 / 修改评价。一个账号对一部电影只保留一条:已有则更新,没有则插入。
|
||||||
|
* 调用前请自行校验 rating 在 1-10、content 非空且未超长。
|
||||||
|
* 成功返回 true。
|
||||||
|
*/
|
||||||
|
function saveReview($movie_id, $admin_id, $rating, $content) {
|
||||||
|
$db = getDB();
|
||||||
|
$rating = (int)$rating;
|
||||||
|
$content = trim((string)$content);
|
||||||
|
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 = ?, updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = ?
|
||||||
|
");
|
||||||
|
$stmt->execute([$rating, $content, $existing['id']]);
|
||||||
|
} else {
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
INSERT INTO reviews (movie_id, admin_id, rating, content) VALUES (?, ?, ?, ?)
|
||||||
|
");
|
||||||
|
$stmt->execute([$movie_id, $admin_id, $rating, $content]);
|
||||||
|
}
|
||||||
|
$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]);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user