feat: ADRF async views (phase1) + native async serializers (phase2) + async cache infra
This commit is contained in:
+113
-105
@@ -2,11 +2,12 @@ import os
|
||||
import uuid
|
||||
import logging
|
||||
|
||||
from asgiref.sync import sync_to_async
|
||||
from utils.email_utils import validate_email_mx
|
||||
from rest_framework.decorators import permission_classes
|
||||
from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
|
||||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||
from rest_framework.views import APIView
|
||||
from adrf.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from django.contrib.auth import authenticate
|
||||
@@ -50,6 +51,11 @@ def _create_login_record(request, user, record_status):
|
||||
create_login_record(request, user, record_status)
|
||||
|
||||
|
||||
def _serialize_user(user):
|
||||
"""同步序列化助手:在 async 视图中通过 sync_to_async 调用,避免 SynchronousOnlyOperation。"""
|
||||
return UserSerializer(user).data
|
||||
|
||||
|
||||
class SendUserEmailAPIView(APIView):
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
@@ -66,7 +72,7 @@ class SendUserEmailAPIView(APIView):
|
||||
),
|
||||
responses={201: success_response, 200: success_response, 400: error_response, 500: error_response},
|
||||
)
|
||||
def post(self, request):
|
||||
async def post(self, request):
|
||||
to_email = request.data.get('to_email', None)
|
||||
|
||||
if to_email is None or to_email == "":
|
||||
@@ -75,7 +81,7 @@ class SendUserEmailAPIView(APIView):
|
||||
status_code=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
if not validate_email_mx(to_email):
|
||||
if not await sync_to_async(validate_email_mx)(to_email):
|
||||
logger.warning(f'[Email] Domain MX check failed: email={to_email}')
|
||||
return create_standardized_error_response(
|
||||
code=ResponseCode.EMAIL_DOMAIN_INVALID,
|
||||
@@ -83,13 +89,13 @@ class SendUserEmailAPIView(APIView):
|
||||
)
|
||||
|
||||
try:
|
||||
user_exists_result = FUser.objects.filter(email=to_email).exists()
|
||||
user_exists_result = await FUser.objects.filter(email=to_email).aexists()
|
||||
|
||||
if not user_exists_result:
|
||||
code = RandCode.get_digit_characters_code_8()
|
||||
default_cache.set(f"register_{to_email}", code, timeout=600)
|
||||
await sync_to_async(default_cache.set)(f"register_{to_email}", code, timeout=600)
|
||||
|
||||
result = submit_task(send_verification_email_task, to_email, code, 'register')
|
||||
result = await sync_to_async(submit_task)(send_verification_email_task, to_email, code, 'register')
|
||||
if result is None:
|
||||
logger.warning(f'[Register] Email send failed: email={to_email}')
|
||||
else:
|
||||
@@ -102,9 +108,9 @@ class SendUserEmailAPIView(APIView):
|
||||
)
|
||||
else:
|
||||
code = RandCode.get_digit_characters_code_8()
|
||||
default_cache.set(f"login_{to_email}", code, timeout=600)
|
||||
await sync_to_async(default_cache.set)(f"login_{to_email}", code, timeout=600)
|
||||
|
||||
result = submit_task(send_verification_email_task, to_email, code, 'login')
|
||||
result = await sync_to_async(submit_task)(send_verification_email_task, to_email, code, 'login')
|
||||
if result is None:
|
||||
logger.warning(f'[Login] Email send failed: email={to_email}')
|
||||
else:
|
||||
@@ -141,7 +147,7 @@ class UserLoginOrRegisterAPIView(APIView):
|
||||
),
|
||||
responses={200: success_response, 400: error_response, 500: error_response},
|
||||
)
|
||||
def post(self, request):
|
||||
async def post(self, request):
|
||||
code = request.data.get('code', None)
|
||||
to_email = request.data.get('email', None)
|
||||
|
||||
@@ -152,11 +158,11 @@ class UserLoginOrRegisterAPIView(APIView):
|
||||
)
|
||||
|
||||
try:
|
||||
user = FUser.objects.filter(email=to_email).first()
|
||||
user = await FUser.objects.filter(email=to_email).afirst()
|
||||
|
||||
if user is None:
|
||||
# Registration flow
|
||||
vcode = default_cache.get(f"register_{to_email}")
|
||||
vcode = await sync_to_async(default_cache.get)(f"register_{to_email}")
|
||||
|
||||
if vcode is None:
|
||||
return create_standardized_error_response(
|
||||
@@ -166,20 +172,21 @@ class UserLoginOrRegisterAPIView(APIView):
|
||||
|
||||
if code == vcode:
|
||||
user_serializer = UserSerializer(data=request.data)
|
||||
if user_serializer.is_valid():
|
||||
user = user_serializer.create_by_email(request.data)
|
||||
if await sync_to_async(user_serializer.is_valid)():
|
||||
user = await sync_to_async(user_serializer.create_by_email)(request.data)
|
||||
refresh = RefreshToken.for_user(user)
|
||||
user_data = await sync_to_async(_serialize_user)(user)
|
||||
|
||||
# Prepare response data
|
||||
response_data = {
|
||||
'user': UserSerializer(user).data,
|
||||
'user': user_data,
|
||||
'refresh': str(refresh),
|
||||
'access': str(refresh.access_token),
|
||||
'token_type': 'bearer',
|
||||
'expires_at_timestamp': refresh.access_token.payload['exp']
|
||||
}
|
||||
|
||||
_create_login_record(request, user, 'success')
|
||||
await sync_to_async(_create_login_record)(request, user, 'success')
|
||||
|
||||
return create_standardized_response(
|
||||
data=response_data,
|
||||
@@ -198,7 +205,7 @@ class UserLoginOrRegisterAPIView(APIView):
|
||||
)
|
||||
else:
|
||||
# Login flow
|
||||
vcode = default_cache.get(f"login_{to_email}")
|
||||
vcode = await sync_to_async(default_cache.get)(f"login_{to_email}")
|
||||
|
||||
if vcode is None:
|
||||
return create_standardized_error_response(
|
||||
@@ -208,18 +215,18 @@ class UserLoginOrRegisterAPIView(APIView):
|
||||
|
||||
if code == vcode:
|
||||
refresh = RefreshToken.for_user(user)
|
||||
user_serializer = UserSerializer(user)
|
||||
user_data = await sync_to_async(_serialize_user)(user)
|
||||
|
||||
# Prepare response data
|
||||
response_data = {
|
||||
'user': user_serializer.data,
|
||||
'user': user_data,
|
||||
'refresh': str(refresh),
|
||||
'access': str(refresh.access_token),
|
||||
'token_type': 'bearer',
|
||||
'expires_at_timestamp': refresh.access_token.payload['exp']
|
||||
}
|
||||
|
||||
_create_login_record(request, user, 'success')
|
||||
await sync_to_async(_create_login_record)(request, user, 'success')
|
||||
|
||||
return create_standardized_response(
|
||||
data=response_data,
|
||||
@@ -227,7 +234,7 @@ class UserLoginOrRegisterAPIView(APIView):
|
||||
status_code=status.HTTP_200_OK
|
||||
)
|
||||
else:
|
||||
_create_login_record(request, user, 'failed')
|
||||
await sync_to_async(_create_login_record)(request, user, 'failed')
|
||||
|
||||
return create_standardized_error_response(
|
||||
code=ResponseCode.LOGIN_VERIFICATION_ERROR,
|
||||
@@ -260,7 +267,7 @@ class ForgotPasswordSendCodeAPIView(APIView):
|
||||
),
|
||||
responses={200: success_response, 400: error_response, 500: error_response},
|
||||
)
|
||||
def post(self, request):
|
||||
async def post(self, request):
|
||||
from utils.captcha import check_captcha_required, verify_captcha, record_failure, reset_failures
|
||||
|
||||
to_email = request.data.get('email', None)
|
||||
@@ -271,7 +278,7 @@ class ForgotPasswordSendCodeAPIView(APIView):
|
||||
status_code=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
if not validate_email_mx(to_email):
|
||||
if not await sync_to_async(validate_email_mx)(to_email):
|
||||
return create_standardized_error_response(
|
||||
code=ResponseCode.EMAIL_DOMAIN_INVALID,
|
||||
status_code=status.HTTP_400_BAD_REQUEST
|
||||
@@ -279,7 +286,7 @@ class ForgotPasswordSendCodeAPIView(APIView):
|
||||
|
||||
identifier = to_email
|
||||
operation = 'forgot_send'
|
||||
captcha_required = check_captcha_required(operation, identifier)
|
||||
captcha_required = await sync_to_async(check_captcha_required)(operation, identifier)
|
||||
|
||||
if captcha_required:
|
||||
captcha_key = request.data.get('captcha_key', None)
|
||||
@@ -291,39 +298,39 @@ class ForgotPasswordSendCodeAPIView(APIView):
|
||||
status_code=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
captcha_result = verify_captcha(captcha_key, captcha_code)
|
||||
captcha_result = await sync_to_async(verify_captcha)(captcha_key, captcha_code)
|
||||
if captcha_result == 'expired':
|
||||
return create_standardized_error_response(
|
||||
code=ResponseCode.CAPTCHA_EXPIRED,
|
||||
status_code=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
elif captcha_result == 'wrong':
|
||||
record_failure(operation, identifier)
|
||||
await sync_to_async(record_failure)(operation, identifier)
|
||||
return create_standardized_error_response(
|
||||
code=ResponseCode.CAPTCHA_ERROR,
|
||||
status_code=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
try:
|
||||
user = FUser.objects.filter(email=to_email).first()
|
||||
user = await FUser.objects.filter(email=to_email).afirst()
|
||||
|
||||
if user is None:
|
||||
record_failure(operation, to_email)
|
||||
await sync_to_async(record_failure)(operation, to_email)
|
||||
return create_standardized_error_response(
|
||||
code=ResponseCode.USER_NOT_FOUND,
|
||||
status_code=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
code = RandCode.get_digit_characters_code_8()
|
||||
default_cache.set(f"reset_password_{to_email}", code, timeout=600)
|
||||
await sync_to_async(default_cache.set)(f"reset_password_{to_email}", code, timeout=600)
|
||||
|
||||
result = submit_task(send_reset_password_email_task, to_email, code)
|
||||
result = await sync_to_async(submit_task)(send_reset_password_email_task, to_email, code)
|
||||
if result is None:
|
||||
logger.warning(f'[ResetPassword] Email send failed (both sync and async): email={to_email}')
|
||||
else:
|
||||
logger.info(f'[ResetPassword] Email sent successfully: email={to_email}')
|
||||
|
||||
reset_failures(operation, to_email)
|
||||
await sync_to_async(reset_failures)(operation, to_email)
|
||||
return create_standardized_response(
|
||||
data={"email_sent": True},
|
||||
code=ResponseCode.RESET_CODE_SENT,
|
||||
@@ -331,7 +338,7 @@ class ForgotPasswordSendCodeAPIView(APIView):
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
record_failure(operation, to_email)
|
||||
await sync_to_async(record_failure)(operation, to_email)
|
||||
return create_standardized_error_response(
|
||||
message=f"邮件发送失败: {str(e)}",
|
||||
code=ResponseCode.EMAIL_SEND_FAILED,
|
||||
@@ -360,7 +367,7 @@ class ForgotPasswordResetAPIView(APIView):
|
||||
),
|
||||
responses={200: success_response, 400: error_response, 500: error_response},
|
||||
)
|
||||
def post(self, request):
|
||||
async def post(self, request):
|
||||
from utils.captcha import check_captcha_required, verify_captcha, record_failure, reset_failures
|
||||
|
||||
to_email = request.data.get('email', None)
|
||||
@@ -377,7 +384,7 @@ class ForgotPasswordResetAPIView(APIView):
|
||||
|
||||
identifier = to_email
|
||||
operation = 'forgot_reset'
|
||||
captcha_required = check_captcha_required(operation, identifier)
|
||||
captcha_required = await sync_to_async(check_captcha_required)(operation, identifier)
|
||||
|
||||
if captcha_required:
|
||||
captcha_key = request.data.get('captcha_key', None)
|
||||
@@ -389,28 +396,28 @@ class ForgotPasswordResetAPIView(APIView):
|
||||
status_code=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
captcha_result = verify_captcha(captcha_key, captcha_code)
|
||||
captcha_result = await sync_to_async(verify_captcha)(captcha_key, captcha_code)
|
||||
if captcha_result == 'expired':
|
||||
return create_standardized_error_response(
|
||||
code=ResponseCode.CAPTCHA_EXPIRED,
|
||||
status_code=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
elif captcha_result == 'wrong':
|
||||
record_failure(operation, identifier)
|
||||
await sync_to_async(record_failure)(operation, identifier)
|
||||
return create_standardized_error_response(
|
||||
code=ResponseCode.CAPTCHA_ERROR,
|
||||
status_code=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
if new_password != confirm_password:
|
||||
record_failure(operation, to_email)
|
||||
await sync_to_async(record_failure)(operation, to_email)
|
||||
return create_standardized_error_response(
|
||||
code=ResponseCode.PASSWORD_MISMATCH,
|
||||
status_code=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
if len(new_password) < 8:
|
||||
record_failure(operation, to_email)
|
||||
await sync_to_async(record_failure)(operation, to_email)
|
||||
return create_standardized_error_response(
|
||||
code=ResponseCode.PASSWORD_TOO_SHORT,
|
||||
status_code=status.HTTP_400_BAD_REQUEST
|
||||
@@ -419,51 +426,51 @@ class ForgotPasswordResetAPIView(APIView):
|
||||
has_letter = any(c.isalpha() for c in new_password)
|
||||
has_digit = any(c.isdigit() for c in new_password)
|
||||
if not (has_letter and has_digit):
|
||||
record_failure(operation, to_email)
|
||||
await sync_to_async(record_failure)(operation, to_email)
|
||||
return create_standardized_error_response(
|
||||
code=ResponseCode.PASSWORD_TOO_WEAK,
|
||||
status_code=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
try:
|
||||
vcode = default_cache.get(f"reset_password_{to_email}")
|
||||
vcode = await sync_to_async(default_cache.get)(f"reset_password_{to_email}")
|
||||
|
||||
if vcode is None:
|
||||
record_failure(operation, to_email)
|
||||
await sync_to_async(record_failure)(operation, to_email)
|
||||
return create_standardized_error_response(
|
||||
code=ResponseCode.VERIFICATION_CODE_EXPIRED,
|
||||
status_code=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
if code != vcode:
|
||||
record_failure(operation, to_email)
|
||||
await sync_to_async(record_failure)(operation, to_email)
|
||||
return create_standardized_error_response(
|
||||
code=ResponseCode.VERIFICATION_CODE_ERROR,
|
||||
status_code=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
user = FUser.objects.filter(email=to_email).first()
|
||||
user = await FUser.objects.filter(email=to_email).afirst()
|
||||
|
||||
if user is None:
|
||||
record_failure(operation, to_email)
|
||||
await sync_to_async(record_failure)(operation, to_email)
|
||||
return create_standardized_error_response(
|
||||
code=ResponseCode.USER_NOT_FOUND,
|
||||
status_code=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
user.set_password(new_password)
|
||||
user.save()
|
||||
await sync_to_async(user.set_password)(new_password)
|
||||
await user.asave()
|
||||
|
||||
default_cache.delete(f"reset_password_{to_email}")
|
||||
await sync_to_async(default_cache.delete)(f"reset_password_{to_email}")
|
||||
|
||||
reset_failures(operation, to_email)
|
||||
await sync_to_async(reset_failures)(operation, to_email)
|
||||
return create_standardized_response(
|
||||
code=ResponseCode.PASSWORD_RESET_SUCCESS,
|
||||
status_code=status.HTTP_200_OK
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
record_failure(operation, to_email)
|
||||
await sync_to_async(record_failure)(operation, to_email)
|
||||
return create_standardized_error_response(
|
||||
message=f"服务器内部错误: {str(e)}",
|
||||
code=ResponseCode.SERVER_INTERNAL_ERROR,
|
||||
@@ -478,12 +485,12 @@ class UserUpdateAPIView(APIView):
|
||||
operation_description='获取当前登录用户的个人信息',
|
||||
responses={200: success_response, 401: unauthorized_response},
|
||||
)
|
||||
def get(self, request):
|
||||
async def get(self, request):
|
||||
user = request.user
|
||||
user_serializer = UserSerializer(user)
|
||||
user_data = await sync_to_async(_serialize_user)(user)
|
||||
|
||||
return create_standardized_response(
|
||||
data={'user': user_serializer.data},
|
||||
data={'user': user_data},
|
||||
code=ResponseCode.SUCCESS,
|
||||
status_code=status.HTTP_200_OK
|
||||
)
|
||||
@@ -495,7 +502,7 @@ class UserUpdateAPIView(APIView):
|
||||
request_body=UserUpdateSerializer,
|
||||
responses={200: success_response, 400: error_response, 401: unauthorized_response},
|
||||
)
|
||||
def put(self, request):
|
||||
async def put(self, request):
|
||||
user = request.user
|
||||
serializer = UserUpdateSerializer(
|
||||
user,
|
||||
@@ -504,7 +511,7 @@ class UserUpdateAPIView(APIView):
|
||||
context={'request': request}
|
||||
)
|
||||
|
||||
if not serializer.is_valid():
|
||||
if not await sync_to_async(serializer.is_valid)():
|
||||
errors = serializer.errors
|
||||
first_error = ''
|
||||
for field, msgs in errors.items():
|
||||
@@ -519,23 +526,23 @@ class UserUpdateAPIView(APIView):
|
||||
status_code=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
updated_user = serializer.save()
|
||||
user_serializer = UserSerializer(updated_user)
|
||||
updated_user = await sync_to_async(serializer.save)()
|
||||
user_data = await sync_to_async(_serialize_user)(updated_user)
|
||||
|
||||
try:
|
||||
log_event(
|
||||
await sync_to_async(log_event)(
|
||||
event_type='profile_update',
|
||||
user=updated_user,
|
||||
description=f'更新了{len(serializer.validated_data)}项个人资料',
|
||||
metadata={'updated_fields': list(serializer.validated_data.keys())},
|
||||
request=request
|
||||
)
|
||||
track_user_action(updated_user, 'profile', count=1)
|
||||
await sync_to_async(track_user_action)(updated_user, 'profile', count=1)
|
||||
except Exception as e:
|
||||
logging.getLogger(__name__).warning(f'Profile track failed: {e}')
|
||||
|
||||
return create_standardized_response(
|
||||
data={'user': user_serializer.data},
|
||||
data={'user': user_data},
|
||||
code=ResponseCode.SUCCESS,
|
||||
status_code=status.HTTP_200_OK
|
||||
)
|
||||
@@ -547,8 +554,8 @@ class UserUpdateAPIView(APIView):
|
||||
request_body=UserUpdateSerializer,
|
||||
responses={200: success_response, 400: error_response, 401: unauthorized_response},
|
||||
)
|
||||
def patch(self, request):
|
||||
return self.put(request)
|
||||
async def patch(self, request):
|
||||
return await self.put(request)
|
||||
|
||||
|
||||
class ChangePasswordAPIView(APIView):
|
||||
@@ -568,12 +575,12 @@ class ChangePasswordAPIView(APIView):
|
||||
),
|
||||
responses={200: success_response, 400: error_response, 401: unauthorized_response},
|
||||
)
|
||||
def post(self, request):
|
||||
async def post(self, request):
|
||||
from utils.captcha import check_captcha_required, verify_captcha, record_failure, reset_failures
|
||||
|
||||
identifier = str(request.user.id)
|
||||
operation = 'change_pwd'
|
||||
captcha_required = check_captcha_required(operation, identifier)
|
||||
captcha_required = await sync_to_async(check_captcha_required)(operation, identifier)
|
||||
|
||||
if captcha_required:
|
||||
captcha_key = request.data.get('captcha_key', None)
|
||||
@@ -585,14 +592,14 @@ class ChangePasswordAPIView(APIView):
|
||||
status_code=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
captcha_result = verify_captcha(captcha_key, captcha_code)
|
||||
captcha_result = await sync_to_async(verify_captcha)(captcha_key, captcha_code)
|
||||
if captcha_result == 'expired':
|
||||
return create_standardized_error_response(
|
||||
code=ResponseCode.CAPTCHA_EXPIRED,
|
||||
status_code=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
elif captcha_result == 'wrong':
|
||||
record_failure(operation, identifier)
|
||||
await sync_to_async(record_failure)(operation, identifier)
|
||||
return create_standardized_error_response(
|
||||
code=ResponseCode.CAPTCHA_ERROR,
|
||||
status_code=status.HTTP_400_BAD_REQUEST
|
||||
@@ -603,7 +610,7 @@ class ChangePasswordAPIView(APIView):
|
||||
context={'request': request}
|
||||
)
|
||||
|
||||
if not serializer.is_valid():
|
||||
if not await sync_to_async(serializer.is_valid)():
|
||||
errors = serializer.errors
|
||||
first_error = ''
|
||||
for field, msgs in errors.items():
|
||||
@@ -617,7 +624,7 @@ class ChangePasswordAPIView(APIView):
|
||||
break
|
||||
break
|
||||
|
||||
record_failure(operation, identifier)
|
||||
await sync_to_async(record_failure)(operation, identifier)
|
||||
return create_standardized_error_response(
|
||||
data=errors,
|
||||
code=ResponseCode.PARAMETER_ERROR,
|
||||
@@ -625,15 +632,15 @@ class ChangePasswordAPIView(APIView):
|
||||
status_code=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
user = serializer.save()
|
||||
user_serializer = UserSerializer(user)
|
||||
user = await sync_to_async(serializer.save)()
|
||||
user_data = await sync_to_async(_serialize_user)(user)
|
||||
|
||||
is_new_set = not request.user.has_usable_password() or request.data.get('old_password', '') == ''
|
||||
|
||||
reset_failures(operation, identifier)
|
||||
await sync_to_async(reset_failures)(operation, identifier)
|
||||
return create_standardized_response(
|
||||
data={
|
||||
'user': user_serializer.data,
|
||||
'user': user_data,
|
||||
'is_new_set': is_new_set,
|
||||
},
|
||||
code=ResponseCode.PASSWORD_SET if is_new_set else ResponseCode.PASSWORD_CHANGED,
|
||||
@@ -658,7 +665,7 @@ class UserLoginAPIView(APIView):
|
||||
),
|
||||
responses={200: success_response, 400: error_response, 401: unauthorized_response, 403: error_response},
|
||||
)
|
||||
def post(self, request):
|
||||
async def post(self, request):
|
||||
account = request.data.get('account', None)
|
||||
password = request.data.get('password', None)
|
||||
|
||||
@@ -670,22 +677,22 @@ class UserLoginAPIView(APIView):
|
||||
)
|
||||
|
||||
try:
|
||||
user = authenticate(username=account, password=password)
|
||||
user = await sync_to_async(authenticate)(username=account, password=password)
|
||||
|
||||
if user is not None:
|
||||
if user.is_active:
|
||||
refresh = RefreshToken.for_user(user)
|
||||
user_serializer = UserSerializer(user)
|
||||
user_data = await sync_to_async(_serialize_user)(user)
|
||||
|
||||
response_data = {
|
||||
'user': user_serializer.data,
|
||||
'user': user_data,
|
||||
'refresh': str(refresh),
|
||||
'access': str(refresh.access_token),
|
||||
'token_type': 'bearer',
|
||||
'expires_at_timestamp': refresh.access_token.payload['exp']
|
||||
}
|
||||
|
||||
_create_login_record(request, user, 'success')
|
||||
await sync_to_async(_create_login_record)(request, user, 'success')
|
||||
|
||||
return create_standardized_response(
|
||||
data=response_data,
|
||||
@@ -693,7 +700,7 @@ class UserLoginAPIView(APIView):
|
||||
status_code=status.HTTP_200_OK
|
||||
)
|
||||
else:
|
||||
_create_login_record(request, user, 'failed')
|
||||
await sync_to_async(_create_login_record)(request, user, 'failed')
|
||||
|
||||
return create_standardized_error_response(
|
||||
code=ResponseCode.PARAMETER_ERROR,
|
||||
@@ -701,9 +708,9 @@ class UserLoginAPIView(APIView):
|
||||
status_code=status.HTTP_403_FORBIDDEN
|
||||
)
|
||||
else:
|
||||
login_user = FUser.objects.filter(username=account).first() or FUser.objects.filter(email=account).first()
|
||||
login_user = await FUser.objects.filter(username=account).afirst() or await FUser.objects.filter(email=account).afirst()
|
||||
if login_user:
|
||||
_create_login_record(request, login_user, 'failed')
|
||||
await sync_to_async(_create_login_record)(request, login_user, 'failed')
|
||||
|
||||
return create_standardized_error_response(
|
||||
code=ResponseCode.PARAMETER_ERROR,
|
||||
@@ -744,7 +751,7 @@ class LoginView(APIView):
|
||||
),
|
||||
responses={200: success_response, 400: error_response, 401: unauthorized_response, 403: error_response},
|
||||
)
|
||||
def post(self, request):
|
||||
async def post(self, request):
|
||||
account = request.data.get('account', None)
|
||||
password = request.data.get('password', None)
|
||||
|
||||
@@ -757,7 +764,7 @@ class LoginView(APIView):
|
||||
|
||||
identifier = self._get_identifier(request)
|
||||
operation = 'login'
|
||||
captcha_required = check_captcha_required(operation, identifier)
|
||||
captcha_required = await sync_to_async(check_captcha_required)(operation, identifier)
|
||||
|
||||
if captcha_required:
|
||||
slider_captcha_key = request.data.get('slider_captcha_key', None)
|
||||
@@ -770,9 +777,9 @@ class LoginView(APIView):
|
||||
)
|
||||
|
||||
try:
|
||||
captcha_valid = verify_slider_captcha(slider_captcha_key, int(slider_captcha_x))
|
||||
captcha_valid = await sync_to_async(verify_slider_captcha)(slider_captcha_key, int(slider_captcha_x))
|
||||
if not captcha_valid:
|
||||
record_failure(operation, identifier)
|
||||
await sync_to_async(record_failure)(operation, identifier)
|
||||
return create_standardized_error_response(
|
||||
code=ResponseCode.CAPTCHA_ERROR,
|
||||
status_code=status.HTTP_400_BAD_REQUEST
|
||||
@@ -784,23 +791,23 @@ class LoginView(APIView):
|
||||
)
|
||||
|
||||
try:
|
||||
user = authenticate(username=account, password=password)
|
||||
user = await sync_to_async(authenticate)(username=account, password=password)
|
||||
|
||||
if user is not None:
|
||||
if user.is_active:
|
||||
reset_failures(operation, identifier)
|
||||
await sync_to_async(reset_failures)(operation, identifier)
|
||||
refresh = RefreshToken.for_user(user)
|
||||
user_serializer = UserSerializer(user)
|
||||
user_data = await sync_to_async(_serialize_user)(user)
|
||||
|
||||
response_data = {
|
||||
'user': user_serializer.data,
|
||||
'user': user_data,
|
||||
'refresh': str(refresh),
|
||||
'access': str(refresh.access_token),
|
||||
'token_type': 'bearer',
|
||||
'expires_at_timestamp': refresh.access_token.payload['exp']
|
||||
}
|
||||
|
||||
_create_login_record(request, user, 'success')
|
||||
await sync_to_async(_create_login_record)(request, user, 'success')
|
||||
|
||||
return create_standardized_response(
|
||||
data=response_data,
|
||||
@@ -808,8 +815,8 @@ class LoginView(APIView):
|
||||
status_code=status.HTTP_200_OK
|
||||
)
|
||||
else:
|
||||
record_failure(operation, identifier)
|
||||
_create_login_record(request, user, 'failed')
|
||||
await sync_to_async(record_failure)(operation, identifier)
|
||||
await sync_to_async(_create_login_record)(request, user, 'failed')
|
||||
|
||||
return create_standardized_error_response(
|
||||
code=ResponseCode.PARAMETER_ERROR,
|
||||
@@ -817,10 +824,10 @@ class LoginView(APIView):
|
||||
status_code=status.HTTP_403_FORBIDDEN
|
||||
)
|
||||
else:
|
||||
record_failure(operation, identifier)
|
||||
login_user = FUser.objects.filter(username=account).first() or FUser.objects.filter(email=account).first()
|
||||
await sync_to_async(record_failure)(operation, identifier)
|
||||
login_user = await FUser.objects.filter(username=account).afirst() or await FUser.objects.filter(email=account).afirst()
|
||||
if login_user:
|
||||
_create_login_record(request, login_user, 'failed')
|
||||
await sync_to_async(_create_login_record)(request, login_user, 'failed')
|
||||
|
||||
return create_standardized_error_response(
|
||||
code=ResponseCode.PARAMETER_ERROR,
|
||||
@@ -848,16 +855,17 @@ class PublicProfileAPIView(APIView):
|
||||
],
|
||||
responses={200: success_response, 404: not_found_response},
|
||||
)
|
||||
def get(self, request, user_id):
|
||||
async def get(self, request, user_id):
|
||||
try:
|
||||
user = FUser.objects.get(id=user_id, is_active=True)
|
||||
user = await FUser.objects.aget(id=user_id, is_active=True)
|
||||
except FUser.DoesNotExist:
|
||||
return Response({'error': '用户不存在'}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
try:
|
||||
from article.models import Article
|
||||
article_count = Article.objects.filter(author=user, status='published').count()
|
||||
article_count = await Article.objects.filter(author=user, status='published').acount()
|
||||
recent_articles = Article.objects.filter(author=user, status='published').order_by('-created_at')[:5]
|
||||
recent_articles = [a async for a in recent_articles]
|
||||
articles_data = []
|
||||
for article in recent_articles:
|
||||
articles_data.append({
|
||||
@@ -945,7 +953,7 @@ class UploadAvatarAPIView(APIView):
|
||||
],
|
||||
responses={200: success_response, 400: error_response, 401: unauthorized_response, 500: error_response},
|
||||
)
|
||||
def post(self, request):
|
||||
async def post(self, request):
|
||||
file = request.FILES.get('avatar')
|
||||
|
||||
if not file:
|
||||
@@ -979,17 +987,17 @@ class UploadAvatarAPIView(APIView):
|
||||
|
||||
try:
|
||||
filename = f'avatars/{uuid.uuid4().hex}{ext}'
|
||||
saved_path = default_storage.save(filename, file)
|
||||
saved_path = await sync_to_async(default_storage.save)(filename, file)
|
||||
|
||||
user = request.user
|
||||
if user.avatar and user.avatar.name:
|
||||
try:
|
||||
default_storage.delete(user.avatar.name)
|
||||
await sync_to_async(default_storage.delete)(user.avatar.name)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
user.avatar = saved_path
|
||||
user.save(update_fields=['avatar'])
|
||||
await user.asave(update_fields=['avatar'])
|
||||
|
||||
avatar_url = request.build_absolute_uri(settings.MEDIA_URL + saved_path)
|
||||
|
||||
@@ -1024,9 +1032,9 @@ class FollowToggleView(APIView):
|
||||
404: not_found_response,
|
||||
},
|
||||
)
|
||||
def post(self, request, user_id):
|
||||
async def post(self, request, user_id):
|
||||
try:
|
||||
target_user = FUser.objects.get(id=user_id, is_active=True)
|
||||
target_user = await FUser.objects.aget(id=user_id, is_active=True)
|
||||
except FUser.DoesNotExist:
|
||||
return create_standardized_error_response(
|
||||
code=ResponseCode.NOT_FOUND,
|
||||
@@ -1041,19 +1049,19 @@ class FollowToggleView(APIView):
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
follow, created = Follow.objects.get_or_create(
|
||||
follow, created = await Follow.objects.aget_or_create(
|
||||
follower=request.user,
|
||||
following=target_user,
|
||||
)
|
||||
|
||||
if not created:
|
||||
follow.delete()
|
||||
await follow.adelete()
|
||||
is_following = False
|
||||
else:
|
||||
is_following = True
|
||||
|
||||
follower_count = target_user.followers.count()
|
||||
following_count = request.user.following.count()
|
||||
follower_count = await target_user.followers.acount()
|
||||
following_count = await request.user.following.acount()
|
||||
|
||||
return create_standardized_response(
|
||||
data={
|
||||
|
||||
Reference in New Issue
Block a user