from rest_framework import status from adrf.views import APIView from rest_framework.permissions import IsAuthenticated from asgiref.sync import sync_to_async from drf_yasg.utils import swagger_auto_schema from drf_yasg import openapi import logging from chunyu_project.common_schemas import success_response, error_response, unauthorized_response, not_found_response from utils.response_codes import ( ResponseCode, create_standardized_response, create_standardized_error_response ) from utils.email_utils import validate_email_mx from ..serializers.user_serializers import ( SendEmailCodeSerializer, ChangeEmailSerializer, UserSerializer, ) logger = logging.getLogger(__name__) class SendChangeEmailCodeAPIView(APIView): permission_classes = [IsAuthenticated] @swagger_auto_schema( tags=['用户'], operation_summary='发送修改邮箱验证码', operation_description='向新邮箱发送验证码用于修改邮箱', request_body=openapi.Schema( type=openapi.TYPE_OBJECT, properties={ 'email': openapi.Schema(type=openapi.TYPE_STRING, description='新邮箱地址'), 'captcha_key': openapi.Schema(type=openapi.TYPE_STRING, description='图形验证码 key'), 'captcha_code': openapi.Schema(type=openapi.TYPE_STRING, description='图形验证码'), }, ), responses={200: success_response, 400: error_response, 401: unauthorized_response, 500: error_response}, ) 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_email' captcha_required = await sync_to_async(check_captcha_required)(operation, identifier) if captcha_required: captcha_key = request.data.get('captcha_key', None) captcha_code = request.data.get('captcha_code', None) if not captcha_key or not captcha_code: return create_standardized_error_response( code=ResponseCode.CAPTCHA_REQUIRED, status_code=status.HTTP_400_BAD_REQUEST ) 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': await sync_to_async(record_failure)(operation, identifier) return create_standardized_error_response( code=ResponseCode.CAPTCHA_ERROR, status_code=status.HTTP_400_BAD_REQUEST ) serializer = SendEmailCodeSerializer( data=request.data, context={'request': request} ) # 校验器内含同步 ORM(validate_email 查重)与 cache 写入,线程池兜底 if not await sync_to_async(serializer.is_valid)(): errors = serializer.errors first_error = '' for field, msgs in errors.items(): if isinstance(msgs, list) and msgs: first_error = str(msgs[0]) break await sync_to_async(record_failure)(operation, identifier) return create_standardized_error_response( data=errors, code=ResponseCode.PARAMETER_ERROR, message=first_error or '参数异常', status_code=status.HTTP_400_BAD_REQUEST ) email = serializer.validated_data.get('email') if email and not await sync_to_async(validate_email_mx)(email): logger.warning(f'[ChangeEmail] Domain MX check failed: email={email}') await sync_to_async(record_failure)(operation, identifier) return create_standardized_error_response( code=ResponseCode.EMAIL_DOMAIN_INVALID, status_code=status.HTTP_400_BAD_REQUEST ) try: # save() 内部触发验证码邮件发送(Celery/cache/SMTP 链路),线程池兜底 email = await serializer.asave() await sync_to_async(reset_failures)(operation, identifier) return create_standardized_response( data={'email_sent': True}, code=ResponseCode.EMAIL_CHANGE_CODE_SENT, status_code=status.HTTP_200_OK ) except Exception as e: await sync_to_async(record_failure)(operation, identifier) return create_standardized_error_response( message=f'邮件发送失败: {str(e)}', code=ResponseCode.EMAIL_SEND_FAILED, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR ) class ChangeEmailAPIView(APIView): permission_classes = [IsAuthenticated] @swagger_auto_schema( tags=['用户'], operation_summary='修改邮箱', operation_description='使用邮箱验证码修改用户邮箱', request_body=openapi.Schema( type=openapi.TYPE_OBJECT, properties={ 'email': openapi.Schema(type=openapi.TYPE_STRING, description='新邮箱地址'), 'email_code': openapi.Schema(type=openapi.TYPE_STRING, description='邮箱验证码'), }, ), responses={200: success_response, 400: error_response, 401: unauthorized_response, 500: error_response}, ) async def post(self, request): serializer = ChangeEmailSerializer( data=request.data, context={'request': request} ) # 校验器内含同步 ORM 查询,线程池兜底 if not await sync_to_async(serializer.is_valid)(): errors = serializer.errors first_error = '' for field, msgs in errors.items(): if isinstance(msgs, list) and msgs: first_error = str(msgs[0]) break return create_standardized_error_response( data=errors, code=ResponseCode.PARAMETER_ERROR, message=first_error or '参数异常', status_code=status.HTTP_400_BAD_REQUEST ) try: updated_user = await serializer.asave() user_serializer = UserSerializer(updated_user) return create_standardized_response( data={'user': await user_serializer.adata}, code=ResponseCode.EMAIL_CHANGED, status_code=status.HTTP_200_OK ) except Exception as e: return create_standardized_error_response( message=f'邮箱修改失败: {str(e)}', code=ResponseCode.SERVER_INTERNAL_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR )