Files
chunyu_project/currency/views.py
T

359 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import asyncio
import json
import hashlib
from datetime import datetime
import aiohttp
from asgiref.sync import sync_to_async
from rest_framework.permissions import AllowAny
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 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):
"""
汇率查询视图 - 获取指定基准货币对所有支持货币的汇率
"""
permission_classes = [AllowAny]
@swagger_auto_schema(
tags=['Currency'],
operation_summary='获取最新汇率(GET方式)',
operation_description='基于 open.er-api.com 获取指定基准货币的最新汇率列表',
manual_parameters=[
openapi.Parameter(
'base',
openapi.IN_QUERY,
description='基准货币代码(如 USD, EUR, CNY),默认 USD',
type=openapi.TYPE_STRING,
required=False,
),
],
responses={200: success_response, 400: error_response, 502: error_response}
)
async def get(self, request):
base = request.GET.get('base', 'USD').strip().upper()
if not base:
base = 'USD'
return await self._fetch_rates(base)
@swagger_auto_schema(
tags=['Currency'],
operation_summary='获取最新汇率(POST方式)',
operation_description='基于 open.er-api.com 获取指定基准货币的最新汇率列表',
request_body=openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'base': openapi.Schema(type=openapi.TYPE_STRING, description='基准货币代码(如 USD, EUR, CNY),默认 USD'),
},
),
responses={200: success_response, 400: error_response, 502: error_response}
)
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 await self._fetch_rates(base)
async def _fetch_rates(self, base):
base = base.upper()
cached = await get_cached_data('rates', base)
if cached is not None:
return Response(
{"code": 200, "message": "success (cached)", "data": cached},
status=status.HTTP_200_OK,
)
try:
url = LATEST_RATES_URL.format(base=base)
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 data.get('result') != 'success':
return Response(
{"code": 502, "message": f"汇率服务返回错误:{data.get('error-type', '未知错误')}", "data": None},
status=status.HTTP_502_BAD_GATEWAY,
)
rates_data = {
'base': data.get('base_code', base),
'rates': data.get('rates', {}),
'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', ''),
}
await set_cached_data('rates', rates_data, base)
return Response(
{"code": 200, "message": "success", "data": rates_data},
status=status.HTTP_200_OK,
)
except (aiohttp.ClientError, asyncio.TimeoutError):
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},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
class CurrenciesListView(APIView):
"""
支持的货币列表视图 - 返回所有可用的货币代码
"""
permission_classes = [AllowAny]
COMMON_CURRENCIES = {
'USD': '美元 - United States Dollar',
'EUR': '欧元 - Euro',
'GBP': '英镑 - British Pound Sterling',
'JPY': '日元 - Japanese Yen',
'CNY': '人民币 - Chinese Yuan',
'AUD': '澳元 - Australian Dollar',
'CAD': '加元 - Canadian Dollar',
'CHF': '瑞士法郎 - Swiss Franc',
'HKD': '港币 - Hong Kong Dollar',
'SGD': '新加坡元 - Singapore Dollar',
'KRW': '韩元 - South Korean Won',
'RUB': '俄罗斯卢布 - Russian Ruble',
'INR': '印度卢比 - Indian Rupee',
'THB': '泰铢 - Thai Baht',
'VND': '越南盾 - Vietnamese Dong',
}
@swagger_auto_schema(
tags=['Currency'],
operation_summary='获取支持的货币代码列表',
operation_description='返回系统常用的货币代码及名称(带缓存)',
responses={200: success_response, 500: error_response}
)
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},
status=status.HTTP_200_OK,
)
try:
data = {
'currencies': self.COMMON_CURRENCIES,
'total': len(self.COMMON_CURRENCIES),
'updated_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
}
await set_cached_data('currencies', data)
return Response(
{"code": 200, "message": "success", "data": data},
status=status.HTTP_200_OK,
)
except Exception as e:
return Response(
{"code": 500, "message": f"服务器内部错误:{str(e)}", "data": None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
class CurrencyConvertView(APIView):
"""
货币兑换视图 - 将一种货币的金额转换为另一种货币
"""
permission_classes = [AllowAny]
@swagger_auto_schema(
tags=['Currency'],
operation_summary='货币兑换计算(GET方式)',
operation_description='将源货币金额按最新汇率转换为目标货币金额',
manual_parameters=[
openapi.Parameter(
'from',
openapi.IN_QUERY,
description='源货币代码(如 USD)',
type=openapi.TYPE_STRING,
required=True,
),
openapi.Parameter(
'to',
openapi.IN_QUERY,
description='目标货币代码(如 CNY)',
type=openapi.TYPE_STRING,
required=True,
),
openapi.Parameter(
'amount',
openapi.IN_QUERY,
description='源货币金额(正数)',
type=openapi.TYPE_NUMBER,
required=True,
),
],
responses={200: success_response, 400: error_response, 502: error_response}
)
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 await self._convert(from_code, to_code, amount)
@swagger_auto_schema(
tags=['Currency'],
operation_summary='货币兑换计算(POST方式)',
operation_description='将源货币金额按最新汇率转换为目标货币金额',
request_body=openapi.Schema(
type=openapi.TYPE_OBJECT,
required=['from', 'to', 'amount'],
properties={
'from': openapi.Schema(type=openapi.TYPE_STRING, description='源货币代码(如 USD)'),
'to': openapi.Schema(type=openapi.TYPE_STRING, description='目标货币代码(如 CNY)'),
'amount': openapi.Schema(type=openapi.TYPE_NUMBER, description='源货币金额(正数)'),
},
),
responses={200: success_response, 400: error_response, 502: error_response}
)
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 await self._convert(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},
status=status.HTTP_400_BAD_REQUEST,
)
if not to_code:
return Response(
{"code": 400, "message": "参数错误:to 不能为空", "data": None},
status=status.HTTP_400_BAD_REQUEST,
)
if not amount:
return Response(
{"code": 400, "message": "参数错误:amount 不能为空", "data": None},
status=status.HTTP_400_BAD_REQUEST,
)
try:
amount_value = float(amount)
if amount_value < 0:
return Response(
{"code": 400, "message": "参数错误:amount 不能为负数", "data": None},
status=status.HTTP_400_BAD_REQUEST,
)
except (ValueError, TypeError):
return Response(
{"code": 400, "message": "参数错误:amount 必须是有效的数字", "data": None},
status=status.HTTP_400_BAD_REQUEST,
)
cached_rates = await get_cached_data('rates', from_code)
rates = None
if cached_rates is not None:
rates = cached_rates.get('rates')
if rates is None:
try:
url = LATEST_RATES_URL.format(base=from_code)
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 data.get('result') != 'success':
return Response(
{"code": 502, "message": f"汇率服务返回错误:{data.get('error-type', '未知错误')}", "data": None},
status=status.HTTP_502_BAD_GATEWAY,
)
rates_data = {
'base': data.get('base_code', from_code),
'rates': data.get('rates', {}),
'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', ''),
}
await set_cached_data('rates', rates_data, from_code)
rates = rates_data['rates']
except (aiohttp.ClientError, asyncio.TimeoutError):
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},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
if to_code not in rates:
return Response(
{"code": 400, "message": f"不支持的目标货币代码:{to_code}", "data": None},
status=status.HTTP_400_BAD_REQUEST,
)
rate = float(rates[to_code])
converted_amount = round(amount_value * rate, 4)
result = {
'from': from_code,
'to': to_code,
'amount': amount_value,
'rate': rate,
'converted_amount': converted_amount,
'updated_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
}
return Response(
{"code": 200, "message": "success", "data": result},
status=status.HTTP_200_OK,
)