修复告警定位无效坐标处理

This commit is contained in:
stu2not
2026-06-22 14:33:10 +08:00
parent 9ae5bc8f83
commit 8aeb813a68
5 changed files with 105 additions and 15 deletions

View File

@@ -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(

View File

@@ -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()

View File

@@ -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,
) )

View File

@@ -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:

View File

@@ -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: