from rest_framework.views import APIView from rest_framework.permissions import IsAuthenticated from rest_framework.parsers import JSONParser, MultiPartParser, FormParser from rest_framework.response import Response from rest_framework import status from django.db.models import Q from django.utils import timezone from django.conf import settings from django.core.files.base import ContentFile from datetime import timedelta import uuid import os from drf_yasg.utils import swagger_auto_schema from drf_yasg import openapi from chunyu_project.common_schemas import success_response, error_response, unauthorized_response, not_found_response from .models import FriendRequest, Friendship, Conversation, ConversationParticipant, ChatMessage, FavoriteSticker from .serializers import ( FriendRequestSerializer, FriendshipSerializer, ConversationSerializer, ChatMessageSerializer, UserBriefSerializer ) from utils.response_codes import ResponseCode, create_standardized_response, create_standardized_error_response from user.models import FUser class FriendRequestListView(APIView): permission_classes = [IsAuthenticated] parser_classes = [JSONParser] @swagger_auto_schema( tags=['AI对话'], operation_summary='获取好友请求列表', operation_description='获取当前用户收到或发送的好友请求列表', manual_parameters=[ openapi.Parameter('direction', openapi.IN_QUERY, description='方向: received/sent', type=openapi.TYPE_STRING), openapi.Parameter('status', openapi.IN_QUERY, description='状态: pending/accepted/rejected', type=openapi.TYPE_STRING), ], responses={200: success_response, 401: unauthorized_response}, ) def get(self, request): direction = request.query_params.get('direction', 'received') req_status = request.query_params.get('status', 'pending') if direction == 'sent': queryset = FriendRequest.objects.filter(from_user=request.user, status=req_status) else: queryset = FriendRequest.objects.filter(to_user=request.user, status=req_status) serializer = FriendRequestSerializer(queryset, many=True, context={'request': request}) return create_standardized_response(data=serializer.data, code=ResponseCode.SUCCESS) @swagger_auto_schema( tags=['AI对话'], operation_summary='发送好友请求', request_body=openapi.Schema( type=openapi.TYPE_OBJECT, properties={ 'to_user_id': openapi.Schema(type=openapi.TYPE_INTEGER), 'message': openapi.Schema(type=openapi.TYPE_STRING), }, ), responses={201: success_response, 400: error_response, 401: unauthorized_response, 404: not_found_response}, ) def post(self, request): to_user_id = request.data.get('to_user_id') message = request.data.get('message', '') if not to_user_id: return create_standardized_error_response(code=ResponseCode.VALIDATION_ERROR, message='to_user_id 不能为空', status_code=status.HTTP_400_BAD_REQUEST) if int(to_user_id) == request.user.id: return create_standardized_error_response(code=ResponseCode.VALIDATION_ERROR, message='不能向自己发送好友请求', status_code=status.HTTP_400_BAD_REQUEST) try: to_user = FUser.objects.get(pk=to_user_id) except FUser.DoesNotExist: return create_standardized_error_response(code=ResponseCode.NOT_FOUND, message='用户不存在', status_code=status.HTTP_404_NOT_FOUND) if Friendship.objects.filter( (Q(user1=request.user) & Q(user2=to_user)) | (Q(user1=to_user) & Q(user2=request.user)) ).exists(): return create_standardized_error_response(code=ResponseCode.VALIDATION_ERROR, message='已经是好友关系', status_code=status.HTTP_400_BAD_REQUEST) existing = FriendRequest.objects.filter(from_user=request.user, to_user=to_user, status='pending').first() if existing: return create_standardized_error_response(code=ResponseCode.VALIDATION_ERROR, message='已发送过好友请求', status_code=status.HTTP_400_BAD_REQUEST) reverse = FriendRequest.objects.filter(from_user=to_user, to_user=request.user, status='pending').first() if reverse: return create_standardized_error_response(code=ResponseCode.VALIDATION_ERROR, message='对方已向你发送好友请求,请直接接受', status_code=status.HTTP_400_BAD_REQUEST) friend_request = FriendRequest.objects.create(from_user=request.user, to_user=to_user, message=message) serializer = FriendRequestSerializer(friend_request, context={'request': request}) return create_standardized_response(data=serializer.data, code=ResponseCode.SUCCESS, status_code=status.HTTP_201_CREATED) class FriendRequestAcceptView(APIView): permission_classes = [IsAuthenticated] parser_classes = [JSONParser] @swagger_auto_schema( tags=['AI对话'], 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): try: friend_request = FriendRequest.objects.get(pk=pk, to_user=request.user, status='pending') except FriendRequest.DoesNotExist: return create_standardized_error_response(code=ResponseCode.NOT_FOUND, message='好友请求不存在', status_code=status.HTTP_404_NOT_FOUND) friend_request.status = 'accepted' friend_request.save() u1, u2 = sorted([friend_request.from_user, friend_request.to_user], key=lambda u: u.id) Friendship.objects.get_or_create(user1=u1, user2=u2) return create_standardized_response(data={'status': 'accepted'}, code=ResponseCode.SUCCESS) class FriendRequestRejectView(APIView): permission_classes = [IsAuthenticated] parser_classes = [JSONParser] @swagger_auto_schema( tags=['AI对话'], 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): try: friend_request = FriendRequest.objects.get(pk=pk, to_user=request.user, status='pending') except FriendRequest.DoesNotExist: return create_standardized_error_response(code=ResponseCode.NOT_FOUND, message='好友请求不存在', status_code=status.HTTP_404_NOT_FOUND) friend_request.status = 'rejected' friend_request.save() return create_standardized_response(data={'status': 'rejected'}, code=ResponseCode.SUCCESS) class FriendRequestCancelView(APIView): permission_classes = [IsAuthenticated] parser_classes = [JSONParser] @swagger_auto_schema( tags=['AI对话'], 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): try: friend_request = FriendRequest.objects.get(pk=pk, from_user=request.user, status='pending') except FriendRequest.DoesNotExist: return create_standardized_error_response(code=ResponseCode.NOT_FOUND, message='好友请求不存在', status_code=status.HTTP_404_NOT_FOUND) friend_request.status = 'cancelled' friend_request.save() return create_standardized_response(data={'status': 'cancelled'}, code=ResponseCode.SUCCESS) class FriendListView(APIView): permission_classes = [IsAuthenticated] parser_classes = [JSONParser] @swagger_auto_schema( tags=['AI对话'], operation_summary='获取好友列表', operation_description='获取当前用户的好友列表', responses={200: success_response, 401: unauthorized_response}, ) def get(self, request): friendships = Friendship.objects.filter( Q(user1=request.user) | Q(user2=request.user) ).select_related('user1', 'user2') serializer = FriendshipSerializer(friendships, many=True, context={'request': request}) return create_standardized_response(data=serializer.data, code=ResponseCode.SUCCESS) @swagger_auto_schema( tags=['AI对话'], operation_summary='删除好友', operation_description='解除与指定用户的好友关系', manual_parameters=[openapi.Parameter('user_id', openapi.IN_PATH, description='目标用户ID', type=openapi.TYPE_INTEGER, required=True)], responses={204: success_response, 401: unauthorized_response, 404: not_found_response}, ) def delete(self, request, user_id): try: target_user = FUser.objects.get(pk=user_id) except FUser.DoesNotExist: return create_standardized_error_response(code=ResponseCode.NOT_FOUND, message='用户不存在', status_code=status.HTTP_404_NOT_FOUND) u1, u2 = sorted([request.user, target_user], key=lambda u: u.id) deleted, _ = Friendship.objects.filter(user1=u1, user2=u2).delete() if deleted: return create_standardized_response(code=ResponseCode.SUCCESS, status_code=status.HTTP_204_NO_CONTENT) return create_standardized_error_response(code=ResponseCode.NOT_FOUND, message='好友关系不存在', status_code=status.HTTP_404_NOT_FOUND) class FriendCheckView(APIView): permission_classes = [IsAuthenticated] parser_classes = [JSONParser] @swagger_auto_schema( tags=['AI对话'], operation_summary='检查好友关系', operation_description='检查与指定用户的好友关系及待定好友请求', manual_parameters=[openapi.Parameter('user_id', openapi.IN_PATH, description='目标用户ID', type=openapi.TYPE_INTEGER, required=True)], responses={200: success_response, 401: unauthorized_response, 404: not_found_response}, ) def get(self, request, user_id): try: target_user = FUser.objects.get(pk=user_id) except FUser.DoesNotExist: return create_standardized_error_response(code=ResponseCode.NOT_FOUND, message='用户不存在', status_code=status.HTTP_404_NOT_FOUND) u1, u2 = sorted([request.user, target_user], key=lambda u: u.id) is_friend = Friendship.objects.filter(user1=u1, user2=u2).exists() pending_request = FriendRequest.objects.filter( (Q(from_user=request.user, to_user=target_user) | Q(from_user=target_user, to_user=request.user)), status='pending' ).first() return create_standardized_response(data={ 'is_friend': is_friend, 'pending_request': pending_request.id if pending_request else None, }, code=ResponseCode.SUCCESS) class UserSearchView(APIView): permission_classes = [IsAuthenticated] parser_classes = [JSONParser] @swagger_auto_schema( tags=['AI对话'], operation_summary='搜索用户', operation_description='按用户名或昵称搜索用户', manual_parameters=[openapi.Parameter('q', openapi.IN_QUERY, description='搜索关键词', type=openapi.TYPE_STRING)], responses={200: success_response, 401: unauthorized_response}, ) def get(self, request): q = request.query_params.get('q', '').strip() if not q: return create_standardized_response(data=[], code=ResponseCode.SUCCESS) users = FUser.objects.filter( Q(username__icontains=q) | Q(nickname__icontains=q) ).exclude(id=request.user.id)[:20] friend_ids = set() friendships = Friendship.objects.filter( Q(user1=request.user) | Q(user2=request.user) ) for f in friendships: friend_ids.add(f.user2_id if f.user1 == request.user else f.user1_id) results = [] for user in users: avatar_url = '' if user.avatar and hasattr(user.avatar, 'url'): avatar_url = request.build_absolute_uri(user.avatar.url) results.append({ 'id': user.id, 'username': user.username, 'nickname': getattr(user, 'nickname', '') or user.username, 'avatar': avatar_url, 'is_friend': user.id in friend_ids, }) return create_standardized_response(data=results, code=ResponseCode.SUCCESS) class ConversationListView(APIView): permission_classes = [IsAuthenticated] parser_classes = [JSONParser] @swagger_auto_schema( tags=['AI对话'], operation_summary='获取会话列表', operation_description='获取当前用户参与的所有会话', responses={200: success_response, 401: unauthorized_response}, ) def get(self, request): participations = ConversationParticipant.objects.filter( user=request.user ).select_related('conversation').order_by('-conversation__created_at') conversations = [p.conversation for p in participations] serializer = ConversationSerializer(conversations, many=True, context={'request': request}) return create_standardized_response(data=serializer.data, code=ResponseCode.SUCCESS) @swagger_auto_schema( tags=['AI对话'], operation_summary='创建会话', operation_description='与指定好友创建私聊会话', request_body=openapi.Schema(type=openapi.TYPE_OBJECT, properties={'user_id': openapi.Schema(type=openapi.TYPE_INTEGER, description='目标用户ID')}), responses={201: success_response, 400: error_response, 401: unauthorized_response, 404: not_found_response}, ) def post(self, request): user_id = request.data.get('user_id') if not user_id: return create_standardized_error_response(code=ResponseCode.VALIDATION_ERROR, message='user_id 不能为空', status_code=status.HTTP_400_BAD_REQUEST) if int(user_id) == request.user.id: return create_standardized_error_response(code=ResponseCode.VALIDATION_ERROR, message='不能和自己聊天', status_code=status.HTTP_400_BAD_REQUEST) try: target_user = FUser.objects.get(pk=user_id) except FUser.DoesNotExist: return create_standardized_error_response(code=ResponseCode.NOT_FOUND, message='用户不存在', status_code=status.HTTP_404_NOT_FOUND) u1, u2 = sorted([request.user, target_user], key=lambda u: u.id) if not Friendship.objects.filter(user1=u1, user2=u2).exists(): return create_standardized_error_response(code=ResponseCode.VALIDATION_ERROR, message='只能与好友聊天', status_code=status.HTTP_400_BAD_REQUEST) my_participations = ConversationParticipant.objects.filter( user=request.user, conversation__type='private' ).values_list('conversation_id', flat=True) existing = ConversationParticipant.objects.filter( user=target_user, conversation_id__in=my_participations, conversation__type='private' ).first() if existing: conversation = existing.conversation else: conversation = Conversation.objects.create(type='private') ConversationParticipant.objects.create(conversation=conversation, user=request.user) ConversationParticipant.objects.create(conversation=conversation, user=target_user) serializer = ConversationSerializer(conversation, context={'request': request}) return create_standardized_response(data=serializer.data, code=ResponseCode.SUCCESS, status_code=status.HTTP_201_CREATED) class ConversationMessageView(APIView): permission_classes = [IsAuthenticated] parser_classes = [JSONParser] @swagger_auto_schema( tags=['AI对话'], operation_summary='获取会话消息列表', operation_description='分页获取指定会话的消息列表', manual_parameters=[ openapi.Parameter('pk', 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='每页数量', type=openapi.TYPE_INTEGER), ], responses={200: success_response, 401: unauthorized_response, 404: not_found_response}, ) def get(self, request, pk): try: conversation = Conversation.objects.get(pk=pk) except Conversation.DoesNotExist: return create_standardized_error_response(code=ResponseCode.NOT_FOUND, message='会话不存在', status_code=status.HTTP_404_NOT_FOUND) if not ConversationParticipant.objects.filter(conversation=conversation, user=request.user).exists(): return create_standardized_error_response(code=ResponseCode.VALIDATION_ERROR, message='你不是该会话的参与者', status_code=status.HTTP_403_FORBIDDEN) page = int(request.query_params.get('page', 1)) page_size = int(request.query_params.get('page_size', 50)) offset = (page - 1) * page_size messages = ChatMessage.objects.filter(conversation=conversation).select_related('sender', 'reply_to', 'reply_to__sender') total = messages.count() messages = messages[offset:offset + page_size] serializer = ChatMessageSerializer(messages, many=True, context={'request': request}) return create_standardized_response(data={ 'results': serializer.data, 'total': total, 'page': page, 'page_size': page_size, }, code=ResponseCode.SUCCESS) @swagger_auto_schema( tags=['AI对话'], operation_summary='发送会话消息', operation_description='向指定会话发送文本消息,支持回复引用', manual_parameters=[openapi.Parameter('pk', openapi.IN_PATH, description='会话ID', type=openapi.TYPE_INTEGER, required=True)], request_body=openapi.Schema( type=openapi.TYPE_OBJECT, properties={ 'content': openapi.Schema(type=openapi.TYPE_STRING, description='消息内容'), 'msg_type': openapi.Schema(type=openapi.TYPE_STRING, description='消息类型 text/image/file'), 'file_url': openapi.Schema(type=openapi.TYPE_STRING, description='文件URL'), 'reply_to': openapi.Schema(type=openapi.TYPE_INTEGER, description='回复的消息ID'), }, ), responses={201: success_response, 400: error_response, 401: unauthorized_response, 404: not_found_response}, ) def post(self, request, pk): try: conversation = Conversation.objects.get(pk=pk) except Conversation.DoesNotExist: return create_standardized_error_response(code=ResponseCode.NOT_FOUND, message='会话不存在', status_code=status.HTTP_404_NOT_FOUND) if not ConversationParticipant.objects.filter(conversation=conversation, user=request.user).exists(): return create_standardized_error_response(code=ResponseCode.VALIDATION_ERROR, message='你不是该会话的参与者', status_code=status.HTTP_403_FORBIDDEN) content = request.data.get('content', '').strip() if not content: return create_standardized_error_response(code=ResponseCode.VALIDATION_ERROR, message='消息内容不能为空', status_code=status.HTTP_400_BAD_REQUEST) msg_type = request.data.get('msg_type', 'text') file_url = request.data.get('file_url', '') reply_to_id = request.data.get('reply_to') reply_to = None if reply_to_id: try: reply_to = ChatMessage.objects.get(pk=reply_to_id, conversation=conversation) except ChatMessage.DoesNotExist: pass message = ChatMessage.objects.create( conversation=conversation, sender=request.user, content=content, msg_type=msg_type, file_url=file_url, reply_to=reply_to, ) serializer = ChatMessageSerializer(message, context={'request': request}) return create_standardized_response(data=serializer.data, code=ResponseCode.SUCCESS, status_code=status.HTTP_201_CREATED) class ConversationClearView(APIView): permission_classes = [IsAuthenticated] parser_classes = [JSONParser] @swagger_auto_schema( tags=['AI对话'], 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 delete(self, request, pk): try: conversation = Conversation.objects.get(pk=pk) except Conversation.DoesNotExist: return create_standardized_error_response(code=ResponseCode.NOT_FOUND, message='会话不存在', status_code=status.HTTP_404_NOT_FOUND) if not ConversationParticipant.objects.filter(conversation=conversation, user=request.user).exists(): return create_standardized_error_response(code=ResponseCode.VALIDATION_ERROR, message='你不是该会话的参与者', status_code=status.HTTP_403_FORBIDDEN) # 物理删除该会话下所有消息 deleted_count, _ = ChatMessage.objects.filter(conversation=conversation).delete() return create_standardized_response(data={'deleted': deleted_count}, code=ResponseCode.SUCCESS) permission_classes = [IsAuthenticated] parser_classes = [JSONParser] @swagger_auto_schema( tags=['AI对话'], 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): try: participant = ConversationParticipant.objects.get(conversation_id=pk, user=request.user) except ConversationParticipant.DoesNotExist: return create_standardized_error_response(code=ResponseCode.NOT_FOUND, message='会话不存在', status_code=status.HTTP_404_NOT_FOUND) participant.last_read_at = timezone.now() participant.save(update_fields=['last_read_at']) return create_standardized_response(data={'read': True}, code=ResponseCode.SUCCESS) class MessageRecallView(APIView): permission_classes = [IsAuthenticated] parser_classes = [JSONParser] @swagger_auto_schema( tags=['AI对话'], operation_summary='撤回消息', operation_description='在发送后2分钟内可撤回自己发送的消息', manual_parameters=[openapi.Parameter('pk', openapi.IN_PATH, description='消息ID', type=openapi.TYPE_INTEGER, required=True)], responses={200: success_response, 400: error_response, 401: unauthorized_response, 404: not_found_response}, ) def post(self, request, pk): try: message = ChatMessage.objects.get(pk=pk) except ChatMessage.DoesNotExist: return create_standardized_error_response(code=ResponseCode.NOT_FOUND, message='消息不存在', status_code=status.HTTP_404_NOT_FOUND) if message.sender != request.user: return create_standardized_error_response(code=ResponseCode.VALIDATION_ERROR, message='只能撤回自己发送的消息', status_code=status.HTTP_403_FORBIDDEN) if timezone.now() - message.created_at > timedelta(minutes=2): return create_standardized_error_response(code=ResponseCode.VALIDATION_ERROR, message='超过2分钟无法撤回', status_code=status.HTTP_400_BAD_REQUEST) message.is_recalled = True message.content = '你撤回了一条消息' message.save(update_fields=['is_recalled', 'content']) return create_standardized_response(data={'recalled': True}, code=ResponseCode.SUCCESS) class MessageDeleteView(APIView): permission_classes = [IsAuthenticated] parser_classes = [JSONParser] @swagger_auto_schema( tags=['AI对话'], 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 delete(self, request, pk): try: message = ChatMessage.objects.get(pk=pk) except ChatMessage.DoesNotExist: return create_standardized_error_response(code=ResponseCode.NOT_FOUND, message='消息不存在', status_code=status.HTTP_404_NOT_FOUND) if message.sender != request.user: return create_standardized_error_response(code=ResponseCode.VALIDATION_ERROR, message='只能删除自己发送的消息', status_code=status.HTTP_403_FORBIDDEN) message.delete() return create_standardized_response(data={'deleted': True}, code=ResponseCode.SUCCESS) class FileUploadView(APIView): permission_classes = [IsAuthenticated] parser_classes = [MultiPartParser, FormParser] @swagger_auto_schema( tags=['AI对话'], operation_summary='上传聊天文件', operation_description='上传聊天消息中的文件,返回可访问的URL', manual_parameters=[openapi.Parameter('file', openapi.IN_FORM, description='文件', type=openapi.TYPE_FILE, required=True)], responses={200: success_response, 400: error_response, 401: unauthorized_response}, ) def post(self, request): file = request.FILES.get('file') if not file: return create_standardized_error_response(code=ResponseCode.VALIDATION_ERROR, message='请选择文件', status_code=status.HTTP_400_BAD_REQUEST) ext = os.path.splitext(file.name)[1] date_path = timezone.now().strftime('%Y/%m/%d') filename = f'{uuid.uuid4().hex}{ext}' filepath = f'chat_files/{date_path}/{filename}' from django.core.files.storage import default_storage saved_path = default_storage.save(filepath, file) url = request.build_absolute_uri(settings.MEDIA_URL + saved_path) return create_standardized_response(data={ 'url': url, 'file_name': file.name, 'file_size': file.size, }, code=ResponseCode.SUCCESS) class StickerUploadView(APIView): permission_classes = [IsAuthenticated] parser_classes = [MultiPartParser, FormParser] @swagger_auto_schema( tags=['AI对话'], operation_summary='上传表情', operation_description='上传图片作为个人表情收藏', manual_parameters=[openapi.Parameter('file', openapi.IN_FORM, description='图片文件', type=openapi.TYPE_FILE, required=True)], responses={200: success_response, 400: error_response, 401: unauthorized_response}, ) def post(self, request): file = request.FILES.get('file') if not file: return create_standardized_error_response(code=ResponseCode.VALIDATION_ERROR, message='请选择文件', status_code=status.HTTP_400_BAD_REQUEST) if not file.content_type.startswith('image/'): return create_standardized_error_response(code=ResponseCode.VALIDATION_ERROR, message='只能上传图片文件', status_code=status.HTTP_400_BAD_REQUEST) ext = os.path.splitext(file.name)[1] or '.png' filename = f'{uuid.uuid4().hex}{ext}' filepath = f'chat_stickers/{request.user.id}/{filename}' from django.core.files.storage import default_storage saved_path = default_storage.save(filepath, file) sticker = FavoriteSticker.objects.create( user=request.user, image=saved_path, ) url = request.build_absolute_uri(sticker.image.url) return create_standardized_response(data={ 'id': sticker.id, 'url': url, }, code=ResponseCode.SUCCESS) class FavoriteStickerView(APIView): permission_classes = [IsAuthenticated] parser_classes = [MultiPartParser, FormParser, JSONParser] @swagger_auto_schema( tags=['AI对话'], operation_summary='获取收藏表情列表', responses={200: success_response, 401: unauthorized_response}, ) def get(self, request): stickers = FavoriteSticker.objects.filter(user=request.user) data = [] for s in stickers: data.append({ 'id': s.id, 'url': request.build_absolute_uri(s.image.url) if s.image else '', }) return create_standardized_response(data=data, code=ResponseCode.SUCCESS) @swagger_auto_schema( tags=['AI对话'], operation_summary='添加收藏表情', manual_parameters=[openapi.Parameter('image', openapi.IN_FORM, description='图片文件', type=openapi.TYPE_FILE, required=True)], responses={200: success_response, 400: error_response, 401: unauthorized_response}, ) def post(self, request): file = request.FILES.get('image') if not file: return create_standardized_error_response(code=ResponseCode.VALIDATION_ERROR, message='请选择图片', status_code=status.HTTP_400_BAD_REQUEST) if not file.content_type.startswith('image/'): return create_standardized_error_response(code=ResponseCode.VALIDATION_ERROR, message='只能上传图片', status_code=status.HTTP_400_BAD_REQUEST) ext = os.path.splitext(file.name)[1] or '.png' filename = f'{uuid.uuid4().hex}{ext}' filepath = f'chat_stickers/{request.user.id}/{filename}' from django.core.files.storage import default_storage saved_path = default_storage.save(filepath, file) sticker = FavoriteSticker.objects.create( user=request.user, image=saved_path, ) url = request.build_absolute_uri(sticker.image.url) return create_standardized_response(data={ 'id': sticker.id, 'url': url, }, code=ResponseCode.SUCCESS) @swagger_auto_schema( tags=['AI对话'], operation_summary='删除收藏表情', manual_parameters=[openapi.Parameter('pk', openapi.IN_PATH, description='表情ID', type=openapi.TYPE_INTEGER, required=True)], responses={204: success_response, 401: unauthorized_response, 404: not_found_response}, ) def delete(self, request, pk): try: sticker = FavoriteSticker.objects.get(pk=pk, user=request.user) sticker.delete() return Response(status=status.HTTP_204_NO_CONTENT) except FavoriteSticker.DoesNotExist: return create_standardized_error_response(code=ResponseCode.NOT_FOUND, message='收藏不存在', status_code=status.HTTP_404_NOT_FOUND) class FavoriteStickerFromMessageView(APIView): permission_classes = [IsAuthenticated] parser_classes = [JSONParser] @swagger_auto_schema( tags=['AI对话'], operation_summary='从消息收藏表情', operation_description='将消息中的图片收藏为个人表情', request_body=openapi.Schema( type=openapi.TYPE_OBJECT, properties={ 'message_id': openapi.Schema(type=openapi.TYPE_INTEGER, description='消息ID'), }, ), responses={200: success_response, 400: error_response, 401: unauthorized_response, 404: not_found_response}, ) def post(self, request): message_id = request.data.get('message_id') if not message_id: return create_standardized_error_response(code=ResponseCode.VALIDATION_ERROR, message='message_id 不能为空', status_code=status.HTTP_400_BAD_REQUEST) try: message = ChatMessage.objects.get(pk=message_id) except ChatMessage.DoesNotExist: return create_standardized_error_response(code=ResponseCode.NOT_FOUND, message='消息不存在', status_code=status.HTTP_404_NOT_FOUND) file_url = message.file_url or message.content if not file_url: return create_standardized_error_response(code=ResponseCode.VALIDATION_ERROR, message='该消息没有可收藏的图片', status_code=status.HTTP_400_BAD_REQUEST) try: import urllib.request if file_url.startswith('http'): req = urllib.request.Request(file_url, headers={'User-Agent': 'Mozilla/5.0'}) response = urllib.request.urlopen(req, timeout=10) image_data = response.read() else: local_path = os.path.join(settings.MEDIA_ROOT, file_url.replace(settings.MEDIA_URL, '')) with open(local_path, 'rb') as f: image_data = f.read() ext = '.png' if '.' in file_url.split('/')[-1]: ext = '.' + file_url.split('/')[-1].split('.')[-1].split('?')[0] filename = f'{uuid.uuid4().hex}{ext}' filepath = f'chat_stickers/{request.user.id}/{filename}' from django.core.files.storage import default_storage saved_path = default_storage.save(filepath, ContentFile(image_data)) sticker = FavoriteSticker.objects.create( user=request.user, image=saved_path, ) url = request.build_absolute_uri(sticker.image.url) return create_standardized_response(data={ 'id': sticker.id, 'url': url, }, code=ResponseCode.SUCCESS) except Exception as e: return create_standardized_error_response(code=ResponseCode.VALIDATION_ERROR, message='保存失败', status_code=status.HTTP_400_BAD_REQUEST)