电影影评+评论:修半成品并补全 /movies/<id>/{reviews,review,comments} #27

用户原话(2026-10-28):「鸿蒙端对齐网页端」。观影详情只能看平均分,
不能写影评/评论。盘点发现服务端其实有份**半成品且带 bug**:

· comments GET 写死"只有我写过影评才返回" —— 错,改为独立 reviews 端点;
· comments POST 调 addComment 参数**完全错位**(parent_id 传给了 feelings、
  target_person 传给了 parentId)—— 评论实际存不进;
· 缺影评/评论的删除端点。

现在对齐 Web 端 reviews 四条路由:
· GET    /movies/<id>/reviews   全部影评(含评论树)+ 我的影评 + 统计 + 感受目录
· POST   /movies/<id>/review    写/改我的影评
· DELETE /movies/<id>/review    删我的影评
· POST   /movies/<id>/comments  评论/回复(review_id/content/parent_id)
· DELETE /movies/<id>/comments?comment_id=N  删我的评论

判据:_i27_movie_review_api.py 22/0(写/改影评 · 列表含 mine · 评论/回复 ·
删评论级联 · 删影评 · 越权删别人影评被拒),_i27_falsify.py 注掉写分支变红。
This commit is contained in:
2026-10-28 22:29:55 +08:00
parent ad71c80f48
commit b6dc60452a
+78 -15
View File
@@ -2220,24 +2220,44 @@ final class ApiHandler
]); ]);
} }
if ($sub === 'comments') { /* ────────────────────────────────────────────────────────────────
🔴 工单 #27(用户 2026-10-28「鸿蒙端对齐网页端」):影评 + 评论。
这里原来是一份**半成品且带 bug**:
· `comments` GET 写死"只有我写过影评才返回"——错,应返回全部;
· `comments` POST 只能评论"我自己的影评",且 `addComment` 参数
**完全错位**(`parent_id` 传给了 `feelings`、`target_person`
传给了 `parentId`)——评论实际上存不进;
· 缺影评/评论的**删除**端点。
现在对齐 Web 端 `movies/review_save / review_del / comment_add /
comment_del` 四条路由,端点是:
GET /movies/<id>/reviews 全部影评(含评论)+ 我的影评
POST /movies/<id>/review 写/改我的影评(rating/content/feelings)
DELETE /movies/<id>/review 删我的影评
POST /movies/<id>/comments 评论/回复某条影评(review_id/content/parent_id)
DELETE /movies/<id>/comments?comment_id=N 删我的评论
⚠️ review_id / comment_id 走 body 或 query(`Api::param`)
—— 路由只解析三段,第 4 段会被丢(tasks 同款教训)。
⚠️ 评论归属校验:`addComment` 内部已判 review_id 归属,
拒跨影评评论(见 MovieReview::addComment)。
──────────────────────────────────────────────────────────────── */
if ($sub === 'reviews') {
if ($method === 'GET') { if ($method === 'GET') {
$rv = MovieReview::mine($id, $pid); Api::ok([
Api::ok($rv ? MovieReview::forEvent($id) : []); 'items' => MovieReview::forEvent($id),
} 'mine' => MovieReview::mine($id, $pid),
if ($method === 'POST') { 'stats' => MovieReview::stats($id),
Api::requireWrite(); 'catalog' => MovieReview::CATALOG,
$rv = MovieReview::mine($id, $pid); ]);
if (!$rv) { Api::fail('先写一条评价,才能评论。', 'unprocessable', 422); }
$txt = Api::str('content', '', 4000);
if ($txt === '') { Api::fail('评论不能是空的。', 'unprocessable', 422); }
$cid = MovieReview::addComment((int)$rv['id'], $pid, $txt,
Api::int('parent_id'), Api::str('target_person', ''));
if ($cid <= 0) { Api::fail('评论没保存下来(可能内容太长)。', 'unprocessable', 422); }
Api::ok(['id' => $cid], 201);
} }
} }
if ($sub === 'review' && ($method === 'POST' || $method === 'PUT' || $method === 'PATCH')) { if ($sub === 'review') {
if ($method === 'GET') {
Api::ok(MovieReview::mine($id, $pid));
}
if ($method === 'POST' || $method === 'PUT' || $method === 'PATCH') {
Api::requireWrite(); Api::requireWrite();
$my = MovieReview::mine($id, $pid); $my = MovieReview::mine($id, $pid);
$err = MovieReview::save( $err = MovieReview::save(
@@ -2250,6 +2270,49 @@ final class ApiHandler
if ($err !== '') { Api::fail($err, 'unprocessable', 422); } if ($err !== '') { Api::fail($err, 'unprocessable', 422); }
Api::ok(MovieReview::mine($id, $pid)); Api::ok(MovieReview::mine($id, $pid));
} }
if ($method === 'DELETE') {
Api::requireWrite();
$my = MovieReview::mine($id, $pid);
if ($my === null) {
Api::fail('你没有这条记录的评价。', 'not_found', 404);
}
if (!MovieReview::deleteMine((int)$my['id'], $pid)) {
Api::fail('只能删除自己的评价。', 'forbidden', 403);
}
Api::ok(['deleted' => (int)$my['id']]);
}
Api::fail('不支持的方法。', 'method_not_allowed', 405);
}
if ($sub === 'comments') {
if ($method === 'POST') {
Api::requireWrite();
$reviewId = Api::int('review_id');
if ($reviewId <= 0) {
Api::fail('要评论哪条评价?缺 `review_id`。', 'unprocessable', 422);
}
$txt = Api::str('content', '', 4000);
if ($txt === '') { Api::fail('评论不能是空的。', 'unprocessable', 422); }
$cid = MovieReview::addComment(
$reviewId, $pid, $txt,
array_values((array)Api::param('feelings', [])),
Api::int('parent_id')
);
if (is_string($cid)) { Api::fail($cid, 'unprocessable', 422); }
Api::ok(['id' => $cid], 201);
}
if ($method === 'DELETE') {
Api::requireWrite();
$cid = Api::int('comment_id');
if ($cid <= 0) {
Api::fail('要删哪条评论?缺 `comment_id`。', 'unprocessable', 422);
}
if (!MovieReview::deleteComment($cid, $pid)) {
Api::fail('只能删除自己的评论。', 'forbidden', 403);
}
Api::ok(['deleted' => $cid]);
}
Api::fail('不支持的方法。', 'method_not_allowed', 405);
}
if ($method === 'GET') { Api::ok($m); } if ($method === 'GET') { Api::ok($m); }
if ($method === 'PATCH' || $method === 'PUT') { if ($method === 'PATCH' || $method === 'PUT') {