|
|
|
|
@ -11,7 +11,11 @@ class NetworkService:
|
|
|
|
|
# 实现构造函数逻辑
|
|
|
|
|
self.api_key = None
|
|
|
|
|
self.cache = {}
|
|
|
|
|
# 设置默认headers以避免被服务器拒绝
|
|
|
|
|
self.session = requests.Session()
|
|
|
|
|
self.session.headers.update({
|
|
|
|
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
|
|
|
|
|
})
|
|
|
|
|
# 天气缓存相关属性
|
|
|
|
|
self._cached_weather_data = None # 缓存的天气数据
|
|
|
|
|
self._cached_location = None # 缓存的定位信息
|
|
|
|
|
@ -43,39 +47,42 @@ class NetworkService:
|
|
|
|
|
return False
|
|
|
|
|
return (time.time() - self._weather_cache_timestamp) < 1800 # 30分钟
|
|
|
|
|
|
|
|
|
|
def get_user_ip(self):
|
|
|
|
|
"""获取用户IP地址 - 使用多个备用服务"""
|
|
|
|
|
# 首先尝试获取本地IP
|
|
|
|
|
try:
|
|
|
|
|
import socket
|
|
|
|
|
hostname = socket.gethostname()
|
|
|
|
|
local_ip = socket.gethostbyname(hostname)
|
|
|
|
|
if local_ip and not local_ip.startswith("127."):
|
|
|
|
|
print(f"获取到本地IP: {local_ip}")
|
|
|
|
|
return local_ip
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"获取本地IP失败: {e}")
|
|
|
|
|
|
|
|
|
|
# 如果本地IP获取失败,使用备用外部服务
|
|
|
|
|
def get_user_ip(self) -> Optional[str]:
|
|
|
|
|
"""获取用户公网IP地址,使用多个备用服务"""
|
|
|
|
|
# 备用IP获取服务列表
|
|
|
|
|
ip_services = [
|
|
|
|
|
"https://httpbin.org/ip",
|
|
|
|
|
"https://api.ipify.org?format=json",
|
|
|
|
|
"https://ipapi.co/json/"
|
|
|
|
|
"https://ident.me/.json"
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
for service in ip_services:
|
|
|
|
|
for service_url in ip_services:
|
|
|
|
|
try:
|
|
|
|
|
print(f"尝试从 {service} 获取IP地址...")
|
|
|
|
|
response = self.session.get(service, timeout=3, verify=False)
|
|
|
|
|
if response.status_code == 200:
|
|
|
|
|
data = response.json()
|
|
|
|
|
ip = data.get("origin") or data.get("ip") or data.get("ip_address")
|
|
|
|
|
if ip:
|
|
|
|
|
print(f"成功从 {service} 获取IP: {ip}")
|
|
|
|
|
return ip
|
|
|
|
|
print(f"尝试获取IP地址: {service_url}")
|
|
|
|
|
response = self.session.get(service_url, timeout=10)
|
|
|
|
|
response.raise_for_status() # 检查HTTP错误
|
|
|
|
|
|
|
|
|
|
data = response.json()
|
|
|
|
|
# 不同服务返回的IP字段可能不同
|
|
|
|
|
ip = (data.get('origin') or
|
|
|
|
|
data.get('ip') or
|
|
|
|
|
data.get('IPAddress') or
|
|
|
|
|
data.get('query'))
|
|
|
|
|
|
|
|
|
|
if ip:
|
|
|
|
|
# 如果IP包含逗号,取第一个(可能有代理)
|
|
|
|
|
if ',' in ip:
|
|
|
|
|
ip = ip.split(',')[0].strip()
|
|
|
|
|
print(f"成功获取IP地址: {ip}")
|
|
|
|
|
return ip
|
|
|
|
|
else:
|
|
|
|
|
print(f"无法从响应中提取IP地址: {data}")
|
|
|
|
|
except requests.exceptions.RequestException as e:
|
|
|
|
|
print(f"网络请求错误 ({service_url}): {e}")
|
|
|
|
|
except ValueError as e:
|
|
|
|
|
print(f"JSON解析错误 ({service_url}): {e}")
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"从 {service} 获取IP失败: {e}")
|
|
|
|
|
continue
|
|
|
|
|
print(f"获取IP时发生未知错误 ({service_url}): {e}")
|
|
|
|
|
|
|
|
|
|
print("所有IP获取服务都失败了,使用默认IP")
|
|
|
|
|
return "8.8.8.8" # 使用Google DNS作为默认IP
|
|
|
|
|
@ -134,22 +141,44 @@ class NetworkService:
|
|
|
|
|
print("无法获取IP地址,使用默认天气数据")
|
|
|
|
|
return self.get_default_weather()
|
|
|
|
|
|
|
|
|
|
# 2. 根据IP获取地理位置
|
|
|
|
|
# 注意:这里使用免费的IP地理位置API,实际应用中可能需要更精确的服务
|
|
|
|
|
location_url = f"http://ip-api.com/json/{ip}"
|
|
|
|
|
print(f"请求地理位置: {location_url}")
|
|
|
|
|
location_response = self.session.get(location_url, timeout=5, verify=False)
|
|
|
|
|
location_data = location_response.json()
|
|
|
|
|
print(f"地理位置响应: {location_data}")
|
|
|
|
|
# 2. 根据IP获取地理位置 - 增加重试机制并使用更稳定的API
|
|
|
|
|
location_success = False
|
|
|
|
|
location_data = None
|
|
|
|
|
# 使用ipapi.co API,它比ip-api.com更稳定
|
|
|
|
|
location_url = f"https://ipapi.co/{ip}/json/"
|
|
|
|
|
|
|
|
|
|
for attempt in range(3): # 最多尝试3次
|
|
|
|
|
try:
|
|
|
|
|
print(f"请求地理位置 (尝试 {attempt + 1}/3): {location_url}")
|
|
|
|
|
location_response = self.session.get(location_url, timeout=10)
|
|
|
|
|
location_response.raise_for_status() # 检查HTTP错误
|
|
|
|
|
location_data = location_response.json()
|
|
|
|
|
print(f"地理位置响应: {location_data}")
|
|
|
|
|
|
|
|
|
|
# ipapi.co 使用不同的状态字段
|
|
|
|
|
if 'error' not in location_data:
|
|
|
|
|
location_success = True
|
|
|
|
|
break
|
|
|
|
|
else:
|
|
|
|
|
error_msg = location_data.get('reason', 'Unknown error')
|
|
|
|
|
print(f"地理位置获取失败,错误: {error_msg}")
|
|
|
|
|
time.sleep(1) # 等待1秒后重试
|
|
|
|
|
except requests.exceptions.RequestException as e:
|
|
|
|
|
print(f"网络请求错误 (尝试 {attempt + 1}/3): {e}")
|
|
|
|
|
time.sleep(2) # 等待2秒后重试
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"获取地理位置时出错 (尝试 {attempt + 1}/3): {e}")
|
|
|
|
|
time.sleep(2) # 等待2秒后重试
|
|
|
|
|
|
|
|
|
|
if location_data.get("status") != "success":
|
|
|
|
|
if not location_success:
|
|
|
|
|
print("地理位置获取失败,使用默认天气数据")
|
|
|
|
|
return self.get_default_weather()
|
|
|
|
|
|
|
|
|
|
# ipapi.co 使用不同的字段名
|
|
|
|
|
city = location_data.get("city", "Unknown")
|
|
|
|
|
print(f"获取到的城市: {city}")
|
|
|
|
|
|
|
|
|
|
if not city:
|
|
|
|
|
if not city or city == "Unknown":
|
|
|
|
|
print("无法获取城市名称,使用默认天气数据")
|
|
|
|
|
return self.get_default_weather()
|
|
|
|
|
|
|
|
|
|
@ -157,29 +186,47 @@ class NetworkService:
|
|
|
|
|
self._cached_location = {
|
|
|
|
|
"ip": ip,
|
|
|
|
|
"city": city,
|
|
|
|
|
"country": location_data.get("country", "Unknown"),
|
|
|
|
|
"region": location_data.get("regionName", "Unknown")
|
|
|
|
|
"country": location_data.get("country_name", "Unknown"),
|
|
|
|
|
"region": location_data.get("region", "Unknown")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# 3. 调用天气API获取天气数据
|
|
|
|
|
# 注意:这里使用OpenWeatherMap API作为示例,需要API密钥
|
|
|
|
|
# 在实际应用中,需要设置有效的API密钥
|
|
|
|
|
if self.api_key:
|
|
|
|
|
weather_url = f"http://api.openweathermap.org/data/2.5/weather?q={city}&appid={self.api_key}&units=metric&lang=zh_cn"
|
|
|
|
|
weather_response = self.session.get(weather_url, timeout=5, verify=False)
|
|
|
|
|
weather_data = weather_response.json()
|
|
|
|
|
weather_success = False
|
|
|
|
|
weather_data = None
|
|
|
|
|
for attempt in range(3): # 最多尝试3次
|
|
|
|
|
try:
|
|
|
|
|
weather_url = f"http://api.openweathermap.org/data/2.5/weather?q={city}&appid={self.api_key}&units=metric&lang=zh_cn"
|
|
|
|
|
print(f"请求天气数据 (尝试 {attempt + 1}/3): {weather_url}")
|
|
|
|
|
weather_response = self.session.get(weather_url, timeout=10)
|
|
|
|
|
weather_response.raise_for_status() # 检查HTTP错误
|
|
|
|
|
weather_data = weather_response.json()
|
|
|
|
|
print(f"天气响应状态码: {weather_response.status_code}")
|
|
|
|
|
|
|
|
|
|
# 4. 解析并格式化数据
|
|
|
|
|
if weather_response.status_code == 200:
|
|
|
|
|
formatted_weather = {
|
|
|
|
|
"city": city,
|
|
|
|
|
"temperature": weather_data["main"]["temp"],
|
|
|
|
|
"description": weather_data["weather"][0]["description"],
|
|
|
|
|
"humidity": weather_data["main"]["humidity"],
|
|
|
|
|
"wind_speed": weather_data["wind"]["speed"]
|
|
|
|
|
}
|
|
|
|
|
# 5. 返回天气信息字典
|
|
|
|
|
return formatted_weather
|
|
|
|
|
else:
|
|
|
|
|
print(f"天气API返回错误状态码: {weather_response.status_code}")
|
|
|
|
|
time.sleep(1) # 等待1秒后重试
|
|
|
|
|
except requests.exceptions.RequestException as e:
|
|
|
|
|
print(f"网络请求错误 (尝试 {attempt + 1}/3): {e}")
|
|
|
|
|
time.sleep(2) # 等待2秒后重试
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"获取天气数据时出错 (尝试 {attempt + 1}/3): {e}")
|
|
|
|
|
time.sleep(2) # 等待2秒后重试
|
|
|
|
|
|
|
|
|
|
# 4. 解析并格式化数据
|
|
|
|
|
if weather_response.status_code == 200:
|
|
|
|
|
formatted_weather = {
|
|
|
|
|
"city": city,
|
|
|
|
|
"temperature": weather_data["main"]["temp"],
|
|
|
|
|
"description": weather_data["weather"][0]["description"],
|
|
|
|
|
"humidity": weather_data["main"]["humidity"],
|
|
|
|
|
"wind_speed": weather_data["wind"]["speed"]
|
|
|
|
|
}
|
|
|
|
|
# 5. 返回天气信息字典
|
|
|
|
|
return formatted_weather
|
|
|
|
|
print("所有天气API请求都失败了")
|
|
|
|
|
else:
|
|
|
|
|
# 当没有API密钥时,使用免费的天气API获取真实数据
|
|
|
|
|
# 首先尝试获取城市ID(需要映射城市名到ID)
|
|
|
|
|
@ -239,74 +286,87 @@ class NetworkService:
|
|
|
|
|
weather_city_id = "101010100" # 默认北京ID
|
|
|
|
|
print(f"使用默认城市ID获取天气: {city} -> {weather_city_id}")
|
|
|
|
|
|
|
|
|
|
# 使用免费天气API获取天气数据
|
|
|
|
|
try:
|
|
|
|
|
# 使用和风天气免费API的替代方案 - sojson天气API
|
|
|
|
|
weather_url = f"http://t.weather.sojson.com/api/weather/city/{weather_city_id}"
|
|
|
|
|
print(f"请求天气数据: {weather_url}")
|
|
|
|
|
weather_response = self.session.get(weather_url, timeout=5, verify=False)
|
|
|
|
|
print(f"天气响应状态码: {weather_response.status_code}")
|
|
|
|
|
weather_data = weather_response.json()
|
|
|
|
|
print(f"天气数据响应: {weather_data}")
|
|
|
|
|
|
|
|
|
|
if weather_data.get("status") == 200:
|
|
|
|
|
# 解析天气数据
|
|
|
|
|
current_data = weather_data.get("data", {})
|
|
|
|
|
wendu = current_data.get("wendu", "N/A")
|
|
|
|
|
shidu = current_data.get("shidu", "N/A")
|
|
|
|
|
forecast = current_data.get("forecast", [])
|
|
|
|
|
|
|
|
|
|
# 获取第一个预报项作为当前天气
|
|
|
|
|
current_weather = forecast[0] if forecast else {}
|
|
|
|
|
weather_type = current_weather.get("type", "晴")
|
|
|
|
|
|
|
|
|
|
# 获取生活指数信息
|
|
|
|
|
lifetips = []
|
|
|
|
|
if current_weather:
|
|
|
|
|
# 从预报数据中提取生活提示
|
|
|
|
|
ganmao = current_weather.get("ganmao", "")
|
|
|
|
|
if ganmao:
|
|
|
|
|
lifetips.append(f"感冒指数: {ganmao}")
|
|
|
|
|
|
|
|
|
|
# 添加其他生活指数(基于天气类型推断)
|
|
|
|
|
if "雨" in weather_type:
|
|
|
|
|
lifetips.append("出行建议: 记得带伞")
|
|
|
|
|
elif "晴" in weather_type:
|
|
|
|
|
lifetips.append("出行建议: 适合户外活动")
|
|
|
|
|
elif "雪" in weather_type:
|
|
|
|
|
lifetips.append("出行建议: 注意防滑保暖")
|
|
|
|
|
elif "雾" in weather_type or "霾" in weather_type:
|
|
|
|
|
lifetips.append("健康提醒: 减少户外运动")
|
|
|
|
|
|
|
|
|
|
# 温度相关建议
|
|
|
|
|
temp = float(wendu) if wendu != "N/A" else 20
|
|
|
|
|
if temp > 30:
|
|
|
|
|
lifetips.append("穿衣建议: 注意防暑降温")
|
|
|
|
|
elif temp < 5:
|
|
|
|
|
lifetips.append("穿衣建议: 注意保暖防寒")
|
|
|
|
|
elif temp < 15:
|
|
|
|
|
lifetips.append("穿衣建议: 适当添加衣物")
|
|
|
|
|
else:
|
|
|
|
|
lifetips.append("穿衣建议: 天气舒适")
|
|
|
|
|
# 使用免费天气API获取天气数据 - 增加重试机制
|
|
|
|
|
weather_success = False
|
|
|
|
|
weather_data = None
|
|
|
|
|
for attempt in range(3): # 最多尝试3次
|
|
|
|
|
try:
|
|
|
|
|
# 使用和风天气免费API的替代方案 - sojson天气API
|
|
|
|
|
weather_url = f"http://t.weather.sojson.com/api/weather/city/{weather_city_id}"
|
|
|
|
|
print(f"请求天气数据 (尝试 {attempt + 1}/3): {weather_url}")
|
|
|
|
|
weather_response = self.session.get(weather_url, timeout=10)
|
|
|
|
|
weather_response.raise_for_status() # 检查HTTP错误
|
|
|
|
|
weather_data = weather_response.json()
|
|
|
|
|
print(f"天气数据响应: {weather_data}")
|
|
|
|
|
|
|
|
|
|
formatted_weather = {
|
|
|
|
|
"city": city,
|
|
|
|
|
"temperature": float(wendu) if wendu != "N/A" else 20,
|
|
|
|
|
"description": weather_type,
|
|
|
|
|
"humidity": shidu.replace("%", "") if shidu != "N/A" else "60",
|
|
|
|
|
"wind_speed": "3.5", # 默认风速
|
|
|
|
|
"lifetips": lifetips # 生活提示列表
|
|
|
|
|
}
|
|
|
|
|
print(f"成功获取天气数据: {formatted_weather}")
|
|
|
|
|
if weather_data.get("status") == 200:
|
|
|
|
|
weather_success = True
|
|
|
|
|
break
|
|
|
|
|
else:
|
|
|
|
|
print(f"天气API返回错误状态: {weather_data.get('status')}")
|
|
|
|
|
time.sleep(1) # 等待1秒后重试
|
|
|
|
|
except requests.exceptions.RequestException as e:
|
|
|
|
|
print(f"网络请求错误 (尝试 {attempt + 1}/3): {e}")
|
|
|
|
|
time.sleep(2) # 等待2秒后重试
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"获取免费天气数据时出错 (尝试 {attempt + 1}/3): {e}")
|
|
|
|
|
time.sleep(2) # 等待2秒后重试
|
|
|
|
|
|
|
|
|
|
if weather_success:
|
|
|
|
|
# 解析天气数据
|
|
|
|
|
current_data = weather_data.get("data", {})
|
|
|
|
|
wendu = current_data.get("wendu", "N/A")
|
|
|
|
|
shidu = current_data.get("shidu", "N/A")
|
|
|
|
|
forecast = current_data.get("forecast", [])
|
|
|
|
|
|
|
|
|
|
# 获取第一个预报项作为当前天气
|
|
|
|
|
current_weather = forecast[0] if forecast else {}
|
|
|
|
|
weather_type = current_weather.get("type", "晴")
|
|
|
|
|
|
|
|
|
|
# 获取生活指数信息
|
|
|
|
|
lifetips = []
|
|
|
|
|
if current_weather:
|
|
|
|
|
# 从预报数据中提取生活提示
|
|
|
|
|
ganmao = current_weather.get("ganmao", "")
|
|
|
|
|
if ganmao:
|
|
|
|
|
lifetips.append(f"感冒指数: {ganmao}")
|
|
|
|
|
|
|
|
|
|
# 缓存天气数据
|
|
|
|
|
self.set_weather_cache(formatted_weather, self._cached_location)
|
|
|
|
|
return formatted_weather
|
|
|
|
|
else:
|
|
|
|
|
print(f"天气API返回错误状态: {weather_data.get('status')}")
|
|
|
|
|
# 添加其他生活指数(基于天气类型推断)
|
|
|
|
|
if "雨" in weather_type:
|
|
|
|
|
lifetips.append("出行建议: 记得带伞")
|
|
|
|
|
elif "晴" in weather_type:
|
|
|
|
|
lifetips.append("出行建议: 适合户外活动")
|
|
|
|
|
elif "雪" in weather_type:
|
|
|
|
|
lifetips.append("出行建议: 注意防滑保暖")
|
|
|
|
|
elif "雾" in weather_type or "霾" in weather_type:
|
|
|
|
|
lifetips.append("健康提醒: 减少户外运动")
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"获取免费天气数据时出错: {e}")
|
|
|
|
|
# 温度相关建议
|
|
|
|
|
temp = float(wendu) if wendu != "N/A" else 20
|
|
|
|
|
if temp > 30:
|
|
|
|
|
lifetips.append("穿衣建议: 注意防暑降温")
|
|
|
|
|
elif temp < 5:
|
|
|
|
|
lifetips.append("穿衣建议: 注意保暖防寒")
|
|
|
|
|
elif temp < 15:
|
|
|
|
|
lifetips.append("穿衣建议: 适当添加衣物")
|
|
|
|
|
else:
|
|
|
|
|
lifetips.append("穿衣建议: 天气舒适")
|
|
|
|
|
|
|
|
|
|
formatted_weather = {
|
|
|
|
|
"city": city,
|
|
|
|
|
"temperature": float(wendu) if wendu != "N/A" else 20,
|
|
|
|
|
"description": weather_type,
|
|
|
|
|
"humidity": shidu.replace("%", "") if shidu != "N/A" else "60",
|
|
|
|
|
"wind_speed": "3.5", # 默认风速
|
|
|
|
|
"lifetips": lifetips # 生活提示列表
|
|
|
|
|
}
|
|
|
|
|
print(f"成功获取天气数据: {formatted_weather}")
|
|
|
|
|
|
|
|
|
|
# 缓存天气数据
|
|
|
|
|
self.set_weather_cache(formatted_weather, self._cached_location)
|
|
|
|
|
return formatted_weather
|
|
|
|
|
else:
|
|
|
|
|
print("所有免费天气API请求都失败了")
|
|
|
|
|
|
|
|
|
|
# 如果以上都失败,返回默认数据
|
|
|
|
|
default_weather = {
|
|
|
|
|
|