feat: Add real-time visitor map with IP geolocation

- Add latitude/longitude columns to visitor_logs model
- Update visitor_service to fetch and store coordinates from ip-api.com
- Add /admin/map-data API endpoint for map visualization
- Create VisitorMap component using Leaflet/OpenStreetMap
- Integrate map into visitor-stats admin page
- 30-second auto-refresh with animation for new visitors
- Color-coded markers (red: active, blue: recent)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
AutonetSellCar Deploy
2026-01-05 18:31:22 +09:00
parent 4858965087
commit b8f0ae4d28
7 changed files with 469 additions and 3 deletions

View File

@@ -15,7 +15,7 @@ from sqlalchemy import func
from ..models.visitor import VisitorLog, VisitorDailyStats, VisitorSession
# IP Geolocation service (free, 45 req/min limit)
IP_API_URL = "http://ip-api.com/json/{ip}?fields=status,country,countryCode,regionName,city"
IP_API_URL = "http://ip-api.com/json/{ip}?fields=status,country,countryCode,regionName,city,lat,lon"
# Cache for IP geolocation results (in-memory, simple)
_geo_cache: Dict[str, Dict] = {}
@@ -77,7 +77,7 @@ async def get_geo_info(ip: str) -> Optional[Dict]:
if ip.startswith(('127.', '192.168.', '10.', '172.16.', '172.17.', '172.18.', '172.19.',
'172.20.', '172.21.', '172.22.', '172.23.', '172.24.', '172.25.',
'172.26.', '172.27.', '172.28.', '172.29.', '172.30.', '172.31.', 'localhost', '::1')):
return {"country": "Local", "country_code": "LO", "region": "", "city": ""}
return {"country": "Local", "country_code": "LO", "region": "", "city": "", "latitude": None, "longitude": None}
try:
async with httpx.AsyncClient() as client:
@@ -94,6 +94,8 @@ async def get_geo_info(ip: str) -> Optional[Dict]:
"country_code": data.get("countryCode", ""),
"region": data.get("regionName", ""),
"city": data.get("city", ""),
"latitude": data.get("lat"),
"longitude": data.get("lon"),
}
# Cache the result
_geo_cache[ip] = result
@@ -165,6 +167,8 @@ async def log_visit(
country_code=geo_info.get("country_code"),
city=geo_info.get("city"),
region=geo_info.get("region"),
latitude=geo_info.get("latitude"),
longitude=geo_info.get("longitude"),
utm_source=utm_source,
utm_medium=utm_medium,
utm_campaign=utm_campaign,