#评论系统与交互:递归评论与点赞功能
📂 所属阶段:第四阶段 — 实战演练(道满博客)
🔗 相关章节:数据库关系设计 · 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 层的嵌套评论,建议显示"查看回复"折叠。
🔗 扩展阅读

