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, )