Files
banban/mini-program/app/routers/devices.py

254 lines
7.9 KiB
Python

import logging
from collections.abc import Mapping
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException, Query, Request
from pydantic import BaseModel
from sqlalchemy import text
try:
from app.security import get_current_user_id
from app.service import get_db_session
from app.schemas.location import (
DeviceLocationCurrentResponse,
DeviceLocationTrajectoryItem,
DeviceLocationTrajectoryResponse,
)
from app.service.location import get_device_current_location, get_device_trajectory
except ModuleNotFoundError:
from security import get_current_user_id
from service import get_db_session
from schemas.location import (
DeviceLocationCurrentResponse,
DeviceLocationTrajectoryItem,
DeviceLocationTrajectoryResponse,
)
from service.location import get_device_current_location, get_device_trajectory
router = APIRouter(prefix="/devices", tags=["devices"])
logger = logging.getLogger("app.devices")
class DeviceMessageItem(BaseModel):
id: int
conversation_id: int
role_key: str
is_user: bool
speaker: str
content: str
timestamp: float
created_at: datetime
class DeviceMessageListResponse(BaseModel):
items: list[DeviceMessageItem]
total: int
next_cursor: int | None = None
def _ensure_device_access(db, device_id: str, user_id: int) -> None:
row = (
db.execute(
text(
"""
SELECT 1
FROM device_bindings
WHERE device_id = :device_id
AND owner_user_id = :user_id
AND status = 1
LIMIT 1
"""
),
{"device_id": device_id, "user_id": user_id},
)
.mappings()
.first()
)
if row is None:
raise HTTPException(status_code=404, detail="device not found")
def _row_to_message_item(row: Mapping) -> DeviceMessageItem:
is_user = bool(row["is_user"])
return DeviceMessageItem(
id=int(row["id"]),
conversation_id=int(row["conversation_id"]),
role_key=str(row["role_key"]),
is_user=is_user,
speaker="user" if is_user else "assistant",
content=str(row["content"]),
timestamp=float(row["timestamp"]),
created_at=row["created_at"],
)
def _row_to_current_location_response(row: Mapping) -> DeviceLocationCurrentResponse:
return DeviceLocationCurrentResponse(
child_id=int(row["child_id"]),
child_name=row.get("child_name"),
device_id=str(row["device_id"]),
coord_type=str(row["coord_type"]),
lat=float(row["lat"]),
lng=float(row["lng"]),
accuracy_m=row["accuracy_m"],
altitude_m=float(row["altitude_m"]) if row["altitude_m"] is not None else None,
speed_mps=float(row["speed_mps"]) if row["speed_mps"] is not None else None,
heading_deg=row["heading_deg"],
source=int(row["source"]),
battery_pct=row["battery_pct"],
device_time=row["device_time"],
server_time=row["server_time"],
updated_at=row["updated_at"],
)
def _row_to_trajectory_item(row: Mapping, *, child_name: str | None) -> DeviceLocationTrajectoryItem:
return DeviceLocationTrajectoryItem(
id=int(row["id"]),
child_id=int(row["child_id"]),
child_name=child_name,
device_id=str(row["device_id"]),
coord_type=str(row["coord_type"]),
lat=float(row["lat"]),
lng=float(row["lng"]),
accuracy_m=row["accuracy_m"],
altitude_m=float(row["altitude_m"]) if row["altitude_m"] is not None else None,
speed_mps=float(row["speed_mps"]) if row["speed_mps"] is not None else None,
heading_deg=row["heading_deg"],
source=int(row["source"]),
battery_pct=row["battery_pct"],
device_time=row["device_time"],
server_time=row["server_time"],
created_at=row["created_at"],
)
@router.get("/{device_id}/messages", response_model=DeviceMessageListResponse)
def list_device_messages(
device_id: str,
request: Request,
cursor: int | None = Query(default=None, ge=1),
limit: int = Query(default=20, ge=1, le=100),
current_user_id: int = Depends(get_current_user_id),
db=Depends(get_db_session),
) -> DeviceMessageListResponse:
_ensure_device_access(db=db, device_id=device_id, user_id=current_user_id)
params = {"device_id": device_id, "limit": limit + 1}
where = "ch.device_id = :device_id"
if cursor is not None:
where += " AND cm.id < :cursor"
params["cursor"] = cursor
rows = (
db.execute(
text(
f"""
SELECT
cm.id,
ch.id AS conversation_id,
ch.role_key,
cm.is_user,
cm.content,
cm.timestamp,
cm.created_at
FROM conversation_messages AS cm
JOIN conversation_histories AS ch
ON ch.id = cm.conversation_id
WHERE {where}
ORDER BY cm.id DESC
LIMIT :limit
"""
),
params,
)
.mappings()
.all()
)
has_more = len(rows) > limit
rows = rows[:limit]
next_cursor = int(rows[-1]["id"]) if has_more and rows else None
logger.info(
"listed device ai messages",
extra={
"event": "device_messages",
"request_id": getattr(request.state, "request_id", None),
"user_id": current_user_id,
"device_id": device_id,
"returned_count": len(rows),
},
)
return DeviceMessageListResponse(
items=[_row_to_message_item(row) for row in rows],
total=len(rows),
next_cursor=next_cursor,
)
@router.get("/{device_id}/location", response_model=DeviceLocationCurrentResponse)
def get_current_device_location(
device_id: str,
request: Request,
current_user_id: int = Depends(get_current_user_id),
db=Depends(get_db_session),
) -> DeviceLocationCurrentResponse:
row = get_device_current_location(db=db, device_id=device_id, user_id=current_user_id)
logger.info(
"device current location fetched",
extra={
"event": "device_current_location",
"request_id": getattr(request.state, "request_id", None),
"user_id": current_user_id,
"device_id": device_id,
"child_id": int(row["child_id"]),
},
)
return _row_to_current_location_response(row)
@router.get("/{device_id}/trajectory", response_model=DeviceLocationTrajectoryResponse)
def get_device_location_trajectory(
device_id: str,
request: Request,
start_at: datetime | None = Query(default=None),
end_at: datetime | None = Query(default=None),
limit: int = Query(default=200, ge=1, le=1000),
current_user_id: int = Depends(get_current_user_id),
db=Depends(get_db_session),
) -> DeviceLocationTrajectoryResponse:
if start_at and end_at and start_at > end_at:
raise HTTPException(status_code=422, detail="start_at must be earlier than end_at")
access, rows = get_device_trajectory(
db=db,
device_id=device_id,
user_id=current_user_id,
start_at=start_at,
end_at=end_at,
limit=limit,
)
logger.info(
"device trajectory fetched",
extra={
"event": "device_trajectory",
"request_id": getattr(request.state, "request_id", None),
"user_id": current_user_id,
"device_id": device_id,
"child_id": access.child_id,
"count": len(rows),
},
)
return DeviceLocationTrajectoryResponse(
items=[_row_to_trajectory_item(row, child_name=access.child_name) for row in rows],
total=len(rows),
start_at=start_at,
end_at=end_at,
)