from rest_framework import generics, status from rest_framework.permissions import IsAuthenticated, AllowAny, IsAuthenticatedOrReadOnly from rest_framework.parsers import JSONParser, MultiPartParser, FormParser from rest_framework.pagination import PageNumberPagination from rest_framework.views import APIView from rest_framework.response import Response from django.shortcuts import get_object_or_404 from django.db.models import F from django.db import transaction from drf_yasg.utils import swagger_auto_schema from drf_yasg import openapi from utils.response_codes import ResponseCode, create_standardized_response, create_standardized_error_response from message.utils import create_message from chunyu_project.common_schemas import success_response, error_response, unauthorized_response, not_found_response from user.models import FUser from user.utils import track_task from .models import Article, ArticleComment, ArticleLike, ArticleFavorite, ArticleCommentLike from .serializers import ( ArticleListSerializer, ArticleDetailSerializer, ArticleCreateUpdateSerializer, ArticleCommentSerializer, ArticleManageSerializer, ) class ArticlePagination(PageNumberPagination): page_size = 10 page_size_query_param = 'page_size' max_page_size = 100 def get_paginated_response(self, data): return Response({ 'code': 10000, 'message': 'Success', 'data': { 'count': self.page.paginator.count, 'next': self.get_next_link(), 'previous': self.get_previous_link(), 'results': data, } }) class ArticleListCreateView(generics.ListCreateAPIView): parser_classes = [JSONParser, MultiPartParser, FormParser] pagination_class = ArticlePagination def get_permissions(self): if self.request.method == 'GET': return [AllowAny()] return [IsAuthenticated()] def get_serializer_class(self): if self.request.method == 'POST': return ArticleCreateUpdateSerializer return ArticleListSerializer def get_queryset(self): qs = Article.objects.filter(status='published') category = self.request.query_params.get('category') if category and category != 'all': qs = qs.filter(category=category) tag = self.request.query_params.get('tag') if tag: qs = qs.filter(tags__contains=[tag]) search = self.request.query_params.get('search') if search: from django.db.models import Q qs = qs.filter( Q(title__icontains=search) | Q(excerpt__icontains=search) | Q(tags__overlap=[search]) ) ordering = self.request.query_params.get('ordering', '-created_at') return qs.order_by('-is_top', ordering) @swagger_auto_schema( tags=['文章'], operation_summary='获取文章列表', operation_description='分页获取已发布文章列表,支持按分类和排序筛选,登录用户可查看收藏状态', manual_parameters=[ openapi.Parameter('category', openapi.IN_QUERY, description='文章分类筛选', type=openapi.TYPE_STRING), openapi.Parameter('tag', openapi.IN_QUERY, description='文章标签筛选', type=openapi.TYPE_STRING), openapi.Parameter('search', openapi.IN_QUERY, description='搜索关键词(标题、摘要、标签)', type=openapi.TYPE_STRING), openapi.Parameter('ordering', openapi.IN_QUERY, description='排序方式,默认 -created_at', type=openapi.TYPE_STRING), openapi.Parameter('page', openapi.IN_QUERY, description='页码', type=openapi.TYPE_INTEGER), openapi.Parameter('page_size', openapi.IN_QUERY, description='每页数量,默认10,最大100', type=openapi.TYPE_INTEGER), ], responses={200: success_response} ) def list(self, request, *args, **kwargs): queryset = self.filter_queryset(self.get_queryset()) page = self.paginate_queryset(queryset) if page is not None: serializer = self.get_serializer(page, many=True) data = serializer.data if request.user.is_authenticated: from .models import ArticleFavorite fav_ids = set( ArticleFavorite.objects.filter(user=request.user).values_list('article_id', flat=True) ) for item in data: item['is_favorited'] = item['id'] in fav_ids else: for item in data: item['is_favorited'] = False return self.get_paginated_response(data) serializer = self.get_serializer(queryset, many=True) data = serializer.data if request.user.is_authenticated: from .models import ArticleFavorite fav_ids = set( ArticleFavorite.objects.filter(user=request.user).values_list('article_id', flat=True) ) for item in data: item['is_favorited'] = item['id'] in fav_ids else: for item in data: item['is_favorited'] = False return Response({'code': 10000, 'message': 'Success', 'data': data}) @swagger_auto_schema( tags=['文章'], operation_summary='创建文章', operation_description='创建新文章,需要登录', request_body=ArticleCreateUpdateSerializer, responses={ 201: success_response, 400: error_response, 401: unauthorized_response, } ) def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) if serializer.is_valid(): serializer.save() track_task(request.user, 'post') return create_standardized_response( data=serializer.data, code=ResponseCode.SUCCESS, message='文章创建成功', status_code=status.HTTP_201_CREATED ) return create_standardized_error_response( data=serializer.errors, code=ResponseCode.VALIDATION_ERROR, message='提交失败', status_code=status.HTTP_400_BAD_REQUEST ) class ArticleDetailView(APIView): parser_classes = [JSONParser, MultiPartParser, FormParser] permission_classes = [IsAuthenticatedOrReadOnly] @swagger_auto_schema( tags=['文章'], operation_summary='获取文章详情', operation_description='获取指定文章的详细信息,同时增加浏览量,登录用户可查看收藏状态', manual_parameters=[ openapi.Parameter('pk', openapi.IN_PATH, description='文章ID', type=openapi.TYPE_INTEGER, required=True), ], responses={ 200: success_response, 404: not_found_response, } ) def get(self, request, pk): article = get_object_or_404(Article, pk=pk) Article.objects.filter(pk=pk).update(views=F('views') + 1) article.refresh_from_db() serializer = ArticleDetailSerializer(article, context={'request': request}) data = serializer.data if request.user.is_authenticated: data['is_favorited'] = ArticleFavorite.objects.filter( user=request.user, article=article ).exists() else: data['is_favorited'] = False data['favorites_count'] = ArticleFavorite.objects.filter(article=article).count() return create_standardized_response(data=data, code=ResponseCode.SUCCESS) @swagger_auto_schema( tags=['文章'], operation_summary='更新文章', operation_description='更新指定文章,仅文章作者可操作', manual_parameters=[ openapi.Parameter('pk', openapi.IN_PATH, description='文章ID', type=openapi.TYPE_INTEGER, required=True), ], request_body=ArticleCreateUpdateSerializer, responses={ 200: success_response, 400: error_response, 401: unauthorized_response, 403: error_response, 404: not_found_response, } ) def put(self, request, pk): article = get_object_or_404(Article, pk=pk) if article.author != request.user: return create_standardized_error_response( code=ResponseCode.VALIDATION_ERROR, message='无权操作', status_code=status.HTTP_403_FORBIDDEN ) serializer = ArticleCreateUpdateSerializer(article, data=request.data, partial=True, context={'request': request}) if serializer.is_valid(): serializer.save() return create_standardized_response(data=serializer.data, code=ResponseCode.SUCCESS, message='文章更新成功') return create_standardized_error_response( data=serializer.errors, code=ResponseCode.VALIDATION_ERROR, status_code=status.HTTP_400_BAD_REQUEST ) @swagger_auto_schema( tags=['文章'], operation_summary='删除文章', operation_description='删除指定文章,仅文章作者可操作', manual_parameters=[ openapi.Parameter('pk', openapi.IN_PATH, description='文章ID', type=openapi.TYPE_INTEGER, required=True), ], responses={ 204: success_response, 401: unauthorized_response, 403: error_response, 404: not_found_response, } ) def delete(self, request, pk): article = get_object_or_404(Article, pk=pk) if article.author != request.user: return create_standardized_error_response( code=ResponseCode.VALIDATION_ERROR, message='无权操作', status_code=status.HTTP_403_FORBIDDEN ) article.delete() return create_standardized_response(code=ResponseCode.SUCCESS, message='文章删除成功', status_code=status.HTTP_204_NO_CONTENT) class ArticleCommentListCreateView(generics.ListCreateAPIView): parser_classes = [JSONParser, MultiPartParser] pagination_class = ArticlePagination def get_permissions(self): if self.request.method == 'GET': return [AllowAny()] return [IsAuthenticated()] serializer_class = ArticleCommentSerializer def get_queryset(self): article_id = self.kwargs['article_id'] return ArticleComment.objects.filter(article_id=article_id, parent__isnull=True) @swagger_auto_schema( tags=['文章'], operation_summary='获取文章评论列表', operation_description='获取指定文章的顶级评论列表,包含子评论', manual_parameters=[ openapi.Parameter('article_id', openapi.IN_PATH, description='文章ID', type=openapi.TYPE_INTEGER, required=True), openapi.Parameter('page', openapi.IN_QUERY, description='页码', type=openapi.TYPE_INTEGER), openapi.Parameter('page_size', openapi.IN_QUERY, description='每页数量,默认10', type=openapi.TYPE_INTEGER), ], responses={ 200: success_response, 404: not_found_response, } ) def list(self, request, *args, **kwargs): queryset = self.get_queryset() page = self.paginate_queryset(queryset) if page is not None: serializer = self.get_serializer(page, many=True) return self.get_paginated_response(serializer.data) serializer = self.get_serializer(queryset, many=True) return create_standardized_response(data=serializer.data, code=ResponseCode.SUCCESS) @swagger_auto_schema( tags=['文章'], operation_summary='发表文章评论', operation_description='对指定文章发表评论,支持回复其他评论,需要登录', manual_parameters=[ openapi.Parameter('article_id', openapi.IN_PATH, description='文章ID', type=openapi.TYPE_INTEGER, required=True), ], request_body=ArticleCommentSerializer, responses={ 201: success_response, 400: error_response, 401: unauthorized_response, 404: not_found_response, } ) def create(self, request, *args, **kwargs): article_id = self.kwargs['article_id'] article = get_object_or_404(Article, pk=article_id) serializer = self.get_serializer(data=request.data) if serializer.is_valid(): with transaction.atomic(): comment = serializer.save(user=request.user, article=article) comment_id = comment.id comment_content = serializer.data.get('content', '') article_title = article.title article_author_id = article.author_id parent_comment_id = comment.parent_id parent_comment_user_id = comment.parent.user_id if comment.parent else None user_id = request.user.id user_nickname = request.user.nickname or request.user.username transaction.on_commit(lambda: self._send_notification_messages( article_id, article_title, article_author_id, comment_id, comment_content, parent_comment_id, parent_comment_user_id, user_id, user_nickname )) track_task(request.user, 'post') return create_standardized_response( data=serializer.data, code=ResponseCode.SUCCESS, message='评论成功', status_code=status.HTTP_201_CREATED ) return create_standardized_error_response( data=serializer.errors, code=ResponseCode.VALIDATION_ERROR, status_code=status.HTTP_400_BAD_REQUEST ) def _send_notification_messages(self, article_id, article_title, article_author_id, comment_id, comment_content, parent_comment_id, parent_comment_user_id, user_id, user_nickname): """异步发送通知消息""" import re as re_module from user.models import FUser try: sender_user = FUser.objects.get(id=user_id) except FUser.DoesNotExist: return if parent_comment_id and parent_comment_user_id and parent_comment_user_id != user_id: try: parent_user = FUser.objects.get(id=parent_comment_user_id) create_message( recipient=parent_user, sender=sender_user, msg_type='reply', title=f'{user_nickname} 回复了你的评论', content=comment_content, extra_info=f'在《{article_title}》文章下', target_link=f'/article/{article_id}#comment-{comment_id}', target_type='comment', target_id=comment_id, ) except FUser.DoesNotExist: pass if article_author_id != user_id and (not parent_comment_id or parent_comment_user_id != article_author_id): try: article_author = FUser.objects.get(id=article_author_id) create_message( recipient=article_author, sender=sender_user, msg_type='reply', title=f'{user_nickname} 评论了你的文章', content=comment_content, extra_info=f'在《{article_title}》文章下', target_link=f'/article/{article_id}#comment-{comment_id}', target_type='comment', target_id=comment_id, ) except FUser.DoesNotExist: pass at_matches = re_module.findall(r'@(\w+)', comment_content) if at_matches: mentioned_users = FUser.objects.filter(username__in=at_matches).exclude(id=user_id) for mentioned_user in mentioned_users: if mentioned_user.id != article_author_id: create_message( recipient=mentioned_user, sender=sender_user, msg_type='at_me', title=f'{user_nickname} @了你', content=comment_content, extra_info=f'在《{article_title}》文章下', target_link=f'/article/{article_id}#comment-{comment_id}', target_type='comment', target_id=comment_id, ) class ArticleLikeToggleView(APIView): permission_classes = [IsAuthenticated] @swagger_auto_schema( tags=['文章'], operation_summary='点赞/取消点赞文章', operation_description='切换文章点赞状态,已点赞则取消,未点赞则点赞,需要登录', manual_parameters=[ openapi.Parameter('pk', openapi.IN_PATH, description='文章ID', type=openapi.TYPE_INTEGER, required=True), ], responses={ 200: success_response, 401: unauthorized_response, 404: not_found_response, } ) def post(self, request, pk): article = get_object_or_404(Article, pk=pk) like, created = ArticleLike.objects.get_or_create(user=request.user, article=article) if not created: like.delete() Article.objects.filter(pk=pk).update(likes=F("likes") - 1) article.refresh_from_db() article.likes = max(0, article.likes) return create_standardized_response( data={'liked': False, 'likes_count': article.likes}, code=ResponseCode.SUCCESS ) article.likes = F('likes') + 1 article.save(update_fields=['likes']) article.refresh_from_db() if created and article.author != request.user: create_message( recipient=article.author, sender=request.user, msg_type='like', title=f'{request.user.nickname or request.user.username} 赞了你的文章', content=f'《{article.title}》', extra_info='', target_link=f'/article/{pk}', target_type='article', target_id=pk, ) return create_standardized_response( data={'liked': True, 'likes_count': article.likes}, code=ResponseCode.SUCCESS ) class ArticleFavoriteToggleView(APIView): permission_classes = [IsAuthenticated] @swagger_auto_schema( tags=['文章'], operation_summary='收藏/取消收藏文章', operation_description='切换文章收藏状态,已收藏则取消,未收藏则收藏,需要登录', manual_parameters=[ openapi.Parameter('pk', openapi.IN_PATH, description='文章ID', type=openapi.TYPE_INTEGER, required=True), ], responses={ 200: success_response, 401: unauthorized_response, 404: not_found_response, } ) def post(self, request, pk): article = get_object_or_404(Article, pk=pk) fav, created = ArticleFavorite.objects.get_or_create(user=request.user, article=article) if not created: fav.delete() count = article.article_favorites.count() return create_standardized_response( data={'favorited': False, 'favorites_count': count}, code=ResponseCode.SUCCESS ) count = article.article_favorites.count() return create_standardized_response( data={'favorited': True, 'favorites_count': count}, code=ResponseCode.SUCCESS ) class ArticleCommentLikeToggleView(APIView): permission_classes = [IsAuthenticated] @swagger_auto_schema( tags=['文章'], operation_summary='点赞/取消点赞评论', operation_description='切换文章评论点赞状态,已点赞则取消,未点赞则点赞,需要登录', manual_parameters=[ openapi.Parameter('pk', openapi.IN_PATH, description='评论ID', type=openapi.TYPE_INTEGER, required=True), ], responses={ 200: success_response, 401: unauthorized_response, 404: not_found_response, } ) def post(self, request, pk): comment = get_object_or_404(ArticleComment, pk=pk) like, created = ArticleCommentLike.objects.get_or_create(user=request.user, comment=comment) if not created: like.delete() ArticleComment.objects.filter(pk=pk).update(likes=F("likes") - 1) comment.refresh_from_db() comment.likes = max(0, comment.likes) return create_standardized_response( data={'liked': False, 'likes_count': comment.likes}, code=ResponseCode.SUCCESS ) comment.likes = comment.comment_likes.count() comment.save(update_fields=['likes']) return create_standardized_response( data={'liked': True, 'likes_count': comment.likes}, code=ResponseCode.SUCCESS ) class MyArticleListView(generics.ListAPIView): permission_classes = [IsAuthenticated] serializer_class = ArticleManageSerializer parser_classes = [JSONParser] def get_queryset(self): qs = Article.objects.filter(author=self.request.user) st = self.request.query_params.get('status') if st: qs = qs.filter(status=st) return qs.order_by('-updated_at') @swagger_auto_schema( tags=['文章'], operation_summary='获取我的文章列表', operation_description='获取当前登录用户的所有文章,支持按状态筛选', manual_parameters=[ openapi.Parameter('status', openapi.IN_QUERY, description='文章状态筛选(published/draft)', type=openapi.TYPE_STRING), ], responses={ 200: success_response, 401: unauthorized_response, } ) def list(self, request, *args, **kwargs): queryset = self.get_queryset() serializer = self.get_serializer(queryset, many=True) return create_standardized_response(data=serializer.data, code=ResponseCode.SUCCESS) class MyArticleBatchView(APIView): permission_classes = [IsAuthenticated] parser_classes = [JSONParser] @swagger_auto_schema( tags=['文章'], operation_summary='批量操作文章', operation_description='批量发布、转为草稿或删除文章,需要登录', request_body=openapi.Schema( type=openapi.TYPE_OBJECT, required=['ids', 'action'], properties={ 'ids': openapi.Schema( type=openapi.TYPE_ARRAY, items=openapi.Schema(type=openapi.TYPE_INTEGER), description='文章ID列表', ), 'action': openapi.Schema( type=openapi.TYPE_STRING, enum=['publish', 'draft', 'delete'], description='操作类型:publish-发布,draft-转草稿,delete-删除', ), }, ), responses={ 200: success_response, 400: error_response, 401: unauthorized_response, } ) def post(self, request): ids = request.data.get('ids', []) action = request.data.get('action', '') if not ids or action not in ('publish', 'draft', 'delete'): return create_standardized_error_response( code=ResponseCode.VALIDATION_ERROR, message='参数错误', status_code=status.HTTP_400_BAD_REQUEST ) qs = Article.objects.filter(id__in=ids, author=request.user) if action == 'delete': count = qs.delete()[0] elif action == 'publish': count = qs.update(status='published') elif action == 'draft': count = qs.update(status='draft') return create_standardized_response( data={'affected': count}, code=ResponseCode.SUCCESS, message=f'批量操作成功,影响 {count} 篇文章' ) class ArticleToggleTopView(APIView): permission_classes = [IsAuthenticated] @swagger_auto_schema( tags=['文章'], operation_summary='切换文章置顶状态', operation_description='切换文章的置顶状态,仅文章作者可操作,需要登录', manual_parameters=[ openapi.Parameter('pk', openapi.IN_PATH, description='文章ID', type=openapi.TYPE_INTEGER, required=True), ], responses={ 200: success_response, 401: unauthorized_response, 403: error_response, 404: not_found_response, } ) def post(self, request, pk): article = get_object_or_404(Article, pk=pk) if article.author != request.user: return create_standardized_error_response( code=ResponseCode.VALIDATION_ERROR, message='无权操作', status_code=status.HTTP_403_FORBIDDEN ) article.is_top = not article.is_top article.save(update_fields=['is_top']) return create_standardized_response( data={'is_top': article.is_top}, code=ResponseCode.SUCCESS, message='置顶状态已更新' )