import asyncio from datetime import datetime import aiohttp 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" AIR_QUALITY_URL = "https://air-quality-api.open-meteo.com/v1/air-quality" def get_air_quality_level(pm2_5): """ 根据PM2.5浓度获取空气质量等级 """ if pm2_5 <= 10: return "优" elif pm2_5 <= 25: return "良" elif pm2_5 <= 50: return "轻度污染" elif pm2_5 <= 75: return "中度污染" else: return "重度污染" class AirQualityView(APIView): """ 空气质量查询视图 - 基于 Open-Meteo Air Quality API(免费,无需API Key) """ permission_classes = [AllowAny] @swagger_auto_schema( tags=['Air Quality'], operation_summary='获取空气质量信息(GET方式)', operation_description='通过城市名称获取实时空气质量信息,支持PM2.5、PM10、臭氧、二氧化氮等指标', manual_parameters=[ openapi.Parameter( 'city', openapi.IN_QUERY, description='城市名称(支持中文、英文,如:北京、Beijing、上海、Shanghai)', type=openapi.TYPE_STRING, required=True ), 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() 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 ) return await self._fetch_air_quality(city, lang) @swagger_auto_schema( tags=['Air Quality'], operation_summary='获取空气质量信息(POST方式)', operation_description='通过城市名称获取实时空气质量信息,支持PM2.5、PM10、臭氧、二氧化氮等指标', request_body=openapi.Schema( type=openapi.TYPE_OBJECT, required=['city'], properties={ 'city': openapi.Schema(type=openapi.TYPE_STRING, description='城市名称(支持中文、英文)'), '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 '' 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 ) return await self._fetch_air_quality(city, lang) async def _fetch_air_quality(self, city, lang): """ 调用 Open-Meteo Air Quality 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', '') # 第二步:获取空气质量数据 air_quality_params = { 'latitude': latitude, 'longitude': longitude, 'current': 'pm10,pm2_5,carbon_monoxide,nitrogen_dioxide,sulphur_dioxide,ozone,us_aqi', 'timezone': 'auto' } async with session.get(AIR_QUALITY_URL, params=air_quality_params) as aq_response: if aq_response.status != 200: return Response( {"code": 502, "message": "空气质量服务异常,请稍后重试", "data": None}, status=status.HTTP_502_BAD_GATEWAY ) aq_data = await aq_response.json() current = aq_data.get('current', {}) pm2_5 = current.get('pm2_5', 0) # 构建响应 air_quality_info = { 'city': city_name, 'country': country, 'pm2_5': pm2_5, 'pm10': current.get('pm10', 0), 'ozone': current.get('ozone', 0), 'nitrogen_dioxide': current.get('nitrogen_dioxide', 0), 'sulphur_dioxide': current.get('sulphur_dioxide', 0), 'carbon_monoxide': current.get('carbon_monoxide', 0), 'air_quality_level': get_air_quality_level(pm2_5), 'updated_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S') } return Response( {"code": 200, "message": "success", "data": air_quality_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 )