Files
chunyu_project/user/views/user.py
T

1066 lines
42 KiB
Python

import os
import uuid
import logging
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 rest_framework.response import Response
from rest_framework import status
from django.contrib.auth import authenticate
from django.conf import settings
from django.core.files.storage import default_storage
from utils import RandCode
from django.core.cache import caches
default_cache = caches['default']
session_cache = caches['session']
celery_cache = caches['celery']
from ..models import FUser, LoginRecord, Follow
from rest_framework_simplejwt.tokens import RefreshToken
from ..serializers.user_serializers import UserSerializer, ChangePasswordSerializer, UserUpdateSerializer
from ..tasks import send_verification_email_task, send_reset_password_email_task
from logs.utils import log_event
from .tasks import track_user_action
from utils.captcha import check_captcha_required, record_failure, reset_failures
from utils.slider_captcha import verify_slider_captcha, SliderCaptchaError
from utils.safe_task import submit_task
from utils.response_codes import (
ResponseCode,
create_standardized_response,
create_standardized_error_response
)
from drf_yasg.utils import swagger_auto_schema
logger = logging.getLogger(__name__)
from drf_yasg import openapi
from chunyu_project.common_schemas import success_response, error_response, unauthorized_response, not_found_response
def _get_client_ip(request):
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
return x_forwarded_for.split(',')[0].strip()
return request.META.get('REMOTE_ADDR', '')
def _create_login_record(request, user, record_status):
from user.services import create_login_record
create_login_record(request, user, record_status)
class SendUserEmailAPIView(APIView):
permission_classes = [AllowAny]
@swagger_auto_schema(
tags=['认证'],
operation_summary='发送邮箱验证码',
operation_description='向指定邮箱发送验证码,若邮箱未注册则发送注册验证码,已注册则发送登录验证码',
request_body=openapi.Schema(
type=openapi.TYPE_OBJECT,
required=['to_email'],
properties={
'to_email': openapi.Schema(type=openapi.TYPE_STRING, description='目标邮箱地址'),
}
),
responses={201: success_response, 200: success_response, 400: error_response, 500: error_response},
)
def post(self, request):
to_email = request.data.get('to_email', None)
if to_email is None or to_email == "":
return create_standardized_error_response(
code=ResponseCode.EMAIL_EMPTY,
status_code=status.HTTP_400_BAD_REQUEST
)
if not 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,
status_code=status.HTTP_400_BAD_REQUEST
)
try:
user_exists_result = FUser.objects.filter(email=to_email).exists()
if not user_exists_result:
code = RandCode.get_digit_characters_code_8()
default_cache.set(f"register_{to_email}", code, timeout=600)
result = 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:
logger.info(f'[Register] Email sent successfully: email={to_email}')
return create_standardized_response(
data={"email_sent": True, "type": "register"},
code=ResponseCode.EMAIL_SENT_REGISTER,
status_code=status.HTTP_201_CREATED
)
else:
code = RandCode.get_digit_characters_code_8()
default_cache.set(f"login_{to_email}", code, timeout=600)
result = 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:
logger.info(f'[Login] Email sent successfully: email={to_email}')
return create_standardized_response(
data={"email_sent": True, "type": "login"},
code=ResponseCode.EMAIL_SENT_LOGIN,
status_code=status.HTTP_200_OK
)
except Exception as e:
return create_standardized_error_response(
message=f"邮件发送失败: {str(e)}",
code=ResponseCode.EMAIL_SEND_FAILED,
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
)
class UserLoginOrRegisterAPIView(APIView):
permission_classes = [AllowAny]
@swagger_auto_schema(
tags=['认证'],
operation_summary='邮箱验证码登录或注册',
operation_description='使用邮箱和验证码进行登录或注册,新用户自动注册并返回token',
request_body=openapi.Schema(
type=openapi.TYPE_OBJECT,
required=['email', 'code'],
properties={
'email': openapi.Schema(type=openapi.TYPE_STRING, description='邮箱地址'),
'code': openapi.Schema(type=openapi.TYPE_STRING, description='验证码'),
'username': openapi.Schema(type=openapi.TYPE_STRING, description='用户名(注册时可选)'),
}
),
responses={200: success_response, 400: error_response, 500: error_response},
)
def post(self, request):
code = request.data.get('code', None)
to_email = request.data.get('email', None)
if code is None or to_email is None or code == "" or to_email == "":
return create_standardized_error_response(
code=ResponseCode.PARAMETER_ERROR,
status_code=status.HTTP_400_BAD_REQUEST
)
try:
user = FUser.objects.filter(email=to_email).first()
if user is None:
# Registration flow
vcode = default_cache.get(f"register_{to_email}")
if vcode is None:
return create_standardized_error_response(
code=ResponseCode.VERIFICATION_CODE_EXPIRED,
status_code=status.HTTP_400_BAD_REQUEST
)
if code == vcode:
user_serializer = UserSerializer(data=request.data)
if user_serializer.is_valid():
user = user_serializer.create_by_email(request.data)
refresh = RefreshToken.for_user(user)
# Prepare response data
response_data = {
'user': UserSerializer(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')
return create_standardized_response(
data=response_data,
code=ResponseCode.REGISTRATION_SUCCESS,
status_code=status.HTTP_200_OK
)
else:
return create_standardized_error_response(
code=ResponseCode.USER_DATA_INVALID,
status_code=status.HTTP_400_BAD_REQUEST
)
else:
return create_standardized_error_response(
code=ResponseCode.VERIFICATION_CODE_ERROR,
status_code=status.HTTP_400_BAD_REQUEST
)
else:
# Login flow
vcode = default_cache.get(f"login_{to_email}")
if vcode is None:
return create_standardized_error_response(
code=ResponseCode.LOGIN_VERIFICATION_EXPIRED,
status_code=status.HTTP_400_BAD_REQUEST
)
if code == vcode:
refresh = RefreshToken.for_user(user)
user_serializer = UserSerializer(user)
# Prepare response data
response_data = {
'user': user_serializer.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')
return create_standardized_response(
data=response_data,
code=ResponseCode.LOGIN_SUCCESS,
status_code=status.HTTP_200_OK
)
else:
_create_login_record(request, user, 'failed')
return create_standardized_error_response(
code=ResponseCode.LOGIN_VERIFICATION_ERROR,
status_code=status.HTTP_400_BAD_REQUEST
)
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
)
class ForgotPasswordSendCodeAPIView(APIView):
permission_classes = [AllowAny]
@swagger_auto_schema(
tags=['认证'],
operation_summary='发送忘记密码验证码',
operation_description='向已注册邮箱发送重置密码验证码,失败次数过多需要验证码',
request_body=openapi.Schema(
type=openapi.TYPE_OBJECT,
required=['email'],
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, 500: error_response},
)
def post(self, request):
from utils.captcha import check_captcha_required, verify_captcha, record_failure, reset_failures
to_email = request.data.get('email', None)
if not to_email:
return create_standardized_error_response(
code=ResponseCode.EMAIL_EMPTY,
status_code=status.HTTP_400_BAD_REQUEST
)
if not validate_email_mx(to_email):
return create_standardized_error_response(
code=ResponseCode.EMAIL_DOMAIN_INVALID,
status_code=status.HTTP_400_BAD_REQUEST
)
identifier = to_email
operation = 'forgot_send'
captcha_required = 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 = 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)
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()
if user is None:
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)
result = 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)
return create_standardized_response(
data={"email_sent": True},
code=ResponseCode.RESET_CODE_SENT,
status_code=status.HTTP_200_OK
)
except Exception as e:
record_failure(operation, to_email)
return create_standardized_error_response(
message=f"邮件发送失败: {str(e)}",
code=ResponseCode.EMAIL_SEND_FAILED,
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
)
class ForgotPasswordResetAPIView(APIView):
permission_classes = [AllowAny]
@swagger_auto_schema(
tags=['认证'],
operation_summary='重置密码',
operation_description='使用验证码重置密码,密码需至少8位且包含字母和数字',
request_body=openapi.Schema(
type=openapi.TYPE_OBJECT,
required=['email', 'code', 'new_password', 'confirm_password'],
properties={
'email': openapi.Schema(type=openapi.TYPE_STRING, description='注册邮箱地址'),
'code': openapi.Schema(type=openapi.TYPE_STRING, description='验证码'),
'new_password': openapi.Schema(type=openapi.TYPE_STRING, description='新密码'),
'confirm_password': 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, 500: error_response},
)
def post(self, request):
from utils.captcha import check_captcha_required, verify_captcha, record_failure, reset_failures
to_email = request.data.get('email', None)
code = request.data.get('code', None)
new_password = request.data.get('new_password', None)
confirm_password = request.data.get('confirm_password', None)
if not to_email or not code or not new_password or not confirm_password:
return create_standardized_error_response(
code=ResponseCode.PARAMETER_ERROR,
message="邮箱、验证码和密码不能为空",
status_code=status.HTTP_400_BAD_REQUEST
)
identifier = to_email
operation = 'forgot_reset'
captcha_required = 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 = 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)
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)
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)
return create_standardized_error_response(
code=ResponseCode.PASSWORD_TOO_SHORT,
status_code=status.HTTP_400_BAD_REQUEST
)
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)
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}")
if vcode is None:
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)
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()
if user is None:
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()
default_cache.delete(f"reset_password_{to_email}")
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)
return create_standardized_error_response(
message=f"服务器内部错误: {str(e)}",
code=ResponseCode.SERVER_INTERNAL_ERROR,
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
)
class UserUpdateAPIView(APIView):
@swagger_auto_schema(
tags=['用户'],
operation_summary='获取当前用户信息',
operation_description='获取当前登录用户的个人信息',
responses={200: success_response, 401: unauthorized_response},
)
def get(self, request):
user = request.user
user_serializer = UserSerializer(user)
return create_standardized_response(
data={'user': user_serializer.data},
code=ResponseCode.SUCCESS,
status_code=status.HTTP_200_OK
)
@swagger_auto_schema(
tags=['用户'],
operation_summary='更新用户信息',
operation_description='更新当前登录用户的个人信息(部分更新)',
request_body=UserUpdateSerializer,
responses={200: success_response, 400: error_response, 401: unauthorized_response},
)
def put(self, request):
user = request.user
serializer = UserUpdateSerializer(
user,
data=request.data,
partial=True,
context={'request': request}
)
if not 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
)
updated_user = serializer.save()
user_serializer = UserSerializer(updated_user)
try:
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)
except Exception as e:
logging.getLogger(__name__).warning(f'Profile track failed: {e}')
return create_standardized_response(
data={'user': user_serializer.data},
code=ResponseCode.SUCCESS,
status_code=status.HTTP_200_OK
)
@swagger_auto_schema(
tags=['用户'],
operation_summary='部分更新用户信息',
operation_description='部分更新当前登录用户的个人信息',
request_body=UserUpdateSerializer,
responses={200: success_response, 400: error_response, 401: unauthorized_response},
)
def patch(self, request):
return self.put(request)
class ChangePasswordAPIView(APIView):
@swagger_auto_schema(
tags=['用户'],
operation_summary='修改密码',
operation_description='修改当前用户密码,需通过验证码验证,密码需至少8位且包含字母和数字',
request_body=openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'old_password': openapi.Schema(type=openapi.TYPE_STRING, description='旧密码(首次设置可为空)'),
'new_password': openapi.Schema(type=openapi.TYPE_STRING, description='新密码'),
'confirm_password': 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},
)
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)
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 = 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)
return create_standardized_error_response(
code=ResponseCode.CAPTCHA_ERROR,
status_code=status.HTTP_400_BAD_REQUEST
)
serializer = ChangePasswordSerializer(
data=request.data,
context={'request': request}
)
if not 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
elif isinstance(msgs, dict):
for sub_msgs in msgs.values():
if isinstance(sub_msgs, list) and sub_msgs:
first_error = str(sub_msgs[0])
break
break
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
)
user = serializer.save()
user_serializer = UserSerializer(user)
is_new_set = not request.user.has_usable_password() or request.data.get('old_password', '') == ''
reset_failures(operation, identifier)
return create_standardized_response(
data={
'user': user_serializer.data,
'is_new_set': is_new_set,
},
code=ResponseCode.PASSWORD_SET if is_new_set else ResponseCode.PASSWORD_CHANGED,
status_code=status.HTTP_200_OK
)
class UserLoginAPIView(APIView):
permission_classes = [AllowAny]
@swagger_auto_schema(
tags=['认证'],
operation_summary='账号密码登录',
operation_description='使用账号和密码进行登录,返回JWT token',
request_body=openapi.Schema(
type=openapi.TYPE_OBJECT,
required=['account', 'password'],
properties={
'account': openapi.Schema(type=openapi.TYPE_STRING, description='账号(用户名或邮箱)'),
'password': openapi.Schema(type=openapi.TYPE_STRING, description='密码'),
}
),
responses={200: success_response, 400: error_response, 401: unauthorized_response, 403: error_response},
)
def post(self, request):
account = request.data.get('account', None)
password = request.data.get('password', None)
if not account or not password:
return create_standardized_error_response(
code=ResponseCode.PARAMETER_ERROR,
message="账号和密码不能为空",
status_code=status.HTTP_400_BAD_REQUEST
)
try:
user = authenticate(username=account, password=password)
if user is not None:
if user.is_active:
refresh = RefreshToken.for_user(user)
user_serializer = UserSerializer(user)
response_data = {
'user': user_serializer.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')
return create_standardized_response(
data=response_data,
code=ResponseCode.LOGIN_SUCCESS,
status_code=status.HTTP_200_OK
)
else:
_create_login_record(request, user, 'failed')
return create_standardized_error_response(
code=ResponseCode.PARAMETER_ERROR,
message="账号已被禁用",
status_code=status.HTTP_403_FORBIDDEN
)
else:
login_user = FUser.objects.filter(username=account).first() or FUser.objects.filter(email=account).first()
if login_user:
_create_login_record(request, login_user, 'failed')
return create_standardized_error_response(
code=ResponseCode.PARAMETER_ERROR,
message="账号或密码错误",
status_code=status.HTTP_401_UNAUTHORIZED
)
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
)
class LoginView(APIView):
permission_classes = [AllowAny]
def _get_identifier(self, request):
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
return x_forwarded_for.split(',')[0].strip()
return request.META.get('REMOTE_ADDR')
@swagger_auto_schema(
tags=['认证'],
operation_summary='登录(含滑块验证码)',
operation_description='使用账号和密码登录,失败次数过多需通过滑块验证码验证',
request_body=openapi.Schema(
type=openapi.TYPE_OBJECT,
required=['account', 'password'],
properties={
'account': openapi.Schema(type=openapi.TYPE_STRING, description='账号'),
'password': openapi.Schema(type=openapi.TYPE_STRING, description='密码'),
'slider_captcha_key': openapi.Schema(type=openapi.TYPE_STRING, description='滑块验证码key(需要时必填)'),
'slider_captcha_x': openapi.Schema(type=openapi.TYPE_INTEGER, description='滑块X坐标(需要时必填)'),
}
),
responses={200: success_response, 400: error_response, 401: unauthorized_response, 403: error_response},
)
def post(self, request):
account = request.data.get('account', None)
password = request.data.get('password', None)
if not account or not password:
return create_standardized_error_response(
code=ResponseCode.PARAMETER_ERROR,
message="账号和密码不能为空",
status_code=status.HTTP_400_BAD_REQUEST
)
identifier = self._get_identifier(request)
operation = 'login'
captcha_required = check_captcha_required(operation, identifier)
if captcha_required:
slider_captcha_key = request.data.get('slider_captcha_key', None)
slider_captcha_x = request.data.get('slider_captcha_x', None)
if not slider_captcha_key or slider_captcha_x is None:
return create_standardized_error_response(
code=ResponseCode.CAPTCHA_REQUIRED,
status_code=status.HTTP_400_BAD_REQUEST
)
try:
captcha_valid = verify_slider_captcha(slider_captcha_key, int(slider_captcha_x))
if not captcha_valid:
record_failure(operation, identifier)
return create_standardized_error_response(
code=ResponseCode.CAPTCHA_ERROR,
status_code=status.HTTP_400_BAD_REQUEST
)
except SliderCaptchaError:
return create_standardized_error_response(
code=ResponseCode.CAPTCHA_EXPIRED,
status_code=status.HTTP_400_BAD_REQUEST
)
try:
user = authenticate(username=account, password=password)
if user is not None:
if user.is_active:
reset_failures(operation, identifier)
refresh = RefreshToken.for_user(user)
user_serializer = UserSerializer(user)
response_data = {
'user': user_serializer.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')
return create_standardized_response(
data=response_data,
code=ResponseCode.LOGIN_SUCCESS,
status_code=status.HTTP_200_OK
)
else:
record_failure(operation, identifier)
_create_login_record(request, user, 'failed')
return create_standardized_error_response(
code=ResponseCode.PARAMETER_ERROR,
message="账号已被禁用",
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()
if login_user:
_create_login_record(request, login_user, 'failed')
return create_standardized_error_response(
code=ResponseCode.PARAMETER_ERROR,
message="账号或密码错误",
status_code=status.HTTP_401_UNAUTHORIZED
)
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
)
class PublicProfileAPIView(APIView):
permission_classes = [AllowAny]
@swagger_auto_schema(
tags=['用户'],
operation_summary='获取公开用户资料',
operation_description='获取指定用户的公开资料信息,包含文章列表和统计数据',
manual_parameters=[
openapi.Parameter('user_id', openapi.IN_PATH, description='用户ID', type=openapi.TYPE_INTEGER),
],
responses={200: success_response, 404: not_found_response},
)
def get(self, request, user_id):
try:
user = FUser.objects.get(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()
recent_articles = Article.objects.filter(author=user, status='published').order_by('-created_at')[:5]
articles_data = []
for article in recent_articles:
articles_data.append({
'id': article.id,
'title': article.title,
'excerpt': article.excerpt or '',
'cover_image': article.cover_image.url if article.cover_image else '',
'views': article.views,
'likes': article.likes,
'tags': article.tags or [],
'created_at': article.created_at.strftime('%Y-%m-%d'),
})
except Exception:
article_count = 0
articles_data = []
# Get analytics data for the last 7 days
from datetime import date, timedelta
today = date.today()
analytics = {
'reading_trend': [0, 0, 0, 0, 0, 0, 0], # placeholder for 7 days
'comment_count': 0,
'like_count': 0,
'follower_count': 0,
'trends': {
'reading_growth': '+0%',
'like_growth': '+0%',
'comment_growth': '+0%',
}
}
is_owner = request.user.is_authenticated and request.user.id == user_id
data: dict = {
'id': user.id,
'username': user.username,
'avatar': user.avatar.url if user.avatar else '',
'bio': user.bio or '',
'article_count': article_count,
'recent_articles': articles_data,
'follower_count': 0,
'following_count': 0,
'like_count': 0,
}
if is_owner:
data.update({
'points': user.points,
'coins': user.coins,
'gender': user.get_gender_display(),
'location': user.location or '',
'date_joined': user.date_joined.strftime('%Y-%m-%d'),
'analytics': analytics,
})
else:
data.update({
'gender': '',
'location': '',
'date_joined': None,
})
return Response(data)
ALLOWED_AVATAR_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']
ALLOWED_AVATAR_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp']
MAX_AVATAR_SIZE = 2 * 1024 * 1024
class UploadAvatarAPIView(APIView):
permission_classes = [IsAuthenticated]
parser_classes = [MultiPartParser, FormParser]
@swagger_auto_schema(
tags=['用户'],
operation_summary='上传头像',
operation_description='上传用户头像,支持JPG/PNG/GIF/WEBP格式,最大2MB',
manual_parameters=[
openapi.Parameter(
'avatar', openapi.IN_FORM,
description='头像文件',
type=openapi.TYPE_FILE,
required=True,
),
],
responses={200: success_response, 400: error_response, 401: unauthorized_response, 500: error_response},
)
def post(self, request):
file = request.FILES.get('avatar')
if not file:
return create_standardized_error_response(
code=ResponseCode.VALIDATION_ERROR,
message='请选择头像文件',
status_code=status.HTTP_400_BAD_REQUEST
)
if file.content_type not in ALLOWED_AVATAR_TYPES:
return create_standardized_error_response(
code=ResponseCode.VALIDATION_ERROR,
message='头像格式仅支持 JPG、PNG、GIF、WEBP',
status_code=status.HTTP_400_BAD_REQUEST
)
ext = os.path.splitext(file.name)[1].lower()
if ext not in ALLOWED_AVATAR_EXTENSIONS:
return create_standardized_error_response(
code=ResponseCode.VALIDATION_ERROR,
message='头像格式仅支持 JPG、PNG、GIF、WEBP',
status_code=status.HTTP_400_BAD_REQUEST
)
if file.size > MAX_AVATAR_SIZE:
return create_standardized_error_response(
code=ResponseCode.VALIDATION_ERROR,
message='头像大小不能超过 2MB',
status_code=status.HTTP_400_BAD_REQUEST
)
try:
filename = f'avatars/{uuid.uuid4().hex}{ext}'
saved_path = default_storage.save(filename, file)
user = request.user
if user.avatar and user.avatar.name:
try:
default_storage.delete(user.avatar.name)
except Exception:
pass
user.avatar = saved_path
user.save(update_fields=['avatar'])
avatar_url = request.build_absolute_uri(settings.MEDIA_URL + saved_path)
return create_standardized_response(
data={'avatar': avatar_url},
code=ResponseCode.SUCCESS,
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
)
class FollowToggleView(APIView):
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
@swagger_auto_schema(
tags=['用户'],
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 post(self, request, user_id):
try:
target_user = FUser.objects.get(id=user_id, is_active=True)
except FUser.DoesNotExist:
return create_standardized_error_response(
code=ResponseCode.NOT_FOUND,
message='用户不存在',
status_code=status.HTTP_404_NOT_FOUND,
)
if target_user == request.user:
return create_standardized_error_response(
code=ResponseCode.VALIDATION_ERROR,
message='不能关注自己',
status_code=status.HTTP_400_BAD_REQUEST,
)
follow, created = Follow.objects.get_or_create(
follower=request.user,
following=target_user,
)
if not created:
follow.delete()
is_following = False
else:
is_following = True
follower_count = target_user.followers.count()
following_count = request.user.following.count()
return create_standardized_response(
data={
'is_following': is_following,
'follower_count': follower_count,
'following_count': following_count,
},
code=ResponseCode.SUCCESS,
)