diff --git a/fys-admin/src/main/java/com/fuyuanshen/global/mqtt/rule/xinghan/XinghanDeviceDataRule.java b/fys-admin/src/main/java/com/fuyuanshen/global/mqtt/rule/xinghan/XinghanDeviceDataRule.java index 8739b3a7..f0898420 100644 --- a/fys-admin/src/main/java/com/fuyuanshen/global/mqtt/rule/xinghan/XinghanDeviceDataRule.java +++ b/fys-admin/src/main/java/com/fuyuanshen/global/mqtt/rule/xinghan/XinghanDeviceDataRule.java @@ -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 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 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 newInFenceGfids = new HashSet<>(); + List 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 oldInList = RedisUtils.getCacheObject(fenceStatusKey); + Set oldInFenceGfids = (oldInList == null || oldInList.isEmpty()) + ? Collections.emptySet() + : oldInList.stream() + .map(o -> o.getLong("gfid")) + .collect(Collectors.toSet()); + + // 6. 计算出入事件(使用高效的 Set 操作) + Set enteredGfids = new HashSet<>(newInFenceGfids); + enteredGfids.removeAll(oldInFenceGfids);// 进入:这次有,上次没有 + + Set 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 allChangedGfids = new HashSet<>(); + allChangedGfids.addAll(enteredGfids); + allChangedGfids.addAll(exitedGfids); + + Map fenceMap = new HashMap<>(); + if (CollUtil.isNotEmpty(allChangedGfids)) { + List fenceList = deviceGeoFenceMapper.selectVoList( + new LambdaQueryWrapper().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 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 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; } + + } diff --git a/fys-admin/src/main/java/com/fuyuanshen/global/mqtt/rule/xinghan/XinghanSendAlarmMessageRule.java b/fys-admin/src/main/java/com/fuyuanshen/global/mqtt/rule/xinghan/XinghanSendAlarmMessageRule.java index 5d929943..a483817e 100644 --- a/fys-admin/src/main/java/com/fuyuanshen/global/mqtt/rule/xinghan/XinghanSendAlarmMessageRule.java +++ b/fys-admin/src/main/java/com/fuyuanshen/global/mqtt/rule/xinghan/XinghanSendAlarmMessageRule.java @@ -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! —— 成功标记 diff --git a/fys-admin/src/main/java/com/fuyuanshen/web/controller/device/fence/DeviceGeoFenceController.java b/fys-admin/src/main/java/com/fuyuanshen/web/controller/device/fence/DeviceGeoFenceController.java index 6ad03a08..34506027 100644 --- a/fys-admin/src/main/java/com/fuyuanshen/web/controller/device/fence/DeviceGeoFenceController.java +++ b/fys-admin/src/main/java/com/fuyuanshen/web/controller/device/fence/DeviceGeoFenceController.java @@ -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 addFenceTerminal(@RequestBody FenceTerminalBo bo) { + return toAjax(deviceGeoFenceService.addFenceTerminal(bo)); + } + /** + * 删除电子围栏终端 + * + * @param bo + * @return + */ + @PostMapping("/delTerminal") + public R delFenceTerminal(@RequestBody FenceTerminalBo bo) { + return toAjax(deviceGeoFenceService.delFenceTerminal(bo)); + } + } diff --git a/fys-admin/src/main/resources/application.yml b/fys-admin/src/main/resources/application.yml index be595503..af8e8356 100644 --- a/fys-admin/src/main/resources/application.yml +++ b/fys-admin/src/main/resources/application.yml @@ -132,6 +132,8 @@ tenant: - app_menu - app_user_role - app_role_menu + - track_service + - device_fence_terminal # MyBatisPlus配置 # https://baomidou.com/config/ diff --git a/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/controller/DeviceController.java b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/controller/DeviceController.java index 991c04a9..d3bbce2e 100644 --- a/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/controller/DeviceController.java +++ b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/controller/DeviceController.java @@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.fuyuanshen.common.core.domain.R; import com.fuyuanshen.common.core.domain.model.LoginUser; import com.fuyuanshen.common.core.utils.file.FileUtil; +import com.fuyuanshen.common.mybatis.core.page.PageQuery; import com.fuyuanshen.common.mybatis.core.page.TableDataInfo; import com.fuyuanshen.common.satoken.utils.LoginHelper; import com.fuyuanshen.common.web.core.BaseController; @@ -72,6 +73,12 @@ public class DeviceController extends BaseController { return deviceService.queryAll(criteria, page); } + @GetMapping(value = "/pageTerminal") + public TableDataInfo deviceFenecSelect(DeviceQueryCriteria criteria) throws IOException { + Page page = new Page<>(criteria.getPageNum(), criteria.getPageSize()); + return deviceService.queryAllTerminal(criteria, page); + } + // @Log("新增设备") @Operation(summary = "新增设备") diff --git a/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/controller/TrackServiceController.java b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/controller/TrackServiceController.java new file mode 100644 index 00000000..d831d537 --- /dev/null +++ b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/controller/TrackServiceController.java @@ -0,0 +1,143 @@ +package com.fuyuanshen.equipment.controller; + +import java.util.List; + +import com.fuyuanshen.equipment.domain.Device; +import com.fuyuanshen.equipment.domain.bo.TerminalDelBo; +import com.fuyuanshen.equipment.domain.bo.TerminalDeviceBo; +import com.fuyuanshen.equipment.domain.bo.TerminalQueryBo; +import com.fuyuanshen.equipment.domain.bo.TrackServiceBo; +import com.fuyuanshen.equipment.domain.dto.TerminalDeviceDto; +import com.fuyuanshen.equipment.domain.vo.TrackServiceVo; +import com.fuyuanshen.equipment.service.ITrackServiceService; +import lombok.RequiredArgsConstructor; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.constraints.*; +import cn.dev33.satoken.annotation.SaCheckPermission; +import org.springframework.web.bind.annotation.*; +import org.springframework.validation.annotation.Validated; +import com.fuyuanshen.common.idempotent.annotation.RepeatSubmit; +import com.fuyuanshen.common.log.annotation.Log; +import com.fuyuanshen.common.web.core.BaseController; +import com.fuyuanshen.common.mybatis.core.page.PageQuery; +import com.fuyuanshen.common.core.domain.R; +import com.fuyuanshen.common.core.validate.AddGroup; +import com.fuyuanshen.common.core.validate.EditGroup; +import com.fuyuanshen.common.log.enums.BusinessType; +import com.fuyuanshen.common.excel.utils.ExcelUtil; +import com.fuyuanshen.common.mybatis.core.page.TableDataInfo; + +/** + * 轨迹服务 + * + * @author Lion Li + * @date 2025-11-24 + */ +@Validated +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/trackService") +public class TrackServiceController extends BaseController { + + private final ITrackServiceService trackServiceService; + + /** + * 查询轨迹服务列表 + */ + @SaCheckPermission("equipment:trackService:list") + @GetMapping("/list") + public TableDataInfo list(TrackServiceBo bo, PageQuery pageQuery) { + return trackServiceService.queryPageList(bo, pageQuery); + } + + /** + * 轨迹服务列表 + */ + @SaCheckPermission("equipment:trackService:export") + @Log(title = "轨迹服务", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(TrackServiceBo bo, HttpServletResponse response) { + List list = trackServiceService.queryList(bo); + ExcelUtil.exportExcel(list, "轨迹服务", TrackServiceVo.class, response); + } + + /** + * 获取轨迹服务详细信息 + * + * @param id 主键 + */ + @GetMapping("/{id}") + public R getInfo(@NotNull(message = "主键不能为空") + @PathVariable Long id) { + return R.ok(trackServiceService.queryById(id)); + } + + /** + * 新增轨迹服务 + */ + @SaCheckPermission("equipment:trackService:add") + @Log(title = "轨迹服务", businessType = BusinessType.INSERT) + @PostMapping(value = "/add") + public R add(@Validated(AddGroup.class) @RequestBody TrackServiceBo bo) { + return toAjax(trackServiceService.insertByBo(bo)); + } + + /** + * 修改轨迹服务 + */ + @SaCheckPermission("equipment:trackService:edit") + @Log(title = "轨迹服务", businessType = BusinessType.UPDATE) + @PostMapping(value = "/update") + public R edit(@Validated(EditGroup.class) @RequestBody TrackServiceBo bo) { + return toAjax(trackServiceService.updateByBo(bo)); + } + + /** + * 删除轨迹服务 + * + * @param ids 主键串 + */ + @SaCheckPermission("equipment:trackService:remove") + @Log(title = "轨迹服务", businessType = BusinessType.DELETE) + @DeleteMapping(value = "/delete") + public R remove(@NotEmpty(message = "主键不能为空") + @PathVariable Long[] ids) { + return toAjax(trackServiceService.deleteWithValidByIds(List.of(ids), true)); + } + + @Log(title = "高德添加终端设备") + @PostMapping(value = "/terminal") + public R terminalDevice(@RequestBody TerminalDeviceBo model) { + trackServiceService.terminal(model); + return R.ok(); + } + + @Log(title = "高德移除终端设备") + @DeleteMapping(value = "/delTerminal/{ids}") + public R delTerminalDevice(@NotEmpty(message = "主键不能为空") + @PathVariable Long[] ids) { + return toAjax(trackServiceService.delTerminalDevice(List.of(ids), true)); + } + + @Log(title = "高德移除终端设备") + @DeleteMapping(value = "/del/Terminal") + public R delTerminalDevice(TerminalDelBo bo) { + return toAjax(trackServiceService.delTerminalDevice(bo, true)); + } + + /** + * 高德终端设备列表 + */ + @GetMapping("/listTerminal") + public TableDataInfo listTerminal(TerminalQueryBo bo) { + return trackServiceService.listTerminal(bo); + } + + /** + * 高德终端设备列表(关键字查询) + */ + @GetMapping("/searchTerminal") + public TableDataInfo searchTerminal(TerminalQueryBo bo) { + return trackServiceService.searchTerminal(bo); + } +} diff --git a/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/Device.java b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/Device.java index 967029dc..9ef76303 100644 --- a/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/Device.java +++ b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/Device.java @@ -167,4 +167,20 @@ public class Device extends TenantEntity { */ private Integer onlineStatus; + /** + * 高德服务ID + */ + @Schema(title = "服务ID(高德)") + private Long sid; + /** + * 高德终端ID + */ + @Schema(title = "终端ID(高德)") + private Long tid; + /** + * 高德轨迹ID + */ + @Schema(title = "轨迹ID(高德)") + private Long trid; + } diff --git a/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/DeviceFenceAccessRecord.java b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/DeviceFenceAccessRecord.java index 95c67d6c..d92792c8 100644 --- a/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/DeviceFenceAccessRecord.java +++ b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/DeviceFenceAccessRecord.java @@ -2,6 +2,7 @@ package com.fuyuanshen.equipment.domain; import com.baomidou.mybatisplus.annotation.*; import com.fuyuanshen.common.mybatis.core.domain.BaseEntity; +import com.fuyuanshen.common.tenant.core.TenantEntity; import lombok.Data; import lombok.EqualsAndHashCode; import java.util.Date; @@ -18,7 +19,7 @@ import java.io.Serial; @Data @EqualsAndHashCode(callSuper = true) @TableName("device_fence_access_record") -public class DeviceFenceAccessRecord extends BaseEntity { +public class DeviceFenceAccessRecord extends TenantEntity { @Serial private static final long serialVersionUID = 1L; diff --git a/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/DeviceFenceTerminal.java b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/DeviceFenceTerminal.java new file mode 100644 index 00000000..51014637 --- /dev/null +++ b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/DeviceFenceTerminal.java @@ -0,0 +1,26 @@ +package com.fuyuanshen.equipment.domain; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +/** + * 围栏终端关联 + * + * @author Lion Li + */ +@Data +@TableName("device_fence_terminal") +public class DeviceFenceTerminal { + /** + * 围栏ID + */ + @TableId(type = IdType.INPUT) + private Long fenceId; + + /** + * 设备ID + */ + private Long deviceId; +} diff --git a/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/DeviceGeoFence.java b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/DeviceGeoFence.java index 7cade915..bdaf9f63 100644 --- a/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/DeviceGeoFence.java +++ b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/DeviceGeoFence.java @@ -58,6 +58,14 @@ public class DeviceGeoFence extends BaseEntity { * 是否激活 */ private Long isActive; + /** + * 高德服务ID + */ + private Long sid; + /** + * 高德围栏ID + */ + private Long gfid; } diff --git a/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/TrackService.java b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/TrackService.java new file mode 100644 index 00000000..200a375a --- /dev/null +++ b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/TrackService.java @@ -0,0 +1,41 @@ +package com.fuyuanshen.equipment.domain; + +import com.baomidou.mybatisplus.annotation.*; +import com.fuyuanshen.common.mybatis.core.domain.BaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serial; + +/** + * 轨迹服务对象 track_service + * + * @author Lion Li + * @date 2025-11-24 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("track_service") +public class TrackService extends BaseEntity { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * + */ + @TableId(value = "id") + private Long id; + + /** + * 猎鹰服务ID + */ + private Long sid; + + /** + * 服务名称 + */ + private String sname; + + +} diff --git a/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/bo/DeviceGeoFenceBo.java b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/bo/DeviceGeoFenceBo.java index a2506385..67a02b47 100644 --- a/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/bo/DeviceGeoFenceBo.java +++ b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/bo/DeviceGeoFenceBo.java @@ -70,6 +70,14 @@ public class DeviceGeoFenceBo extends BaseEntity { * 更新时间 */ private Date updateTime; + /** + * 高德服务ID + */ + private Long sid; + /** + * 高德围栏ID + */ + private Long gfid; } diff --git a/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/bo/FenceTerminalBo.java b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/bo/FenceTerminalBo.java new file mode 100644 index 00000000..ddf9e13a --- /dev/null +++ b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/bo/FenceTerminalBo.java @@ -0,0 +1,17 @@ +package com.fuyuanshen.equipment.domain.bo; + +import lombok.Data; + +import java.util.List; + +@Data +public class FenceTerminalBo { + /** + * 围栏ID + */ + private Long fenceId; + /** + * 设备IDs + */ + private List deviceIds; +} diff --git a/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/bo/TerminalDelBo.java b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/bo/TerminalDelBo.java new file mode 100644 index 00000000..2a92dd8f --- /dev/null +++ b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/bo/TerminalDelBo.java @@ -0,0 +1,19 @@ +package com.fuyuanshen.equipment.domain.bo; + +import lombok.Data; + +@Data +public class TerminalDelBo { + /** + * 猎鹰服务ID + */ + private Long sid; + /** + * 轨迹ID + */ + private Long trid; + /** + * 终端ID + */ + private Long tid; +} diff --git a/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/bo/TerminalDeviceBo.java b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/bo/TerminalDeviceBo.java new file mode 100644 index 00000000..fa5fcaa6 --- /dev/null +++ b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/bo/TerminalDeviceBo.java @@ -0,0 +1,18 @@ +package com.fuyuanshen.equipment.domain.bo; + +import com.fuyuanshen.equipment.domain.form.DeviceForm; +import lombok.Data; + +import java.util.List; + +@Data +public class TerminalDeviceBo { + /** + * 猎鹰服务ID + */ + private Long sid; + /** + * 设备列表 + */ + List deviceList; +} diff --git a/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/bo/TerminalQueryBo.java b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/bo/TerminalQueryBo.java new file mode 100644 index 00000000..c2ea5410 --- /dev/null +++ b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/bo/TerminalQueryBo.java @@ -0,0 +1,32 @@ +package com.fuyuanshen.equipment.domain.bo; + +import lombok.Data; +import lombok.Value; + +@Data +public class TerminalQueryBo { + /** + * 猎鹰服务ID + */ + private Long sid; + /** + * 名称查询 + */ + private String name; + /** + * 终端ID + */ + private Long tid; + /** + * 关键字搜索 + */ + private String keywords; + /** + * 分页 + */ + private int page = 1; + /** + * 每页数量 + */ + private int pagesize = 10; +} diff --git a/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/bo/TrackServiceBo.java b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/bo/TrackServiceBo.java new file mode 100644 index 00000000..6da709a5 --- /dev/null +++ b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/bo/TrackServiceBo.java @@ -0,0 +1,36 @@ +package com.fuyuanshen.equipment.domain.bo; + +import com.fuyuanshen.common.mybatis.core.domain.BaseEntity; +import com.fuyuanshen.equipment.domain.TrackService; +import io.github.linpeilie.annotations.AutoMapper; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 轨迹服务业务对象 track_service + * + * @author Lion Li + * @date 2025-11-24 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@AutoMapper(target = TrackService.class, reverseConvertGenerate = false) +public class TrackServiceBo extends BaseEntity { + + /** + * + */ + private Long id; + + /** + * 猎鹰服务ID + */ + private Long sid; + + /** + * 服务名称 + */ + private String sname; + + +} \ No newline at end of file diff --git a/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/dto/TerminalDeviceDto.java b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/dto/TerminalDeviceDto.java new file mode 100644 index 00000000..5489ff93 --- /dev/null +++ b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/dto/TerminalDeviceDto.java @@ -0,0 +1,29 @@ +package com.fuyuanshen.equipment.domain.dto; + +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor // 必须 +public class TerminalDeviceDto { + /** + * 终端设备名称 + */ + private String name; + /** + * 终端设备ID + */ + private Long tid; + /** + * 终端设备描述 + */ + private String desc; + /** + * 终端设备创建时间 + */ + private String createtime; + /** + * 终端设备最后定位时间 + */ + private String locatetime; +} diff --git a/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/query/DeviceQueryCriteria.java b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/query/DeviceQueryCriteria.java index 5258b1e5..40f28652 100644 --- a/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/query/DeviceQueryCriteria.java +++ b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/query/DeviceQueryCriteria.java @@ -122,6 +122,18 @@ public class DeviceQueryCriteria extends BaseEntity { * online_status */ private Integer onlineStatus; + /** + * 高德终端ID是否存在 + */ + private Boolean isTid; + /** + * 高德服务ID + */ + private String sid; + /** + * 围栏ID + */ + private Long fenceId; /** * 绑定状态 diff --git a/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/vo/DeviceGeoFenceVo.java b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/vo/DeviceGeoFenceVo.java index cefb305b..9587171a 100644 --- a/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/vo/DeviceGeoFenceVo.java +++ b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/vo/DeviceGeoFenceVo.java @@ -101,6 +101,14 @@ public class DeviceGeoFenceVo implements Serializable { */ // @ExcelProperty(value = "更新时间") private Date updateTime; + /** + * 高德服务ID + */ + private Long sid; + /** + * 高德围栏ID + */ + private Long gfid; public void setAreaTypeNameByAreaType() { diff --git a/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/vo/TrackServiceVo.java b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/vo/TrackServiceVo.java new file mode 100644 index 00000000..40bbc0b1 --- /dev/null +++ b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/vo/TrackServiceVo.java @@ -0,0 +1,50 @@ +package com.fuyuanshen.equipment.domain.vo; + +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; +import com.fuyuanshen.common.excel.annotation.ExcelDictFormat; +import com.fuyuanshen.common.excel.convert.ExcelDictConvert; +import com.fuyuanshen.equipment.domain.TrackService; +import io.github.linpeilie.annotations.AutoMapper; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Date; + + + +/** + * 轨迹服务视图对象 track_service + * + * @author Lion Li + * @date 2025-11-24 + */ +@Data +@ExcelIgnoreUnannotated +@AutoMapper(target = TrackService.class) +public class TrackServiceVo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * + */ + @ExcelProperty(value = "") + private Long id; + + /** + * 猎鹰服务ID + */ + @ExcelProperty(value = "猎鹰服务ID") + private Long sid; + + /** + * 服务名称 + */ + @ExcelProperty(value = "服务名称") + private String sname; + + +} diff --git a/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/vo/WebDeviceVo.java b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/vo/WebDeviceVo.java index 9f29bef3..0127fe0c 100644 --- a/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/vo/WebDeviceVo.java +++ b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/vo/WebDeviceVo.java @@ -1,6 +1,7 @@ package com.fuyuanshen.equipment.domain.vo; import cn.idev.excel.annotation.ExcelProperty; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.io.Serializable; @@ -103,5 +104,20 @@ public class WebDeviceVo implements Serializable { * 分享用户数量 */ private Integer shareUsersNumber; + /** + * 高德服务ID + */ + @Schema(title = "服务ID(高德)") + private Long sid; + /** + * 高德终端ID + */ + @Schema(title = "终端ID(高德)") + private Long tid; + /** + * 高德轨迹ID + */ + @Schema(title = "轨迹ID(高德)") + private Long trid; } diff --git a/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/mapper/DeviceFenceTerminalMapper.java b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/mapper/DeviceFenceTerminalMapper.java new file mode 100644 index 00000000..8823dbc4 --- /dev/null +++ b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/mapper/DeviceFenceTerminalMapper.java @@ -0,0 +1,7 @@ +package com.fuyuanshen.equipment.mapper; + +import com.fuyuanshen.common.mybatis.core.mapper.BaseMapperPlus; +import com.fuyuanshen.equipment.domain.DeviceFenceTerminal; + +public interface DeviceFenceTerminalMapper extends BaseMapperPlus { +} diff --git a/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/mapper/DeviceMapper.java b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/mapper/DeviceMapper.java index b846c205..18fc9a26 100644 --- a/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/mapper/DeviceMapper.java +++ b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/mapper/DeviceMapper.java @@ -32,6 +32,8 @@ public interface DeviceMapper extends BaseMapper { List findAll(@Param("criteria") DeviceQueryCriteria criteria); + IPage findAllTerminal(@Param("criteria") DeviceQueryCriteria criteria, Page page); + List findAllDevices(@Param("criteria") DeviceQueryCriteria criteria); /** diff --git a/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/mapper/TrackServiceMapper.java b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/mapper/TrackServiceMapper.java new file mode 100644 index 00000000..6cc2b1ce --- /dev/null +++ b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/mapper/TrackServiceMapper.java @@ -0,0 +1,15 @@ +package com.fuyuanshen.equipment.mapper; + +import com.fuyuanshen.common.mybatis.core.mapper.BaseMapperPlus; +import com.fuyuanshen.equipment.domain.TrackService; +import com.fuyuanshen.equipment.domain.vo.TrackServiceVo; + +/** + * 轨迹服务Mapper接口 + * + * @author Lion Li + * @date 2025-11-24 + */ +public interface TrackServiceMapper extends BaseMapperPlus { + +} \ No newline at end of file diff --git a/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/service/DeviceService.java b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/service/DeviceService.java index d283534e..45cfef59 100644 --- a/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/service/DeviceService.java +++ b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/service/DeviceService.java @@ -30,6 +30,8 @@ public interface DeviceService extends IService { */ TableDataInfo queryAll(DeviceQueryCriteria criteria, Page page) throws IOException; + TableDataInfo queryAllTerminal(DeviceQueryCriteria criteria, Page page) throws IOException; + /** * 查询所有数据不分页 * diff --git a/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/service/IDeviceGeoFenceService.java b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/service/IDeviceGeoFenceService.java index 65eaa44f..b270b1b5 100644 --- a/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/service/IDeviceGeoFenceService.java +++ b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/service/IDeviceGeoFenceService.java @@ -6,6 +6,7 @@ import com.fuyuanshen.common.mybatis.core.page.PageQuery; import com.fuyuanshen.equipment.domain.Device; import com.fuyuanshen.equipment.domain.DeviceGeoFence; 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; @@ -78,4 +79,8 @@ public interface IDeviceGeoFenceService extends IService { * @return 位置检查结果 */ FenceCheckResponse checkPosition(FenceCheckRequest request); + + Boolean addFenceTerminal(FenceTerminalBo bo); + + Boolean delFenceTerminal(FenceTerminalBo bo); } diff --git a/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/service/ITrackServiceService.java b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/service/ITrackServiceService.java new file mode 100644 index 00000000..b2dad949 --- /dev/null +++ b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/service/ITrackServiceService.java @@ -0,0 +1,103 @@ +package com.fuyuanshen.equipment.service; + +import com.fuyuanshen.common.mybatis.core.page.PageQuery; +import com.fuyuanshen.common.mybatis.core.page.TableDataInfo; +import com.fuyuanshen.equipment.domain.bo.TerminalDelBo; +import com.fuyuanshen.equipment.domain.bo.TerminalDeviceBo; +import com.fuyuanshen.equipment.domain.bo.TerminalQueryBo; +import com.fuyuanshen.equipment.domain.bo.TrackServiceBo; +import com.fuyuanshen.equipment.domain.dto.TerminalDeviceDto; +import com.fuyuanshen.equipment.domain.vo.TrackServiceVo; + +import java.util.Collection; +import java.util.List; + +/** + * 轨迹服务Service接口 + * + * @author Lion Li + * @date 2025-11-24 + */ +public interface ITrackServiceService { + + /** + * 查询轨迹服务 + * + * @param id 主键 + * @return 轨迹服务 + */ + TrackServiceVo queryById(Long id); + + /** + * 分页查询轨迹服务列表 + * + * @param bo 查询条件 + * @param pageQuery 分页参数 + * @return 轨迹服务分页列表 + */ + TableDataInfo queryPageList(TrackServiceBo bo, PageQuery pageQuery); + + /** + * 查询符合条件的轨迹服务列表 + * + * @param bo 查询条件 + * @return 轨迹服务列表 + */ + List queryList(TrackServiceBo bo); + + /** + * 新增轨迹服务 + * + * @param bo 轨迹服务 + * @return 是否新增成功 + */ + Boolean insertByBo(TrackServiceBo bo); + + /** + * 修改轨迹服务 + * + * @param bo 轨迹服务 + * @return 是否修改成功 + */ + Boolean updateByBo(TrackServiceBo bo); + + /** + * 校验并批量删除轨迹服务信息 + * + * @param ids 待删除的主键集合 + * @param isValid 是否进行有效性校验 + * @return 是否删除成功 + */ + Boolean deleteWithValidByIds(Collection ids, Boolean isValid); + + /** + * 高德添加终端设备 + * + * @param terminalDeviceBo 添加集合 + */ + void terminal(TerminalDeviceBo terminalDeviceBo); + /** + * 高德移除终端设备 + * + * @param ids 待删除的主键集合 + * @param isValid 是否进行有效性校验 + * @return 是否删除成功 + */ + Boolean delTerminalDevice(Collection ids, Boolean isValid); + + Boolean delTerminalDevice(TerminalDelBo bo, Boolean isValid); + /** + * 查询设备列表 + * + * @param bo 查询条件 + * @return 设备列表 + */ + TableDataInfo listTerminal(TerminalQueryBo bo); + /** + * 关键字查询设备 + * + * @param bo 查询条件 + * @return 设备列表 + */ + TableDataInfo searchTerminal(TerminalQueryBo bo); +} diff --git a/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/service/impl/DeviceGeoFenceServiceImpl.java b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/service/impl/DeviceGeoFenceServiceImpl.java index 38906164..65035586 100644 --- a/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/service/impl/DeviceGeoFenceServiceImpl.java +++ b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/service/impl/DeviceGeoFenceServiceImpl.java @@ -1,5 +1,6 @@ package com.fuyuanshen.equipment.service.impl; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; @@ -10,14 +11,17 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.fuyuanshen.equipment.domain.Device; +import com.fuyuanshen.equipment.domain.DeviceFenceTerminal; import com.fuyuanshen.equipment.domain.DeviceGeoFence; import com.fuyuanshen.equipment.domain.bo.DeviceFenceAccessRecordBo; import com.fuyuanshen.equipment.domain.bo.DeviceFenceStatusBo; 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.DeviceFenceStatusVo; import com.fuyuanshen.equipment.domain.vo.DeviceGeoFenceVo; +import com.fuyuanshen.equipment.mapper.DeviceFenceTerminalMapper; import com.fuyuanshen.equipment.mapper.DeviceGeoFenceMapper; import com.fuyuanshen.equipment.mapper.DeviceMapper; import com.fuyuanshen.equipment.service.IDeviceFenceAccessRecordService; @@ -25,6 +29,7 @@ import com.fuyuanshen.equipment.service.IDeviceFenceStatusService; import com.fuyuanshen.equipment.service.IDeviceGeoFenceService; import com.fuyuanshen.equipment.utils.map.GeoFenceChecker; import com.fuyuanshen.equipment.utils.map.GetAddressFromLatUtil; +import com.fuyuanshen.system.domain.SysRoleMenu; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; @@ -48,6 +53,7 @@ public class DeviceGeoFenceServiceImpl extends ServiceImpl list = new ArrayList<>(); + for (Long deviceId : bo.getDeviceIds()) { + DeviceFenceTerminal rm = new DeviceFenceTerminal(); + rm.setFenceId(bo.getFenceId()); + rm.setDeviceId(deviceId); + list.add(rm); + } + if (!list.isEmpty()) { + return deviceFenceTerminalMapper.insertBatch(list); + } + return false; + } + + @Override + public Boolean delFenceTerminal(FenceTerminalBo bo) { + if (bo.getFenceId() == null || bo.getDeviceIds() == null || bo.getDeviceIds().isEmpty()) { + throw new IllegalArgumentException("围栏ID或设备ID不能为空"); + } + LambdaUpdateWrapper lambda = new LambdaUpdateWrapper() + .eq(DeviceFenceTerminal::getFenceId, bo.getFenceId()) + .in(DeviceFenceTerminal::getDeviceId, bo.getDeviceIds()); + return deviceFenceTerminalMapper.delete(lambda) > 0; + } + } \ No newline at end of file diff --git a/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/service/impl/DeviceServiceImpl.java b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/service/impl/DeviceServiceImpl.java index da9372dc..797c4a09 100644 --- a/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/service/impl/DeviceServiceImpl.java +++ b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/service/impl/DeviceServiceImpl.java @@ -126,6 +126,26 @@ public class DeviceServiceImpl extends ServiceImpl impleme return new TableDataInfo(records, devices.getTotal()); } + @Override + public TableDataInfo queryAllTerminal(DeviceQueryCriteria criteria, Page page) throws IOException { + // 角色管理员 + Long userId = LoginHelper.getUserId(); + List roles = roleService.selectRolesAuthByUserId(userId); + boolean isAdmin = false; + if (CollectionUtil.isNotEmpty(roles)) { + for (SysRoleVo role : roles) { + if (role.getRoleKey().equals("admin")) { + isAdmin = true; + criteria.setIsAdmin(true); + break; + } + } + } + + IPage devices = deviceMapper.findAllTerminal(criteria, page); + return new TableDataInfo(devices.getRecords(), devices.getTotal()); + } + @Override public List queryAll(DeviceQueryCriteria criteria) { diff --git a/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/service/impl/TrackServiceServiceImpl.java b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/service/impl/TrackServiceServiceImpl.java new file mode 100644 index 00000000..ed5aea16 --- /dev/null +++ b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/service/impl/TrackServiceServiceImpl.java @@ -0,0 +1,404 @@ +package com.fuyuanshen.equipment.service.impl; + +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONObject; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.fuyuanshen.common.core.utils.MapstructUtils; +import com.fuyuanshen.common.core.utils.StringUtils; +import com.fuyuanshen.common.mybatis.core.page.TableDataInfo; +import com.fuyuanshen.common.mybatis.core.page.PageQuery; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.fuyuanshen.equipment.domain.Device; +import com.fuyuanshen.equipment.domain.TrackService; +import com.fuyuanshen.equipment.domain.bo.TerminalDelBo; +import com.fuyuanshen.equipment.domain.bo.TerminalDeviceBo; +import com.fuyuanshen.equipment.domain.bo.TerminalQueryBo; +import com.fuyuanshen.equipment.domain.bo.TrackServiceBo; +import com.fuyuanshen.equipment.domain.dto.TerminalDeviceDto; +import com.fuyuanshen.equipment.domain.form.DeviceForm; +import com.fuyuanshen.equipment.domain.vo.TrackServiceVo; +import com.fuyuanshen.equipment.mapper.DeviceMapper; +import com.fuyuanshen.equipment.mapper.TrackServiceMapper; +import com.fuyuanshen.equipment.service.ITrackServiceService; +import com.fuyuanshen.equipment.utils.map.AmapTrackUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * 轨迹服务Service业务层处理 + * + * @author Lion Li + * @date 2025-11-24 + */ +@Slf4j +@RequiredArgsConstructor +@Service +public class TrackServiceServiceImpl implements ITrackServiceService { + + private final TrackServiceMapper baseMapper; + private final DeviceMapper deviceMapper; + private final AmapTrackUtil amapTrackUtil; + + /** + * 查询轨迹服务 + * + * @param id 主键 + * @return 轨迹服务 + */ + @Override + public TrackServiceVo queryById(Long id){ + return baseMapper.selectVoById(id); + } + + /** + * 分页查询轨迹服务列表 + * + * @param bo 查询条件 + * @param pageQuery 分页参数 + * @return 轨迹服务分页列表 + */ + @Override + public TableDataInfo queryPageList(TrackServiceBo bo, PageQuery pageQuery) { + LambdaQueryWrapper lqw = buildQueryWrapper(bo); + Page result = baseMapper.selectVoPage(pageQuery.build(), lqw); + return TableDataInfo.build(result); + } + + /** + * 查询符合条件的轨迹服务列表 + * + * @param bo 查询条件 + * @return 轨迹服务列表 + */ + @Override + public List queryList(TrackServiceBo bo) { + LambdaQueryWrapper lqw = buildQueryWrapper(bo); + return baseMapper.selectVoList(lqw); + } + + private LambdaQueryWrapper buildQueryWrapper(TrackServiceBo bo) { + Map params = bo.getParams(); + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.orderByAsc(TrackService::getId); + lqw.eq(bo.getSid() != null, TrackService::getSid, bo.getSid()); + lqw.like(StringUtils.isNotBlank(bo.getSname()), TrackService::getSname, bo.getSname()); + return lqw; + } + + /** + * 新增轨迹服务 + * + * @param bo 轨迹服务 + * @return 是否新增成功 + */ + @Override + public Boolean insertByBo(TrackServiceBo bo) { + TrackService add = MapstructUtils.convert(bo, TrackService.class); + validEntityBeforeSave(add); + /* 1. 先查高德侧是否已存在同名服务 */ + JSONObject listResult = amapTrackUtil.listService(); + if (listResult == null || listResult.getInt("errcode") != 10000) { + throw new RuntimeException("查询高德轨迹服务列表失败"); + } + log.warn("查询: {}", listResult); + String serviceName = add.getSname(); + String sid = null; + + // 取 data 数组,遍历匹配 name + JSONArray serviceArray = listResult.getByPath("data.results", JSONArray.class); + if (serviceArray != null) { + for (int i = 0; i < serviceArray.size(); i++) { + JSONObject item = serviceArray.getJSONObject(i); + if (serviceName.equals(item.getStr("name"))) { + sid = item.getStr("sid"); // 已存在,直接复用 + break; + } + } + } + + /* 2. 不存在才创建 */ + if (sid == null) { + JSONObject createResult = amapTrackUtil.createService(serviceName); + if (createResult == null || createResult.getInt("errcode") != 10000) { + throw new RuntimeException("生成高德轨迹服务失败"); + } + sid = createResult.getByPath("data.sid", String.class); + } + + add.setSid(Long.valueOf(sid)); + boolean flag = baseMapper.insert(add) > 0; + if (flag) { + bo.setId(add.getId()); + } + return flag; + } + + /** + * 修改轨迹服务 + * + * @param bo 轨迹服务 + * @return 是否修改成功 + */ + @Override + public Boolean updateByBo(TrackServiceBo bo) { + TrackService update = MapstructUtils.convert(bo, TrackService.class); + validEntityBeforeSave(update); + if(update.getId() == null) { + throw new RuntimeException("轨迹服务ID不能为空"); + } + + return baseMapper.updateById(update) > 0; + } + + /** + * 保存前的数据校验 + */ + private void validEntityBeforeSave(TrackService entity){ + if(entity == null) { + throw new RuntimeException("请输入信息"); + } + //TODO 做一些数据校验,如唯一约束 + if(!StringUtils.isNotEmpty(entity.getSname())) { + throw new RuntimeException("轨迹服务名称不能为空"); + } + } + + /** + * 校验并批量删除轨迹服务信息 + * + * @param ids 待删除的主键集合 + * @param isValid 是否进行有效性校验 + * @return 是否删除成功 + */ + @Override + public Boolean deleteWithValidByIds(Collection ids, Boolean isValid) { + if(isValid){ + //TODO 做一些业务上的校验,判断是否需要校验 + } + return baseMapper.deleteByIds(ids) > 0; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean delTerminalDevice(Collection ids, Boolean isValid) { + if (CollectionUtils.isEmpty(ids)) { + throw new IllegalArgumentException("请选择要移除的设备"); + } + + // 1. 批量查询(一次 SQL,性能提升 10 倍+) + List deviceList = deviceMapper.selectList( + new LambdaQueryWrapper() + .in(Device::getId, ids) + .select(Device::getId, Device::getSid, Device::getTid, Device::getTrid) + ); + + if (CollectionUtils.isEmpty(deviceList)) { + log.info("未找到要移除的设备,ids={}", ids); + return true; // 幂等:没找到也算成功 + } + + // 2. 业务校验(可插拔) + if (isValid) { + validateBeforeDelete(deviceList); // 你自己实现具体校验逻辑 + } + + // 3. 批量清理高德资源 + 本地数据(精准对应,顺序正确) + for (Device device : deviceList) { + Long sid = device.getSid(); + Long tid = device.getTid(); + Long trid = device.getTrid(); + + if (sid == null) { + log.warn("设备无高德服务ID,跳过高德清理,deviceId={}", device.getId()); + continue; + } + + // 先删轨迹(必须 tid + trid),再删终端(只需 tid) + if (trid != null && tid != null) { + safeDeleteTrack(sid, tid, trid); + } + if (tid != null) { + safeDeleteTerminal(sid, tid); + } + } + + // 4. 批量更新数据库(性能极高,避免 N+1 update) + int updated = deviceMapper.update(null, + new LambdaUpdateWrapper() + .in(Device::getId, ids) + .set(Device::getTid, null) + .set(Device::getTrid, null) + .set(Device::getSid, null) + ); + + log.info("成功解绑高德终端,设备数量={},实际更新记录数={}", deviceList.size(), updated); + return true; + } + + @Override + public Boolean delTerminalDevice(TerminalDelBo bo, Boolean isValid) { + var list = deviceMapper.selectList(new LambdaQueryWrapper() + .eq(Device::getSid, bo.getSid()) + .eq(Device::getTid, bo.getTid())); + if (list.isEmpty()) { + throw new IllegalArgumentException("本地设备已绑定不允许删除"); + } + // 先删轨迹(必须 tid + trid),再删终端(只需 tid) + if (bo.getSid() != null && bo.getTid() != null) { + safeDeleteTrack(bo.getSid(), bo.getTid(), bo.getTrid()); + } + if (bo.getTid() != null) { + safeDeleteTerminal(bo.getSid(),bo.getTid()); + } + return true; + } + + /** + * 查询终端列表 + * + * @param bo 查询参数 + * @return 终端列表 + */ + @Override + public TableDataInfo listTerminal(TerminalQueryBo bo) { + JSONObject terminalRes = amapTrackUtil.listTerminal(bo.getSid(), bo.getTid(), bo.getName(), bo.getPage()); + List list = terminalRes.getByPath("data.results", JSONArray.class) + .toList(TerminalDeviceDto.class); + Integer count = terminalRes.getByPath("data.count", Integer.class); + return new TableDataInfo(list, count); + } + + /** + * 搜索终端列表 + * + * @param bo 搜索参数 + * @return 终端列表 + */ + @Override + public TableDataInfo searchTerminal(TerminalQueryBo bo) { + JSONObject terminalRes = amapTrackUtil.searchTerminal(bo.getSid(), bo.getKeywords(), bo.getPage(), bo.getPagesize()); + log.info("终端列表:{}", terminalRes); + List list = terminalRes.getByPath("data.results", JSONArray.class) + .toList(TerminalDeviceDto.class); + Integer count = terminalRes.getByPath("data.count", Integer.class); + return new TableDataInfo(list, count); + } + + /** + * 业务校验钩子 + */ + private void validateBeforeDelete(List deviceList) { + // 示例:校验设备是否在线、是否有未完成订单等 + // throw new BusinessException("xxx设备正在使用中,不能解绑"); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void terminal(TerminalDeviceBo bo) { + if (bo.getSid() == null) { + throw new IllegalArgumentException("高德服务ID不能为空"); + } + if (CollectionUtils.isEmpty(bo.getDeviceList())) { + throw new IllegalArgumentException("请选择设备"); + } + + Long sid = bo.getSid(); + + // 使用对象封装,便于补偿时知道 tid 和 trid 的对应关系(关键!) + class CreatedResource { + Long tid; + Long trid; + } + List createdResources = new ArrayList<>(); + + try { + for (DeviceForm form : bo.getDeviceList()) { + String terminalName = String.format("%s_%s", form.getTypeName(), form.getDeviceImei()); + + // Step 1: 创建终端 + JSONObject terminalRes = amapTrackUtil.createTerminal(sid, terminalName); + if (!isSuccess(terminalRes)) { + throw new RuntimeException("创建设备终端失败:" + terminalRes); + } + Long tid = terminalRes.getByPath("data.tid", Long.class); + + // Step 2: 创建轨迹 + JSONObject trackRes = amapTrackUtil.createTrace(sid, tid); + if (!isSuccess(trackRes)) { + throw new RuntimeException("创建轨迹失败:" + trackRes); + } + Long trid = trackRes.getByPath("data.trid", Long.class); + + // 3. 更新本地设备 + Device device = new Device(); + device.setId(form.getId()); + device.setTid(tid); + device.setTrid(trid); + device.setSid(sid); + deviceMapper.updateById(device); + + // 记录本次创建的资源(用于精确补偿) + CreatedResource resource = new CreatedResource(); + resource.tid = tid; + resource.trid = trid; + createdResources.add(resource); + + // 可落盘日志(用于极端情况人工排查) + log.warn("高德添加设备终端 sid={} tid={} trid={} deviceId={}", + sid, tid, trid, form.getId()); + } + + // 全部成功 → 清空内存集合 + createdResources.clear(); + + } catch (Exception e) { + log.warn("绑定高德终端失败,正在执行补偿清理,已创建资源数={}", createdResources.size(), e); + + // 精确补偿:先删轨迹(需要 sid + tid + trid),再删终端(需要 sid + tid) + for (CreatedResource r : createdResources) { + if (r.trid != null) { + safeDeleteTrack(sid, r.tid, r.trid); // 现在传 3 个参数 + } + if (r.tid != null) { + safeDeleteTerminal(sid, r.tid); + } + } + + throw e; // 抛异常让调用方知道失败 + } + } + + /** 安全删除轨迹 */ + private void safeDeleteTrack(Long sid, Long tid, Long trid) { + try { + // 假设你的工具方法是这样定义的: + // amapTrackUtil.deleteTrack(sid, tid, trid); + amapTrackUtil.deleteTrace(sid, tid, trid); + log.info("补偿删除轨迹成功 sid={} tid={} trid={}", sid, tid, trid); + } catch (Exception ex) { + log.error("补偿删除轨迹失败 sid={} tid={} trid={},请人工介入或等定时任务处理", sid, tid, trid, ex); + } + } + + /** 安全删除终端 */ + private void safeDeleteTerminal(Long sid, Long tid) { + try { + amapTrackUtil.deleteTerminal(sid, tid); + log.info("补偿删除终端成功 sid={} tid={}", sid, tid); + } catch (Exception ex) { + log.error("补偿删除终端失败 sid={} tid={},请人工介入或等定时任务处理", sid, tid, ex); + } + } + + private boolean isSuccess(JSONObject json) { + return json != null && Objects.equals(json.getInt("errcode"), 10000); + } +} \ No newline at end of file diff --git a/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/utils/map/AmapTrackUtil.java b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/utils/map/AmapTrackUtil.java new file mode 100644 index 00000000..9a4fba9a --- /dev/null +++ b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/utils/map/AmapTrackUtil.java @@ -0,0 +1,276 @@ +package com.fuyuanshen.equipment.utils.map; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.HttpUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.*; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import java.net.URI; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; + +/** + * 高德猎鹰轨迹服务 工具类 + *

+ * 优化点: + * 1. 引入 Slf4j 增加日志记录 + * 2. 使用 UriComponentsBuilder 处理 URL 拼接和参数编码 + * 3. 统一异常处理和请求封装 + * 4. 提取常量 + *

+ * 官网文档:https://lbs.amap.com/api/track + */ +@Slf4j +@Component +public class AmapTrackUtil { + + private static final String BASE_URL = "https://tsapi.amap.com/v1/track"; + +// @Value("${gaode.key:}") + private final String key = "84a12a692ae378effdf741e16d584cd3"; + + + /* ========================== 1. 轨迹服务 ========================== */ + + /** 创建服务 */ + public JSONObject createService(String name) { + return sendRequest(HttpMethod.POST, "/service/add", builder -> builder.queryParam("name", name), null); + } + + /** 更新服务 */ + public JSONObject updateService(String sid, String name) { + return sendRequest(HttpMethod.POST, "/service/update", + builder -> builder.queryParam("sid", sid).queryParam("name", name), null); + } + + /** 查询服务列表 */ + public JSONObject listService() { + return sendRequest(HttpMethod.GET, "/service/list", null, null); + } + + /** 删除服务 */ + public JSONObject deleteService(String sid) { + return sendRequest(HttpMethod.POST, "/service/delete", builder -> builder.queryParam("sid", sid), null); + } + + /* ========================== 2. 终端 ========================== */ + + /** 创建终端 */ + public JSONObject createTerminal(Long sid, String name) { + return sendRequest(HttpMethod.POST, "/terminal/add", + builder -> builder.queryParam("sid", sid).queryParam("name", name), null); + } + + /** 查询终端列表 */ + public JSONObject listTerminal(Long sid, Long tid, String name, int page) { + return sendRequest(HttpMethod.GET, "/terminal/list", + builder -> { + builder.queryParam("sid", sid).queryParam("page", page); + if (StrUtil.isNotBlank(name)) { + builder.queryParam("name", name); + } + if (tid != null) { + builder.queryParam("tid", tid); + } + }, null); + } + + /** 查询终端列表 */ + public JSONObject searchTerminal(Long sid, String keywords, int page, int pageSize) { + return sendRequest(HttpMethod.GET, "/terminal/list", + builder -> builder.queryParam("sid", sid).queryParam("keywords", keywords).queryParam("page", page).queryParam("pageSize", pageSize), null); + } + + /** 删除终端 */ + public JSONObject deleteTerminal(Long sid, Long tid) { + return sendRequest(HttpMethod.POST, "/terminal/delete", + builder -> builder.queryParam("sid", sid).queryParam("tid", tid), null); + } + + /* ========================== 3. 轨迹 ========================== */ + + /** 创建轨迹 */ + public JSONObject createTrace(Long sid, Long tid) { + return sendRequest(HttpMethod.POST, "/trace/add", + builder -> builder.queryParam("sid", sid).queryParam("tid", tid), null); + } + + /** 查询轨迹(按 trid) */ + public JSONObject queryTrace(String sid, String tid, String trid) { + return sendRequest(HttpMethod.GET, "/terminal/trsearch", + builder -> builder.queryParam("sid", sid).queryParam("tid", tid).queryParam("trid", trid), null); + } + + /** 查询轨迹(按时间段) */ + public JSONObject queryTrace(String sid, String tid, long start, long end) { + return sendRequest(HttpMethod.GET, "/terminal/trsearch", + builder -> builder.queryParam("sid", sid) + .queryParam("tid", tid) + .queryParam("starttime", start) + .queryParam("endtime", end), null); + } + + /** 删除轨迹 */ + public JSONObject deleteTrace(Long sid, Long tid, Long trid) { + return sendRequest(HttpMethod.POST, "/trace/delete", + builder -> builder.queryParam("sid", sid).queryParam("tid", tid).queryParam("trid", trid), null); + } + + /* ========================== 4. 轨迹点上传 ========================== */ + + /** + * 单点/批量上传轨迹点 + * 注意:上传点的参数较多,且位于 Body 中 + */ + public JSONObject uploadPoints(Long sid, Long tid, Long trid, Object points) { + Map bodyMap = new HashMap<>(); + bodyMap.put("sid", sid); + bodyMap.put("tid", tid); + bodyMap.put("trid", trid); + bodyMap.put("points", points); + return sendRequest(HttpMethod.POST, "/point/upload", null, bodyMap); + } + + /* ========================== 5. 围栏 ========================== */ + + /** 创建圆形围栏 */ + public JSONObject createCircleFence(String name, String center, int radius, String sid, String... tids) { + return sendRequest(HttpMethod.POST, "/geofence/add/circle", builder -> { + builder.queryParam("name", name) + .queryParam("center", center) + .queryParam("radius", radius) + .queryParam("sid", sid); + if (tids != null && tids.length > 0) { + builder.queryParam("tids", String.join(",", tids)); + } + }, null); + } + + /** 创建多边形围栏 */ + public JSONObject createPolygonFence(String name, String points, String sid, String... tids) { + return sendRequest(HttpMethod.POST, "/geofence/add/polygon", builder -> { + builder.queryParam("name", name) + .queryParam("points", points) + .queryParam("sid", sid); + if (tids != null && tids.length > 0) { + builder.queryParam("tids", String.join(",", tids)); + } + }, null); + } + + /** 绑定/解绑终端到围栏 */ + public JSONObject bindTerminalToFence(String sid, String gfid, String... tids) { + return sendRequest(HttpMethod.POST, "/geofence/terminal/bind", + builder -> builder.queryParam("sid", sid) + .queryParam("gfid", gfid) + .queryParam("tids", String.join(",", tids)) + .queryParam("enable", "1"), null); + } + + /** 查询终端与围栏当前状态(在/不在) */ + public JSONObject queryTerminalFenceStatus(Long sid, String gfids, Long tid) { + return sendRequest(HttpMethod.GET, "/geofence/status/terminal", + builder -> { + builder.queryParam("sid", sid) + .queryParam("tid", tid); + if (gfids != null) { + builder.queryParam("gfids", gfids); + } + }, null); + } + + /** 删除围栏 */ + public JSONObject deleteFence(String sid, String gfid) { + return sendRequest(HttpMethod.POST, "/geofence/delete", + builder -> builder.queryParam("sid", sid).queryParam("gfid", gfid), null); + } + + /* ========================== 6. 轨迹纠偏&里程 ========================== */ + + /** 轨迹纠偏(驾车) */ + public JSONObject correctDriving(String sid, String tid, String trid, int gap, int angle, int speed, int accuracy) { + return sendRequest(HttpMethod.POST, "/terminal/correct", + builder -> builder.queryParam("sid", sid) + .queryParam("tid", tid) + .queryParam("trid", trid) + .queryParam("gap", gap) + .queryParam("angle", angle) + .queryParam("speed", speed) + .queryParam("accuracy", accuracy), null); + } + + /** 计算轨迹里程 */ + public JSONObject calDistance(String sid, String tid, String trid) { + return sendRequest(HttpMethod.GET, "/terminal/distance", + builder -> builder.queryParam("sid", sid).queryParam("tid", tid).queryParam("trid", trid), null); + } + + /* ========================== 7. 空间检索 ========================== */ + + /** 圆形范围内终端检索 */ + public JSONObject searchTerminalInCircle(String sid, String center, int radius) { + return sendRequest(HttpMethod.GET, "/terminal/aroundsearch", + builder -> builder.queryParam("sid", sid).queryParam("center", center).queryParam("radius", radius), null); + } + + /* ========================== 核心请求封装 ========================== */ + + /** + * 统一请求处理方法 + * + * @param method HTTP方法 + * @param path API路径 + * @param paramsConsumer URL参数构建器 + * @param body Body参数 (POST JSON时使用) + * @return 响应JSONObject + */ + private JSONObject sendRequest(HttpMethod method, String path, + Consumer paramsConsumer, + Object body) { + try { + UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(BASE_URL + path) + .queryParam("key", key); + if (paramsConsumer != null) paramsConsumer.accept(builder); + String url = builder.build().encode().toString(); + + String result; + if (body == null) { + result = method == HttpMethod.GET ? + HttpUtil.get(url) : HttpUtil.post(url, ""); + } else { + String json = JSONUtil.toJsonStr(body); + result = HttpUtil.post(url, json); + } + + log.info("Gaode Track Request: {} {}", method, url); + + if (StrUtil.isNotBlank(result)) { + JSONObject json = JSONUtil.parseObj(result); + if (json.getInt("errcode", -1) != 10000) { + log.warn("高德轨迹服务错误: {}", json); + } + return json; + } else { + return createErrorJson(-1, "empty response"); + } + } catch (Exception e) { + log.error("高德轨迹服务异常", e); + return createErrorJson(-1, e.getMessage()); + } + } + + private JSONObject createErrorJson(int code, String msg) { + JSONObject err = new JSONObject(); + err.set("errcode", code); + err.set("errmsg", msg); + return err; + } +} \ No newline at end of file diff --git a/fys-modules/fys-equipment/src/main/resources/mapper/equipment/DeviceMapper.xml b/fys-modules/fys-equipment/src/main/resources/mapper/equipment/DeviceMapper.xml index 0e7ff3c7..cb9b26f3 100644 --- a/fys-modules/fys-equipment/src/main/resources/mapper/equipment/DeviceMapper.xml +++ b/fys-modules/fys-equipment/src/main/resources/mapper/equipment/DeviceMapper.xml @@ -43,7 +43,7 @@ SELECT da.id AS id, d.id AS deviceId, d.device_name, d.bluetooth_name, d.group_id, d.pub_topic, d.sub_topic, d.device_pic,d.online_status, - d.device_mac, d.device_sn, d.update_by, + d.device_mac, d.device_sn, d.update_by,d.sid,d.tid,d.trid, d.device_imei, d.update_time, dg.id AS device_type, d.remark, d.binding_status, t.type_name AS typeName, da.assignee_id AS customerId, da.assignee_name AS customerName, @@ -86,6 +86,15 @@ and da.create_time between #{criteria.params.beginTime} and #{criteria.params.endTime} + + and d.sid = #{criteria.sid} + + + and d.tid is not null + + + and d.tid is null + AND da.assignee_id = #{criteria.currentOwnerId} @@ -97,6 +106,46 @@ ORDER BY create_time DESC + + +