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
+65 -48
View File
@@ -1,19 +1,44 @@
import asyncio
import json
import hashlib
from datetime import datetime
import requests
import aiohttp
from asgiref.sync import sync_to_async
from rest_framework.permissions import AllowAny
from rest_framework.views import APIView
from adrf.views import APIView
from rest_framework.response import Response
from rest_framework import status
from drf_yasg.utils import swagger_auto_schema
from drf_yasg import openapi
from chunyu_project.common_schemas import success_response, error_response
from .cache import get_cached_data, set_cached_data
from django.core.cache import caches
_cache = caches['default']
CACHE_TIMEOUT = 900
LATEST_RATES_URL = "https://open.er-api.com/v6/latest/{base}"
def _make_cache_key(prefix, *args, **kwargs):
raw = json.dumps({'args': args, 'kwargs': kwargs}, sort_keys=True, default=str)
suffix = hashlib.md5(raw.encode('utf-8')).hexdigest()
return f'currency_{prefix}_{suffix}'
async def get_cached_data(prefix, *args, **kwargs):
cache_key = _make_cache_key(prefix, *args, **kwargs)
# django-redis 为同步客户端,sync_to_async 兜底
return await sync_to_async(_cache.get)(cache_key)
async def set_cached_data(prefix, data, *args, timeout=CACHE_TIMEOUT, **kwargs):
cache_key = _make_cache_key(prefix, *args, **kwargs)
await sync_to_async(_cache.set)(cache_key, data, timeout=timeout)
class CurrencyRatesView(APIView):
"""
汇率查询视图 - 获取指定基准货币对所有支持货币的汇率
@@ -35,11 +60,11 @@ class CurrencyRatesView(APIView):
],
responses={200: success_response, 400: error_response, 502: error_response}
)
def get(self, request):
async def get(self, request):
base = request.GET.get('base', 'USD').strip().upper()
if not base:
base = 'USD'
return self._fetch_rates(base)
return await self._fetch_rates(base)
@swagger_auto_schema(
tags=['Currency'],
@@ -53,16 +78,16 @@ class CurrencyRatesView(APIView):
),
responses={200: success_response, 400: error_response, 502: error_response}
)
def post(self, request):
async def post(self, request):
base = request.data.get('base', 'USD').strip().upper() if isinstance(request.data, dict) else 'USD'
if not base:
base = 'USD'
return self._fetch_rates(base)
return await self._fetch_rates(base)
def _fetch_rates(self, base):
async def _fetch_rates(self, base):
base = base.upper()
cached = get_cached_data('rates', base)
cached = await get_cached_data('rates', base)
if cached is not None:
return Response(
{"code": 200, "message": "success (cached)", "data": cached},
@@ -71,15 +96,16 @@ class CurrencyRatesView(APIView):
try:
url = LATEST_RATES_URL.format(base=base)
response = requests.get(url, timeout=10)
timeout = aiohttp.ClientTimeout(total=10)
async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.get(url) as response:
if response.status != 200:
return Response(
{"code": 502, "message": "汇率服务异常,请稍后重试", "data": None},
status=status.HTTP_502_BAD_GATEWAY,
)
data = await response.json()
if response.status_code != 200:
return Response(
{"code": 502, "message": "汇率服务异常,请稍后重试", "data": None},
status=status.HTTP_502_BAD_GATEWAY,
)
data = response.json()
if data.get('result') != 'success':
return Response(
{"code": 502, "message": f"汇率服务返回错误:{data.get('error-type', '未知错误')}", "data": None},
@@ -93,23 +119,18 @@ class CurrencyRatesView(APIView):
'next_update': data.get('time_next_update_utc', ''),
}
set_cached_data('rates', rates_data, base)
await set_cached_data('rates', rates_data, base)
return Response(
{"code": 200, "message": "success", "data": rates_data},
status=status.HTTP_200_OK,
)
except requests.exceptions.Timeout:
except (aiohttp.ClientError, asyncio.TimeoutError):
return Response(
{"code": 502, "message": "请求超时,请稍后重试", "data": None},
status=status.HTTP_502_BAD_GATEWAY,
)
except requests.exceptions.ConnectionError:
return Response(
{"code": 502, "message": "网络连接异常,请检查网络后重试", "data": None},
status=status.HTTP_502_BAD_GATEWAY,
)
except Exception as e:
return Response(
{"code": 500, "message": f"服务器内部错误:{str(e)}", "data": None},
@@ -147,8 +168,8 @@ class CurrenciesListView(APIView):
operation_description='返回系统常用的货币代码及名称(带缓存)',
responses={200: success_response, 500: error_response}
)
def get(self, request):
cached = get_cached_data('currencies')
async def get(self, request):
cached = await get_cached_data('currencies')
if cached is not None:
return Response(
{"code": 200, "message": "success (cached)", "data": cached},
@@ -162,7 +183,7 @@ class CurrenciesListView(APIView):
'updated_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
}
set_cached_data('currencies', data)
await set_cached_data('currencies', data)
return Response(
{"code": 200, "message": "success", "data": data},
@@ -210,11 +231,11 @@ class CurrencyConvertView(APIView):
],
responses={200: success_response, 400: error_response, 502: error_response}
)
def get(self, request):
async def get(self, request):
from_code = request.GET.get('from', '').strip().upper()
to_code = request.GET.get('to', '').strip().upper()
amount = request.GET.get('amount', '').strip()
return self._convert(from_code, to_code, amount)
return await self._convert(from_code, to_code, amount)
@swagger_auto_schema(
tags=['Currency'],
@@ -231,14 +252,14 @@ class CurrencyConvertView(APIView):
),
responses={200: success_response, 400: error_response, 502: error_response}
)
def post(self, request):
async def post(self, request):
from_code = request.data.get('from', '').strip().upper() if isinstance(request.data, dict) else ''
to_code = request.data.get('to', '').strip().upper() if isinstance(request.data, dict) else ''
amount = request.data.get('amount', '') if isinstance(request.data, dict) else ''
amount = str(amount).strip() if amount else ''
return self._convert(from_code, to_code, amount)
return await self._convert(from_code, to_code, amount)
def _convert(self, from_code, to_code, amount):
async def _convert(self, from_code, to_code, amount):
if not from_code:
return Response(
{"code": 400, "message": "参数错误:from 不能为空", "data": None},
@@ -268,7 +289,7 @@ class CurrencyConvertView(APIView):
status=status.HTTP_400_BAD_REQUEST,
)
cached_rates = get_cached_data('rates', from_code)
cached_rates = await get_cached_data('rates', from_code)
rates = None
if cached_rates is not None:
@@ -277,15 +298,16 @@ class CurrencyConvertView(APIView):
if rates is None:
try:
url = LATEST_RATES_URL.format(base=from_code)
response = requests.get(url, timeout=10)
timeout = aiohttp.ClientTimeout(total=10)
async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.get(url) as response:
if response.status != 200:
return Response(
{"code": 502, "message": "汇率服务异常,请稍后重试", "data": None},
status=status.HTTP_502_BAD_GATEWAY,
)
data = await response.json()
if response.status_code != 200:
return Response(
{"code": 502, "message": "汇率服务异常,请稍后重试", "data": None},
status=status.HTTP_502_BAD_GATEWAY,
)
data = response.json()
if data.get('result') != 'success':
return Response(
{"code": 502, "message": f"汇率服务返回错误:{data.get('error-type', '未知错误')}", "data": None},
@@ -298,19 +320,14 @@ class CurrencyConvertView(APIView):
'last_updated': data.get('time_last_update_utc', datetime.now().strftime('%Y-%m-%d %H:%M:%S')),
'next_update': data.get('time_next_update_utc', ''),
}
set_cached_data('rates', rates_data, from_code)
await set_cached_data('rates', rates_data, from_code)
rates = rates_data['rates']
except requests.exceptions.Timeout:
except (aiohttp.ClientError, asyncio.TimeoutError):
return Response(
{"code": 502, "message": "请求超时,请稍后重试", "data": None},
status=status.HTTP_502_BAD_GATEWAY,
)
except requests.exceptions.ConnectionError:
return Response(
{"code": 502, "message": "网络连接异常,请检查网络后重试", "data": None},
status=status.HTTP_502_BAD_GATEWAY,
)
except Exception as e:
return Response(
{"code": 500, "message": f"服务器内部错误:{str(e)}", "data": None},