forked from dyf/fys-Multi-tenant
feat(equipment): 新增高德轨迹服务相关功能与设备终端管理
- 新增 AmapTrackUtil 工具类,封装高德猎鹰轨迹服务 API 调用 - 在 Device 实体中增加高德服务、终端、轨迹 ID 字段(sid, tid, trid) - 新增设备终端分页查询接口 /pageTerminal 及对应实现 - 新增围栏与设备关联实体 DeviceFenceTerminal 及 Mapper - 扩展 DeviceGeoFence 相关注入高德服务及围栏 ID 字段 - 新增添加/删除围栏终端绑定接口及业务逻辑 - 新增轨迹服务模块(TrackService)包括 Controller、Service、BO、DTO 等完整结构 - 在 DeviceMapper.xml 中补充终端相关字段查询及筛选条件 - 新增 TerminalDeviceBo、TerminalDelBo、TerminalQueryBo 等数据传输对象 - 补充设备查询条件支持高德终端状态及服务 ID 过滤 - 新增围栏终端关联表 device_fence_terminal 并注册至菜单配置 - 完善设备分配逻辑以兼容角色权限判断及终端信息展示
This commit is contained in:
@ -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.fastjson2.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,
|
||||
shake > 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.isAnyBlank(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> locationInfo = 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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@ -110,7 +110,7 @@ public class XinghanSendAlarmMessageRule implements MqttMessageRule {
|
||||
dto.setMessage(String.format("%s设备已收到通知!", latestLog.getDeviceName()));
|
||||
dto.setUserIds(List.of(latestLog.getCreateBy()));
|
||||
SseMessageUtils.publishMessage(dto);
|
||||
}, 5, TimeUnit.SECONDS);
|
||||
}, 2, TimeUnit.SECONDS);
|
||||
return;
|
||||
}
|
||||
// 1. cover! —— 成功标记
|
||||
|
||||
@ -3,6 +3,7 @@ package com.fuyuanshen.web.controller.device.fence;
|
||||
import java.util.List;
|
||||
|
||||
import com.fuyuanshen.equipment.domain.bo.DeviceGeoFenceBo;
|
||||
import com.fuyuanshen.equipment.domain.bo.FenceTerminalBo;
|
||||
import com.fuyuanshen.equipment.domain.dto.FenceCheckResponse;
|
||||
import com.fuyuanshen.equipment.domain.query.FenceCheckRequest;
|
||||
import com.fuyuanshen.equipment.domain.vo.DeviceGeoFenceVo;
|
||||
@ -129,4 +130,25 @@ public class DeviceGeoFenceController extends BaseController {
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加电子围栏终端
|
||||
*
|
||||
* @param bo
|
||||
* @return
|
||||
*/
|
||||
@PostMapping("/addTerminal")
|
||||
public R<Void> addFenceTerminal(@RequestBody FenceTerminalBo bo) {
|
||||
return toAjax(deviceGeoFenceService.addFenceTerminal(bo));
|
||||
}
|
||||
/**
|
||||
* 删除电子围栏终端
|
||||
*
|
||||
* @param bo
|
||||
* @return
|
||||
*/
|
||||
@PostMapping("/delTerminal")
|
||||
public R<Void> delFenceTerminal(@RequestBody FenceTerminalBo bo) {
|
||||
return toAjax(deviceGeoFenceService.delFenceTerminal(bo));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -132,6 +132,8 @@ tenant:
|
||||
- app_menu
|
||||
- app_user_role
|
||||
- app_role_menu
|
||||
- track_service
|
||||
- device_fence_terminal
|
||||
|
||||
# MyBatisPlus配置
|
||||
# https://baomidou.com/config/
|
||||
|
||||
Reference in New Issue
Block a user