sync from local backup

This commit is contained in:
chunyu
2026-08-05 23:59:15 +08:00
commit d48ec1747c
358 changed files with 34384 additions and 0 deletions
+341
View File
@@ -0,0 +1,341 @@
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,
)