164 lines
6.4 KiB
Python
164 lines
6.4 KiB
Python
import asyncio
|
|
|
|
import aiohttp
|
|
from utils.async_cache import aget_cache, aset_cache, adelete_cache
|
|
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 django.core.cache import caches
|
|
|
|
default_cache = caches['default']
|
|
|
|
IP_API_URL = "http://ip-api.com/json/{ip}?lang=zh-CN"
|
|
IP_CACHE_TIMEOUT = 3600 # 1小时
|
|
REQUEST_TIMEOUT = 5 # 5秒超时
|
|
|
|
|
|
class GetIPDataView(APIView):
|
|
"""
|
|
IP定位视图 - 基于 ip-api.com 免费地理定位服务
|
|
"""
|
|
permission_classes = [AllowAny]
|
|
|
|
@swagger_auto_schema(
|
|
tags=['API'],
|
|
operation_summary='获取IP地理信息',
|
|
operation_description='通过IP地址获取地理定位信息,支持指定IP或自动获取客户端IP',
|
|
manual_parameters=[
|
|
openapi.Parameter(
|
|
'ip',
|
|
openapi.IN_QUERY,
|
|
description='要查询的IP地址(可选,不传则自动获取客户端IP)',
|
|
type=openapi.TYPE_STRING,
|
|
required=False
|
|
),
|
|
],
|
|
responses={
|
|
200: openapi.Response(
|
|
description="Success",
|
|
schema=openapi.Schema(
|
|
type=openapi.TYPE_OBJECT,
|
|
properties={
|
|
'code': openapi.Schema(type=openapi.TYPE_INTEGER, example=200),
|
|
'message': openapi.Schema(type=openapi.TYPE_STRING, example='success'),
|
|
'data': openapi.Schema(
|
|
type=openapi.TYPE_OBJECT,
|
|
properties={
|
|
'ip': openapi.Schema(type=openapi.TYPE_STRING, description='IP地址'),
|
|
'country': openapi.Schema(type=openapi.TYPE_STRING, description='国家'),
|
|
'regionName': openapi.Schema(type=openapi.TYPE_STRING, description='省份/地区'),
|
|
'city': openapi.Schema(type=openapi.TYPE_STRING, description='城市'),
|
|
'isp': openapi.Schema(type=openapi.TYPE_STRING, description='ISP服务商'),
|
|
'lat': openapi.Schema(type=openapi.TYPE_NUMBER, description='纬度'),
|
|
'lon': openapi.Schema(type=openapi.TYPE_NUMBER, description='经度'),
|
|
'timezone': openapi.Schema(type=openapi.TYPE_STRING, description='时区'),
|
|
}
|
|
),
|
|
}
|
|
)
|
|
),
|
|
400: openapi.Response(description="参数错误"),
|
|
502: openapi.Response(description="第三方服务异常"),
|
|
}
|
|
)
|
|
async def get(self, request):
|
|
# 获取要查询的IP地址
|
|
ip = request.GET.get('ip', '').strip()
|
|
if not ip:
|
|
ip = self._get_client_ip(request)
|
|
|
|
if not ip:
|
|
return Response(
|
|
{"code": 400, "message": "无法获取IP地址", "data": None},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
return await self._fetch_ip_location(ip)
|
|
|
|
def _get_client_ip(self, request):
|
|
"""
|
|
从 request.META 中获取客户端真实IP地址
|
|
"""
|
|
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
|
|
if x_forwarded_for:
|
|
ip = x_forwarded_for.split(',')[0].strip()
|
|
if ip:
|
|
return ip
|
|
|
|
x_real_ip = request.META.get('HTTP_X_REAL_IP')
|
|
if x_real_ip:
|
|
return x_real_ip.strip()
|
|
|
|
remote_addr = request.META.get('REMOTE_ADDR')
|
|
if remote_addr:
|
|
return remote_addr.strip()
|
|
|
|
return None
|
|
|
|
async def _fetch_ip_location(self, ip):
|
|
"""
|
|
调用 ip-api.com 获取IP地理信息,支持Redis缓存(全异步:aiohttp + sync_to_async 缓存兜底)
|
|
"""
|
|
cache_key = f"ip_location:{ip}"
|
|
|
|
# 先尝试从缓存获取(django-redis 为同步客户端,用 sync_to_async 兜底)
|
|
cached_data = await aget_cache(cache_key)
|
|
if cached_data:
|
|
return Response(
|
|
{"code": 200, "message": "success", "data": cached_data},
|
|
status=status.HTTP_200_OK
|
|
)
|
|
|
|
try:
|
|
url = IP_API_URL.format(ip=ip)
|
|
timeout = aiohttp.ClientTimeout(total=REQUEST_TIMEOUT)
|
|
async with aiohttp.ClientSession(timeout=timeout) as session:
|
|
async with session.get(url) as resp:
|
|
if resp.status != 200:
|
|
return Response(
|
|
{"code": 502, "message": "IP定位服务异常,请稍后重试", "data": None},
|
|
status=status.HTTP_502_BAD_GATEWAY
|
|
)
|
|
result = await resp.json()
|
|
|
|
if result.get('status') != 'success':
|
|
return Response(
|
|
{"code": 400, "message": f"IP查询失败:{result.get('message', '未知错误')}", "data": None},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
ip_info = {
|
|
'ip': result.get('query', ip),
|
|
'country': result.get('country', ''),
|
|
'regionName': result.get('regionName', ''),
|
|
'city': result.get('city', ''),
|
|
'isp': result.get('isp', ''),
|
|
'lat': result.get('lat', 0),
|
|
'lon': result.get('lon', 0),
|
|
'timezone': result.get('timezone', ''),
|
|
}
|
|
|
|
# 写入缓存(sync_to_async 兜底)
|
|
await aset_cache(cache_key, ip_info, IP_CACHE_TIMEOUT)
|
|
|
|
return Response(
|
|
{"code": 200, "message": "success", "data": ip_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
|
|
)
|