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