@ -1,8 +1,13 @@
package com.fuyuanshen.global.mqtt.rule.xinghan ;
import cn.hutool.core.bean.BeanUtil ;
import cn.hutool.core.collection.CollUtil ;
import cn.hutool.core.util.ObjectUtil ;
import cn.hutool.core.util.StrUtil ;
import com.alibaba.fast json2 .JSONObject ;
import cn.hutool. json.JSONArray ;
import cn.hutool.json.JSONObject ;
import com.alibaba.fastjson2.JSONWriter ;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper ;
import com.fasterxml.jackson.databind.ObjectMapper ;
import com.fuyuanshen.common.core.constant.GlobalConstants ;
import com.fuyuanshen.common.core.utils.StringUtils ;
@ -10,11 +15,20 @@ import com.fuyuanshen.common.core.utils.date.DurationUtils;
import com.fuyuanshen.common.json.utils.JsonUtils ;
import com.fuyuanshen.common.redis.utils.RedisUtils ;
import com.fuyuanshen.equipment.domain.Device ;
import com.fuyuanshen.equipment.domain.DeviceFenceAccessRecord ;
import com.fuyuanshen.equipment.domain.DeviceGeoFence ;
import com.fuyuanshen.equipment.domain.bo.DeviceAlarmBo ;
import com.fuyuanshen.equipment.domain.bo.DeviceFenceAccessRecordBo ;
import com.fuyuanshen.equipment.domain.vo.DeviceAlarmVo ;
import com.fuyuanshen.equipment.domain.vo.DeviceGeoFenceVo ;
import com.fuyuanshen.equipment.mapper.DeviceAlarmMapper ;
import com.fuyuanshen.equipment.mapper.DeviceFenceAccessRecordMapper ;
import com.fuyuanshen.equipment.mapper.DeviceGeoFenceMapper ;
import com.fuyuanshen.equipment.service.DeviceService ;
import com.fuyuanshen.equipment.service.IDeviceAlarmService ;
import com.fuyuanshen.equipment.service.IDeviceFenceAccessRecordService ;
import com.fuyuanshen.equipment.service.IDeviceGeoFenceService ;
import com.fuyuanshen.equipment.utils.map.AmapTrackUtil ;
import com.fuyuanshen.equipment.utils.map.GetAddressFromLatUtil ;
import com.fuyuanshen.equipment.utils.map.LngLonUtil ;
import com.fuyuanshen.global.mqtt.base.MqttMessageRule ;
@ -30,13 +44,16 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j ;
import org.redisson.api.RLock ;
import org.redisson.api.RedissonClient ;
import org.springframework.scheduling.annotation.Async ;
import org.springframework.stereotype.Component ;
import org.springframework.beans.factory.annotation.Autowired ;
import java.text.DecimalFormat ;
import java.time.Duration ;
import java.util.* ;
import java.util.concurrent.CompletableFuture ;
import java.util.concurrent.TimeUnit ;
import java.util.stream.Collectors ;
import static com.fuyuanshen.common.core.constant.GlobalConstants.FUNCTION_ACCESS_KEY ;
import static com.fuyuanshen.common.core.constant.GlobalConstants.GLOBAL_REDIS_KEY ;
@ -72,6 +89,12 @@ public class XinghanDeviceDataRule implements MqttMessageRule {
private final IDeviceAlarmService deviceAlarmService ;
private final DeviceService deviceService ;
private final DeviceAlarmMapper deviceAlarmMapper ;
private final AmapTrackUtil amapTrackUtil ;
private final IDeviceFenceAccessRecordService deviceFenceAccessRecordService ;
private final DeviceFenceAccessRecordMapper deviceFenceAccessRecordMapper ;
private final DeviceGeoFenceMapper deviceGeoFenceMapper ;
/** 位置未发生明显变化的距离阈值(米),可通过配置中心动态调整 */
private final double MOVEMENT_THRESHOLD_METER = 10 . 0 ;
@Override
public void execute ( MqttRuleContext context ) {
@ -127,16 +150,20 @@ public class XinghanDeviceDataRule implements MqttMessageRule {
* 入口: 保存报警( SOS 与 Shake 完全独立)
*/
public void saveAlarm ( String deviceImei , MqttXinghanJson status ) {
int sos = Optional . ofNullable ( status . getStaSOSGrade ( ) ) . orElse ( 0 ) ;
// 1. 处理 SOS 报警
handleSingleAlarm ( deviceImei ,
sos > 0 ,
AlarmTypeEnum . SOS ) ;
int shake = Optional . ofNullable ( status . getStaShakeBit ( ) ) . orElse ( 0 ) ;
// 2. 处理 Shake 报警
handleSingleAlarm ( deviceImei ,
s hake > 0 ,
AlarmTypeEnum . SHAKE ) ;
try {
int sos = Optional . ofNullable ( status . getStaSOSGrade ( ) ) . orElse ( 0 ) ;
// 1. 处理 SOS 报警
handleSingleAlarm ( deviceImei ,
sos > 0 ,
AlarmTypeEnum . SOS ) ;
int shake = Optional . ofNullable ( status . getStaShakeBit ( ) ) . orElse ( 0 ) ;
// 2. 处理 Shake 报警
handleSingleAlarm ( deviceImei ,
shake > 0 ,
AlarmTypeEnum . SHAKE ) ;
} catch ( Exception e ) {
log . error ( " 异步保存报警( SOS 与 Shake 完全独立)报错: device={}, error={} " , deviceImei , e . getMessage ( ) , e ) ;
}
}
/**
@ -236,7 +263,7 @@ public class XinghanDeviceDataRule implements MqttMessageRule {
String location = RedisUtils . getCacheObject (
GLOBAL_REDIS_KEY + DEVICE_KEY_PREFIX + deviceImei + DEVICE_LOCATION_KEY_PREFIX ) ;
if ( StrUtil . isNotBlank ( location ) ) {
bo . setLocation ( JSONObject . parseObject ( location ) . getString ( " address " ) ) ;
bo . setLocation ( com . alibaba . fastjson2 . JSONObject. parseObject ( location ) . getString ( " address " ) ) ;
}
return bo ;
}
@ -259,63 +286,312 @@ public class XinghanDeviceDataRule implements MqttMessageRule {
public void asyncSendLocationToRedisWithFuture ( String deviceImei , String latitude , String longitude ) {
CompletableFuture . runAsync ( ( ) - > {
try {
if ( StringUtils . isBlank ( latitude ) | | StringUtils . isBlank ( longitude ) ) {
if ( StringUtils . isAny Blank ( deviceImei , latitude , longitude ) ) {
log . warn ( " 位置上报参数为空, deviceImei={} " , deviceImei ) ;
return ;
}
//log.info("位置上报, deviceImei={}, lat={}, lon={}", deviceImei, latitude, longitude);
// 1. 解析当前上报的经纬度
Double curLat = parseDoubleSafe ( latitude . trim ( ) ) ;
Double curLon = parseDoubleSafe ( longitude . trim ( ) ) ;
if ( curLat = = null | | curLon = = null ) {
log . warn ( " 经纬度格式错误, 直接更新, deviceImei={}, lat={}, lon={} " , deviceImei , latitude , longitude ) ;
doSaveLocation ( deviceImei , latitude , longitude ) ;
return ;
}
String [ ] latArr = latitude . split ( " \\ . " ) ;
String [ ] lonArr = longitude . split ( " \\ . " ) ;
// 将位置信息存储到Redis中
String redisKey = GlobalConstants . GLOBAL_REDIS_KEY + DEVICE_KEY_PREFIX + deviceImei + DEVICE_LOCATION_KEY_PREFIX ;
String redisObj = RedisUtils . getCacheObject ( redisKey ) ;
JSONObject jsonOBj = JSONObject . parseObject ( redisObj ) ;
if ( jsonOBj ! = null ) {
String str1 = latArr [ 0 ] + " . " + latArr [ 1 ] . substring ( 0 , 4 ) ;
String str2 = lonArr [ 0 ] + " . " + lonArr [ 1 ] . substring ( 0 , 4 ) ;
String cacheLatitude = jsonOBj . getString ( " wgs84_latitude " ) ;
String cacheLongitude = jsonOBj . getString ( " wgs84_longitude " ) ;
String [ ] latArr1 = cacheLatitude . split ( " \\ . " ) ;
String [ ] lonArr1 = cacheLongitude . split ( " \\ . " ) ;
// 2. 读取 Redis 中缓存的上一次位置
String redisKey = GlobalConstants . GLOBAL_REDIS_KEY + DEVICE_KEY_PREFIX + deviceImei + DEVICE_LOCATION_KEY_PREFIX ;
String cachedJson = RedisUtils . getCacheObject ( redisKey ) ;
String cacheStr1 = latArr1 [ 0 ] + " . " + latArr1 [ 1 ] . substring ( 0 , 4 ) ;
String cacheStr2 = lonArr1 [ 0 ] + " . " + lonArr1 [ 1 ] . substring ( 0 , 4 ) ;
if ( str1 . equals ( cacheStr1 ) & & str2 . equals ( cacheStr2 ) ) {
log . info ( " 位置信息未发生变化: device={}, lat={}, lon={} " , deviceImei , latitude , longitude ) ;
return ;
if ( StringUtils . isNotBlank ( cachedJson ) ) {
com . alibaba . fastjson2 . JSONObject cachedObj = com . alibaba . fastjson2 . JSONObject . parseObject ( cachedJson ) ;
String cachedWgs84Lat = cachedObj . getString ( " wgs84_latitude " ) ;
String cachedWgs84Lon = cachedObj . getString ( " wgs84_ longitude " ) ;
Double oldLat = parseDoubleSafe ( cachedWgs84Lat ) ;
Double oldLon = parseDoubleSafe ( cachedWgs84Lon ) ;
if ( oldLat ! = null & & oldLon ! = null ) {
double distance = haversine ( oldLat , oldLon , curLat , curLon ) ;
if ( distance < = MOVEMENT_THRESHOLD_METER ) {
log . info ( " 位置未发生明显变化({}米 <= {}米),不更新 Redis, deviceImei={}, lat={}, lon={} " ,
distance , MOVEMENT_THRESHOLD_METER , deviceImei , latitude , longitude ) ;
return ;
}
}
}
// 构造位置信息对象
Map < String , Object > loc ationInfo = new LinkedHashMap < > ( ) ;
double [ ] doubles = LngLonUtil . gps84_To_Gcj02 ( Double . parseDouble ( latitude ) , Double . parseDouble ( longitude ) ) ;
locationInfo . put ( " deviceImei " , deviceImei ) ;
locationInfo . put ( " latitude " , doubles [ 0 ] ) ;
locationInfo . put ( " longitude " , doubles [ 1 ] ) ;
locationInfo . put ( " wgs84_latitude " , latitude ) ;
locationInfo . put ( " wgs84_longitude " , longitude ) ;
String address = GetAddressFromLatUtil . getAdd ( String . valueOf ( doubles [ 1 ] ) , String . valueOf ( doubles [ 0 ] ) ) ;
locationInfo . put ( " address " , address ) ;
locationInfo . put ( " timestamp " , System . currentTimeMillis ( ) ) ;
// 3. 位置有明显变化,执行保存
doSaveLocation ( deviceImei , latitude , longitude ) ;
String locationJson = JsonUtils . toJsonString ( locationInfo ) ;
// 存储到Redis
RedisUtils . setCacheObject ( redisKey , locationJson ) ;
// 存储到一个列表中,保留历史位置信息
// String locationHistoryKey = GlobalConstants.GLOBAL_REDIS_KEY+DeviceRedisKeyConstants.DEVICE_LOCATION_HISTORY_KEY_PREFIX + deviceImei;
// RedisUtils.addCacheList(locationHistoryKey, locationJson);
// RedisUtils.expire(locationHistoryKey, Duration.ofDays(90));
storeDeviceTrajectoryWithSortedSet ( deviceImei , locationJson ) ;
log . info ( " 位置信息已异步发送到Redis: device={}, lat={}, lon={} " , deviceImei , latitude , longitude ) ;
} catch ( Exception e ) {
log . error ( " 异步发送位置信息到Redis时出错: device={}, error ={} " , deviceImei , e . getMessage ( ) , e ) ;
log . error ( " 异步发送位置信息到 Redis 失败, deviceImei ={} " , deviceImei , e ) ;
}
} ) ;
}
/** 真正执行保存逻辑(抽取出来便于测试和阅读) */
private void doSaveLocation ( String deviceImei , String wgs84Lat , String wgs84Lon ) {
// Map<String, Object> locationInfo = new LinkedHashMap<>();
// locationInfo.put("deviceImei", deviceImei);
// locationInfo.put("latitude", gcj02[0]); // GCJ02 纬度
// locationInfo.put("longitude", gcj02[1]); // GCJ02 经度
// locationInfo.put("wgs84_latitude", wgs84Lat);
// locationInfo.put("wgs84_longitude", wgs84Lon);
//
//
// locationInfo.put("address", StringUtils.defaultIfBlank(address, "未知"));
// locationInfo.put("timestamp", System.currentTimeMillis());
//
// String locationJson = JsonUtils.toJsonString(locationInfo);
// 使用 fastjson2 零 GC 序列化
// WGS84 → GCJ02( 火星坐标)
double [ ] gcj02 = LngLonUtil . gps84_To_Gcj02 (
Double . parseDouble ( wgs84Lat ) ,
Double . parseDouble ( wgs84Lon )
) ;
String gcj02Lat = String . format ( " %.6f " , gcj02 [ 0 ] ) ;
String gcj02Lon = String . format ( " %.6f " , gcj02 [ 1 ] ) ;
// 逆地理编码(可自行决定是否异步)
String address = GetAddressFromLatUtil . getAdd ( gcj02Lon , gcj02Lat ) ;
String locationJson = buildLocationJsonFastJSON2 ( deviceImei , wgs84Lat , wgs84Lon , gcj02Lon , gcj02Lat , address ) ;
// 主位置信息(最新一条)
String redisKey = GlobalConstants . GLOBAL_REDIS_KEY + DEVICE_KEY_PREFIX + deviceImei + DEVICE_LOCATION_KEY_PREFIX ;
RedisUtils . setCacheObject ( redisKey , locationJson ) ;
// 轨迹存储( SortedSet, 按时间戳排序)
storeDeviceTrajectoryWithSortedSet ( deviceImei , locationJson ) ;
// 轨迹上传 查询检测对象与围栏关系 记录设备进出围栏事件
uploadTrackPointAsync ( deviceImei , gcj02Lat , gcj02Lon , address ) ;
log . info ( " 位置信息已更新 Redis, deviceImei={}, wgs84=({},{}) " , deviceImei , wgs84Lat , wgs84Lon ) ;
}
private String buildLocationJsonFastJSON2 (
String deviceImei ,
String wgs84Lat , String wgs84Lon ,
String gcj02Lon , String gcj02Lat , String address ) {
long timestamp = System . currentTimeMillis ( ) ;
// 直接用默认 writer, 零配置, 自动零 GC
try ( JSONWriter w = JSONWriter . of ( ) ) {
w . startObject ( ) ;
w . writeString ( " deviceImei " ) ; w . writeColon ( ) ; w . writeString ( deviceImei ) ; w . writeComma ( ) ;
w . writeString ( " latitude " ) ; w . writeColon ( ) ; w . writeString ( gcj02Lat ) ; w . writeComma ( ) ;
w . writeString ( " longitude " ) ; w . writeColon ( ) ; w . writeString ( gcj02Lon ) ; w . writeComma ( ) ;
w . writeString ( " wgs84_latitude " ) ; w . writeColon ( ) ; w . writeString ( wgs84Lat ) ; w . writeComma ( ) ;
w . writeString ( " wgs84_longitude " ) ; w . writeColon ( ) ; w . writeString ( wgs84Lon ) ; w . writeComma ( ) ;
w . writeString ( " address " ) ; w . writeColon ( ) ; w . writeString ( StringUtils . defaultIfBlank ( address , " 未知 " ) ) ; w . writeComma ( ) ;
w . writeString ( " timestamp " ) ; w . writeColon ( ) ; w . writeInt64 ( timestamp ) ;
w . endObject ( ) ;
return w . toString ( ) ;
}
}
/**
* 上传轨迹点并处理电子围栏出入事件(高德猎鹰服务)
* 优化点:
* 1. 避免重复查询数据库(围栏信息批量缓存)
* 2. 防御性编程:全面空指针防护
* 3. Redis 操作原子性 + 合理 TTL
* 4. 减少不必要的对象创建和流操作
* 5. 精确的事件时间使用 locationTime
* 6. 失败不阻塞主流程,记录关键错误
*/
private void uploadTrackPointAsync ( String deviceImei ,
String gcj02Lat ,
String gcj02Lon , String address ) {
if ( StrUtil . hasBlank ( deviceImei , gcj02Lat , gcj02Lon ) ) {
log . warn ( " 上传轨迹点参数非法, deviceImei={}, lat={}, lon={} " , deviceImei , gcj02Lat , gcj02Lon ) ;
return ;
}
long locationTime = System . currentTimeMillis ( ) ;
String fenceStatusKey = GLOBAL_REDIS_KEY + DEVICE_KEY_PREFIX + deviceImei + " :terminal:status " ;
try {
// 1. 查询设备信息( 建议加缓存, 热点设备可大幅降低DB压力)
Device device = deviceService . selectDeviceByImei ( deviceImei ) ;
if ( device = = null | | device . getSid ( ) = = null | | device . getTid ( ) = = null ) {
log . warn ( " 设备不存在或未完成高德终端创建, imei={} " , deviceImei ) ;
return ;
}
Long trid = ObjectUtil . defaultIfNull ( device . getTrid ( ) , 0L ) ; // trid 可能为空
// 2. 上传轨迹点
JSONObject point = new JSONObject ( ) ;
point . set ( " location " , String . format ( " %s,%s " , gcj02Lon , gcj02Lat ) ) ;
point . set ( " locatetime " , locationTime ) ;
JSONArray pointsArray = new JSONArray ( ) ;
pointsArray . add ( point ) ;
log . info ( " 上传轨迹点开始, deviceImei={}, point={} " , deviceImei , point ) ;
JSONObject uploadResult = amapTrackUtil . uploadPoints ( device . getSid ( ) , device . getTid ( ) , trid , pointsArray ) ;
if ( uploadResult = = null | | uploadResult . getInt ( " errcode " , - 1 ) ! = 10000 ) {
return ;
}
log . info ( " 上传轨迹点成功, deviceImei={}, result={} " , deviceImei , uploadResult ) ;
// 3. 查询当前围栏状态
JSONObject fenceResult = amapTrackUtil . queryTerminalFenceStatus ( device . getSid ( ) , null , device . getTid ( ) ) ;
if ( fenceResult = = null | | fenceResult . getInt ( " errcode " , - 1 ) ! = 10000 ) {
log . warn ( " 查询设备围栏状态失败, imei={}, result={} " , deviceImei , fenceResult ) ;
return ;
}
log . info ( " 查询设备围栏状态成功, imei={}, result={} " , deviceImei , fenceResult ) ;
JSONArray results = fenceResult . getByPath ( " data.results " , JSONArray . class ) ;
if ( results = = null | | results . isEmpty ( ) ) {
// 没有任何围栏关系,清空旧状态
RedisUtils . deleteObject ( fenceStatusKey ) ;
return ;
}
// 4. 当前在围栏内的 gfid 集合
Set < Long > newInFenceGfids = new HashSet < > ( ) ;
List < JSONObject > currentInList = new ArrayList < > ( ) ;
for ( Object obj : results ) {
JSONObject item = ( JSONObject ) obj ;
if ( item . getInt ( " in " , 0 ) = = 1 ) {
Long gfid = item . getLong ( " gfid " ) ;
if ( gfid ! = null ) {
newInFenceGfids . add ( gfid ) ;
currentInList . add ( item ) ;
}
}
}
// 5. 获取上一次的围栏状态
List < JSONObject > oldInList = RedisUtils . getCacheObject ( fenceStatusKey ) ;
Set < Long > oldInFenceGfids = ( oldInList = = null | | oldInList . isEmpty ( ) )
? Collections . emptySet ( )
: oldInList . stream ( )
. map ( o - > o . getLong ( " gfid " ) )
. collect ( Collectors . toSet ( ) ) ;
// 6. 计算出入事件(使用高效的 Set 操作)
Set < Long > enteredGfids = new HashSet < > ( newInFenceGfids ) ;
enteredGfids . removeAll ( oldInFenceGfids ) ; // 进入:这次有,上次没有
Set < Long > exitedGfids = new HashSet < > ( oldInFenceGfids ) ;
exitedGfids . removeAll ( newInFenceGfids ) ; // 离开:上次有,这次没有
Date eventTime = new Date ( locationTime ) ;
Double latitude = Double . valueOf ( gcj02Lat ) ;
Double longitude = Double . valueOf ( gcj02Lon ) ;
// 批量查询围栏信息(关键优化:避免 N+1 查询)
Set < Long > allChangedGfids = new HashSet < > ( ) ;
allChangedGfids . addAll ( enteredGfids ) ;
allChangedGfids . addAll ( exitedGfids ) ;
Map < Long , DeviceGeoFenceVo > fenceMap = new HashMap < > ( ) ;
if ( CollUtil . isNotEmpty ( allChangedGfids ) ) {
List < DeviceGeoFenceVo > fenceList = deviceGeoFenceMapper . selectVoList (
new LambdaQueryWrapper < DeviceGeoFence > ( ) . in ( DeviceGeoFence : : getGfid , allChangedGfids ) ) ;
fenceMap = fenceList . stream ( )
. filter ( Objects : : nonNull )
. collect ( Collectors . toMap ( DeviceGeoFenceVo : : getGfid , v - > v , ( a , b ) - > a ) ) ;
}
// 7. 记录进入事件
if ( CollUtil . isNotEmpty ( enteredGfids ) ) {
List < DeviceFenceAccessRecord > enterRecords = new ArrayList < > ( ) ;
for ( Long gfid : enteredGfids ) {
DeviceFenceAccessRecord record = buildFenceRecord ( device , fenceMap . get ( gfid ) , 1L , latitude , longitude , eventTime , address ) ;
if ( record ! = null ) {
enterRecords . add ( record ) ;
}
}
deviceFenceAccessRecordMapper . insertBatch ( enterRecords ) ;
log . info ( " 设备进入围栏, imei={}, gfids={} " , deviceImei , enteredGfids ) ;
}
// 8. 记录离开事件
if ( CollUtil . isNotEmpty ( exitedGfids ) ) {
List < DeviceFenceAccessRecord > exitRecords = new ArrayList < > ( ) ;
for ( Long gfid : exitedGfids ) {
DeviceFenceAccessRecord record = buildFenceRecord ( device , fenceMap . get ( gfid ) , 2L , latitude , longitude , eventTime , address ) ;
if ( record ! = null ) {
exitRecords . add ( record ) ;
}
}
deviceFenceAccessRecordMapper . insertBatch ( exitRecords ) ;
log . info ( " 设备离开围栏, imei={}, gfids={} " , deviceImei , exitedGfids ) ;
}
// 9. 更新 Redis 状态( TTL 5分钟, 防止频繁查询)
if ( CollUtil . isNotEmpty ( currentInList ) ) {
RedisUtils . setCacheObject ( fenceStatusKey , currentInList , Duration . ofMinutes ( 5 ) ) ;
} else {
RedisUtils . deleteObject ( fenceStatusKey ) ;
}
} catch ( Exception e ) {
log . error ( " 上传轨迹点并处理围栏事件异常, imei={}, lat={}, lon={}, time={} " ,
deviceImei , gcj02Lat , gcj02Lon , locationTime , e ) ;
// 可落库待重试或发告警,此处不抛异常影响定位主流程
}
}
/**
* 构建围栏出入记录(提取公共逻辑,避免重复代码)
*/
private DeviceFenceAccessRecord buildFenceRecord ( Device device ,
DeviceGeoFenceVo fence ,
Long eventType ,
Double latitude ,
Double longitude ,
Date eventTime , String address ) {
if ( fence = = null | | fence . getId ( ) = = null ) {
log . warn ( " 围栏信息不存在, gfid 可能已被删除或未绑定 " ) ;
return null ;
}
DeviceFenceAccessRecord bo = new DeviceFenceAccessRecord ( ) ;
bo . setDeviceId ( device . getId ( ) . toString ( ) ) ;
bo . setFenceId ( fence . getId ( ) ) ;
bo . setEventType ( eventType ) ; // 1=进入, 2=离开
bo . setAccuracy ( 20L ) ;
bo . setLatitude ( latitude ) ;
bo . setLongitude ( longitude ) ;
bo . setEventTime ( eventTime ) ;
bo . setTenantId ( device . getTenantId ( ) ) ;
bo . setCreateBy ( device . getCreateBy ( ) ) ;
bo . setCreateDept ( device . getCreateDept ( ) ) ;
bo . setEventAddress ( address ) ;
return bo ;
}
/** 安全解析 double, 解析失败返回 null */
private Double parseDoubleSafe ( String str ) {
if ( StringUtils . isBlank ( str ) ) return null ;
try {
return Double . parseDouble ( str . trim ( ) ) ;
} catch ( NumberFormatException e ) {
return null ;
}
}
/** Haversine 公式计算两点球面距离(米) */
private double haversine ( double lat1 , double lon1 , double lat2 , double lon2 ) {
double dLat = Math . toRadians ( lat2 - lat1 ) ;
double dLon = Math . toRadians ( lon2 - lon1 ) ;
double a = Math . sin ( dLat / 2 ) * Math . sin ( dLat / 2 ) +
Math . cos ( Math . toRadians ( lat1 ) ) * Math . cos ( Math . toRadians ( lat2 ) ) *
Math . sin ( dLon / 2 ) * Math . sin ( dLon / 2 ) ;
double c = 2 * Math . atan2 ( Math . sqrt ( a ) , Math . sqrt ( 1 - a ) ) ;
/** 地球平均半径(米) */
double EARTH_RADIUS = 6371_393 . 0 ;
return EARTH_RADIUS * c ;
}
/**
* 存储设备30天历史轨迹到Redis (使用Sorted Set)
*/
@ -358,4 +634,6 @@ public class XinghanDeviceDataRule implements MqttMessageRule {
return map ;
}
}