init commit

This commit is contained in:
2025-09-25 08:05:25 +09:00
commit 4d7551d4f1
56 changed files with 5977 additions and 0 deletions

View File

@@ -0,0 +1,312 @@
from fastapi import FastAPI, HTTPException, Depends, Query
from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, text
from shared.config import settings
from shared.database import get_db
from shared.cache import CacheService
from services.location_service.models import UserLocation, LocationHistory
from services.user_service.main import get_current_user
from services.user_service.models import User
from pydantic import BaseModel, Field
from typing import List, Optional
from datetime import datetime, timedelta
import math
app = FastAPI(title="Location Service", version="1.0.0")
# CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=settings.CORS_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
class LocationUpdate(BaseModel):
latitude: float = Field(..., ge=-90, le=90)
longitude: float = Field(..., ge=-180, le=180)
accuracy: Optional[float] = Field(None, ge=0)
altitude: Optional[float] = None
speed: Optional[float] = Field(None, ge=0)
heading: Optional[float] = Field(None, ge=0, le=360)
class LocationResponse(BaseModel):
user_id: int
latitude: float
longitude: float
accuracy: Optional[float]
updated_at: datetime
class Config:
from_attributes = True
class NearbyUserResponse(BaseModel):
user_id: int
latitude: float
longitude: float
distance_meters: float
last_seen: datetime
def calculate_distance(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
"""Calculate distance between two points using Haversine formula (in meters)"""
R = 6371000 # Earth's radius in meters
lat1_rad = math.radians(lat1)
lat2_rad = math.radians(lat2)
delta_lat = math.radians(lat2 - lat1)
delta_lon = math.radians(lon2 - lon1)
a = (math.sin(delta_lat / 2) * math.sin(delta_lat / 2) +
math.cos(lat1_rad) * math.cos(lat2_rad) *
math.sin(delta_lon / 2) * math.sin(delta_lon / 2))
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
distance = R * c
return distance
@app.post("/api/v1/update-location")
async def update_user_location(
location_data: LocationUpdate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
"""Update user's current location"""
if not current_user.location_sharing_enabled:
raise HTTPException(status_code=403, detail="Location sharing is disabled")
# Update or create current location
result = await db.execute(
select(UserLocation).filter(UserLocation.user_id == current_user.id)
)
user_location = result.scalars().first()
if user_location:
user_location.latitude = location_data.latitude
user_location.longitude = location_data.longitude
user_location.accuracy = location_data.accuracy
user_location.altitude = location_data.altitude
user_location.speed = location_data.speed
user_location.heading = location_data.heading
else:
user_location = UserLocation(
user_id=current_user.id,
latitude=location_data.latitude,
longitude=location_data.longitude,
accuracy=location_data.accuracy,
altitude=location_data.altitude,
speed=location_data.speed,
heading=location_data.heading,
)
db.add(user_location)
# Save to history
location_history = LocationHistory(
user_id=current_user.id,
latitude=location_data.latitude,
longitude=location_data.longitude,
accuracy=location_data.accuracy,
recorded_at=datetime.utcnow(),
)
db.add(location_history)
await db.commit()
# Cache location for fast access
await CacheService.set_location(
current_user.id,
location_data.latitude,
location_data.longitude,
expire=300 # 5 minutes
)
return {"message": "Location updated successfully"}
@app.get("/api/v1/user-location/{user_id}", response_model=LocationResponse)
async def get_user_location(
user_id: int,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
"""Get specific user's location (if sharing is enabled)"""
# Check if requested user exists and has location sharing enabled
result = await db.execute(select(User).filter(User.id == user_id))
target_user = result.scalars().first()
if not target_user:
raise HTTPException(status_code=404, detail="User not found")
if not target_user.location_sharing_enabled and target_user.id != current_user.id:
raise HTTPException(status_code=403, detail="User has disabled location sharing")
# Try cache first
cached_location = await CacheService.get_location(user_id)
if cached_location:
lat, lng = cached_location
return LocationResponse(
user_id=user_id,
latitude=lat,
longitude=lng,
accuracy=None,
updated_at=datetime.utcnow()
)
# Get from database
result = await db.execute(
select(UserLocation).filter(UserLocation.user_id == user_id)
)
user_location = result.scalars().first()
if not user_location:
raise HTTPException(status_code=404, detail="Location not found")
return LocationResponse.model_validate(user_location)
@app.get("/api/v1/nearby-users", response_model=List[NearbyUserResponse])
async def get_nearby_users(
latitude: float = Query(..., ge=-90, le=90),
longitude: float = Query(..., ge=-180, le=180),
radius_km: float = Query(1.0, ge=0.1, le=10.0),
limit: int = Query(50, ge=1, le=200),
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
"""Find users within specified radius"""
# Convert radius to degrees (approximate)
# 1 degree ≈ 111 km
radius_deg = radius_km / 111.0
# Query for nearby users with location sharing enabled
# Using bounding box for initial filtering (more efficient than distance calculation)
query = text("""
SELECT
ul.user_id,
ul.latitude,
ul.longitude,
ul.updated_at,
u.location_sharing_enabled
FROM user_locations ul
JOIN users u ON ul.user_id = u.id
WHERE u.location_sharing_enabled = true
AND u.is_active = true
AND ul.user_id != :current_user_id
AND ul.latitude BETWEEN :lat_min AND :lat_max
AND ul.longitude BETWEEN :lng_min AND :lng_max
AND ul.updated_at > :time_threshold
LIMIT :limit_val
""")
time_threshold = datetime.utcnow() - timedelta(minutes=15) # Only recent locations
result = await db.execute(query, {
"current_user_id": current_user.id,
"lat_min": latitude - radius_deg,
"lat_max": latitude + radius_deg,
"lng_min": longitude - radius_deg,
"lng_max": longitude + radius_deg,
"time_threshold": time_threshold,
"limit_val": limit
})
nearby_users = []
for row in result:
# Calculate exact distance
distance = calculate_distance(
latitude, longitude,
row.latitude, row.longitude
)
# Filter by exact radius
if distance <= radius_km * 1000: # Convert km to meters
nearby_users.append(NearbyUserResponse(
user_id=row.user_id,
latitude=row.latitude,
longitude=row.longitude,
distance_meters=distance,
last_seen=row.updated_at
))
# Sort by distance
nearby_users.sort(key=lambda x: x.distance_meters)
return nearby_users
@app.get("/api/v1/location-history")
async def get_location_history(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
hours: int = Query(24, ge=1, le=168), # Max 1 week
limit: int = Query(100, ge=1, le=1000)
):
"""Get user's location history"""
time_threshold = datetime.utcnow() - timedelta(hours=hours)
result = await db.execute(
select(LocationHistory)
.filter(
LocationHistory.user_id == current_user.id,
LocationHistory.recorded_at >= time_threshold
)
.order_by(LocationHistory.recorded_at.desc())
.limit(limit)
)
history = result.scalars().all()
return [
{
"latitude": entry.latitude,
"longitude": entry.longitude,
"accuracy": entry.accuracy,
"recorded_at": entry.recorded_at
}
for entry in history
]
@app.delete("/api/v1/location")
async def delete_user_location(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
"""Delete user's current location"""
# Delete current location
result = await db.execute(
select(UserLocation).filter(UserLocation.user_id == current_user.id)
)
user_location = result.scalars().first()
if user_location:
await db.delete(user_location)
await db.commit()
# Clear cache
await CacheService.delete(f"location:{current_user.id}")
return {"message": "Location deleted successfully"}
@app.get("/api/v1/health")
async def health_check():
"""Health check endpoint"""
return {"status": "healthy", "service": "location-service"}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8003)