Django实现可嵌套的多级评论系统


评论系统是增强用户互动、构建社区氛围的核心功能之一。从简单的单级回复到复杂的嵌套式讨论,评论系统的设计直接影响着用户体验与内容生态。尤其是多级评论,它能清晰呈现对话脉络,让用户轻松追踪讨论线索,在博客论坛电商平台等场景中备受青睐。

本文将聚焦于如何使用 Django 框架快速实现一个功能完善的多级评论系统。我们不仅会探讨嵌套评论的数据模型设计、递归查询等核心技术点,还会提供可直接运行的代码示例,包括前端展示与后端逻辑的完整实现。无论你是 Django 初学者,还是希望为现有项目添加评论功能的开发者,都能通过本文的演示,快速掌握多级评论系统的构建方法,让你的应用轻松拥有媲美主流平台的互动能力。

本系统采用 Django 经典的 MTV(Model - Template - View)架构进行设计。

1. 模块划分

  • 用户管理模块:负责用户的注册和登录功能。
  • 评论管理模块:负责评论的发表、显示和回复功能。
  • 数据库模块:负责数据的存储和管理。

2. 模型设计

1. 评论模型Comment

在构建评论系统的过程中,评论模型的设计是至关重要的一环,尤其是在处理深层回复数据时,一个合理的评论模型能够确保系统的高效性和可扩展性。那么,究竟该如何设计出一个合理的评论模型呢?

我们在日常上网过程中,会接触到各式各样的评论系统,像短视频平台、外卖平台、电商平台等。这些评论系统通常包含几个关键要素:用户信息,它明确了评论的发布者;评论时间,记录了评论产生的时刻;回复对象,表明了评论是针对谁或哪条评论作出的回应;当然,最重要的就是评论内容,它承载了用户的观点和想法。

进一步观察这些评论系统,我们会发现评论之间存在着复杂的关系。以论坛场景为例,有楼主发起话题,随后其他用户围绕该话题进行评论,形成 “盖楼” 的效果;有的用户直接回复楼主,表达自己的看法;还有用户会针对某条具体的评论展开争论。这种复杂的关系可以抽象为树形结构来存储评论信息,即评论之间存在父子关系。基于此,我们可以设计出一个简单而有效的评论模型:

class Comment(models.Model):
    # 关联到 User 模型,使用外键,当用户删除时,对应评论也删除
    user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='comments')
    # 评论内容
    content = models.TextField()
    # 评论时间
    created_at = models.DateTimeField(auto_now_add=True)
    # 父级评论(自关联)
    parent = models.ForeignKey(
        'self',     # 关联模型自身
        on_delete=models.CASCADE,       # 父评论被删除后,子评论一并删除
        null=True,      # 允许为空,即代表顶级评论
        blank=True,
        related_name='replies'  # 通过comment.replies.all()获取所有回复
    )

    # 获取所有子回复(递归)
    def get_all_replies(self):
        replies = []
        for reply in self.replies.all():
            replies.append(reply)
            replies.extend(reply.get_all_replies())
        return replies

    # 获取父评论的用户名,用于显示回复的指向信息
    def get_parent_username(self):
        if self.parent:
            return self.parent.user.username
        return None

模型的递归函数get_all_replies()是一个设计的要点,在模板中可以使用模板标签来实现顶级评论下所有回复的遍历并渲染数据。而get_parent_username()用于显示回复所回复的对象是谁。

2. 其他模型

用户注册登录模型可参考文章:

Django实现登陆注册

3. 视图设计

对于评论和回复我们可以设计出两个视图函数进行处理提交的表单数据:

1. comment视图

comment视图主要处理发布的顶级评论,从提交的表单中获取数据然后进行简单的验证后创建评论数据到数据库中。

def comment(request):
    if request.user.is_authenticated:
        username = request.user.username
    else:
        username = "访客"
    if request.method == 'POST':
        # 登录状态验证
        if not request.user.is_authenticated:
            messages.error(request, '请先登录再发表评论')
            return redirect('user_login')
        # 获取表单数据
        content = request.POST.get('content')
        # 数据验证
        if not content.strip():
            messages.error(request, '评论内容不能为空')
            return redirect('comment')
        # 创建评论
        Comment.objects.create(content=content, user=request.user)
        return redirect('comment')
    # 查询数据库中评论列表
    comments = Comment.objects.filter(parent__isnull=True).order_by('-created_at')
    return render(request, 'comment.html', {
        'username': username,
        'comments': comments,
    })

2. reply视图

reply视图用于处理回复内容,相比于comment比较复杂。思路如下:

  1. 前端用户操作:用户提交回复表单,通过form表单的action属性指向目标URL,即向reply发送http请求并附带上所回复评论(即父评论)的id
  2. 视图函数处理:数据通过url.py的配置,传递给视图函数reply。函数获取父评论对象,并创建子评论(子评论的parent会设置为获取的父评论)。这样就形成了一个父子关系。
  3. 前端渲染数据:在顶级评论内进行回复数据的遍历并渲染。
def reply(request, comment_id):
    # 获取被回复的评论对象
    comment = Comment.objects.get(id=comment_id)
    if request.method == 'POST':
        # 登录状态验证
        if not request.user.is_authenticated:
            messages.error(request, '请先登录再发表回复')
            return redirect('user_login')

        # 获取回复表单数据
        reply_content = request.POST.get('reply_content')
        # 数据验证
        if not reply_content.strip():
            messages.error(request, '回复内容不能为空')
            return redirect('comment')
        # 创建回复
        Comment.objects.create(
            content=reply_content,
            user=request.user,
            parent=comment
        )
        return redirect('comment')
    return render(request, 'comment.html', {
        'comment': comment,
    })

3. 其他视图

注册登录视图参考文章:

Django实现登陆注册

4. 模板设计

1. comment.html

合理的使用模板标签遍历渲染数据也是实现多级嵌套评论的关键的一步,核心代码如下:

<!-- 评论表单 -->
<div class="comment-form">
    <h2 class="section-title">发表评论</h2>
    <form action="{% url 'comment' %}" method="post">
        {% csrf_token %}
        <div class="form-group">
            <textarea name="content" rows="4" placeholder="分享你的想法..."></textarea>
        </div>
        <button type="submit" class="btn">发布评论</button>
    </form>
</div>

<!-- 评论列表 -->
<div class="comments-section">
    <h2 class="section-title">评论列表</h2>
    <!-- 遍历显示评论 -->
    {% for comment in comments %}
        <div class="comment">
            <div class="comment-header">
                <div>
                    <span class="comment-author">{{ comment.user.username }}</span>
                </div>
                <button type="button" onclick="toggleReplyForm({{ comment.id }})" class="reply-btn">
                    <i class="fa fa-reply"></i> 回复
                </button>
            </div>
            <div class="comment-timestamp">{{ comment.created_at }}</div>
            <div class="comment-content">{{ comment.content }}</div>

            <!-- 回复表单 -->
            <div class="reply-form" id="reply-form-{{ comment.id }}">
                <form action="{% url 'reply' comment.id %}" method="post">
                    {% csrf_token %}
                    <textarea name="reply_content" rows="3" placeholder="回复 {{ comment.user.username }}..."></textarea>
                    <button type="submit" class="btn">提交回复</button>
                </form>
            </div>

            <!-- 回复列表 -->
            <div class="replies">
                <!-- 通过模型中定义的递归函数遍历评论的所有回复 -->
                {% for reply in comment.get_all_replies %}
                    <div class="reply">
                        <div class="reply-header">
                            <div>
                                <span class="reply-author">{{ reply.user.username }}</span>
                                <!-- 通过模型中定义的函数显示回复的用户名 -->
                                <span class="reply-to">回复 {{ reply.get_parent_username }}</span>
                            </div>
                            <button type="button" onclick="toggleReplyForm({{ reply.id }})" class="reply-btn">
                                <i class="fa fa-reply"></i> 回复
                            </button>
                        </div>
                        <div class="reply-timestamp">{{ reply.created_at }}</div>
                        <div class="reply-content">{{ reply.content }}</div>
                        <!-- 回复表单 -->
                        <div class="reply-form" id="reply-form-{{ reply.id }}">
                            <form action="{% url 'reply' reply.id %}" method="post">
                                {% csrf_token %}
                                <textarea name="reply_content" rows="3" placeholder="回复 {{ reply.user.username }}..."></textarea>
                                <button type="submit" class="btn">提交回复</button>
                            </form>
                        </div>
                    </div>
                {% endfor %}
            </div>
        </div>
    {% endfor %}
</div>

对于评论列表的渲染需要解释一下:

1. 渲染所有顶级评论

先通过comment视图返回的所有顶级评论数据comments,进行顶级评论的遍历。

<!-- 遍历显示评论 -->
{% for comment in comments %}
    <div class="comment">
        ....

2. 顶级评论回复表单处理

提交表单数据,向/reply发送http请求并附带所回复的评论的id给视图函数,视图reply通过收到的数据创建回复,并标记回复目标parent,即父评论。

<!-- 回复表单 -->
<div class="reply-form" id="reply-form-{{ comment.id }}">
    <form action="{% url 'reply' comment.id %}" method="post">
        {% csrf_token %}
        <textarea name="reply_content" rows="3" placeholder="回复 {{ comment.user.username }}..."></textarea>
        <button type="submit" class="btn">提交回复</button>
    </form>
</div>

3. 顶级评论下所有回复的渲染

通过comment模型中定义的递归函数get_all_replies获取所有回复评论,进行遍历。回复内容与顶级评论基本一致,但是多了一个回复目标,即通过模型中定义的get_parent_username函数获取发表其父评论的用户名。

对回复的评论再次进行回复时,回复表单会将此条被回复的评论的idreply.id通过URL将数据传递给reply视图,并创建评论,给评论加上parent属性,形成新的父子关系。

举个例子:

顶级评论(父),顶级评论的回复(子),回复的回复(孙)

顶级评论(父)通过{% for comment in comments %}遍历,在遍历内{% for reply in comment.get_all_replies %}的二次遍历将顶级评论的子孙或是重孙都遍历出来(因为使用的是递归函数),其子孙的回复表单使用{% url 'reply' reply.id %}reply就是当前要回复的子孙对象,而reply.id就是要回复的子孙评论的id,也就意味着表单中填写的新内容是要回复哪个子孙,在视图中会处理这一关系。

<!-- 回复列表 -->
<div class="replies">
    <!-- 通过模型中定义的递归函数遍历评论的所有回复 -->
    {% for reply in comment.get_all_replies %}
        <div class="reply">
            <div class="reply-header">
                <div>
                    <span class="reply-author">{{ reply.user.username }}</span>
                    <!-- 通过模型中定义的函数显示回复的用户名 -->
                    <span class="reply-to">回复 {{ reply.get_parent_username }}</span>
                </div>
                <button type="button" onclick="toggleReplyForm({{ reply.id }})" class="reply-btn">
                    <i class="fa fa-reply"></i> 回复
                </button>
            </div>
            <div class="reply-timestamp">{{ reply.created_at }}</div>
            <div class="reply-content">{{ reply.content }}</div>
            <!-- 回复表单 -->
            <div class="reply-form" id="reply-form-{{ reply.id }}">
                <form action="{% url 'reply' reply.id %}" method="post">
                    {% csrf_token %}
                    <textarea name="reply_content" rows="3" placeholder="回复 {{ reply.user.username }}..."></textarea>
                    <button type="submit" class="btn">提交回复</button>
                </form>
            </div>
        </div>
    {% endfor %}
</div>

2. 其他模板

注册登录模板参考文章:

Django实现登陆注册

5. 其他配置

1. url.py

urlpatterns = [
    path('admin/', admin.site.urls),
    path('comment/', views.comment, name='comment'),
    path('reply/<int:comment_id>/', views.reply, name='reply'),
    path('user_register/', views.user_register, name='user_register'),
    path('user_login/', views.user_login, name='user_login'),
]

2. admin.py

@admin.register(Comment)
class CommentAdmin(admin.ModelAdmin):
    def get_username(self, obj):
        return obj.user.username
    get_username.short_description = '用户名'
    list_display = ('get_username', 'content', 'created_at')

6. 效果展示

7. 完整项目结构

django_comment/
├── comment/
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── migrations/
│   ├── models.py
│   ├── tests.py
│   └── views.py
├── django_comment/
│   ├── __init__.py
│   ├── asgi.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── templates/
│   ├── comment.html
│   ├── login.html
│   └── register.html
├── result/
│   └── result.png
├── manage.py
└── README.md

8. 源代码

bird-six/django_comment: django框架实现的多级评论嵌套系统

0 条评论

发表评论

暂无评论,欢迎发表您的观点!