修复告警定位无效坐标处理
This commit is contained in:
@@ -56,9 +56,7 @@ function formatOptionalNumber(value?: number | null, digits = 1): string | null
|
|||||||
function getLocationAddress(point: DeviceLocation | DeviceTrajectoryPoint): string {
|
function getLocationAddress(point: DeviceLocation | DeviceTrajectoryPoint): string {
|
||||||
const address = String(point.address || '').trim()
|
const address = String(point.address || '').trim()
|
||||||
if (address) return address
|
if (address) return address
|
||||||
if (point.address_resolve_status === 0) return '地点解析中'
|
return '地址解析中'
|
||||||
if (point.address_resolve_status === 2) return '地点暂时无法解析'
|
|
||||||
return '当前地点未知'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildLocationDetailLines(
|
function buildLocationDetailLines(
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
|
from datetime import datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
@@ -17,6 +18,12 @@ class DeviceAlarmDAO(BaseDAO):
|
|||||||
location: Mapping[str, Any] | None = None,
|
location: Mapping[str, Any] | None = None,
|
||||||
) -> int:
|
) -> int:
|
||||||
location = location or {}
|
location = location or {}
|
||||||
|
address = str(location.get("address") or "").strip() or None
|
||||||
|
address_status = location.get("address_resolve_status")
|
||||||
|
if address and address_status is None:
|
||||||
|
address_status = 1
|
||||||
|
elif address_status is None and location.get("lat") is not None and location.get("lng") is not None:
|
||||||
|
address_status = 0
|
||||||
result = await self.execute(
|
result = await self.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO device_alarm_events (
|
INSERT INTO device_alarm_events (
|
||||||
@@ -28,6 +35,8 @@ class DeviceAlarmDAO(BaseDAO):
|
|||||||
lat,
|
lat,
|
||||||
lng,
|
lng,
|
||||||
location_updated_at,
|
location_updated_at,
|
||||||
|
address,
|
||||||
|
address_resolved_at,
|
||||||
address_resolve_status,
|
address_resolve_status,
|
||||||
created_at
|
created_at
|
||||||
)
|
)
|
||||||
@@ -40,6 +49,8 @@ class DeviceAlarmDAO(BaseDAO):
|
|||||||
:lat,
|
:lat,
|
||||||
:lng,
|
:lng,
|
||||||
:location_updated_at,
|
:location_updated_at,
|
||||||
|
:address,
|
||||||
|
:address_resolved_at,
|
||||||
:address_resolve_status,
|
:address_resolve_status,
|
||||||
CURRENT_TIMESTAMP
|
CURRENT_TIMESTAMP
|
||||||
)
|
)
|
||||||
@@ -53,7 +64,9 @@ class DeviceAlarmDAO(BaseDAO):
|
|||||||
"lat": location.get("lat"),
|
"lat": location.get("lat"),
|
||||||
"lng": location.get("lng"),
|
"lng": location.get("lng"),
|
||||||
"location_updated_at": location.get("updated_at"),
|
"location_updated_at": location.get("updated_at"),
|
||||||
"address_resolve_status": 0 if location.get("lat") is not None and location.get("lng") is not None else None,
|
"address": address,
|
||||||
|
"address_resolved_at": location.get("address_resolved_at"),
|
||||||
|
"address_resolve_status": address_status,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
await self.commit()
|
await self.commit()
|
||||||
@@ -100,7 +113,10 @@ class DeviceAlarmDAO(BaseDAO):
|
|||||||
coord_type,
|
coord_type,
|
||||||
lat,
|
lat,
|
||||||
lng,
|
lng,
|
||||||
updated_at
|
updated_at,
|
||||||
|
address,
|
||||||
|
address_resolved_at,
|
||||||
|
address_resolve_status
|
||||||
FROM child_location_current
|
FROM child_location_current
|
||||||
WHERE child_id = :child_id
|
WHERE child_id = :child_id
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
@@ -144,6 +160,15 @@ class DeviceAlarmDAO(BaseDAO):
|
|||||||
) -> bool:
|
) -> bool:
|
||||||
lat = location.get("lat")
|
lat = location.get("lat")
|
||||||
lng = location.get("lng")
|
lng = location.get("lng")
|
||||||
|
address = str(location.get("address") or "").strip() or None
|
||||||
|
address_status = location.get("address_resolve_status")
|
||||||
|
address_resolved_at = location.get("address_resolved_at")
|
||||||
|
if address and address_status is None:
|
||||||
|
address_status = 1
|
||||||
|
elif address_status is None and lat is not None and lng is not None:
|
||||||
|
address_status = 0
|
||||||
|
if address and address_resolved_at is None:
|
||||||
|
address_resolved_at = datetime.now()
|
||||||
result = await self.execute(
|
result = await self.execute(
|
||||||
text(
|
text(
|
||||||
"""
|
"""
|
||||||
@@ -152,8 +177,8 @@ class DeviceAlarmDAO(BaseDAO):
|
|||||||
lat = :lat,
|
lat = :lat,
|
||||||
lng = :lng,
|
lng = :lng,
|
||||||
location_updated_at = :location_updated_at,
|
location_updated_at = :location_updated_at,
|
||||||
address = NULL,
|
address = :address,
|
||||||
address_resolved_at = NULL,
|
address_resolved_at = :address_resolved_at,
|
||||||
address_resolve_status = :address_resolve_status
|
address_resolve_status = :address_resolve_status
|
||||||
WHERE alarm_id = :alarm_id
|
WHERE alarm_id = :alarm_id
|
||||||
"""
|
"""
|
||||||
@@ -164,7 +189,9 @@ class DeviceAlarmDAO(BaseDAO):
|
|||||||
"lat": lat,
|
"lat": lat,
|
||||||
"lng": lng,
|
"lng": lng,
|
||||||
"location_updated_at": location.get("updated_at"),
|
"location_updated_at": location.get("updated_at"),
|
||||||
"address_resolve_status": 0 if lat is not None and lng is not None else None,
|
"address": address,
|
||||||
|
"address_resolved_at": address_resolved_at,
|
||||||
|
"address_resolve_status": address_status,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
await self.commit()
|
await self.commit()
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ except ModuleNotFoundError:
|
|||||||
|
|
||||||
router = APIRouter(prefix="/devices", tags=["devices"])
|
router = APIRouter(prefix="/devices", tags=["devices"])
|
||||||
logger = logging.getLogger("app.devices")
|
logger = logging.getLogger("app.devices")
|
||||||
|
LOCATION_ADDRESS_PENDING_TEXT = "地址解析中"
|
||||||
|
|
||||||
|
|
||||||
class DeviceMessageItem(BaseModel):
|
class DeviceMessageItem(BaseModel):
|
||||||
@@ -236,6 +237,11 @@ def _row_to_current_location_response(
|
|||||||
stale: bool = False,
|
stale: bool = False,
|
||||||
realtime: bool = True,
|
realtime: bool = True,
|
||||||
) -> DeviceLocationCurrentResponse:
|
) -> DeviceLocationCurrentResponse:
|
||||||
|
address = str(row.get("address") or "").strip() or None
|
||||||
|
address_resolve_status = int(row["address_resolve_status"]) if row.get("address_resolve_status") is not None else None
|
||||||
|
if address is None and address_resolve_status != 1:
|
||||||
|
address = LOCATION_ADDRESS_PENDING_TEXT
|
||||||
|
|
||||||
return DeviceLocationCurrentResponse(
|
return DeviceLocationCurrentResponse(
|
||||||
child_id=int(row["child_id"]),
|
child_id=int(row["child_id"]),
|
||||||
child_name=row.get("child_name"),
|
child_name=row.get("child_name"),
|
||||||
@@ -252,9 +258,9 @@ def _row_to_current_location_response(
|
|||||||
device_time=row["device_time"],
|
device_time=row["device_time"],
|
||||||
server_time=row["server_time"],
|
server_time=row["server_time"],
|
||||||
updated_at=row["updated_at"],
|
updated_at=row["updated_at"],
|
||||||
address=row.get("address"),
|
address=address,
|
||||||
address_resolved_at=row.get("address_resolved_at"),
|
address_resolved_at=row.get("address_resolved_at"),
|
||||||
address_resolve_status=int(row["address_resolve_status"]) if row.get("address_resolve_status") is not None else None,
|
address_resolve_status=address_resolve_status,
|
||||||
stale=stale,
|
stale=stale,
|
||||||
realtime=realtime,
|
realtime=realtime,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from typing import Any
|
|||||||
|
|
||||||
from banban.dao.device_alarm import DeviceAlarmDAO
|
from banban.dao.device_alarm import DeviceAlarmDAO
|
||||||
from banban.service.amap_geocode import amap_geocode_service
|
from banban.service.amap_geocode import amap_geocode_service
|
||||||
|
from banban.service.location import is_valid_coordinate_pair
|
||||||
from services.task_manager import task_manager
|
from services.task_manager import task_manager
|
||||||
from services.database_service_base import DatabaseServiceBase
|
from services.database_service_base import DatabaseServiceBase
|
||||||
from utils.logger import session_logger
|
from utils.logger import session_logger
|
||||||
@@ -34,6 +35,13 @@ class DeviceAlarmService(DatabaseServiceBase):
|
|||||||
location = None
|
location = None
|
||||||
if child_id is not None:
|
if child_id is not None:
|
||||||
location = await dao.get_current_location_snapshot(child_id=child_id)
|
location = await dao.get_current_location_snapshot(child_id=child_id)
|
||||||
|
if location and not is_valid_coordinate_pair(location.get("lat"), location.get("lng")):
|
||||||
|
session_logger.warning(
|
||||||
|
device_id,
|
||||||
|
"alarm_address",
|
||||||
|
f"忽略无效当前定位快照: child_id={child_id}, lat={location.get('lat')}, lng={location.get('lng')}",
|
||||||
|
)
|
||||||
|
location = None
|
||||||
|
|
||||||
alarm_id = await dao.create(
|
alarm_id = await dao.create(
|
||||||
device_id=device_id,
|
device_id=device_id,
|
||||||
@@ -42,7 +50,13 @@ class DeviceAlarmService(DatabaseServiceBase):
|
|||||||
source_msg_id=source_msg_id,
|
source_msg_id=source_msg_id,
|
||||||
location=location,
|
location=location,
|
||||||
)
|
)
|
||||||
if resolve_address and alarm_id and location and location.get("lat") is not None and location.get("lng") is not None:
|
if (
|
||||||
|
resolve_address
|
||||||
|
and alarm_id
|
||||||
|
and location
|
||||||
|
and is_valid_coordinate_pair(location.get("lat"), location.get("lng"))
|
||||||
|
and not str(location.get("address") or "").strip()
|
||||||
|
):
|
||||||
await task_manager.create_task(
|
await task_manager.create_task(
|
||||||
self.resolve_alarm_address(
|
self.resolve_alarm_address(
|
||||||
alarm_id=alarm_id,
|
alarm_id=alarm_id,
|
||||||
@@ -90,7 +104,12 @@ class DeviceAlarmService(DatabaseServiceBase):
|
|||||||
device_id: str,
|
device_id: str,
|
||||||
location: Mapping[str, Any],
|
location: Mapping[str, Any],
|
||||||
) -> bool:
|
) -> bool:
|
||||||
if location.get("lat") is None or location.get("lng") is None:
|
if not is_valid_coordinate_pair(location.get("lat"), location.get("lng")):
|
||||||
|
session_logger.warning(
|
||||||
|
device_id,
|
||||||
|
"alarm_address",
|
||||||
|
f"忽略无效告警定位: alarm_id={alarm_id}, lat={location.get('lat')}, lng={location.get('lng')}",
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
updated = await self.update_alarm_location(alarm_id=alarm_id, location=location)
|
updated = await self.update_alarm_location(alarm_id=alarm_id, location=location)
|
||||||
@@ -116,7 +135,13 @@ class DeviceAlarmService(DatabaseServiceBase):
|
|||||||
|
|
||||||
async def resolve_alarm_current_location(self, *, alarm_id: int, device_id: str) -> bool:
|
async def resolve_alarm_current_location(self, *, alarm_id: int, device_id: str) -> bool:
|
||||||
alarm = await self.get_alarm_event(alarm_id=alarm_id, device_id=device_id)
|
alarm = await self.get_alarm_event(alarm_id=alarm_id, device_id=device_id)
|
||||||
if not alarm or alarm.get("lat") is None or alarm.get("lng") is None:
|
if not alarm:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if str(alarm.get("address") or "").strip():
|
||||||
|
return True
|
||||||
|
|
||||||
|
if not is_valid_coordinate_pair(alarm.get("lat"), alarm.get("lng")):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
await self.resolve_alarm_address(
|
await self.resolve_alarm_address(
|
||||||
@@ -135,6 +160,8 @@ class DeviceAlarmService(DatabaseServiceBase):
|
|||||||
lat: float,
|
lat: float,
|
||||||
lng: float,
|
lng: float,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
if not is_valid_coordinate_pair(lat, lng):
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
result = await amap_geocode_service.reverse_geocode(lng=lng, lat=lat)
|
result = await amap_geocode_service.reverse_geocode(lng=lng, lat=lat)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
|||||||
@@ -22,6 +22,20 @@ except ModuleNotFoundError:
|
|||||||
ADDRESS_RESOLVE_PENDING = 0
|
ADDRESS_RESOLVE_PENDING = 0
|
||||||
ADDRESS_RESOLVE_SUCCESS = 1
|
ADDRESS_RESOLVE_SUCCESS = 1
|
||||||
ADDRESS_RESOLVE_FAILED = 2
|
ADDRESS_RESOLVE_FAILED = 2
|
||||||
|
INVALID_COORDINATE_EPSILON = 0.0000001
|
||||||
|
|
||||||
|
|
||||||
|
def is_valid_coordinate_pair(latitude: float | None, longitude: float | None) -> bool:
|
||||||
|
if latitude is None or longitude is None:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
lat = float(latitude)
|
||||||
|
lng = float(longitude)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return False
|
||||||
|
if lat < -90 or lat > 90 or lng < -180 or lng > 180:
|
||||||
|
return False
|
||||||
|
return not (abs(lat) < INVALID_COORDINATE_EPSILON and abs(lng) < INVALID_COORDINATE_EPSILON)
|
||||||
|
|
||||||
|
|
||||||
class LocationService(DatabaseServiceBase):
|
class LocationService(DatabaseServiceBase):
|
||||||
@@ -42,11 +56,19 @@ class LocationService(DatabaseServiceBase):
|
|||||||
device_id: str,
|
device_id: str,
|
||||||
serial_number: str,
|
serial_number: str,
|
||||||
payload: DeviceLocationReportRequest,
|
payload: DeviceLocationReportRequest,
|
||||||
) -> tuple[Any, Mapping[str, Any]]:
|
) -> tuple[Any, Mapping[str, Any] | None]:
|
||||||
device_identity = await im_service.authenticate_device_identity(
|
device_identity = await im_service.authenticate_device_identity(
|
||||||
device_id=device_id,
|
device_id=device_id,
|
||||||
serial_number=serial_number,
|
serial_number=serial_number,
|
||||||
)
|
)
|
||||||
|
if not is_valid_coordinate_pair(payload.lat, payload.lng):
|
||||||
|
session_logger.warning(
|
||||||
|
device_id,
|
||||||
|
"location",
|
||||||
|
f"忽略无效设备定位: lat={payload.lat}, lng={payload.lng}",
|
||||||
|
)
|
||||||
|
return device_identity, None
|
||||||
|
|
||||||
db_session = await self.get_session()
|
db_session = await self.get_session()
|
||||||
try:
|
try:
|
||||||
dao = LocationDAO(db_session)
|
dao = LocationDAO(db_session)
|
||||||
@@ -123,7 +145,13 @@ class LocationService(DatabaseServiceBase):
|
|||||||
battery_pct: int | None = None,
|
battery_pct: int | None = None,
|
||||||
device_time: datetime | None = None,
|
device_time: datetime | None = None,
|
||||||
) -> Mapping[str, Any] | None:
|
) -> Mapping[str, Any] | None:
|
||||||
if latitude is None or longitude is None:
|
if not is_valid_coordinate_pair(latitude, longitude):
|
||||||
|
if latitude is not None and longitude is not None:
|
||||||
|
session_logger.warning(
|
||||||
|
device_id,
|
||||||
|
"location",
|
||||||
|
f"忽略无效MQTT定位: lat={latitude}, lng={longitude}",
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -173,6 +201,8 @@ class LocationService(DatabaseServiceBase):
|
|||||||
async def _queue_location_address_resolution(self, row: Mapping[str, Any] | None) -> None:
|
async def _queue_location_address_resolution(self, row: Mapping[str, Any] | None) -> None:
|
||||||
if not row or row.get("lat") is None or row.get("lng") is None:
|
if not row or row.get("lat") is None or row.get("lng") is None:
|
||||||
return
|
return
|
||||||
|
if not is_valid_coordinate_pair(row.get("lat"), row.get("lng")):
|
||||||
|
return
|
||||||
if not row.get("should_resolve_address"):
|
if not row.get("should_resolve_address"):
|
||||||
return
|
return
|
||||||
await task_manager.create_task(
|
await task_manager.create_task(
|
||||||
@@ -194,6 +224,8 @@ class LocationService(DatabaseServiceBase):
|
|||||||
lat: float,
|
lat: float,
|
||||||
lng: float,
|
lng: float,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
if not is_valid_coordinate_pair(lat, lng):
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
result = await amap_geocode_service.reverse_geocode(lng=lng, lat=lat)
|
result = await amap_geocode_service.reverse_geocode(lng=lng, lat=lat)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
|||||||
Reference in New Issue
Block a user