Files
chunyu_project/weather/views.py
T

226 lines
8.8 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.
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
)