Initial commit: Unified API Gateway v3.0.0
This commit is contained in:
+19
@@ -0,0 +1,19 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*.egg-info/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
|
||||||
|
# Environment / secrets
|
||||||
|
.env
|
||||||
|
*.key
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# System files
|
||||||
|
Thumbs.db
|
||||||
|
.DS_Store
|
||||||
|
desktop.ini
|
||||||
@@ -0,0 +1,578 @@
|
|||||||
|
"""
|
||||||
|
Unified API Gateway v3.0
|
||||||
|
Wraps all external APIs in /api/v3/ format
|
||||||
|
Supports precise city name matching (adcode.txt)
|
||||||
|
Server deployment version
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import requests
|
||||||
|
from flask import Flask, request, jsonify
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
# ========== Configuration ==========
|
||||||
|
# Time API - using api.zxki.cn (simple and reliable)
|
||||||
|
TIME_API_URL = "https://api.zxki.cn/api/time"
|
||||||
|
|
||||||
|
# Weather API - sojson (uses citykey)
|
||||||
|
WEATHER_SOJSON_URL = "http://t.weather.sojson.com/api/weather/city/{}"
|
||||||
|
|
||||||
|
# Weather API - uapis.cn (general weather interface)
|
||||||
|
WEATHER_UAPIS_URL = "https://uapis.cn/api/v1/misc/weather"
|
||||||
|
|
||||||
|
# Phone number lookup API
|
||||||
|
PHONE_INFO_URL = "https://uapis.cn/api/v1/misc/phoneinfo"
|
||||||
|
|
||||||
|
# IP Geolocation APIs
|
||||||
|
IP_MYIP_URL = "https://uapis.cn/api/v1/network/myip"
|
||||||
|
IP_INFO_URL = "https://uapis.cn/api/v1/network/ipinfo"
|
||||||
|
IP_SB_URL = "https://api.ip.sb/geoip/{}"
|
||||||
|
|
||||||
|
# ========== City Code Mapping ==========
|
||||||
|
CITY_CODE_MAP = {}
|
||||||
|
|
||||||
|
def load_city_codes():
|
||||||
|
"""Load city code mapping from adcode.txt"""
|
||||||
|
global CITY_CODE_MAP
|
||||||
|
try:
|
||||||
|
with open('adcode.txt', 'r', encoding='utf-8') as f:
|
||||||
|
for line in f:
|
||||||
|
line = line.strip()
|
||||||
|
if line and '=' in line:
|
||||||
|
code, city = line.split('=', 1)
|
||||||
|
city_name = city.strip()
|
||||||
|
code_value = code.strip()
|
||||||
|
CITY_CODE_MAP[city_name] = code_value
|
||||||
|
print(f"Loaded {len(CITY_CODE_MAP)} city codes")
|
||||||
|
except FileNotFoundError:
|
||||||
|
print("Error: adcode.txt not found, city lookup unavailable")
|
||||||
|
raise SystemExit("Missing adcode.txt file")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading adcode.txt: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def search_city_code(query):
|
||||||
|
"""
|
||||||
|
Search for city code
|
||||||
|
1. Remove city/county/district suffixes and search
|
||||||
|
2. If multiple matches found, search again with suffix
|
||||||
|
3. If still multiple matches, return error
|
||||||
|
4. Single-character search not supported
|
||||||
|
"""
|
||||||
|
if not query or len(query.strip()) == 0:
|
||||||
|
return None, "City name cannot be empty"
|
||||||
|
|
||||||
|
query = query.strip()
|
||||||
|
|
||||||
|
# Single-character search not supported
|
||||||
|
if len(query) <= 1:
|
||||||
|
return None, "Single-character search not supported, please enter full city name"
|
||||||
|
|
||||||
|
# Remove suffix
|
||||||
|
clean_query = re.sub(r'[市县区州]$', '', query)
|
||||||
|
|
||||||
|
# First search: exact match on name without suffix
|
||||||
|
if clean_query in CITY_CODE_MAP:
|
||||||
|
return CITY_CODE_MAP[clean_query], None
|
||||||
|
|
||||||
|
# If suffix was removed and original query exists, try original
|
||||||
|
if clean_query != query and query in CITY_CODE_MAP:
|
||||||
|
return CITY_CODE_MAP[query], None
|
||||||
|
|
||||||
|
# Search for partial matches
|
||||||
|
matches = {}
|
||||||
|
for city, code in CITY_CODE_MAP.items():
|
||||||
|
# City name equals query without suffix
|
||||||
|
if city == clean_query:
|
||||||
|
matches[city] = code
|
||||||
|
# Query without suffix is a prefix of city name
|
||||||
|
elif city.startswith(clean_query):
|
||||||
|
# Require city to be at least 1 char longer than query to avoid vague matches
|
||||||
|
if len(city) - len(clean_query) >= 1:
|
||||||
|
matches[city] = code
|
||||||
|
|
||||||
|
# If first search found results
|
||||||
|
if matches:
|
||||||
|
if len(matches) == 1:
|
||||||
|
city, code = list(matches.items())[0]
|
||||||
|
return code, None
|
||||||
|
else:
|
||||||
|
# Multiple results, try adding original suffix back
|
||||||
|
if clean_query != query:
|
||||||
|
# Search for city names containing original query
|
||||||
|
refined_matches = {}
|
||||||
|
for city, code in CITY_CODE_MAP.items():
|
||||||
|
if query in city:
|
||||||
|
refined_matches[city] = code
|
||||||
|
|
||||||
|
if len(refined_matches) == 1:
|
||||||
|
city, code = list(refined_matches.items())[0]
|
||||||
|
return code, None
|
||||||
|
elif len(refined_matches) > 1:
|
||||||
|
cities = list(refined_matches.keys())
|
||||||
|
return None, f"Multiple cities found: {', '.join(cities)}, please provide a more specific city name"
|
||||||
|
else:
|
||||||
|
cities = list(matches.keys())
|
||||||
|
return None, f"Multiple cities found: {', '.join(cities)}, please provide a more specific city name"
|
||||||
|
else:
|
||||||
|
cities = list(matches.keys())
|
||||||
|
return None, f"Multiple cities found: {', '.join(cities)}, please provide a more specific city name"
|
||||||
|
|
||||||
|
# No match found
|
||||||
|
return None, f"City '{query}' not found"
|
||||||
|
|
||||||
|
# ========== Generic Request Function ==========
|
||||||
|
def safe_request(url, method='GET', params=None, timeout=10):
|
||||||
|
"""Safe HTTP request handler"""
|
||||||
|
try:
|
||||||
|
if method == 'GET':
|
||||||
|
resp = requests.get(url, params=params, timeout=timeout)
|
||||||
|
elif method == 'POST':
|
||||||
|
resp = requests.post(url, json=params, timeout=timeout)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if resp.status_code == 200:
|
||||||
|
return resp.json()
|
||||||
|
elif resp.status_code == 404:
|
||||||
|
return {"error": "not_found", "message": "Resource not found"}
|
||||||
|
else:
|
||||||
|
return {"error": f"HTTP {resp.status_code}", "message": resp.text}
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
return {"error": "timeout", "message": "Request timeout"}
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
return {"error": "connection_error", "message": "Connection failed"}
|
||||||
|
except requests.RequestException as e:
|
||||||
|
return {"error": "request_failed", "message": str(e)}
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return {"error": "json_decode_failed", "message": "Response parsing failed"}
|
||||||
|
|
||||||
|
# ========== Response Formatting ==========
|
||||||
|
def success_response(data, message="Success"):
|
||||||
|
"""Unified success response format"""
|
||||||
|
return jsonify({
|
||||||
|
"code": 200,
|
||||||
|
"message": message,
|
||||||
|
"data": data
|
||||||
|
})
|
||||||
|
|
||||||
|
def error_response(code, message, http_status=400):
|
||||||
|
"""Unified error response format"""
|
||||||
|
return jsonify({
|
||||||
|
"code": code,
|
||||||
|
"message": message
|
||||||
|
}), http_status
|
||||||
|
|
||||||
|
# ========== Helper: Get Local Time ==========
|
||||||
|
def get_local_time():
|
||||||
|
"""Get local time as fallback when external API fails"""
|
||||||
|
now = datetime.now()
|
||||||
|
weekdays_cn = ["星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"]
|
||||||
|
weekdays_short = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"]
|
||||||
|
weekdays_en = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"timestamp": str(int(now.timestamp())),
|
||||||
|
"timestamp_ms": str(int(now.timestamp() * 1000)),
|
||||||
|
"datetime": now.strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
|
"date_cn": now.strftime("%Y年%m月%d日"),
|
||||||
|
"date": now.strftime("%Y年%m月%d日"),
|
||||||
|
"time": now.strftime("%H:%M:%S"),
|
||||||
|
"week_num": str(now.isocalendar()[1]),
|
||||||
|
"week_cn": weekdays_cn[now.weekday()],
|
||||||
|
"week_short_cn": weekdays_short[now.weekday()],
|
||||||
|
"week_en": weekdays_en[now.weekday()]
|
||||||
|
}
|
||||||
|
|
||||||
|
# ========== API Routes ==========
|
||||||
|
|
||||||
|
@app.route('/api/v3/time', methods=['GET'])
|
||||||
|
def get_time():
|
||||||
|
"""
|
||||||
|
Get current time
|
||||||
|
GET /api/v3/time
|
||||||
|
"""
|
||||||
|
result = safe_request(TIME_API_URL)
|
||||||
|
|
||||||
|
if result and "date" in result and "time" in result:
|
||||||
|
try:
|
||||||
|
date_str = result.get("date", "")
|
||||||
|
time_str = result.get("time", "")
|
||||||
|
week_str = result.get("week", "")
|
||||||
|
|
||||||
|
try:
|
||||||
|
dt = datetime.strptime(f"{date_str} {time_str}", "%Y年%m月%d日 %H:%M:%S")
|
||||||
|
timestamp = int(dt.timestamp())
|
||||||
|
timestamp_ms = int(dt.timestamp() * 1000)
|
||||||
|
datetime_iso = dt.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
except ValueError:
|
||||||
|
now = datetime.now()
|
||||||
|
try:
|
||||||
|
year = int(re.search(r'(\d{4})年', date_str).group(1))
|
||||||
|
month = int(re.search(r'年(\d{2})月', date_str).group(1))
|
||||||
|
day = int(re.search(r'月(\d{2})日', date_str).group(1))
|
||||||
|
hour, minute, second = map(int, time_str.split(':'))
|
||||||
|
dt = datetime(year, month, day, hour, minute, second)
|
||||||
|
timestamp = int(dt.timestamp())
|
||||||
|
timestamp_ms = int(dt.timestamp() * 1000)
|
||||||
|
datetime_iso = dt.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
except:
|
||||||
|
now = datetime.now()
|
||||||
|
timestamp = int(now.timestamp())
|
||||||
|
timestamp_ms = int(now.timestamp() * 1000)
|
||||||
|
datetime_iso = now.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
|
weekdays_cn = ["星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"]
|
||||||
|
weekdays_short = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"]
|
||||||
|
weekdays_en = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
|
||||||
|
|
||||||
|
weekday_index = 0
|
||||||
|
if week_str in weekdays_cn:
|
||||||
|
weekday_index = weekdays_cn.index(week_str)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
weekday_index = dt.weekday()
|
||||||
|
except:
|
||||||
|
weekday_index = datetime.now().weekday()
|
||||||
|
|
||||||
|
formatted_data = {
|
||||||
|
"timestamp": str(timestamp),
|
||||||
|
"timestamp_ms": str(timestamp_ms),
|
||||||
|
"datetime": datetime_iso,
|
||||||
|
"date_cn": date_str,
|
||||||
|
"date": date_str,
|
||||||
|
"time": time_str,
|
||||||
|
"week_num": str(weekday_index + 1),
|
||||||
|
"week_cn": weekdays_cn[weekday_index],
|
||||||
|
"week_short_cn": weekdays_short[weekday_index],
|
||||||
|
"week_en": weekdays_en[weekday_index]
|
||||||
|
}
|
||||||
|
|
||||||
|
return success_response(formatted_data, "Time retrieved successfully")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error parsing time response: {e}")
|
||||||
|
return success_response(get_local_time(), "Time retrieved from local system (fallback)")
|
||||||
|
|
||||||
|
print("Warning: Time API unavailable, using local time")
|
||||||
|
return success_response(get_local_time(), "Time retrieved from local system")
|
||||||
|
|
||||||
|
@app.route('/api/v3/weather/sojson', methods=['GET'])
|
||||||
|
def get_weather_sojson():
|
||||||
|
"""
|
||||||
|
Get weather (sojson API, uses citykey)
|
||||||
|
GET /api/v3/weather/sojson?city=Linyi
|
||||||
|
"""
|
||||||
|
city_name = request.args.get('city', '').strip()
|
||||||
|
|
||||||
|
if not city_name:
|
||||||
|
return error_response(400, "Please provide city name parameter ?city=city_name")
|
||||||
|
|
||||||
|
city_code, error_msg = search_city_code(city_name)
|
||||||
|
|
||||||
|
if error_msg:
|
||||||
|
return error_response(404, error_msg)
|
||||||
|
|
||||||
|
url = WEATHER_SOJSON_URL.format(city_code)
|
||||||
|
result = safe_request(url)
|
||||||
|
|
||||||
|
if result and result.get("status") == 200:
|
||||||
|
return success_response(result, "Weather retrieved successfully")
|
||||||
|
elif result and "error" in result:
|
||||||
|
return error_response(500, result.get("message", "Weather service error"), 500)
|
||||||
|
else:
|
||||||
|
return error_response(500, "Weather service temporarily unavailable", 500)
|
||||||
|
|
||||||
|
@app.route('/api/v3/weather', methods=['GET'])
|
||||||
|
def get_weather_uapis():
|
||||||
|
"""
|
||||||
|
Get weather (uapis.cn general weather API)
|
||||||
|
GET /api/v3/weather?city=Beijing
|
||||||
|
GET /api/v3/weather?adcode=110000
|
||||||
|
GET /api/v3/weather (auto IP geolocation)
|
||||||
|
"""
|
||||||
|
params = {}
|
||||||
|
|
||||||
|
for key in ['city', 'adcode', 'extended', 'forecast', 'hourly', 'minutely', 'indices', 'lang']:
|
||||||
|
value = request.args.get(key)
|
||||||
|
if value is not None:
|
||||||
|
if key in ['extended', 'forecast', 'hourly', 'minutely', 'indices']:
|
||||||
|
params[key] = value.lower() == 'true'
|
||||||
|
else:
|
||||||
|
params[key] = value
|
||||||
|
|
||||||
|
result = safe_request(WEATHER_UAPIS_URL, params=params)
|
||||||
|
|
||||||
|
if result:
|
||||||
|
if "error" in result:
|
||||||
|
return error_response(500, result.get("message", "Weather service error"), 500)
|
||||||
|
return success_response(result, "Weather retrieved successfully")
|
||||||
|
else:
|
||||||
|
return error_response(500, "Weather service temporarily unavailable", 500)
|
||||||
|
|
||||||
|
@app.route('/api/v3/phone', methods=['GET'])
|
||||||
|
def get_phone_info():
|
||||||
|
"""
|
||||||
|
Query phone number location
|
||||||
|
GET /api/v3/phone?phone=13800138000
|
||||||
|
"""
|
||||||
|
phone = request.args.get('phone', '').strip()
|
||||||
|
|
||||||
|
if not phone:
|
||||||
|
return error_response(400, "Please provide phone number parameter ?phone=phone_number")
|
||||||
|
|
||||||
|
if not re.match(r'^1[3-9]\d{9}$', phone):
|
||||||
|
return error_response(400, "Invalid phone number format, please enter an 11-digit Chinese mainland phone number")
|
||||||
|
|
||||||
|
result = safe_request(PHONE_INFO_URL, params={"phone": phone})
|
||||||
|
|
||||||
|
if result:
|
||||||
|
if "error" in result:
|
||||||
|
return error_response(400, result.get("message", "Query failed"), 400)
|
||||||
|
return success_response(result, "Phone number lookup successful")
|
||||||
|
else:
|
||||||
|
return error_response(500, "Phone number lookup service temporarily unavailable", 500)
|
||||||
|
|
||||||
|
# ========== IP Geolocation Routes ==========
|
||||||
|
|
||||||
|
@app.route('/api/v3/ip/me', methods=['GET'])
|
||||||
|
def get_my_ip():
|
||||||
|
"""
|
||||||
|
Get current client's public IP address
|
||||||
|
GET /api/v3/ip/me
|
||||||
|
GET /api/v3/ip/me?source=commercial (returns more detailed info)
|
||||||
|
"""
|
||||||
|
source = request.args.get('source', '')
|
||||||
|
params = {}
|
||||||
|
if source:
|
||||||
|
params['source'] = source
|
||||||
|
|
||||||
|
result = safe_request(IP_MYIP_URL, params=params if params else None)
|
||||||
|
|
||||||
|
if result and "ip" in result:
|
||||||
|
# Format the response
|
||||||
|
formatted_data = {
|
||||||
|
"ip": result.get("ip", ""),
|
||||||
|
"region": result.get("region", ""),
|
||||||
|
"isp": result.get("isp", ""),
|
||||||
|
"llc": result.get("llc", ""),
|
||||||
|
"asn": result.get("asn", ""),
|
||||||
|
"latitude": result.get("latitude", ""),
|
||||||
|
"longitude": result.get("longitude", ""),
|
||||||
|
"beginip": result.get("beginip", ""),
|
||||||
|
"endip": result.get("endip", "")
|
||||||
|
}
|
||||||
|
# Add commercial fields if available
|
||||||
|
if "district" in result:
|
||||||
|
formatted_data["district"] = result.get("district", "")
|
||||||
|
if "time_zone" in result:
|
||||||
|
formatted_data["time_zone"] = result.get("time_zone", "")
|
||||||
|
|
||||||
|
return success_response(formatted_data, "IP geolocation successful")
|
||||||
|
elif result and "error" in result:
|
||||||
|
return error_response(500, result.get("message", "IP service error"), 500)
|
||||||
|
else:
|
||||||
|
return error_response(500, "IP geolocation service temporarily unavailable", 500)
|
||||||
|
|
||||||
|
@app.route('/api/v3/ip', methods=['GET'])
|
||||||
|
def get_ip_info():
|
||||||
|
"""
|
||||||
|
Query IP information
|
||||||
|
GET /api/v3/ip?ip=8.8.8.8
|
||||||
|
GET /api/v3/ip?ip=8.8.8.8&source=commercial
|
||||||
|
GET /api/v3/ip (auto-detect current IP)
|
||||||
|
"""
|
||||||
|
ip = request.args.get('ip', '').strip()
|
||||||
|
source = request.args.get('source', '')
|
||||||
|
|
||||||
|
# If no IP provided, use myip endpoint
|
||||||
|
if not ip:
|
||||||
|
# Use IP_SB as fallback for auto-detection
|
||||||
|
try:
|
||||||
|
resp = requests.get("https://api.ip.sb/geoip", timeout=5)
|
||||||
|
if resp.status_code == 200:
|
||||||
|
data = resp.json()
|
||||||
|
formatted_data = {
|
||||||
|
"ip": data.get("ip", ""),
|
||||||
|
"region": f"{data.get('country', '')} {data.get('region', '')} {data.get('city', '')}".strip(),
|
||||||
|
"isp": data.get("isp", ""),
|
||||||
|
"organization": data.get("organization", ""),
|
||||||
|
"asn": data.get("asn", ""),
|
||||||
|
"latitude": data.get("latitude", ""),
|
||||||
|
"longitude": data.get("longitude", ""),
|
||||||
|
"country": data.get("country", ""),
|
||||||
|
"country_code": data.get("country_code", ""),
|
||||||
|
"region_code": data.get("region_code", ""),
|
||||||
|
"city": data.get("city", "")
|
||||||
|
}
|
||||||
|
return success_response(formatted_data, "IP geolocation successful")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Fallback to uapis myip
|
||||||
|
result = safe_request(IP_MYIP_URL)
|
||||||
|
if result and "ip" in result:
|
||||||
|
formatted_data = {
|
||||||
|
"ip": result.get("ip", ""),
|
||||||
|
"region": result.get("region", ""),
|
||||||
|
"isp": result.get("isp", ""),
|
||||||
|
"llc": result.get("llc", ""),
|
||||||
|
"asn": result.get("asn", ""),
|
||||||
|
"latitude": result.get("latitude", ""),
|
||||||
|
"longitude": result.get("longitude", ""),
|
||||||
|
"beginip": result.get("beginip", ""),
|
||||||
|
"endip": result.get("endip", "")
|
||||||
|
}
|
||||||
|
return success_response(formatted_data, "IP geolocation successful")
|
||||||
|
return error_response(500, "Unable to determine IP address", 500)
|
||||||
|
|
||||||
|
# Query specific IP
|
||||||
|
# Try uapis first
|
||||||
|
params = {"ip": ip}
|
||||||
|
if source:
|
||||||
|
params["source"] = source
|
||||||
|
|
||||||
|
result = safe_request(IP_INFO_URL, params=params)
|
||||||
|
|
||||||
|
if result and "ip" in result:
|
||||||
|
formatted_data = {
|
||||||
|
"ip": result.get("ip", ""),
|
||||||
|
"region": result.get("region", ""),
|
||||||
|
"isp": result.get("isp", ""),
|
||||||
|
"llc": result.get("llc", ""),
|
||||||
|
"asn": result.get("asn", ""),
|
||||||
|
"latitude": result.get("latitude", ""),
|
||||||
|
"longitude": result.get("longitude", ""),
|
||||||
|
"beginip": result.get("beginip", ""),
|
||||||
|
"endip": result.get("endip", "")
|
||||||
|
}
|
||||||
|
return success_response(formatted_data, "IP geolocation successful")
|
||||||
|
|
||||||
|
# Fallback to ip.sb
|
||||||
|
try:
|
||||||
|
url = IP_SB_URL.format(ip)
|
||||||
|
resp = requests.get(url, timeout=5)
|
||||||
|
if resp.status_code == 200:
|
||||||
|
data = resp.json()
|
||||||
|
formatted_data = {
|
||||||
|
"ip": data.get("ip", ip),
|
||||||
|
"region": f"{data.get('country', '')} {data.get('region', '')} {data.get('city', '')}".strip(),
|
||||||
|
"isp": data.get("isp", ""),
|
||||||
|
"organization": data.get("organization", ""),
|
||||||
|
"asn": data.get("asn", ""),
|
||||||
|
"latitude": data.get("latitude", ""),
|
||||||
|
"longitude": data.get("longitude", ""),
|
||||||
|
"country": data.get("country", ""),
|
||||||
|
"country_code": data.get("country_code", ""),
|
||||||
|
"region_code": data.get("region_code", ""),
|
||||||
|
"city": data.get("city", "")
|
||||||
|
}
|
||||||
|
return success_response(formatted_data, "IP geolocation successful")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return error_response(500, "IP geolocation service temporarily unavailable", 500)
|
||||||
|
|
||||||
|
@app.route('/api/v3/ip/advanced', methods=['GET'])
|
||||||
|
def get_ip_advanced():
|
||||||
|
"""
|
||||||
|
Advanced IP query with commercial data source
|
||||||
|
GET /api/v3/ip/advanced?ip=8.8.8.8
|
||||||
|
"""
|
||||||
|
ip = request.args.get('ip', '').strip()
|
||||||
|
source = request.args.get('source', 'commercial')
|
||||||
|
|
||||||
|
if not ip:
|
||||||
|
return error_response(400, "Please provide IP address parameter ?ip=8.8.8.8")
|
||||||
|
|
||||||
|
params = {"ip": ip, "source": source}
|
||||||
|
result = safe_request(IP_INFO_URL, params=params)
|
||||||
|
|
||||||
|
if result and "ip" in result:
|
||||||
|
return success_response(result, "IP geolocation successful")
|
||||||
|
elif result and "error" in result:
|
||||||
|
return error_response(500, result.get("message", "IP service error"), 500)
|
||||||
|
else:
|
||||||
|
return error_response(500, "IP geolocation service temporarily unavailable", 500)
|
||||||
|
|
||||||
|
@app.route('/api/v3', methods=['GET'])
|
||||||
|
def api_index():
|
||||||
|
"""API directory"""
|
||||||
|
return jsonify({
|
||||||
|
"code": 200,
|
||||||
|
"message": "Unified API Gateway v3.0",
|
||||||
|
"version": "3.0.0",
|
||||||
|
"endpoints": {
|
||||||
|
"time": {
|
||||||
|
"path": "/api/v3/time",
|
||||||
|
"method": "GET",
|
||||||
|
"description": "Get current time",
|
||||||
|
"example": "/api/v3/time"
|
||||||
|
},
|
||||||
|
"weather_sojson": {
|
||||||
|
"path": "/api/v3/weather/sojson",
|
||||||
|
"method": "GET",
|
||||||
|
"description": "Get weather (sojson, uses citykey)",
|
||||||
|
"params": {"city": "City name"},
|
||||||
|
"example": "/api/v3/weather/sojson?city=Linyi"
|
||||||
|
},
|
||||||
|
"weather": {
|
||||||
|
"path": "/api/v3/weather",
|
||||||
|
"method": "GET",
|
||||||
|
"description": "Get weather (uapis.cn general API)",
|
||||||
|
"params": {"city": "City name", "forecast": "Enable forecast", "extended": "Enable extended fields"},
|
||||||
|
"example": "/api/v3/weather?city=Beijing&forecast=true"
|
||||||
|
},
|
||||||
|
"phone": {
|
||||||
|
"path": "/api/v3/phone",
|
||||||
|
"method": "GET",
|
||||||
|
"description": "Query phone number location",
|
||||||
|
"params": {"phone": "Phone number"},
|
||||||
|
"example": "/api/v3/phone?phone=13800138000"
|
||||||
|
},
|
||||||
|
"ip_me": {
|
||||||
|
"path": "/api/v3/ip/me",
|
||||||
|
"method": "GET",
|
||||||
|
"description": "Get your public IP address",
|
||||||
|
"params": {"source": "commercial (optional)"},
|
||||||
|
"example": "/api/v3/ip/me"
|
||||||
|
},
|
||||||
|
"ip": {
|
||||||
|
"path": "/api/v3/ip",
|
||||||
|
"method": "GET",
|
||||||
|
"description": "Query IP information",
|
||||||
|
"params": {"ip": "IP address (optional)", "source": "commercial (optional)"},
|
||||||
|
"example": "/api/v3/ip?ip=8.8.8.8"
|
||||||
|
},
|
||||||
|
"ip_advanced": {
|
||||||
|
"path": "/api/v3/ip/advanced",
|
||||||
|
"method": "GET",
|
||||||
|
"description": "Advanced IP query with commercial data",
|
||||||
|
"params": {"ip": "IP address", "source": "commercial (default)"},
|
||||||
|
"example": "/api/v3/ip/advanced?ip=8.8.8.8"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
# ========== Error Handlers ==========
|
||||||
|
@app.errorhandler(404)
|
||||||
|
def not_found(error):
|
||||||
|
return jsonify({
|
||||||
|
"code": 404,
|
||||||
|
"message": "Endpoint not found, visit /api/v3 to see available endpoints"
|
||||||
|
}), 404
|
||||||
|
|
||||||
|
@app.errorhandler(500)
|
||||||
|
def server_error(error):
|
||||||
|
return jsonify({
|
||||||
|
"code": 500,
|
||||||
|
"message": "Internal server error"
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
# ========== Startup ==========
|
||||||
|
if __name__ == '__main__':
|
||||||
|
load_city_codes()
|
||||||
|
app.run(host='0.0.0.0', port=880, debug=False)
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 DVS (dvs-dvsxt)
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
# 🌐 Unified API Gateway
|
||||||
|
|
||||||
|
> A unified API gateway providing **time, weather, phone number lookup, and IP geolocation** services via a consistent `/api/v3/` interface.
|
||||||
|
|
||||||
|
The **Unified API Gateway** wraps multiple external APIs behind a clean, unified `/api/v3/` REST interface. It provides current time, weather (by city / adcode / auto-IP), phone number location, and IP geolocation — all with consistent response formatting and graceful fallbacks.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
| Endpoint | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| 🕐 **Time** | Get current time with timestamp / datetime / Chinese & English weekday |
|
||||||
|
| 🌤️ **Weather** | Weather by city name / adcode / auto-IP (sojson + uapis.cn dual source) |
|
||||||
|
| 📱 **Phone Lookup** | Query Chinese mainland phone number location & carrier |
|
||||||
|
| 🌍 **IP Geolocation** | Query IP info / your public IP / advanced commercial data |
|
||||||
|
| 🎯 **Precise City Matching** | Smart city name → adcode matching (from `adcode.txt`) |
|
||||||
|
| ⚡ **Graceful Fallback** | Automatic fallback when external APIs fail |
|
||||||
|
| 📦 **Unified Format** | Consistent `{code, message, data}` response structure |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Quick Start
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
- Python 3.8+
|
||||||
|
- `flask`, `requests`
|
||||||
|
|
||||||
|
### Install & Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install dependencies
|
||||||
|
pip install flask requests
|
||||||
|
|
||||||
|
# Run the gateway (loads adcode.txt, listens on port 880)
|
||||||
|
python API.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# API directory / index
|
||||||
|
curl http://localhost:880/api/v3
|
||||||
|
|
||||||
|
# Current time
|
||||||
|
curl http://localhost:880/api/v3/time
|
||||||
|
|
||||||
|
# Weather by city
|
||||||
|
curl "http://localhost:880/api/v3/weather?city=Beijing"
|
||||||
|
|
||||||
|
# Weather by adcode
|
||||||
|
curl "http://localhost:880/api/v3/weather?adcode=110000"
|
||||||
|
|
||||||
|
# Phone number lookup
|
||||||
|
curl "http://localhost:880/api/v3/phone?phone=13800138000"
|
||||||
|
|
||||||
|
# Your public IP
|
||||||
|
curl "http://localhost:880/api/v3/ip/me"
|
||||||
|
|
||||||
|
# IP geolocation
|
||||||
|
curl "http://localhost:880/api/v3/ip?ip=8.8.8.8"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔌 API Endpoints
|
||||||
|
|
||||||
|
### GET `/api/v3`
|
||||||
|
API directory / index — lists all available endpoints.
|
||||||
|
|
||||||
|
### GET `/api/v3/time`
|
||||||
|
Get current time.
|
||||||
|
- **Params**: none
|
||||||
|
- **Response**: timestamp, datetime, date (CN), time, week (CN/EN)
|
||||||
|
|
||||||
|
### GET `/api/v3/weather/sojson`
|
||||||
|
Get weather (sojson API, uses citykey).
|
||||||
|
- **Params**: `city` (city name, e.g. `Linyi`)
|
||||||
|
|
||||||
|
### GET `/api/v3/weather`
|
||||||
|
Get weather (uapis.cn general weather API).
|
||||||
|
- **Params**:
|
||||||
|
- `city` — city name
|
||||||
|
- `adcode` — administrative region code
|
||||||
|
- `forecast` / `extended` / `hourly` / `minutely` / `indices` — booleans
|
||||||
|
- `lang` — language
|
||||||
|
- **Note**: with no params, auto-detects by IP
|
||||||
|
|
||||||
|
### GET `/api/v3/phone`
|
||||||
|
Query phone number location.
|
||||||
|
- **Params**: `phone` — 11-digit Chinese mainland mobile number (validated `^1[3-9]\d{9}$`)
|
||||||
|
|
||||||
|
### GET `/api/v3/ip/me`
|
||||||
|
Get your public IP address.
|
||||||
|
- **Params**: `source=commercial` (optional, returns more detail)
|
||||||
|
|
||||||
|
### GET `/api/v3/ip`
|
||||||
|
Query IP information.
|
||||||
|
- **Params**: `ip` (IP address, optional — auto-detects if omitted), `source=commercial` (optional)
|
||||||
|
|
||||||
|
### GET `/api/v3/ip/advanced`
|
||||||
|
Advanced IP query with commercial data source.
|
||||||
|
- **Params**: `ip` (required), `source` (default `commercial`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 Response Format
|
||||||
|
|
||||||
|
**Success:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"message": "Success",
|
||||||
|
"data": { }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 404,
|
||||||
|
"message": "Error description"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🗂️ Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
Unified-API-Gateway/
|
||||||
|
├── API.py # Flask API gateway (main program)
|
||||||
|
├── adcode.txt # City name → adcode mapping data
|
||||||
|
└── README.md # This document
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📄 License
|
||||||
|
|
||||||
|
This project is licensed under the **MIT License**. See [LICENSE](LICENSE) for details.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🙏 Credits
|
||||||
|
|
||||||
|
- Time API: [api.zxki.cn](https://api.zxki.cn)
|
||||||
|
- Weather APIs: [sojson](http://t.weather.sojson.com), [uapis.cn](https://uapis.cn)
|
||||||
|
- Phone / IP APIs: [uapis.cn](https://uapis.cn), [ip.sb](https://ip.sb)
|
||||||
+2621
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user