feat: ADRF async views (phase1) + native async serializers (phase2) + async cache infra

This commit is contained in:
async-upgrade
2026-09-06 14:26:17 +08:00
parent 9a6577f71e
commit 8f488fcaaa
55 changed files with 2224 additions and 1513 deletions
+9 -8
View File
@@ -1,22 +1,23 @@
from rest_framework import serializers
from adrf.serializers import ModelSerializer, SerializerMethodField
from ..models import Region
class RegionSerializer(serializers.ModelSerializer):
children = serializers.SerializerMethodField()
class RegionSerializer(ModelSerializer):
children = SerializerMethodField()
class Meta:
model = Region
fields = ['id', 'name', 'code', 'level', 'parent_id', 'pinyin', 'children']
def get_children(self, obj):
children = obj.children.all()
if children.exists():
return RegionSerializer(children, many=True).data
async def get_children(self, obj):
children_qs = obj.children.all()
if await children_qs.aexists():
# 递归异步序列化
return [await RegionSerializer(c, context=self.context).adata async for c in children_qs]
return None
class RegionSimpleSerializer(serializers.ModelSerializer):
class RegionSimpleSerializer(ModelSerializer):
class Meta:
model = Region
fields = ['id', 'name', 'code', 'level', 'parent_id', 'pinyin']
+119 -105
View File
@@ -1,14 +1,17 @@
from rest_framework import serializers
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 django.core.cache import caches
from utils.async_cache import aget_cache, aset_cache, adelete_cache
import os
import re
from utils import RandCode
FUser = get_user_model()
default_cache = caches['default']
def get_fields_to_extract(validated_data, fields_to_extract):
@@ -18,7 +21,7 @@ def get_fields_to_extract(validated_data, fields_to_extract):
extracted[field] = validated_data[field]
return extracted
class UserSerializer(serializers.ModelSerializer):
class UserSerializer(ModelSerializer):
PROTECTED_FIELDS = ['is_active', 'password', 'email', 'is_staff', 'is_superuser', 'last_login']
class Meta:
model = FUser
@@ -56,29 +59,30 @@ class UserSerializer(serializers.ModelSerializer):
}
def create_by_email(self, validated_data):
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. 创建用户
user = FUser.objects.create_user(**extracted)
# 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
user.save()
await user.asave()
return user
def update(self, instance, validated_data):
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)
instance.save()
await instance.asave()
return instance
def __init__(self, *args, **kwargs):
@@ -89,42 +93,45 @@ class UserSerializer(serializers.ModelSerializer):
field.required = False
class ChangePasswordSerializer(serializers.Serializer):
old_password = serializers.CharField(required=False, allow_blank=True)
new_password = serializers.CharField(required=True, min_length=8, max_length=128)
confirm_password = serializers.CharField(required=True, min_length=8, max_length=128)
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 serializers.ValidationError('当前密码错误')
raise ValidationError('当前密码错误')
return value
def validate_new_password(self, value):
if len(value) < 8:
raise serializers.ValidationError('密码长度不能少于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 serializers.ValidationError('密码必须包含字母和数字')
raise ValidationError('密码必须包含字母和数字')
return value
def validate(self, attrs):
if attrs['new_password'] != attrs['confirm_password']:
raise serializers.ValidationError({'confirm_password': '两次输入的新密码不一致'})
raise ValidationError({'confirm_password': '两次输入的新密码不一致'})
return attrs
def save(self):
async def asave(self):
from asgiref.sync import sync_to_async
user = self.context['request'].user
user.set_password(self.validated_data['new_password'])
# 密码哈希 CPU 密集,sync_to_async 兜底
await sync_to_async(user.set_password)(self.validated_data['new_password'])
if not user.isSetPassword:
user.isSetPassword = True
user.save()
await user.asave()
return user
class UserUpdateSerializer(serializers.ModelSerializer):
username = serializers.CharField(
class UserUpdateSerializer(ModelSerializer):
username = CharField(
required=False,
max_length=150,
min_length=2,
@@ -133,14 +140,14 @@ class UserUpdateSerializer(serializers.ModelSerializer):
'min_length': '用户名至少2个字符',
}
)
gender = serializers.ChoiceField(
gender = ChoiceField(
required=False,
choices=[(0, '保密'), (1, '男'), (2, '女')],
error_messages={
'invalid_choice': '性别值无效,可选值为:0(保密)、1(男)、2(女)',
}
)
bio = serializers.CharField(
bio = CharField(
required=False,
max_length=500,
allow_blank=True,
@@ -148,7 +155,7 @@ class UserUpdateSerializer(serializers.ModelSerializer):
'max_length': '个人简介不能超过500个字符',
}
)
location = serializers.CharField(
location = CharField(
required=False,
max_length=100,
allow_blank=True,
@@ -156,7 +163,7 @@ class UserUpdateSerializer(serializers.ModelSerializer):
'max_length': '所在地区不能超过100个字符',
}
)
phone_number = serializers.CharField(
phone_number = CharField(
required=False,
max_length=15,
allow_blank=True,
@@ -180,32 +187,33 @@ class UserUpdateSerializer(serializers.ModelSerializer):
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 serializers.ValidationError('该用户名已被使用')
raise ValidationError('该用户名已被使用')
return value
def validate_phone_number(self, value):
if value and not value.isdigit():
raise serializers.ValidationError('手机号只能包含数字')
raise ValidationError('手机号只能包含数字')
return value
def update(self, instance, validated_data):
async def aupdate(self, instance, validated_data):
for attr, value in validated_data.items():
setattr(instance, attr, value)
instance.save()
await instance.asave()
return instance
class PointTransactionSerializer(serializers.Serializer):
id = serializers.IntegerField(read_only=True)
transaction_type = serializers.CharField(read_only=True)
currency_type = serializers.CharField(read_only=True)
amount = serializers.IntegerField(read_only=True)
balance_after = serializers.IntegerField(read_only=True)
description = serializers.CharField(read_only=True)
created_at = serializers.DateTimeField(read_only=True)
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)
def to_representation(self, instance):
async def ato_representation(self, instance):
return {
'id': instance.id,
'transaction_type': instance.transaction_type,
@@ -219,12 +227,12 @@ class PointTransactionSerializer(serializers.Serializer):
}
class SendEmailCodeSerializer(serializers.Serializer):
email = serializers.EmailField(required=True, error_messages={
class SendEmailCodeSerializer(Serializer):
email = EmailField(required=True, error_messages={
'required': '邮箱地址不能为空',
'invalid': '邮箱格式不正确',
})
target = serializers.ChoiceField(
target = ChoiceField(
required=True,
choices=['old', 'new'],
error_messages={
@@ -239,36 +247,38 @@ class SendEmailCodeSerializer(serializers.Serializer):
if target == 'old':
if user.email != value:
raise serializers.ValidationError('请输入当前绑定的邮箱地址')
raise ValidationError('请输入当前绑定的邮箱地址')
elif target == 'new':
if user.email == value:
raise serializers.ValidationError('新邮箱与当前邮箱相同')
raise ValidationError('新邮箱与当前邮箱相同')
# is_valid 由视图侧 sync_to_async 包裹(DB 查询)
if FUser.objects.filter(email=value).exists():
raise serializers.ValidationError('该邮箱已被其他账号使用')
raise ValidationError('该邮箱已被其他账号使用')
return value
def save(self):
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}"
default_cache.set(cache_key, {'code': code, 'email': email}, timeout=600)
await aset_cache(cache_key, {'code': code, 'email': email}, timeout=600)
from ..tasks import send_change_email_task
from utils.safe_task import submit_task
submit_task(send_change_email_task, email, code, target)
await sync_to_async(submit_task)(send_change_email_task, email, code, target)
return email
class BlacklistSerializer(serializers.ModelSerializer):
blocked_user_id = serializers.IntegerField(write_only=True)
blocked_user_username = serializers.CharField(source='blocked_user.username', read_only=True)
blocked_user_email = serializers.CharField(source='blocked_user.email', read_only=True)
blocked_user_avatar = serializers.ImageField(source='blocked_user.avatar', read_only=True)
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
@@ -282,27 +292,28 @@ class BlacklistSerializer(serializers.ModelSerializer):
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 serializers.ValidationError('用户不存在')
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 serializers.ValidationError({'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 serializers.ValidationError({'blocked_user_id': '该用户已在黑名单中'})
raise ValidationError({'blocked_user_id': '该用户已在黑名单中'})
return attrs
def create(self, validated_data):
async def acreate(self, validated_data):
from ..models import Blacklist, FUser
user = self.context['request'].user
blocked_user = FUser.objects.get(id=validated_data['blocked_user_id'])
blacklist = Blacklist.objects.create(
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', '')
@@ -310,12 +321,12 @@ class BlacklistSerializer(serializers.ModelSerializer):
return blacklist
class ChangeEmailSerializer(serializers.Serializer):
new_email = serializers.EmailField(required=True, error_messages={
class ChangeEmailSerializer(Serializer):
new_email = EmailField(required=True, error_messages={
'required': '新邮箱地址不能为空',
'invalid': '邮箱格式不正确',
})
code = serializers.CharField(required=True, max_length=6, min_length=6, error_messages={
code = CharField(required=True, max_length=6, min_length=6, error_messages={
'required': '验证码不能为空',
'min_length': '验证码必须为6位',
'max_length': '验证码必须为6位',
@@ -324,50 +335,52 @@ class ChangeEmailSerializer(serializers.Serializer):
def validate_new_email(self, value):
user = self.context['request'].user
if user.email == value:
raise serializers.ValidationError('新邮箱与当前邮箱相同')
raise ValidationError('新邮箱与当前邮箱相同')
# is_valid 由视图侧 sync_to_async 包裹(DB 查询)
if FUser.objects.filter(email=value).exists():
raise serializers.ValidationError('该邮箱已被其他账号使用')
raise ValidationError('该邮箱已被其他账号使用')
return value
def validate(self, attrs):
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 = default_cache.get(cache_key)
cached = await aget_cache(cache_key)
if cached is None:
raise serializers.ValidationError({'code': '验证码已过期,请重新获取'})
raise ValidationError({'code': '验证码已过期,请重新获取'})
if cached['code'] != code:
raise serializers.ValidationError({'code': '验证码错误'})
raise ValidationError({'code': '验证码错误'})
if cached['email'] != new_email:
raise serializers.ValidationError({'new_email': '邮箱与发送验证码时的邮箱不一致'})
raise ValidationError({'new_email': '邮箱与发送验证码时的邮箱不一致'})
return attrs
def save(self):
async def asave(self):
user = self.context['request'].user
user.email = self.validated_data['new_email']
user.save()
await user.asave()
cache_key_old = f"email_change_old_{user.id}"
cache_key_new = f"email_change_new_{user.id}"
default_cache.delete(cache_key_old)
default_cache.delete(cache_key_new)
await adelete_cache(cache_key_old)
await adelete_cache(cache_key_new)
return user
class LoginRecordSerializer(serializers.Serializer):
id = serializers.IntegerField(read_only=True)
device = serializers.CharField(read_only=True)
ip_address = serializers.IPAddressField(read_only=True)
location = serializers.CharField(read_only=True)
login_time = serializers.DateTimeField(read_only=True)
status = serializers.CharField(read_only=True)
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)
def to_representation(self, instance):
async def ato_representation(self, instance):
return {
'id': instance.id,
'device': instance.device,
@@ -379,12 +392,12 @@ class LoginRecordSerializer(serializers.Serializer):
}
class SendPhoneCodeSerializer(serializers.Serializer):
phone = serializers.CharField(required=True, max_length=15, error_messages={
class SendPhoneCodeSerializer(Serializer):
phone = CharField(required=True, max_length=15, error_messages={
'required': '手机号不能为空',
'max_length': '手机号不能超过15个字符',
})
target = serializers.ChoiceField(
target = ChoiceField(
required=True,
choices=['old', 'new'],
error_messages={
@@ -395,29 +408,29 @@ class SendPhoneCodeSerializer(serializers.Serializer):
def validate_phone(self, value):
if not value.isdigit():
raise serializers.ValidationError('手机号只能包含数字')
raise ValidationError('手机号只能包含数字')
if len(value) != 11:
raise serializers.ValidationError('手机号必须为11位')
raise ValidationError('手机号必须为11位')
return value
def save(self):
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}"
default_cache.set(cache_key, {'code': code, 'phone': phone}, timeout=300)
await aset_cache(cache_key, {'code': code, 'phone': phone}, timeout=300)
return phone
class ChangePhoneSerializer(serializers.Serializer):
new_phone = serializers.CharField(required=True, max_length=15, error_messages={
class ChangePhoneSerializer(Serializer):
new_phone = CharField(required=True, max_length=15, error_messages={
'required': '新手机号不能为空',
'max_length': '手机号不能超过15个字符',
})
code = serializers.CharField(required=True, max_length=6, min_length=6, error_messages={
code = CharField(required=True, max_length=6, min_length=6, error_messages={
'required': '验证码不能为空',
'min_length': '验证码必须为6位',
'max_length': '验证码必须为6位',
@@ -425,39 +438,40 @@ class ChangePhoneSerializer(serializers.Serializer):
def validate_new_phone(self, value):
if not value.isdigit():
raise serializers.ValidationError('手机号只能包含数字')
raise ValidationError('手机号只能包含数字')
if len(value) != 11:
raise serializers.ValidationError('手机号必须为11位')
raise ValidationError('手机号必须为11位')
user = self.context['request'].user
if user.phone_number == value:
raise serializers.ValidationError('新手机号与当前手机号相同')
raise ValidationError('新手机号与当前手机号相同')
return value
def validate(self, attrs):
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 = default_cache.get(cache_key)
cached = await aget_cache(cache_key)
if cached is None:
raise serializers.ValidationError({'code': '验证码已过期,请重新获取'})
raise ValidationError({'code': '验证码已过期,请重新获取'})
if cached['code'] != code:
raise serializers.ValidationError({'code': '验证码错误'})
raise ValidationError({'code': '验证码错误'})
if cached['phone'] != new_phone:
raise serializers.ValidationError({'new_phone': '手机号与发送验证码时的手机号不一致'})
raise ValidationError({'new_phone': '手机号与发送验证码时的手机号不一致'})
return attrs
def save(self):
async def asave(self):
user = self.context['request'].user
user.phone_number = self.validated_data['new_phone']
user.save()
await user.asave()
cache_key_old = f"phone_change_old_{user.id}"
cache_key_new = f"phone_change_new_{user.id}"
default_cache.delete(cache_key_old)
default_cache.delete(cache_key_new)
await adelete_cache(cache_key_old)
await adelete_cache(cache_key_new)
return user