Files
chunyu_project/currency/views.py
T
2026-08-05 23:59:15 +08:00

342 lines
13 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.
from datetime import datetime
import requests
from rest_framework.permissions import AllowAny
from rest_framework.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
LATEST_RATES_URL = "https://open.er-api.com/v6/latest/{base}"
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}
)
def get(self, request):
base = request.GET.get('base', 'USD').strip().upper()
if not base:
base = 'USD'
return 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}
)
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)
def _fetch_rates(self, base):
base = base.upper()
cached = 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)
response = requests.get(url, timeout=10)
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},
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', ''),
}
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:
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},
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}
)
def get(self, request):
cached = 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'),
}
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}
)
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)
@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}
)
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)
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 = 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)
response = requests.get(url, timeout=10)
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},
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', ''),
}
set_cached_data('rates', rates_data, from_code)
rates = rates_data['rates']
except requests.exceptions.Timeout:
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},
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,
)