diff --git a/banban-mini/src/pages/location/index.tsx b/banban-mini/src/pages/location/index.tsx index 6dc0f2d..55385eb 100644 --- a/banban-mini/src/pages/location/index.tsx +++ b/banban-mini/src/pages/location/index.tsx @@ -56,9 +56,7 @@ function formatOptionalNumber(value?: number | null, digits = 1): string | null function getLocationAddress(point: DeviceLocation | DeviceTrajectoryPoint): string { const address = String(point.address || '').trim() if (address) return address - if (point.address_resolve_status === 0) return '地点解析中' - if (point.address_resolve_status === 2) return '地点暂时无法解析' - return '当前地点未知' + return '地址解析中' } function buildLocationDetailLines( diff --git a/talkingq-url/banban/dao/device_alarm.py b/talkingq-url/banban/dao/device_alarm.py index 30231ac..e2e4438 100644 --- a/talkingq-url/banban/dao/device_alarm.py +++ b/talkingq-url/banban/dao/device_alarm.py @@ -1,4 +1,5 @@ from collections.abc import Mapping +from datetime import datetime from typing import Any from sqlalchemy import text @@ -17,6 +18,12 @@ class DeviceAlarmDAO(BaseDAO): location: Mapping[str, Any] | None = None, ) -> int: 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( """ INSERT INTO device_alarm_events ( @@ -28,6 +35,8 @@ class DeviceAlarmDAO(BaseDAO): lat, lng, location_updated_at, + address, + address_resolved_at, address_resolve_status, created_at ) @@ -40,6 +49,8 @@ class DeviceAlarmDAO(BaseDAO): :lat, :lng, :location_updated_at, + :address, + :address_resolved_at, :address_resolve_status, CURRENT_TIMESTAMP ) @@ -53,7 +64,9 @@ class DeviceAlarmDAO(BaseDAO): "lat": location.get("lat"), "lng": location.get("lng"), "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() @@ -100,7 +113,10 @@ class DeviceAlarmDAO(BaseDAO): coord_type, lat, lng, - updated_at + updated_at, + address, + address_resolved_at, + address_resolve_status FROM child_location_current WHERE child_id = :child_id LIMIT 1 @@ -144,6 +160,15 @@ class DeviceAlarmDAO(BaseDAO): ) -> bool: lat = location.get("lat") 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( text( """ @@ -152,8 +177,8 @@ class DeviceAlarmDAO(BaseDAO): lat = :lat, lng = :lng, location_updated_at = :location_updated_at, - address = NULL, - address_resolved_at = NULL, + address = :address, + address_resolved_at = :address_resolved_at, address_resolve_status = :address_resolve_status WHERE alarm_id = :alarm_id """ @@ -164,7 +189,9 @@ class DeviceAlarmDAO(BaseDAO): "lat": lat, "lng": lng, "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() diff --git a/talkingq-url/banban/routers/devices.py b/talkingq-url/banban/routers/devices.py index f36bfd3..6455b1f 100644 --- a/talkingq-url/banban/routers/devices.py +++ b/talkingq-url/banban/routers/devices.py @@ -33,6 +33,7 @@ except ModuleNotFoundError: router = APIRouter(prefix="/devices", tags=["devices"]) logger = logging.getLogger("app.devices") +LOCATION_ADDRESS_PENDING_TEXT = "地址解析中" class DeviceMessageItem(BaseModel): @@ -236,6 +237,11 @@ def _row_to_current_location_response( stale: bool = False, realtime: bool = True, ) -> 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( child_id=int(row["child_id"]), child_name=row.get("child_name"), @@ -252,9 +258,9 @@ def _row_to_current_location_response( device_time=row["device_time"], server_time=row["server_time"], updated_at=row["updated_at"], - address=row.get("address"), + address=address, 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, realtime=realtime, ) diff --git a/talkingq-url/banban/service/device_alarm.py b/talkingq-url/banban/service/device_alarm.py index ff70cb9..b95d8a8 100644 --- a/talkingq-url/banban/service/device_alarm.py +++ b/talkingq-url/banban/service/device_alarm.py @@ -3,6 +3,7 @@ from typing import Any from banban.dao.device_alarm import DeviceAlarmDAO 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.database_service_base import DatabaseServiceBase from utils.logger import session_logger @@ -34,6 +35,13 @@ class DeviceAlarmService(DatabaseServiceBase): location = None if child_id is not None: 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( device_id=device_id, @@ -42,7 +50,13 @@ class DeviceAlarmService(DatabaseServiceBase): source_msg_id=source_msg_id, 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( self.resolve_alarm_address( alarm_id=alarm_id, @@ -90,7 +104,12 @@ class DeviceAlarmService(DatabaseServiceBase): device_id: str, location: Mapping[str, Any], ) -> 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 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: 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 await self.resolve_alarm_address( @@ -135,6 +160,8 @@ class DeviceAlarmService(DatabaseServiceBase): lat: float, lng: float, ) -> None: + if not is_valid_coordinate_pair(lat, lng): + return try: result = await amap_geocode_service.reverse_geocode(lng=lng, lat=lat) except Exception as exc: diff --git a/talkingq-url/banban/service/location.py b/talkingq-url/banban/service/location.py index 337dfc2..2ca064f 100644 --- a/talkingq-url/banban/service/location.py +++ b/talkingq-url/banban/service/location.py @@ -22,6 +22,20 @@ except ModuleNotFoundError: ADDRESS_RESOLVE_PENDING = 0 ADDRESS_RESOLVE_SUCCESS = 1 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): @@ -42,11 +56,19 @@ class LocationService(DatabaseServiceBase): device_id: str, serial_number: str, payload: DeviceLocationReportRequest, - ) -> tuple[Any, Mapping[str, Any]]: + ) -> tuple[Any, Mapping[str, Any] | None]: device_identity = await im_service.authenticate_device_identity( device_id=device_id, 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() try: dao = LocationDAO(db_session) @@ -123,7 +145,13 @@ class LocationService(DatabaseServiceBase): battery_pct: int | None = None, device_time: datetime | None = 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 @dataclass(frozen=True) @@ -173,6 +201,8 @@ class LocationService(DatabaseServiceBase): 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: return + if not is_valid_coordinate_pair(row.get("lat"), row.get("lng")): + return if not row.get("should_resolve_address"): return await task_manager.create_task( @@ -194,6 +224,8 @@ class LocationService(DatabaseServiceBase): lat: float, lng: float, ) -> None: + if not is_valid_coordinate_pair(lat, lng): + return try: result = await amap_geocode_service.reverse_geocode(lng=lng, lat=lat) except Exception as exc: