feat: ADRF async views (phase1) + native async serializers (phase2) + async cache infra
This commit is contained in:
+65
-48
@@ -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},
|
||||
|
||||
Reference in New Issue
Block a user