import asyncio 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 GEOCODING_URL = "https://geocoding-api.open-meteo.com/v1/search" FORECAST_URL = "https://api.open-meteo.com/v1/forecast" WEATHER_CODE_MAP = { 0: "晴朗", 1: "大部晴朗", 2: "多云", 3: "阴天", 45: "雾", 48: "雾凇", 51: "小毛毛雨", 53: "毛毛雨", 55: "大毛毛雨", 56: "冻毛毛雨", 57: "强冻毛毛雨", 61: "小雨", 63: "中雨", 65: "大雨", 66: "冻雨", 67: "强冻雨", 71: "小雪", 73: "中雪", 75: "大雪", 77: "雪粒", 80: "阵雨", 81: "强阵雨", 82: "暴雨", 85: "阵雪", 86: "强阵雪", 95: "雷暴", 96: "雷暴伴小冰雹", 99: "雷暴伴大冰雹", } class WeatherView(APIView): """ 天气查询视图 - 基于 Open-Meteo API(免费,无需API Key) """ permission_classes = [AllowAny] @swagger_auto_schema( tags=['Weather'], operation_summary='获取天气信息(GET方式)', operation_description='通过城市名称获取实时天气信息,支持温度单位和语言设置', manual_parameters=[ openapi.Parameter( 'city', openapi.IN_QUERY, description='城市名称(支持中文、英文,如:北京、Beijing、上海、Shanghai)', type=openapi.TYPE_STRING, required=True ), openapi.Parameter( 'unit', openapi.IN_QUERY, description='温度单位:celsius(摄氏度,默认)、fahrenheit(华氏度)', type=openapi.TYPE_STRING, required=False, enum=['celsius', 'fahrenheit'] ), openapi.Parameter( 'lang', openapi.IN_QUERY, description='返回语言(默认:zh_cn)', type=openapi.TYPE_STRING, required=False, enum=['zh_cn', 'en'] ), ], responses={200: success_response, 400: error_response, 404: error_response, 502: error_response} ) async def get(self, request): city = request.GET.get('city', '').strip() unit = request.GET.get('unit', 'celsius').strip() lang = request.GET.get('lang', 'zh_cn').strip() if not city: return Response( {"code": 400, "message": "参数错误:city不能为空", "data": None}, status=status.HTTP_400_BAD_REQUEST ) if unit not in ['celsius', 'fahrenheit']: return Response( {"code": 400, "message": "参数错误:unit必须是 celsius 或 fahrenheit", "data": None}, status=status.HTTP_400_BAD_REQUEST ) return await self._fetch_weather(city, unit, lang) @swagger_auto_schema( tags=['Weather'], operation_summary='获取天气信息(POST方式)', operation_description='通过城市名称获取实时天气信息,支持温度单位和语言设置', request_body=openapi.Schema( type=openapi.TYPE_OBJECT, required=['city'], properties={ 'city': openapi.Schema(type=openapi.TYPE_STRING, description='城市名称(支持中文、英文)'), 'unit': openapi.Schema(type=openapi.TYPE_STRING, description='温度单位:celsius(默认)、fahrenheit', enum=['celsius', 'fahrenheit']), 'lang': openapi.Schema(type=openapi.TYPE_STRING, description='返回语言(默认:zh_cn)', enum=['zh_cn', 'en']), } ), responses={200: success_response, 400: error_response, 404: error_response, 502: error_response} ) async def post(self, request): city = request.data.get('city', '').strip() if isinstance(request.data, dict) else '' unit = request.data.get('unit', 'celsius').strip() if isinstance(request.data, dict) else 'celsius' lang = request.data.get('lang', 'zh_cn').strip() if isinstance(request.data, dict) else 'zh_cn' if not city: return Response( {"code": 400, "message": "参数错误:city不能为空", "data": None}, status=status.HTTP_400_BAD_REQUEST ) if unit not in ['celsius', 'fahrenheit']: return Response( {"code": 400, "message": "参数错误:unit必须是 celsius 或 fahrenheit", "data": None}, status=status.HTTP_400_BAD_REQUEST ) return await self._fetch_weather(city, unit, lang) async def _fetch_weather(self, city, unit, lang): """ 调用 Open-Meteo API 获取天气数据(aiohttp 全异步) """ timeout = aiohttp.ClientTimeout(total=10) try: async with aiohttp.ClientSession(timeout=timeout) as session: # 第一步:通过地理编码API获取城市坐标 geo_params = { 'name': city, 'count': 1, 'language': 'zh' if lang == 'zh_cn' else 'en', 'format': 'json' } async with session.get(GEOCODING_URL, params=geo_params) as geo_response: if geo_response.status != 200: return Response( {"code": 502, "message": "地理编码服务异常,请稍后重试", "data": None}, status=status.HTTP_502_BAD_GATEWAY ) geo_data = await geo_response.json() results = geo_data.get('results', []) if not results: return Response( {"code": 404, "message": f"未找到城市:{city},请检查城市名称是否正确", "data": None}, status=status.HTTP_404_NOT_FOUND ) location = results[0] latitude = location.get('latitude') longitude = location.get('longitude') city_name = location.get('name', city) country = location.get('country', '') # 第二步:获取天气数据 temp_unit = 'fahrenheit' if unit == 'fahrenheit' else 'celsius' weather_params = { 'latitude': latitude, 'longitude': longitude, 'current': 'temperature_2m,relative_humidity_2m,weather_code,wind_speed_10m,pressure_msl', 'timezone': 'auto', 'temperature_unit': temp_unit } async with session.get(FORECAST_URL, params=weather_params) as weather_response: if weather_response.status != 200: return Response( {"code": 502, "message": "天气服务异常,请稍后重试", "data": None}, status=status.HTTP_502_BAD_GATEWAY ) weather_data = await weather_response.json() current = weather_data.get('current', {}) weather_code = current.get('weather_code', 0) # 构建响应 temp_unit_symbol = '°C' if unit == 'celsius' else '°F' weather_info = { 'city': city_name, 'country': country, 'temperature': current.get('temperature_2m', 0), 'temperature_unit': temp_unit_symbol, 'humidity': current.get('relative_humidity_2m', 0), 'weather': WEATHER_CODE_MAP.get(weather_code, "未知"), 'wind_speed': f"{current.get('wind_speed_10m', 0)} km/h", 'pressure': f"{current.get('pressure_msl', 0)} hPa", 'updated_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S') } return Response( {"code": 200, "message": "success", "data": weather_info}, 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 )