from adrf.serializers import ( ModelSerializer, Serializer, CharField, IntegerField, EmailField, ChoiceField, IPAddressField, DateTimeField, ImageField, ) from rest_framework.exceptions import ValidationError from django.contrib.auth import get_user_model from django.utils import timezone from utils.async_cache import aget_cache, aset_cache, adelete_cache import os import re from utils import RandCode FUser = get_user_model() def get_fields_to_extract(validated_data, fields_to_extract): extracted = {} for field in fields_to_extract: if field in validated_data: extracted[field] = validated_data[field] return extracted class UserSerializer(ModelSerializer): PROTECTED_FIELDS = ['is_active', 'password', 'email', 'is_staff', 'is_superuser', 'last_login'] class Meta: model = FUser fields = [ 'id', 'username', 'email', 'is_active', 'is_staff', 'is_superuser', 'password', 'last_login', 'date_joined', 'phone_number', 'avatar', 'bio', 'isSetPassword', 'gender', 'location', 'points', 'coins', ] extra_kwargs = { 'password': {'write_only': True}, 'is_active': {'read_only': True}, 'is_staff': {'read_only': True}, 'is_superuser': {'read_only': True}, 'last_login': {'read_only': True}, 'date_joined': {'read_only': True}, 'id': {'read_only': True}, 'isSetPassword': {'read_only': True}, 'points': {'read_only': True}, 'coins': {'read_only': True}, } async def acreate_by_email(self, validated_data): fields_to_extract = ['email'] extracted = get_fields_to_extract(validated_data, fields_to_extract) extracted['username'] = extracted['email'] extracted['password'] = RandCode.get_alphanumeric_characters_code_8() # 2. 创建用户(create_user 内含密码哈希,为同步调用,sync_to_async 兜底) from asgiref.sync import sync_to_async user = await sync_to_async(FUser.objects.create_user)(**extracted) user.is_active = True user.is_staff = False user.is_superuser = False await user.asave() return user async def aupdate(self, instance, validated_data): for attr, value in validated_data.items(): if attr in self.PROTECTED_FIELDS : continue setattr(instance, attr, value) await instance.asave() return instance def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # 将所有字段设为非必填 for field_name, field in self.fields.items(): field.required = False class ChangePasswordSerializer(Serializer): old_password = CharField(required=False, allow_blank=True) new_password = CharField(required=True, min_length=8, max_length=128) confirm_password = CharField(required=True, min_length=8, max_length=128) def validate_old_password(self, value): user = self.context['request'].user # check_password 为 CPU 密集(argon2),同步调用(is_valid 由视图 sync_to_async 包裹) if user.has_usable_password() and not user.check_password(value): raise ValidationError('当前密码错误') return value def validate_new_password(self, value): if len(value) < 8: raise ValidationError('密码长度不能少于8位') has_letter = any(c.isalpha() for c in value) has_digit = any(c.isdigit() for c in value) if not (has_letter and has_digit): raise ValidationError('密码必须包含字母和数字') return value def validate(self, attrs): if attrs['new_password'] != attrs['confirm_password']: raise ValidationError({'confirm_password': '两次输入的新密码不一致'}) return attrs async def asave(self): from asgiref.sync import sync_to_async user = self.context['request'].user # 密码哈希 CPU 密集,sync_to_async 兜底 await sync_to_async(user.set_password)(self.validated_data['new_password']) if not user.isSetPassword: user.isSetPassword = True await user.asave() return user class UserUpdateSerializer(ModelSerializer): username = CharField( required=False, max_length=150, min_length=2, error_messages={ 'max_length': '用户名不能超过150个字符', 'min_length': '用户名至少2个字符', } ) gender = ChoiceField( required=False, choices=[(0, '保密'), (1, '男'), (2, '女')], error_messages={ 'invalid_choice': '性别值无效,可选值为:0(保密)、1(男)、2(女)', } ) bio = CharField( required=False, max_length=500, allow_blank=True, error_messages={ 'max_length': '个人简介不能超过500个字符', } ) location = CharField( required=False, max_length=100, allow_blank=True, error_messages={ 'max_length': '所在地区不能超过100个字符', } ) phone_number = CharField( required=False, max_length=15, allow_blank=True, error_messages={ 'max_length': '手机号不能超过15个字符', } ) class Meta: model = FUser fields = [ 'username', 'gender', 'bio', 'location', 'phone_number', ] extra_kwargs = { 'username': {'validators': []}, } def validate_username(self, value): user = self.context['request'].user # is_valid 由视图侧 sync_to_async 包裹(DB 查询) if FUser.objects.filter(username=value).exclude(id=user.id).exists(): raise ValidationError('该用户名已被使用') return value def validate_phone_number(self, value): if value and not value.isdigit(): raise ValidationError('手机号只能包含数字') return value async def aupdate(self, instance, validated_data): for attr, value in validated_data.items(): setattr(instance, attr, value) await instance.asave() return instance class PointTransactionSerializer(Serializer): id = IntegerField(read_only=True) transaction_type = CharField(read_only=True) currency_type = CharField(read_only=True) amount = IntegerField(read_only=True) balance_after = IntegerField(read_only=True) description = CharField(read_only=True) created_at = DateTimeField(read_only=True) async def ato_representation(self, instance): return { 'id': instance.id, 'transaction_type': instance.transaction_type, 'transaction_type_display': instance.get_transaction_type_display(), 'currency_type': instance.currency_type, 'currency_type_display': instance.get_currency_type_display(), 'amount': instance.amount, 'balance_after': instance.balance_after, 'description': instance.description, 'created_at': instance.created_at.strftime('%Y-%m-%d %H:%M:%S'), } class SendEmailCodeSerializer(Serializer): email = EmailField(required=True, error_messages={ 'required': '邮箱地址不能为空', 'invalid': '邮箱格式不正确', }) target = ChoiceField( required=True, choices=['old', 'new'], error_messages={ 'required': '验证目标不能为空', 'invalid_choice': '验证目标无效', } ) def validate_email(self, value): user = self.context['request'].user target = self.initial_data.get('target', '') if target == 'old': if user.email != value: raise ValidationError('请输入当前绑定的邮箱地址') elif target == 'new': if user.email == value: raise ValidationError('新邮箱与当前邮箱相同') # is_valid 由视图侧 sync_to_async 包裹(DB 查询) if FUser.objects.filter(email=value).exists(): raise ValidationError('该邮箱已被其他账号使用') return value async def asave(self): from utils.safe_task import submit_task from asgiref.sync import sync_to_async email = self.validated_data['email'] target = self.validated_data['target'] user = self.context['request'].user code = RandCode.get_digit_characters_code_6() cache_key = f"email_change_{target}_{user.id}" await aset_cache(cache_key, {'code': code, 'email': email}, timeout=600) from ..tasks import send_change_email_task await sync_to_async(submit_task)(send_change_email_task, email, code, target) return email class BlacklistSerializer(ModelSerializer): blocked_user_id = IntegerField(write_only=True) blocked_user_username = CharField(source='blocked_user.username', read_only=True) blocked_user_email = CharField(source='blocked_user.email', read_only=True) blocked_user_avatar = ImageField(source='blocked_user.avatar', read_only=True) class Meta: model = None fields = ['id', 'blocked_user_id', 'blocked_user_username', 'blocked_user_email', 'blocked_user_avatar', 'reason', 'created_at'] read_only_fields = ['id', 'created_at'] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) from ..models import Blacklist self.Meta.model = Blacklist def validate_blocked_user_id(self, value): from ..models import FUser # is_valid 由视图侧 sync_to_async 包裹(DB 查询) try: FUser.objects.get(id=value) except FUser.DoesNotExist: raise ValidationError('用户不存在') return value def validate(self, attrs): user = self.context['request'].user blocked_user_id = attrs.get('blocked_user_id') if user.id == blocked_user_id: raise ValidationError({'blocked_user_id': '不能将自己加入黑名单'}) from ..models import Blacklist if Blacklist.objects.filter(user=user, blocked_user_id=blocked_user_id).exists(): raise ValidationError({'blocked_user_id': '该用户已在黑名单中'}) return attrs async def acreate(self, validated_data): from ..models import Blacklist, FUser user = self.context['request'].user blocked_user = await FUser.objects.aget(id=validated_data['blocked_user_id']) blacklist = await Blacklist.objects.acreate( user=user, blocked_user=blocked_user, reason=validated_data.get('reason', '') ) return blacklist class ChangeEmailSerializer(Serializer): new_email = EmailField(required=True, error_messages={ 'required': '新邮箱地址不能为空', 'invalid': '邮箱格式不正确', }) code = CharField(required=True, max_length=6, min_length=6, error_messages={ 'required': '验证码不能为空', 'min_length': '验证码必须为6位', 'max_length': '验证码必须为6位', }) def validate_new_email(self, value): user = self.context['request'].user if user.email == value: raise ValidationError('新邮箱与当前邮箱相同') # is_valid 由视图侧 sync_to_async 包裹(DB 查询) if FUser.objects.filter(email=value).exists(): raise ValidationError('该邮箱已被其他账号使用') return value async def avalidate(self, attrs): """异步版 validate(验证码在 Redis),由视图在 is_valid 后调用 await serializer.avalidate(attrs) 复核""" user = self.context['request'].user code = attrs['code'] new_email = attrs['new_email'] cache_key = f"email_change_new_{user.id}" cached = await aget_cache(cache_key) if cached is None: raise ValidationError({'code': '验证码已过期,请重新获取'}) if cached['code'] != code: raise ValidationError({'code': '验证码错误'}) if cached['email'] != new_email: raise ValidationError({'new_email': '邮箱与发送验证码时的邮箱不一致'}) return attrs async def asave(self): user = self.context['request'].user user.email = self.validated_data['new_email'] await user.asave() cache_key_old = f"email_change_old_{user.id}" cache_key_new = f"email_change_new_{user.id}" await adelete_cache(cache_key_old) await adelete_cache(cache_key_new) return user class LoginRecordSerializer(Serializer): id = IntegerField(read_only=True) device = CharField(read_only=True) ip_address = IPAddressField(read_only=True) location = CharField(read_only=True) login_time = DateTimeField(read_only=True) status = CharField(read_only=True) async def ato_representation(self, instance): return { 'id': instance.id, 'device': instance.device, 'ip_address': instance.ip_address or '', 'location': instance.location, 'login_time': instance.login_time.strftime('%Y-%m-%d %H:%M:%S'), 'status': instance.status, 'status_display': instance.get_status_display(), } class SendPhoneCodeSerializer(Serializer): phone = CharField(required=True, max_length=15, error_messages={ 'required': '手机号不能为空', 'max_length': '手机号不能超过15个字符', }) target = ChoiceField( required=True, choices=['old', 'new'], error_messages={ 'required': '验证目标不能为空', 'invalid_choice': '验证目标无效', } ) def validate_phone(self, value): if not value.isdigit(): raise ValidationError('手机号只能包含数字') if len(value) != 11: raise ValidationError('手机号必须为11位') return value async def asave(self): phone = self.validated_data['phone'] target = self.validated_data['target'] user = self.context['request'].user code = RandCode.get_digit_characters_code_6() cache_key = f"phone_change_{target}_{user.id}" await aset_cache(cache_key, {'code': code, 'phone': phone}, timeout=300) return phone class ChangePhoneSerializer(Serializer): new_phone = CharField(required=True, max_length=15, error_messages={ 'required': '新手机号不能为空', 'max_length': '手机号不能超过15个字符', }) code = CharField(required=True, max_length=6, min_length=6, error_messages={ 'required': '验证码不能为空', 'min_length': '验证码必须为6位', 'max_length': '验证码必须为6位', }) def validate_new_phone(self, value): if not value.isdigit(): raise ValidationError('手机号只能包含数字') if len(value) != 11: raise ValidationError('手机号必须为11位') user = self.context['request'].user if user.phone_number == value: raise ValidationError('新手机号与当前手机号相同') return value async def avalidate(self, attrs): """异步版 validate(验证码在 Redis),由视图在 is_valid 后调用""" user = self.context['request'].user code = attrs['code'] new_phone = attrs['new_phone'] cache_key = f"phone_change_new_{user.id}" cached = await aget_cache(cache_key) if cached is None: raise ValidationError({'code': '验证码已过期,请重新获取'}) if cached['code'] != code: raise ValidationError({'code': '验证码错误'}) if cached['phone'] != new_phone: raise ValidationError({'new_phone': '手机号与发送验证码时的手机号不一致'}) return attrs async def asave(self): user = self.context['request'].user user.phone_number = self.validated_data['new_phone'] await user.asave() cache_key_old = f"phone_change_old_{user.id}" cache_key_new = f"phone_change_new_{user.id}" await adelete_cache(cache_key_old) await adelete_cache(cache_key_new) return user