小程序后端接入 talkingq 共享库并新增消息与定位能力
This commit is contained in:
327
mini-program/app/service/location.py
Normal file
327
mini-program/app/service/location.py
Normal file
@@ -0,0 +1,327 @@
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
try:
|
||||
from app.db_compat import current_timestamp_sql, select_for_update_clause
|
||||
from app.schemas.location import DeviceLocationReportRequest
|
||||
from app.service.im import DeviceIdentity, authenticate_device_identity
|
||||
except ModuleNotFoundError:
|
||||
from db_compat import current_timestamp_sql, select_for_update_clause
|
||||
from schemas.location import DeviceLocationReportRequest
|
||||
from service.im import DeviceIdentity, authenticate_device_identity
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ParentDeviceAccess:
|
||||
device_id: str
|
||||
child_id: int
|
||||
child_name: str | None
|
||||
|
||||
|
||||
def assert_parent_device_access(db: Session, *, device_id: str, user_id: int) -> ParentDeviceAccess:
|
||||
row = (
|
||||
db.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT
|
||||
db.device_id,
|
||||
db.child_id,
|
||||
c.child_name
|
||||
FROM device_bindings AS db
|
||||
LEFT JOIN children AS c
|
||||
ON c.child_id = db.child_id
|
||||
AND c.status = 1
|
||||
WHERE db.device_id = :device_id
|
||||
AND db.owner_user_id = :user_id
|
||||
AND db.status = 1
|
||||
LIMIT 1
|
||||
"""
|
||||
),
|
||||
{"device_id": device_id, "user_id": user_id},
|
||||
)
|
||||
.mappings()
|
||||
.first()
|
||||
)
|
||||
if not row:
|
||||
raise HTTPException(status_code=404, detail="device not found")
|
||||
if row["child_id"] is None:
|
||||
raise HTTPException(status_code=404, detail="device not bound to a child")
|
||||
|
||||
return ParentDeviceAccess(
|
||||
device_id=str(row["device_id"]),
|
||||
child_id=int(row["child_id"]),
|
||||
child_name=row["child_name"],
|
||||
)
|
||||
|
||||
|
||||
def report_device_location(
|
||||
db: Session,
|
||||
*,
|
||||
device_id: str,
|
||||
serial_number: str,
|
||||
payload: DeviceLocationReportRequest,
|
||||
) -> tuple[DeviceIdentity, Mapping[str, Any]]:
|
||||
device_identity = authenticate_device_identity(
|
||||
db=db,
|
||||
device_id=device_id,
|
||||
serial_number=serial_number,
|
||||
)
|
||||
now_sql = current_timestamp_sql(db)
|
||||
child_id = device_identity.child_id
|
||||
|
||||
try:
|
||||
current_row = _get_current_location_row(db, child_id=child_id, lock=True)
|
||||
params = {
|
||||
"child_id": child_id,
|
||||
"device_id": device_identity.device_id,
|
||||
"coord_type": payload.coord_type,
|
||||
"lat": payload.lat,
|
||||
"lng": payload.lng,
|
||||
"accuracy_m": payload.accuracy_m,
|
||||
"altitude_m": payload.altitude_m,
|
||||
"speed_mps": payload.speed_mps,
|
||||
"heading_deg": payload.heading_deg,
|
||||
"source": payload.source,
|
||||
"battery_pct": payload.battery_pct,
|
||||
"device_time": payload.device_time,
|
||||
}
|
||||
if current_row:
|
||||
db.execute(
|
||||
text(
|
||||
f"""
|
||||
UPDATE child_location_current
|
||||
SET device_id = :device_id,
|
||||
coord_type = :coord_type,
|
||||
lat = :lat,
|
||||
lng = :lng,
|
||||
accuracy_m = :accuracy_m,
|
||||
altitude_m = :altitude_m,
|
||||
speed_mps = :speed_mps,
|
||||
heading_deg = :heading_deg,
|
||||
source = :source,
|
||||
battery_pct = :battery_pct,
|
||||
device_time = :device_time,
|
||||
server_time = {now_sql},
|
||||
updated_at = {now_sql}
|
||||
WHERE child_id = :child_id
|
||||
"""
|
||||
),
|
||||
params,
|
||||
)
|
||||
else:
|
||||
db.execute(
|
||||
text(
|
||||
f"""
|
||||
INSERT INTO child_location_current (
|
||||
child_id,
|
||||
device_id,
|
||||
coord_type,
|
||||
lat,
|
||||
lng,
|
||||
accuracy_m,
|
||||
altitude_m,
|
||||
speed_mps,
|
||||
heading_deg,
|
||||
source,
|
||||
battery_pct,
|
||||
device_time,
|
||||
server_time,
|
||||
updated_at
|
||||
) VALUES (
|
||||
:child_id,
|
||||
:device_id,
|
||||
:coord_type,
|
||||
:lat,
|
||||
:lng,
|
||||
:accuracy_m,
|
||||
:altitude_m,
|
||||
:speed_mps,
|
||||
:heading_deg,
|
||||
:source,
|
||||
:battery_pct,
|
||||
:device_time,
|
||||
{now_sql},
|
||||
{now_sql}
|
||||
)
|
||||
"""
|
||||
),
|
||||
params,
|
||||
)
|
||||
|
||||
history_id = _next_primary_key(db, "child_location_history")
|
||||
insert_history_sql = f"""
|
||||
INSERT INTO child_location_history (
|
||||
{'id,' if history_id is not None else ''}
|
||||
child_id,
|
||||
device_id,
|
||||
coord_type,
|
||||
lat,
|
||||
lng,
|
||||
accuracy_m,
|
||||
altitude_m,
|
||||
speed_mps,
|
||||
heading_deg,
|
||||
source,
|
||||
battery_pct,
|
||||
device_time,
|
||||
server_time,
|
||||
created_at
|
||||
) VALUES (
|
||||
{':id,' if history_id is not None else ''}
|
||||
:child_id,
|
||||
:device_id,
|
||||
:coord_type,
|
||||
:lat,
|
||||
:lng,
|
||||
:accuracy_m,
|
||||
:altitude_m,
|
||||
:speed_mps,
|
||||
:heading_deg,
|
||||
:source,
|
||||
:battery_pct,
|
||||
:device_time,
|
||||
{now_sql},
|
||||
{now_sql}
|
||||
)
|
||||
"""
|
||||
history_params = dict(params)
|
||||
if history_id is not None:
|
||||
history_params["id"] = history_id
|
||||
db.execute(text(insert_history_sql), history_params)
|
||||
db.commit()
|
||||
except Exception:
|
||||
db.rollback()
|
||||
raise
|
||||
|
||||
current_row = _get_current_location_row(db, child_id=child_id, lock=False)
|
||||
if not current_row:
|
||||
raise RuntimeError("current location not found after report")
|
||||
return device_identity, current_row
|
||||
|
||||
|
||||
def get_device_current_location(
|
||||
db: Session,
|
||||
*,
|
||||
device_id: str,
|
||||
user_id: int,
|
||||
) -> Mapping[str, Any]:
|
||||
access = assert_parent_device_access(db, device_id=device_id, user_id=user_id)
|
||||
row = _get_current_location_row(db, child_id=access.child_id, lock=False)
|
||||
if not row or str(row["device_id"]) != device_id:
|
||||
raise HTTPException(status_code=404, detail="location not found")
|
||||
return {**row, "child_name": access.child_name}
|
||||
|
||||
|
||||
def get_device_trajectory(
|
||||
db: Session,
|
||||
*,
|
||||
device_id: str,
|
||||
user_id: int,
|
||||
start_at: datetime | None,
|
||||
end_at: datetime | None,
|
||||
limit: int,
|
||||
) -> tuple[ParentDeviceAccess, list[Mapping[str, Any]]]:
|
||||
access = assert_parent_device_access(db, device_id=device_id, user_id=user_id)
|
||||
params: dict[str, Any] = {
|
||||
"device_id": device_id,
|
||||
"child_id": access.child_id,
|
||||
"fetch_limit": limit,
|
||||
}
|
||||
where = """
|
||||
device_id = :device_id
|
||||
AND child_id = :child_id
|
||||
"""
|
||||
if start_at is not None:
|
||||
where += " AND device_time >= :start_at"
|
||||
params["start_at"] = start_at
|
||||
if end_at is not None:
|
||||
where += " AND device_time <= :end_at"
|
||||
params["end_at"] = end_at
|
||||
|
||||
rows = (
|
||||
db.execute(
|
||||
text(
|
||||
f"""
|
||||
SELECT
|
||||
id,
|
||||
child_id,
|
||||
device_id,
|
||||
coord_type,
|
||||
lat,
|
||||
lng,
|
||||
accuracy_m,
|
||||
altitude_m,
|
||||
speed_mps,
|
||||
heading_deg,
|
||||
source,
|
||||
battery_pct,
|
||||
device_time,
|
||||
server_time,
|
||||
created_at
|
||||
FROM child_location_history
|
||||
WHERE {where}
|
||||
ORDER BY device_time DESC, id DESC
|
||||
LIMIT :fetch_limit
|
||||
"""
|
||||
),
|
||||
params,
|
||||
)
|
||||
.mappings()
|
||||
.all()
|
||||
)
|
||||
rows = list(rows)
|
||||
rows.reverse()
|
||||
return access, rows
|
||||
|
||||
|
||||
def _get_current_location_row(
|
||||
db: Session,
|
||||
*,
|
||||
child_id: int,
|
||||
lock: bool,
|
||||
) -> Mapping[str, Any] | None:
|
||||
lock_clause = select_for_update_clause(db) if lock else ""
|
||||
return (
|
||||
db.execute(
|
||||
text(
|
||||
f"""
|
||||
SELECT
|
||||
child_id,
|
||||
device_id,
|
||||
coord_type,
|
||||
lat,
|
||||
lng,
|
||||
accuracy_m,
|
||||
altitude_m,
|
||||
speed_mps,
|
||||
heading_deg,
|
||||
source,
|
||||
battery_pct,
|
||||
device_time,
|
||||
server_time,
|
||||
updated_at
|
||||
FROM child_location_current
|
||||
WHERE child_id = :child_id
|
||||
LIMIT 1{lock_clause}
|
||||
"""
|
||||
),
|
||||
{"child_id": child_id},
|
||||
)
|
||||
.mappings()
|
||||
.first()
|
||||
)
|
||||
|
||||
|
||||
def _next_primary_key(db: Session, table_name: str) -> int | None:
|
||||
bind = db.get_bind()
|
||||
if bind is None or bind.dialect.name != "sqlite":
|
||||
return None
|
||||
return int(
|
||||
db.execute(text(f"SELECT COALESCE(MAX(id), 0) + 1 FROM {table_name}")).scalar_one()
|
||||
)
|
||||
Reference in New Issue
Block a user