// +---------------------------------------------------------------------- declare (strict_types = 1); namespace addon\blog\controller; use addon\blog\model\BlogArticle as ArticleModel; use addon\blog\model\BlogComment as CommentModel; use think\facade\Request; use think\facade\View; use think\facade\Session; use think\facade\Db; /** * Comment 类 * * @author ywxapp */ class Comment extends BlogFrontend { /** * 公开访问(登录校验由方法内部通过 $this->auth 处理,会员统一以 uid 标识) * @var array */ protected $noNeedLogin = ['*']; /** * 发表评论 */ public function add() { if (! $this->auth->isLogin) { return json(['code' => 0, 'msg' => '请先登录']); } $uid = $this->auth->model->uid; // 限流:同一用户 N 秒内只能发一条,防灌水/刷屏(0 = 不限) $interval = max(0, (int) config('blog.comment_interval', 10)); if ($interval > 0 && \think\facade\Cache::get('blog_comment_lock:' . $uid)) { return json(['code' => 0, 'msg' => '评论太频繁,请稍后再试']); } $data = Request::post(); $validate = new \think\Validate([ 'content|评论内容' => 'require|min:5|max:500', 'article_id|文章ID' => 'require|number', 'parent_id|父评论ID' => 'number' ]); if (!$validate->check($data)) { return json(['code' => 0, 'msg' => $validate->getError()]); } // 只能给「已发布 + 审核通过」的文章评论: // 否则草稿/待审/驳回文章会成为垃圾评论入口,也泄露未公开内容 $article = ArticleModel::where('id', $data['article_id']) ->where('status', 1) ->where('audit_status', 2) ->find(); if (!$article) { return json(['code' => 0, 'msg' => '文章不存在或不可评论']); } if ($data['parent_id'] > 0) { $parentComment = CommentModel::find($data['parent_id']); if (!$parentComment || $parentComment->aid != $data['article_id']) { return json(['code' => 0, 'msg' => '回复的评论不存在']); } } // 先做 HTML 白名单净化(防 XSS),再做敏感词替换 $content = \addon\blog\library\HtmlSanitizer::clean((string) $data['content']); $sensitiveWords = ['垃圾', '广告', '违法', '政治']; $content = str_replace($sensitiveWords, '***', $content); $comment = new CommentModel(); $comment->uid = $uid; $comment->aid = $data['article_id']; $comment->pid = $data['parent_id'] ?? 0; $comment->content = $content; // 评论审核:need_audit 开启时先审后发(0=待审核,见 install.sql 注释),否则直接显示 $needAudit = (int) config('blog.need_audit'); $comment->status = $needAudit ? 0 : 1; $comment->ip = Request::ip(); $comment->user_agent = Request::server('HTTP_USER_AGENT'); $comment->save(); // 只有直接显示的评论才计数;待审核的等后台审核通过后再累加 if (!$needAudit) { $article->comment_count = Db::raw('comment_count + 1'); $article->save(); } if ($interval > 0) { \think\facade\Cache::set('blog_comment_lock:' . $uid, 1, $interval); } return json([ 'code' => 1, 'msg' => '评论发表成功', 'data' => [ 'id' => $comment->id, 'content' => $comment->content, 'create_at' => $comment->create_text, // 前端渲染取 d.user.*,键名必须一致(原先返回 member,导致新评论显示 undefined) 'user' => [ 'uid' => $uid, 'nickname' => $this->auth->model->nickname, 'avatar' => $this->auth->model->avatar ?? '/static/images/avatar.png' ], // 保留 member 兼容旧前端 'member' => [ 'uid' => $uid, 'nickname' => $this->auth->model->nickname, 'avatar' => $this->auth->model->avatar ?? '/static/images/avatar.png' ], 'parent_id' => $comment->pid ] ]); } /** * 删除评论 */ public function delete($id) { if (! $this->auth->isLogin) { return json(['code' => 0, 'msg' => '请先登录']); } $uid = $this->auth->model->uid; $comment = CommentModel::find($id); if (!$comment) { return json(['code' => 0, 'msg' => '评论不存在']); } if ($comment->uid != $uid) { return json(['code' => 0, 'msg' => '无权限操作']); } // 先统计将被删除的条数(含子评论),避免计数漂移(原先固定 -1) $deletedCount = $this->countWithChildren((int) $id); $this->deleteCommentWithChildren($id); $article = ArticleModel::find($comment->aid); if ($article) { $article->comment_count = Db::raw('comment_count - ' . max(1, $deletedCount)); $article->save(); } return json(['code' => 1, 'msg' => '评论已删除']); } /** * 递归统计评论及其子评论的条数 */ private function countWithChildren(int $id): int { $count = 1; $childIds = CommentModel::where('pid', $id)->column('id'); foreach ($childIds as $childId) { $count += $this->countWithChildren((int) $childId); } return $count; } /** * 递归删除评论及其子评论 */ private function deleteCommentWithChildren($commentId) { $comment = CommentModel::find($commentId); if ($comment) { $children = CommentModel::where('pid', $commentId)->select(); foreach ($children as $child) { $this->deleteCommentWithChildren($child->id); } $comment->delete(); } } /** * 评论点赞 */ public function like($id) { if (! $this->auth->isLogin) { return json(['code' => 0, 'msg' => '请先登录']); } $uid = $this->auth->model->uid; $comment = CommentModel::find($id); if (!$comment) { return json(['code' => 0, 'msg' => '评论不存在']); } $likeKey = "comment_like_{$id}_{$uid}"; if (Session::get($likeKey)) { Session::delete($likeKey); $comment->like_count = max(0, $comment->like_count - 1); $action = 'cancel'; } else { Session::set($likeKey, true); $comment->like_count += 1; $action = 'add'; } $comment->save(); return json([ 'code' => 1, 'msg' => $action == 'add' ? '点赞成功' : '已取消点赞', 'count' => $comment->like_count, 'action' => $action ]); } }