You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Django+DRF基于MPTT的嵌套评论API出现详情找不到问题求助

Django MPTT + DRF 嵌套评论系统:深层嵌套后早期评论无法编辑/删除

使用MPTT结合Django Rest Framework(DRF)实现嵌套评论系统,目前嵌套显示正常,所有增删改操作均通过DRF API完成,未直接调用Django ORM。但遇到一个Bug:
当评论嵌套到第7或第8层时,早期的评论(如最早的aaaa、bbbb)在编辑/删除时会返回*「detail: Not found.」*,但列表查询能正常显示这些评论。例如添加评论ggggg后,aaaa和bbbb无法操作;删除ggggg后(其子评论hhhh随CASCADE删除),aaaa和bbbb又恢复正常。

我的代码

models.py

from django.db import models
from django.template.defaultfilters import truncatechars
from mptt.managers import TreeManager

from post.models import Post
from account.models import Account
from mptt.models import MPTTModel, TreeForeignKey


# Create your models here.

# With MPTT
class CommentManager(TreeManager):
    def viewable(self):
        queryset = self.get_queryset().filter(level=0)
        return queryset


class Comment(MPTTModel):
    parent = TreeForeignKey('self', on_delete=models.CASCADE, null=True, blank=True, related_name='comment_children')
    post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='comment_post')
    user = models.ForeignKey(Account, on_delete=models.CASCADE, related_name='comment_account')
    content = models.TextField(max_length=9000)
    created_date = models.DateTimeField(auto_now_add=True)
    updated_date = models.DateTimeField(auto_now=True)
    status = models.BooleanField(default=True)

    objects = CommentManager()

    def __str__(self):
        return f'Comment by {str(self.pk)}-{self.user.full_name.__self__}'

    @property
    def short_content(self):
        return truncatechars(self.content, 99)

    class MPTTMeta:
        # If changing the order - MPTT needs the programmer to go into console and do Comment.objects.rebuild()
        order_insertion_by = ['-created_date']

serializers.py(仅展示评论序列化部分)

class RecursiveField(serializers.Serializer):
    def to_representation(self, value):
        serializer = self.parent.parent.__class__(value, context=self.context)
        return serializer.data

class CommentSerializer(serializers.ModelSerializer):
    post_slug = serializers.SerializerMethodField()
    user = serializers.StringRelatedField(read_only=True)
    user_name = serializers.SerializerMethodField()
    user_id = serializers.PrimaryKeyRelatedField(read_only=True)

    comment_children = RecursiveField(many=True)

    class Meta:
        model = Comment
        fields = '__all__'

    # noinspection PyMethodMayBeStatic
    # noinspection PyBroadException
    def get_post_slug(self, instance):
        try:
            slug = instance.post.slug
            return slug
        except Exception:
            pass

    # noinspection PyMethodMayBeStatic
    # noinspection PyBroadException
    def get_user_name(self, instance):
        try:
            full_name = f'{instance.user.first_name} {instance.user.last_name}'
            return full_name
        except Exception:
            pass

    # noinspection PyMethodMayBeStatic
    def validate_content(self, value):
        if len(value) < COM_MIN_LEN:
            raise serializers.ValidationError('The comment is too short.')
        elif len(value) > COM_MAX_LEN:
            raise serializers.ValidationError('The comment is too long.')
        else:
            return value

    def get_fields(self):
        fields = super(CommentSerializer, self).get_fields()
        fields['comment_children'] = CommentSerializer(many=True, required=False)
        return fields

API视图

class CommentAV(mixins.CreateModelMixin, generics.GenericAPIView):
    # This class only allows users to create comments but not list all comments.  List all comments would
    # be too taxing for the server if the website got tons of comments.
    queryset = Comment.objects.viewable().filter(status=True)
    serializer_class = CommentSerializer

    def post(self, request, *args, **kwargs):
        return self.create(request, *args, **kwargs)

    def perform_create(self, serializer):
        # Overriding perform_create.  Can create comment using the authenticated account.
        # Cannot pretend to be someone else to create comment on his or her behalf.
        commenter = self.request.user
        now = timezone.now()
        before_now = now - timezone.timedelta(seconds=COM_WAIT_TIME)
        # Make sure user can only create comment again after waiting for wait_time.
        this_user_comments = Comment.objects.filter(user=commenter, created_date__lt=now, created_date__gte=before_now)
        if this_user_comments:
            raise ValidationError(f'You have to wait for {COM_WAIT_TIME} seconds before you can post another comment.')
        elif Comment.objects.filter(user=commenter, level__gt=COMMENT_LEVEL_DEPTH):
            raise ValidationError(f'You cannot make another level-deep reply.')
        else:
            serializer.save(user=commenter)

    # By combining perform_create method to filter out only the owner of the comment can edit his or her own
    # comment -- and the permission_classes of IsAuthenticated -- allowing only authenticated user to create
    # comments.  When doing custome permission - such as redefinte BasePermission's has_object_permission,
    # it doesn't work with ListCreateAPIView - because has_object_permission is meant to be used on single instance
    # such as object detail.
    permission_classes = [IsAuthenticated]


class CommentAVAdmin(generics.ListCreateAPIView):
    queryset = Comment.objects.viewable()
    serializer_class = CommentSerializer

    permission_classes = [IsAdminUser]


class CommentDetailAV(generics.RetrieveUpdateDestroyAPIView):
    queryset = Comment.objects.viewable().filter(status=True)
    serializer_class = CommentSerializer

    permission_classes = [CustomAuthenticatedOrReadOnly]

    def destroy(self, request, *args, **kwargs):
        instance = self.get_object()
        if not instance.user.id == self.request.user.id:
            return Response({
                'Error': 'Comment isn\'t deleted! Please log into the owner account of this comment to delete this comment.'},
                status=status.HTTP_400_BAD_REQUEST)
        self.perform_destroy(instance)
        return Response({'Success': 'Comment deleted!'}, status=status.HTTP_204_NO_CONTENT)


class CommentDetailAVAdmin(generics.RetrieveUpdateDestroyAPIView):
    queryset = Comment.objects.viewable()
    serializer_class = CommentSerializer

    permission_classes = [IsAdminUser]


class CommentDetailChildrenAV(generics.RetrieveUpdateDestroyAPIView):
    queryset = Comment.objects.viewable().get_descendants().filter(status=True)
    serializer_class = CommentSerializer

    permission_classes = [CustomAuthenticatedOrReadOnly]

    def destroy(self, request, *args, **kwargs):
        instance = self.get_object()
        if not instance.user.id == self.request.user.id:
            return Response({
                'Error': 'Reply isn\'t deleted! Please log into the owner account of this reply to delete this reply.'},
                status=status.HTTP_400_BAD_REQUEST)
        self.perform_destroy(instance)
        return Response({'Success': 'Comment deleted!'}, status=status.HTTP_204_NO_CONTENT)

博客应用中的API调用

add_comment = requests.post(BLOG_BASE_URL + f'api/post-list/comments/create-comments/',
                                                headers=headers,
                                                data=user_comment)
                                                
add_reply = requests.post(BLOG_BASE_URL + f'api/post-list/comments/create-comments/',
                                              headers=headers,
                                              data=user_reply)

requests.request('PUT', BLOG_BASE_URL + f'api/post-list/comments/{pk}/',
                                 headers=headers,
                                 data=user_comment)

response = requests.request('PUT', BLOG_BASE_URL + f'api/post-list/comments/children/{pk}/',
                                            headers=headers,
                                            data=user_comment)

response = requests.request('DELETE', BLOG_BASE_URL + f'api/post-list/comments/{pk}/', headers=headers)

已尝试的解决措施

  • 执行Comment.objects.rebuild(),未解决问题
  • 在Python shell中可正常查询到出现问题的评论,能调用get_ancestors方法,但API端点仍返回Not Found
  • 重启开发服务器后,所有评论可恢复正常的删改操作
  • 了解到类似问题案例,但自身使用Django默认开发服务器,而非案例中的Apache

内容的提问来源于stack exchange,提问作者Vinh Nguyen

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.10 23:40:26