小程序后端接入 talkingq 共享库并新增消息与定位能力
This commit is contained in:
253
mini-program/app/routers/devices.py
Normal file
253
mini-program/app/routers/devices.py
Normal file
@@ -0,0 +1,253 @@
|
||||
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,
|
||||
)
|
||||
Reference in New Issue
Block a user