评论系统与交互:递归评论与点赞功能

📂 所属阶段:第四阶段 — 实战演练(道满博客)
🔗 相关章节:数据库关系设计 · Flask-Login 实战


1. 评论数据模型

# app/models/comment.py
from datetime import datetime
from app.extensions import db

class Comment(db.Model):
    __tablename__ = "comments"

    id = db.Column(db.Integer, primary_key=True)
    content = db.Column(db.Text, nullable=False)
    likes = db.Column(db.Integer, default=0)
    is_deleted = db.Column(db.Boolean, default=False)  # 软删除
    created_at = db.Column(db.DateTime, default=datetime.utcnow)

    # 外键
    post_id = db.Column(db.Integer, db.ForeignKey("posts.id", ondelete="CASCADE"), nullable=False)
    author_id = db.Column(db.Integer, db.ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
    parent_id = db.Column(db.Integer, db.ForeignKey("comments.id", ondelete="CASCADE"), nullable=True)

    # 关系
    post = db.relationship("Post", back_populates="comments")
    author = db.relationship("User")
    parent = db.relationship("Comment", remote_side=[id], backref=db.backref("replies", lazy="dynamic"))

    def __repr__(self):
        return f"<Comment {self.id} by {self.author_id}>"

2. 评论路由

# app/comments/routes.py
from flask import Blueprint, request, redirect, url_for, flash, jsonify
from flask_login import login_required, current_user
from app.extensions import db
from app.models import Comment, Post

comments_bp = Blueprint("comments", __name__, url_prefix="/comments")

@comments_bp.route("/add", methods=["POST"])
@login_required
def add_comment():
    post_id = request.form.get("post_id", type=int)
    content = request.form.get("content", "").strip()
    parent_id = request.form.get("parent_id", type=int) or None

    if not content:
        flash("评论内容不能为空", "warning")
        return redirect(url_for("articles.detail", post_id=post_id))

    post = Post.query.get_or_404(post_id)

    comment = Comment(
        content=content,
        post_id=post_id,
        author_id=current_user.id,
        parent_id=parent_id,
    )
    db.session.add(comment)
    db.session.commit()
    flash("评论已发布!", "success")
    return redirect(url_for("articles.detail", post_id=post_id))

@comments_bp.route("/<int:comment_id>/delete", methods=["POST"])
@login_required
def delete_comment(comment_id):
    comment = Comment.query.get_or_404(comment_id)

    # 只能删除自己的评论,或管理员可删
    if comment.author_id != current_user.id and not current_user.is_admin:
        flash("无权删除此评论", "danger")
        return redirect(url_for("articles.detail", post_id=comment.post_id))

    # 软删除
    comment.is_deleted = True
    db.session.commit()
    flash("评论已删除", "info")
    return redirect(url_for("articles.detail", post_id=comment.post_id))

@comments_bp.route("/<int:comment_id>/like", methods=["POST"])
@login_required
def like_comment(comment_id):
    comment = Comment.query.get_or_404(comment_id)
    comment.likes += 1
    db.session.commit()
    return jsonify({"likes": comment.likes})

3. 递归评论渲染

# app/utils/comments.py
def build_comment_tree(comments):
    """将扁平评论列表转为树形结构"""
    tree = {}
    root = []

    for comment in comments:
        tree[comment.id] = {"comment": comment, "replies": []}

    for comment in comments:
        if comment.parent_id:
            tree[comment.parent_id]["replies"].append(tree[comment.id])
        else:
            root.append(tree[comment.id])

    return root
<!-- templates/comments/comment_tree.html -->
{% macro render_comments(comments) %}
<ul class="comment-list">
    {% for item in comments %}
    <li class="comment-item">
        <div class="comment">
            <img src="{{ item.comment.author.get_avatar(32) }}" class="comment-avatar">
            <div class="comment-body">
                <div class="comment-meta">
                    <strong>{{ item.comment.author.username }}</strong>
                    <span>{{ item.comment.created_at.strftime('%Y-%m-%d %H:%M') }}</span>
                </div>
                <div class="comment-content">
                    {% if item.comment.is_deleted %}
                        <em class="text-muted">[该评论已删除]</em>
                    {% else %}
                        {{ item.comment.content }}
                    {% endif %}
                </div>
                {% if not item.comment.is_deleted %}
                <div class="comment-actions">
                    <button class="like-btn" data-id="{{ item.comment.id }}">
                        👍 {{ item.comment.likes }}
                    </button>
                    <button class="reply-btn" data-id="{{ item.comment.id }}">回复</button>
                    {% if current_user.is_authenticated and (current_user.id == item.comment.author_id or current_user.is_admin) %}
                    <form method="POST" action="{{ url_for('comments.delete_comment', comment_id=item.comment.id) }}" style="display:inline;">
                        <button type="submit" class="delete-btn">删除</button>
                    </form>
                    {% endif %}
                </div>
                {% endif %}
            </div>
        </div>

        <!-- 递归渲染子评论 -->
        {% if item.replies %}
        <div class="comment-replies">
            {{ render_comments(item.replies) }}
        </div>
        {% endif %}
    </li>
    {% endfor %}
</ul>
{% endmacro %}

4. 小结

评论系统核心:
1. 自引用一对多(parent_id)
2. 递归关系:replies = relationship("Comment", backref="parent")
3. 树形结构:build_comment_tree() 将扁平列表转为树
4. 模板递归:{% macro render_comments() %} 递归调用自身
5. 点赞:AJAX 异步点赞,不刷新页面

💡 性能优化:评论树构建对大量评论较慢,可用缓存。对于深度超过 3 层的嵌套评论,建议显示"查看回复"折叠。


🔗 扩展阅读