feat: ADRF async views (phase1) + native async serializers (phase2) + async cache infra
This commit is contained in:
+32
-30
@@ -1,9 +1,10 @@
|
||||
import base64
|
||||
|
||||
from asgiref.sync import sync_to_async
|
||||
from django.utils import timezone
|
||||
from rest_framework.parsers import MultiPartParser, FormParser
|
||||
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
|
||||
@@ -61,7 +62,7 @@ class BaiduFanyiView(APIView):
|
||||
),
|
||||
responses={202: success_response, 400: error_response}
|
||||
)
|
||||
def post(self, request):
|
||||
async def post(self, request):
|
||||
from_lang = request.data.get('from_lang')
|
||||
to_lang = request.data.get('to_lang')
|
||||
|
||||
@@ -95,15 +96,16 @@ class BaiduFanyiView(APIView):
|
||||
)
|
||||
|
||||
# 优先同步调用百度 API,成功后直接返回翻译结果,不依赖 Celery worker
|
||||
# (百度 API 调用为阻塞网络 I/O,sync_to_async 兜底)
|
||||
try:
|
||||
sync_result = baidu_translate_task(query, from_lang, to_lang)
|
||||
sync_result = await sync_to_async(baidu_translate_task)(query, from_lang, to_lang)
|
||||
except Exception as e:
|
||||
sync_result = {'success': False, 'error_code': 'INNER_ERROR', 'error_msg': str(e)}
|
||||
|
||||
if sync_result.get('success'):
|
||||
baidu_data = sync_result.get('data') or {}
|
||||
user_id = request.user.id if request.user.is_authenticated else None
|
||||
record_translate_usage.delay('text', from_lang, to_lang, len(query), user_id=user_id, ip_address=get_client_ip(request))
|
||||
await sync_to_async(record_translate_usage.delay)('text', from_lang, to_lang, len(query), user_id=user_id, ip_address=get_client_ip(request))
|
||||
return Response({
|
||||
"code": 0,
|
||||
"data": {
|
||||
@@ -115,10 +117,10 @@ class BaiduFanyiView(APIView):
|
||||
|
||||
# 同步失败降级为异步(若 Celery worker 可用)
|
||||
try:
|
||||
task = submit_task(baidu_translate_task, query, from_lang, to_lang)
|
||||
task = await sync_to_async(submit_task)(baidu_translate_task, query, from_lang, to_lang)
|
||||
task_id = task.id if task is not None else None
|
||||
user_id = request.user.id if request.user.is_authenticated else None
|
||||
record_translate_usage.delay('text', from_lang, to_lang, len(query), user_id=user_id, ip_address=get_client_ip(request))
|
||||
await sync_to_async(record_translate_usage.delay)('text', from_lang, to_lang, len(query), user_id=user_id, ip_address=get_client_ip(request))
|
||||
return Response(
|
||||
{
|
||||
"task_id": task_id,
|
||||
@@ -143,7 +145,7 @@ class AutoLangTypeViews(APIView):
|
||||
operation_description='返回支持自动识别的语言类型列表',
|
||||
responses={200: success_response}
|
||||
)
|
||||
def get(self, request):
|
||||
async def get(self, request):
|
||||
return Response(auto_lang, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
@@ -156,7 +158,7 @@ class AllLangTypeViews(APIView):
|
||||
operation_description='返回所有支持的翻译语言类型列表',
|
||||
responses={200: success_response}
|
||||
)
|
||||
def get(self, request):
|
||||
async def get(self, request):
|
||||
return Response(text_languages_flat, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
@@ -169,7 +171,7 @@ class PictureLangTypeViews(APIView):
|
||||
operation_description='返回图片翻译支持的源语言和目标语言类型列表',
|
||||
responses={200: success_response}
|
||||
)
|
||||
def get(self, request):
|
||||
async def get(self, request):
|
||||
return Response({
|
||||
"source_langs": p_lang_type,
|
||||
"target_langs": p_target_lang_type
|
||||
@@ -188,7 +190,7 @@ class SpeechLangTypeViews(APIView):
|
||||
],
|
||||
responses={200: success_response, 400: error_response}
|
||||
)
|
||||
def get(self, request):
|
||||
async def get(self, request):
|
||||
speech_type = request.GET.get("speech_type", "mp3")
|
||||
|
||||
if speech_type not in y_speech_type:
|
||||
@@ -222,7 +224,7 @@ class RecognizeLangTypeViews(APIView):
|
||||
),
|
||||
responses={202: success_response, 400: error_response}
|
||||
)
|
||||
def post(self, request):
|
||||
async def post(self, request):
|
||||
query = request.data.get("q")
|
||||
|
||||
if not query:
|
||||
@@ -231,9 +233,9 @@ class RecognizeLangTypeViews(APIView):
|
||||
if len(query) > 3000:
|
||||
return Response({"message": "请求数据长度过长已经超过3000."}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 优先同步调用百度 API,避免依赖 Celery worker
|
||||
# 优先同步调用百度 API,避免依赖 Celery worker(sync_to_async 兜底阻塞 I/O)
|
||||
try:
|
||||
sync_result = baidu_recognize_lang_task(query)
|
||||
sync_result = await sync_to_async(baidu_recognize_lang_task)(query)
|
||||
except Exception as e:
|
||||
sync_result = {'success': False, 'error_code': 'INNER_ERROR', 'error_msg': str(e)}
|
||||
|
||||
@@ -251,7 +253,7 @@ class RecognizeLangTypeViews(APIView):
|
||||
src_lang = baidu_data.get('src')
|
||||
trans_result = [{'src': query, 'dst': src_lang}] if src_lang else []
|
||||
user_id = request.user.id if request.user.is_authenticated else None
|
||||
record_translate_usage.delay('detect', 'auto', '', len(query), user_id=user_id, ip_address=get_client_ip(request))
|
||||
await sync_to_async(record_translate_usage.delay)('detect', 'auto', '', len(query), user_id=user_id, ip_address=get_client_ip(request))
|
||||
return Response({
|
||||
"code": 0,
|
||||
"data": {
|
||||
@@ -262,10 +264,10 @@ class RecognizeLangTypeViews(APIView):
|
||||
}, status=status.HTTP_200_OK)
|
||||
|
||||
try:
|
||||
task = submit_task(baidu_recognize_lang_task, query)
|
||||
task = await sync_to_async(submit_task)(baidu_recognize_lang_task, query)
|
||||
task_id = task.id if task is not None else None
|
||||
user_id = request.user.id if request.user.is_authenticated else None
|
||||
record_translate_usage.delay('detect', 'auto', '', len(query), user_id=user_id, ip_address=get_client_ip(request))
|
||||
await sync_to_async(record_translate_usage.delay)('detect', 'auto', '', len(query), user_id=user_id, ip_address=get_client_ip(request))
|
||||
return Response(
|
||||
{
|
||||
"task_id": task_id,
|
||||
@@ -297,7 +299,7 @@ class PictureRecognizeViews(APIView):
|
||||
],
|
||||
responses={202: success_response, 400: error_response}
|
||||
)
|
||||
def post(self, request):
|
||||
async def post(self, request):
|
||||
file_data = request.FILES['file'].read()
|
||||
file_data_base64 = base64.b64encode(file_data).decode('utf-8')
|
||||
|
||||
@@ -311,7 +313,7 @@ class PictureRecognizeViews(APIView):
|
||||
return Response({"message": "翻译语种异常."}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
try:
|
||||
task = submit_task(baidu_picture_translate_task, file_data_base64, from_lang, to_lang, picture_type)
|
||||
task = await sync_to_async(submit_task)(baidu_picture_translate_task, file_data_base64, from_lang, to_lang, picture_type)
|
||||
except Exception as e:
|
||||
return Response(
|
||||
{"code": 1, "message": "图片翻译任务提交失败,请稍后重试"},
|
||||
@@ -320,7 +322,7 @@ class PictureRecognizeViews(APIView):
|
||||
task_id = task.id if task is not None else None
|
||||
|
||||
user_id = request.user.id if request.user.is_authenticated else None
|
||||
record_translate_usage.delay('image', from_lang, to_lang, len(file_data), user_id=user_id, ip_address=get_client_ip(request))
|
||||
await sync_to_async(record_translate_usage.delay)('image', from_lang, to_lang, len(file_data), user_id=user_id, ip_address=get_client_ip(request))
|
||||
|
||||
return Response(
|
||||
{
|
||||
@@ -348,7 +350,7 @@ class SpeechRecognitionView(APIView):
|
||||
],
|
||||
responses={202: success_response, 400: error_response}
|
||||
)
|
||||
def post(self, request):
|
||||
async def post(self, request):
|
||||
speech_type = request.GET.get("speech_type")
|
||||
|
||||
if speech_type not in y_speech_type:
|
||||
@@ -367,7 +369,7 @@ class SpeechRecognitionView(APIView):
|
||||
audio_data_base64 = base64.b64encode(voice).decode('utf-8')
|
||||
|
||||
try:
|
||||
task = submit_task(baidu_speech_recognize_task, audio_data_base64, from_lang, to_lang, speech_type)
|
||||
task = await sync_to_async(submit_task)(baidu_speech_recognize_task, audio_data_base64, from_lang, to_lang, speech_type)
|
||||
except Exception as e:
|
||||
return Response(
|
||||
{"code": 1, "message": "语音识别任务提交失败,请稍后重试"},
|
||||
@@ -376,7 +378,7 @@ class SpeechRecognitionView(APIView):
|
||||
task_id = task.id if task is not None else None
|
||||
|
||||
user_id = request.user.id if request.user.is_authenticated else None
|
||||
record_translate_usage.delay('audio', from_lang, to_lang, len(voice), user_id=user_id, ip_address=get_client_ip(request))
|
||||
await sync_to_async(record_translate_usage.delay)('audio', from_lang, to_lang, len(voice), user_id=user_id, ip_address=get_client_ip(request))
|
||||
|
||||
return Response(
|
||||
{
|
||||
@@ -397,16 +399,16 @@ class TaskResultView(APIView):
|
||||
operation_description='通过 task_id 查询异步翻译任务的执行结果',
|
||||
responses={200: '成功返回翻译结果'}
|
||||
)
|
||||
def get(self, request, task_id):
|
||||
async def get(self, request, task_id):
|
||||
try:
|
||||
task_result = AsyncResult(task_id)
|
||||
task_result = await sync_to_async(AsyncResult)(task_id)
|
||||
except Exception:
|
||||
return Response(
|
||||
{"code": 1, "message": "任务查询失败"},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
if not task_result.ready():
|
||||
if not await sync_to_async(task_result.ready)():
|
||||
return Response({
|
||||
"code": 0,
|
||||
"data": {
|
||||
@@ -416,7 +418,7 @@ class TaskResultView(APIView):
|
||||
}
|
||||
})
|
||||
|
||||
if task_result.failed():
|
||||
if await sync_to_async(task_result.failed)():
|
||||
return Response({
|
||||
"code": 1,
|
||||
"message": str(task_result.result) if task_result.result else "任务执行失败"
|
||||
@@ -451,7 +453,7 @@ class TranslateUsageView(APIView):
|
||||
operation_description='返回当前用户/IP的今日使用量、总量以及各类型使用统计',
|
||||
responses={200: success_response}
|
||||
)
|
||||
def get(self, request):
|
||||
async def get(self, request):
|
||||
from ..models import TranslateUsage
|
||||
|
||||
if request.user.is_authenticated:
|
||||
@@ -461,12 +463,12 @@ class TranslateUsageView(APIView):
|
||||
queryset = TranslateUsage.objects.filter(ip_address=ip_address)
|
||||
|
||||
today_start = timezone.localdate()
|
||||
today_count = queryset.filter(created_at__date=today_start).count()
|
||||
total_count = queryset.count()
|
||||
today_count = await queryset.filter(created_at__date=today_start).acount()
|
||||
total_count = await queryset.acount()
|
||||
|
||||
by_type = {}
|
||||
for type_key, _ in TranslateUsage.TRANSLATE_TYPE_CHOICES:
|
||||
by_type[type_key] = queryset.filter(translate_type=type_key).count()
|
||||
by_type[type_key] = await queryset.filter(translate_type=type_key).acount()
|
||||
|
||||
return Response({
|
||||
"code": 0,
|
||||
|
||||
+23
-25
@@ -1,7 +1,10 @@
|
||||
import requests
|
||||
import asyncio
|
||||
|
||||
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
|
||||
@@ -62,7 +65,7 @@ class GetIPDataView(APIView):
|
||||
502: openapi.Response(description="第三方服务异常"),
|
||||
}
|
||||
)
|
||||
def get(self, request):
|
||||
async def get(self, request):
|
||||
# 获取要查询的IP地址
|
||||
ip = request.GET.get('ip', '').strip()
|
||||
if not ip:
|
||||
@@ -74,7 +77,7 @@ class GetIPDataView(APIView):
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
return self._fetch_ip_location(ip)
|
||||
return await self._fetch_ip_location(ip)
|
||||
|
||||
def _get_client_ip(self, request):
|
||||
"""
|
||||
@@ -96,14 +99,14 @@ class GetIPDataView(APIView):
|
||||
|
||||
return None
|
||||
|
||||
def _fetch_ip_location(self, ip):
|
||||
async def _fetch_ip_location(self, ip):
|
||||
"""
|
||||
调用 ip-api.com 获取IP地理信息,支持Redis缓存
|
||||
调用 ip-api.com 获取IP地理信息,支持Redis缓存(全异步:aiohttp + sync_to_async 缓存兜底)
|
||||
"""
|
||||
cache_key = f"ip_location:{ip}"
|
||||
|
||||
# 先尝试从缓存获取
|
||||
cached_data = default_cache.get(cache_key)
|
||||
# 先尝试从缓存获取(django-redis 为同步客户端,用 sync_to_async 兜底)
|
||||
cached_data = await sync_to_async(default_cache.get)(cache_key)
|
||||
if cached_data:
|
||||
return Response(
|
||||
{"code": 200, "message": "success", "data": cached_data},
|
||||
@@ -112,15 +115,15 @@ class GetIPDataView(APIView):
|
||||
|
||||
try:
|
||||
url = IP_API_URL.format(ip=ip)
|
||||
response = requests.get(url, timeout=REQUEST_TIMEOUT)
|
||||
|
||||
if response.status_code != 200:
|
||||
return Response(
|
||||
{"code": 502, "message": "IP定位服务异常,请稍后重试", "data": None},
|
||||
status=status.HTTP_502_BAD_GATEWAY
|
||||
)
|
||||
|
||||
result = response.json()
|
||||
timeout = aiohttp.ClientTimeout(total=REQUEST_TIMEOUT)
|
||||
async with aiohttp.ClientSession(timeout=timeout) as session:
|
||||
async with session.get(url) as resp:
|
||||
if resp.status != 200:
|
||||
return Response(
|
||||
{"code": 502, "message": "IP定位服务异常,请稍后重试", "data": None},
|
||||
status=status.HTTP_502_BAD_GATEWAY
|
||||
)
|
||||
result = await resp.json()
|
||||
|
||||
if result.get('status') != 'success':
|
||||
return Response(
|
||||
@@ -139,24 +142,19 @@ class GetIPDataView(APIView):
|
||||
'timezone': result.get('timezone', ''),
|
||||
}
|
||||
|
||||
# 写入缓存
|
||||
default_cache.set(cache_key, ip_info, IP_CACHE_TIMEOUT)
|
||||
# 写入缓存(sync_to_async 兜底)
|
||||
await sync_to_async(default_cache.set)(cache_key, ip_info, IP_CACHE_TIMEOUT)
|
||||
|
||||
return Response(
|
||||
{"code": 200, "message": "success", "data": ip_info},
|
||||
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},
|
||||
|
||||
Reference in New Issue
Block a user