Files
chunyu_project/chat/views.py
T

736 lines
36 KiB
Python

from adrf.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
import aiohttp
from asgiref.sync import sync_to_async
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},
)
async 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)
queryset = queryset.select_related('from_user', 'to_user')
requests_list = [r async for r in queryset]
serializer = FriendRequestSerializer(requests_list, 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},
)
async 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 = await FUser.objects.aget(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 await Friendship.objects.filter(
(Q(user1=request.user) & Q(user2=to_user)) | (Q(user1=to_user) & Q(user2=request.user))
).aexists():
return create_standardized_error_response(code=ResponseCode.VALIDATION_ERROR, message='已经是好友关系', status_code=status.HTTP_400_BAD_REQUEST)
existing = await FriendRequest.objects.filter(from_user=request.user, to_user=to_user, status='pending').afirst()
if existing:
return create_standardized_error_response(code=ResponseCode.VALIDATION_ERROR, message='已发送过好友请求', status_code=status.HTTP_400_BAD_REQUEST)
reverse = await FriendRequest.objects.filter(from_user=to_user, to_user=request.user, status='pending').afirst()
if reverse:
return create_standardized_error_response(code=ResponseCode.VALIDATION_ERROR, message='对方已向你发送好友请求,请直接接受', status_code=status.HTTP_400_BAD_REQUEST)
friend_request = await FriendRequest.objects.acreate(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},
)
async def post(self, request, pk):
try:
friend_request = await FriendRequest.objects.aget(pk=pk, to_user_id=request.user.id, 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'
await friend_request.asave()
u1, u2 = sorted([friend_request.from_user_id, friend_request.to_user_id])
await Friendship.objects.aget_or_create(user1_id=u1, user2_id=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},
)
async def post(self, request, pk):
try:
friend_request = await FriendRequest.objects.aget(pk=pk, to_user_id=request.user.id, 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'
await friend_request.asave()
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},
)
async def post(self, request, pk):
try:
friend_request = await FriendRequest.objects.aget(pk=pk, from_user_id=request.user.id, 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'
await friend_request.asave()
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},
)
async def get(self, request):
friendships = Friendship.objects.filter(
Q(user1=request.user) | Q(user2=request.user)
).select_related('user1', 'user2')
friendships_list = [f async for f in friendships]
serializer = FriendshipSerializer(friendships_list, 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},
)
async def delete(self, request, user_id):
try:
target_user = await FUser.objects.aget(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.id, target_user.id])
deleted, _ = await Friendship.objects.filter(user1_id=u1, user2_id=u2).adelete()
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},
)
async def get(self, request, user_id):
try:
target_user = await FUser.objects.aget(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.id, target_user.id])
is_friend = await Friendship.objects.filter(user1_id=u1, user2_id=u2).aexists()
pending_request = await FriendRequest.objects.filter(
(Q(from_user_id=request.user.id, to_user_id=target_user.id) | Q(from_user_id=target_user.id, to_user_id=request.user.id)),
status='pending'
).afirst()
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},
)
async 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_id=request.user.id) | Q(user2_id=request.user.id)
).values_list('user1_id', 'user2_id')
async for user1_id, user2_id in friendships:
friend_ids.add(user2_id if user1_id == request.user.id else user1_id)
results = []
async 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},
)
async def get(self, request):
participations = ConversationParticipant.objects.filter(
user=request.user
).select_related('conversation').order_by('-conversation__created_at')
conversations = [p.conversation async for p in participations]
serializer = ConversationSerializer(conversations, many=True, context={'request': request})
# 兜底:ConversationSerializer 的 SerializerMethodField 内部有同步 ORM 查询
data = await sync_to_async(lambda: serializer.data)()
return create_standardized_response(data=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},
)
async 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 = await FUser.objects.aget(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.id, target_user.id])
if not await Friendship.objects.filter(user1_id=u1, user2_id=u2).aexists():
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 = await ConversationParticipant.objects.filter(
user=target_user, conversation_id__in=my_participations, conversation__type='private'
).select_related('conversation').afirst()
if existing:
conversation = existing.conversation
else:
conversation = await Conversation.objects.acreate(type='private')
await ConversationParticipant.objects.acreate(conversation=conversation, user=request.user)
await ConversationParticipant.objects.acreate(conversation=conversation, user=target_user)
serializer = ConversationSerializer(conversation, context={'request': request})
# 兜底:ConversationSerializer 的 SerializerMethodField 内部有同步 ORM 查询
data = await sync_to_async(lambda: serializer.data)()
return create_standardized_response(data=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},
)
async def get(self, request, pk):
try:
conversation = await Conversation.objects.aget(pk=pk)
except Conversation.DoesNotExist:
return create_standardized_error_response(code=ResponseCode.NOT_FOUND, message='会话不存在', status_code=status.HTTP_404_NOT_FOUND)
if not await ConversationParticipant.objects.filter(conversation=conversation, user=request.user).aexists():
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 = await messages.acount()
page_messages = [m async for m in messages[offset:offset + page_size]]
serializer = ChatMessageSerializer(page_messages, many=True, context={'request': request})
# 兜底:'conversation' 外键未预加载,serializer 取值会触发同步 ORM
data = await sync_to_async(lambda: serializer.data)()
return create_standardized_response(data={
'results': 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},
)
async def post(self, request, pk):
try:
conversation = await Conversation.objects.aget(pk=pk)
except Conversation.DoesNotExist:
return create_standardized_error_response(code=ResponseCode.NOT_FOUND, message='会话不存在', status_code=status.HTTP_404_NOT_FOUND)
if not await ConversationParticipant.objects.filter(conversation=conversation, user=request.user).aexists():
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 = await ChatMessage.objects.aget(pk=reply_to_id, conversation=conversation)
except ChatMessage.DoesNotExist:
pass
message = await ChatMessage.objects.acreate(
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},
)
async def delete(self, request, pk):
try:
conversation = await Conversation.objects.aget(pk=pk)
except Conversation.DoesNotExist:
return create_standardized_error_response(code=ResponseCode.NOT_FOUND, message='会话不存在', status_code=status.HTTP_404_NOT_FOUND)
if not await ConversationParticipant.objects.filter(conversation=conversation, user=request.user).aexists():
return create_standardized_error_response(code=ResponseCode.VALIDATION_ERROR, message='你不是该会话的参与者', status_code=status.HTTP_403_FORBIDDEN)
# 物理删除该会话下所有消息
deleted_count, _ = await ChatMessage.objects.filter(conversation=conversation).adelete()
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},
)
async def post(self, request, pk):
try:
participant = await ConversationParticipant.objects.aget(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()
await participant.asave(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},
)
async def post(self, request, pk):
try:
message = await ChatMessage.objects.aget(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_id != request.user.id:
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 = '你撤回了一条消息'
await message.asave(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, 400: error_response, 401: unauthorized_response, 403: error_response, 404: not_found_response},
)
async def delete(self, request, pk):
try:
message = await ChatMessage.objects.aget(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_id != request.user.id:
return create_standardized_error_response(code=ResponseCode.VALIDATION_ERROR, message='只能删除自己发送的消息', status_code=status.HTTP_403_FORBIDDEN)
await message.adelete()
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},
)
async 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 = await sync_to_async(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},
)
async 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 = await sync_to_async(default_storage.save)(filepath, file)
sticker = await FavoriteSticker.objects.acreate(
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},
)
async def get(self, request):
stickers = FavoriteSticker.objects.filter(user=request.user)
data = []
async 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},
)
async 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 = await sync_to_async(default_storage.save)(filepath, file)
sticker = await FavoriteSticker.objects.acreate(
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},
)
async def delete(self, request, pk):
try:
sticker = await FavoriteSticker.objects.aget(pk=pk, user=request.user)
await sticker.adelete()
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},
)
async 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 = await ChatMessage.objects.aget(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:
if file_url.startswith('http'):
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10)) as session:
async with session.get(file_url, headers={'User-Agent': 'Mozilla/5.0'}) as resp:
image_data = await resp.read()
else:
local_path = os.path.join(settings.MEDIA_ROOT, file_url.replace(settings.MEDIA_URL, ''))
def _read_local(p):
with open(p, 'rb') as f:
return f.read()
image_data = await sync_to_async(_read_local)(local_path)
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 = await sync_to_async(default_storage.save)(filepath, ContentFile(image_data))
sticker = await FavoriteSticker.objects.acreate(
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)