5 Commits

Author SHA1 Message Date
8c636d0484 设备维修记录 2025-12-12 14:55:03 +08:00
0c474ae1f3 维修时间 2025-12-11 15:24:46 +08:00
b85664900e 分页查询围栏进出记录列表 2025-12-10 09:39:00 +08:00
dyf
035b24fedd Merge pull request 'feat(equipment): 新增高德轨迹服务相关功能与设备终端管理' (#21) from liwenlong/fys-Multi-tenant:jingquan into jingquan
Reviewed-on: #21
2025-12-03 16:52:09 +08:00
e920cfb860 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 并注册至菜单配置
- 完善设备分配逻辑以兼容角色权限判断及终端信息展示
2025-12-03 11:39:18 +08:00
37 changed files with 1793 additions and 76 deletions

View File

@ -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("位置未发生明显变化({}米 <= {}米),不更新 RedisdeviceImei={}, 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("位置信息已更新 RedisdeviceImei={}, 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;
}
}

View File

@ -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! —— 成功标记

View File

@ -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));
}
}

View File

@ -132,6 +132,8 @@ tenant:
- app_menu
- app_user_role
- app_role_menu
- track_service
- device_fence_terminal
# MyBatisPlus配置
# https://baomidou.com/config/

View File

@ -72,6 +72,12 @@ public class DeviceController extends BaseController {
return deviceService.queryAll(criteria, page);
}
@GetMapping(value = "/pageTerminal")
public TableDataInfo<Device> deviceFenecSelect(DeviceQueryCriteria criteria) throws IOException {
Page<Device> page = new Page<>(criteria.getPageNum(), criteria.getPageSize());
return deviceService.queryAllTerminal(criteria, page);
}
// @Log("新增设备")
@Operation(summary = "新增设备")

View File

@ -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<TrackServiceVo> 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<TrackServiceVo> list = trackServiceService.queryList(bo);
ExcelUtil.exportExcel(list, "轨迹服务", TrackServiceVo.class, response);
}
/**
* 获取轨迹服务详细信息
*
* @param id 主键
*/
@GetMapping("/{id}")
public R<TrackServiceVo> 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<Void> 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<Void> 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<Void> remove(@NotEmpty(message = "主键不能为空")
@PathVariable Long[] ids) {
return toAjax(trackServiceService.deleteWithValidByIds(List.of(ids), true));
}
@Log(title = "高德添加终端设备")
@PostMapping(value = "/terminal")
public R<Void> terminalDevice(@RequestBody TerminalDeviceBo model) {
trackServiceService.terminal(model);
return R.ok();
}
@Log(title = "高德移除终端设备")
@DeleteMapping(value = "/delTerminal/{ids}")
public R<Void> delTerminalDevice(@NotEmpty(message = "主键不能为空")
@PathVariable Long[] ids) {
return toAjax(trackServiceService.delTerminalDevice(List.of(ids), true));
}
@Log(title = "高德移除终端设备")
@DeleteMapping(value = "/del/Terminal")
public R<Void> delTerminalDevice(TerminalDelBo bo) {
return toAjax(trackServiceService.delTerminalDevice(bo, true));
}
/**
* 高德终端设备列表
*/
@GetMapping("/listTerminal")
public TableDataInfo<TerminalDeviceDto> listTerminal(TerminalQueryBo bo) {
return trackServiceService.listTerminal(bo);
}
/**
* 高德终端设备列表(关键字查询)
*/
@GetMapping("/searchTerminal")
public TableDataInfo<TerminalDeviceDto> searchTerminal(TerminalQueryBo bo) {
return trackServiceService.searchTerminal(bo);
}
}

View File

@ -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;
}

View File

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

View File

@ -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;
}

View File

@ -58,6 +58,14 @@ public class DeviceGeoFence extends BaseEntity {
* 是否激活
*/
private Long isActive;
/**
* 高德服务ID
*/
private Long sid;
/**
* 高德围栏ID
*/
private Long gfid;
}

View File

@ -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;
}

View File

@ -70,6 +70,14 @@ public class DeviceGeoFenceBo extends BaseEntity {
* 更新时间
*/
private Date updateTime;
/**
* 高德服务ID
*/
private Long sid;
/**
* 高德围栏ID
*/
private Long gfid;
}

View File

@ -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<Long> deviceIds;
}

View File

@ -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;
}

View File

@ -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<DeviceForm> deviceList;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
/**
* 绑定状态

View File

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

View File

@ -1,6 +1,8 @@
package com.fuyuanshen.equipment.domain.vo;
import java.util.Date;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fuyuanshen.common.tenant.core.TenantEntity;
import com.fuyuanshen.equipment.domain.DeviceRepairRecords;
@ -34,31 +36,39 @@ public class DeviceRepairRecordsVo extends TenantEntity implements Serializable
/**
* 维修记录ID
*/
@ExcelProperty(value = "维修记录ID")
// @ExcelProperty(value = "维修记录ID")
private Long recordId;
/**
* 设备ID
*/
@ExcelProperty(value = "设备ID")
// @ExcelProperty(value = "设备ID")
private String deviceId;
/**
* 设备名称
*/
@ExcelProperty(value = "设备名称")
private String deviceName;
/**
* 维修时间
*/
@ExcelProperty(value = "维修时间")
private Date repairTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@ColumnWidth(20)
private String repairTime;
/**
* 维修部位
* 损坏部位
*/
@ExcelProperty(value = "维修部位")
@ExcelProperty(value = "损坏部位")
private String repairPart;
/**
* 维修原因
* 损坏原因
*/
@ExcelProperty(value = "维修原因")
@ExcelProperty(value = "损坏原因")
private String repairReason;
/**
@ -66,11 +76,7 @@ public class DeviceRepairRecordsVo extends TenantEntity implements Serializable
*/
@ExcelProperty(value = "维修人员")
private String repairPerson;
/**
* 维修人员
*/
@ExcelProperty(value = "设备名称")
private String deviceName;
private List<DeviceRepairImagesVo> images;

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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<DeviceFenceTerminal, DeviceFenceTerminal> {
}

View File

@ -32,6 +32,8 @@ public interface DeviceMapper extends BaseMapper<Device> {
List<Device> findAll(@Param("criteria") DeviceQueryCriteria criteria);
IPage<Device> findAllTerminal(@Param("criteria") DeviceQueryCriteria criteria, Page<Device> page);
List<Device> findAllDevices(@Param("criteria") DeviceQueryCriteria criteria);
/**

View File

@ -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<TrackService, TrackServiceVo> {
}

View File

@ -30,6 +30,8 @@ public interface DeviceService extends IService<Device> {
*/
TableDataInfo<Device> queryAll(DeviceQueryCriteria criteria, Page<Device> page) throws IOException;
TableDataInfo<Device> queryAllTerminal(DeviceQueryCriteria criteria, Page<Device> page) throws IOException;
/**
* 查询所有数据不分页
*

View File

@ -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<DeviceGeoFence> {
* @return 位置检查结果
*/
FenceCheckResponse checkPosition(FenceCheckRequest request);
Boolean addFenceTerminal(FenceTerminalBo bo);
Boolean delFenceTerminal(FenceTerminalBo bo);
}

View File

@ -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<TrackServiceVo> queryPageList(TrackServiceBo bo, PageQuery pageQuery);
/**
* 查询符合条件的轨迹服务列表
*
* @param bo 查询条件
* @return 轨迹服务列表
*/
List<TrackServiceVo> 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<Long> ids, Boolean isValid);
/**
* 高德添加终端设备
*
* @param terminalDeviceBo 添加集合
*/
void terminal(TerminalDeviceBo terminalDeviceBo);
/**
* 高德移除终端设备
*
* @param ids 待删除的主键集合
* @param isValid 是否进行有效性校验
* @return 是否删除成功
*/
Boolean delTerminalDevice(Collection<Long> ids, Boolean isValid);
Boolean delTerminalDevice(TerminalDelBo bo, Boolean isValid);
/**
* 查询设备列表
*
* @param bo 查询条件
* @return 设备列表
*/
TableDataInfo<TerminalDeviceDto> listTerminal(TerminalQueryBo bo);
/**
* 关键字查询设备
*
* @param bo 查询条件
* @return 设备列表
*/
TableDataInfo<TerminalDeviceDto> searchTerminal(TerminalQueryBo bo);
}

View File

@ -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<DeviceGeoFenceMapper
private final IDeviceFenceStatusService fenceStatusService; // 添加此行
private final IDeviceFenceAccessRecordService fenceAccessRecordService; // 添加此行
private final DeviceFenceTerminalMapper deviceFenceTerminalMapper;
@ -269,5 +275,32 @@ public class DeviceGeoFenceServiceImpl extends ServiceImpl<DeviceGeoFenceMapper
return response;
}
@Override
public Boolean addFenceTerminal(FenceTerminalBo bo) {
// 新增围栏与终端关联信息
List<DeviceFenceTerminal> 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<DeviceFenceTerminal> lambda = new LambdaUpdateWrapper<DeviceFenceTerminal>()
.eq(DeviceFenceTerminal::getFenceId, bo.getFenceId())
.in(DeviceFenceTerminal::getDeviceId, bo.getDeviceIds());
return deviceFenceTerminalMapper.delete(lambda) > 0;
}
}

View File

@ -126,6 +126,26 @@ public class DeviceServiceImpl extends ServiceImpl<DeviceMapper, Device> impleme
return new TableDataInfo<Device>(records, devices.getTotal());
}
@Override
public TableDataInfo<Device> queryAllTerminal(DeviceQueryCriteria criteria, Page<Device> page) throws IOException {
// 角色管理员
Long userId = LoginHelper.getUserId();
List<SysRoleVo> 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<Device> devices = deviceMapper.findAllTerminal(criteria, page);
return new TableDataInfo<Device>(devices.getRecords(), devices.getTotal());
}
@Override
public List<Device> queryAll(DeviceQueryCriteria criteria) {

View File

@ -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<TrackServiceVo> queryPageList(TrackServiceBo bo, PageQuery pageQuery) {
LambdaQueryWrapper<TrackService> lqw = buildQueryWrapper(bo);
Page<TrackServiceVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
return TableDataInfo.build(result);
}
/**
* 查询符合条件的轨迹服务列表
*
* @param bo 查询条件
* @return 轨迹服务列表
*/
@Override
public List<TrackServiceVo> queryList(TrackServiceBo bo) {
LambdaQueryWrapper<TrackService> lqw = buildQueryWrapper(bo);
return baseMapper.selectVoList(lqw);
}
private LambdaQueryWrapper<TrackService> buildQueryWrapper(TrackServiceBo bo) {
Map<String, Object> params = bo.getParams();
LambdaQueryWrapper<TrackService> 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<Long> ids, Boolean isValid) {
if(isValid){
//TODO 做一些业务上的校验,判断是否需要校验
}
return baseMapper.deleteByIds(ids) > 0;
}
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean delTerminalDevice(Collection<Long> ids, Boolean isValid) {
if (CollectionUtils.isEmpty(ids)) {
throw new IllegalArgumentException("请选择要移除的设备");
}
// 1. 批量查询(一次 SQL性能提升 10 倍+
List<Device> deviceList = deviceMapper.selectList(
new LambdaQueryWrapper<Device>()
.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<Device>()
.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<Device>()
.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<TerminalDeviceDto> listTerminal(TerminalQueryBo bo) {
JSONObject terminalRes = amapTrackUtil.listTerminal(bo.getSid(), bo.getTid(), bo.getName(), bo.getPage());
List<TerminalDeviceDto> list = terminalRes.getByPath("data.results", JSONArray.class)
.toList(TerminalDeviceDto.class);
Integer count = terminalRes.getByPath("data.count", Integer.class);
return new TableDataInfo<TerminalDeviceDto>(list, count);
}
/**
* 搜索终端列表
*
* @param bo 搜索参数
* @return 终端列表
*/
@Override
public TableDataInfo<TerminalDeviceDto> searchTerminal(TerminalQueryBo bo) {
JSONObject terminalRes = amapTrackUtil.searchTerminal(bo.getSid(), bo.getKeywords(), bo.getPage(), bo.getPagesize());
log.info("终端列表:{}", terminalRes);
List<TerminalDeviceDto> list = terminalRes.getByPath("data.results", JSONArray.class)
.toList(TerminalDeviceDto.class);
Integer count = terminalRes.getByPath("data.count", Integer.class);
return new TableDataInfo<TerminalDeviceDto>(list, count);
}
/**
* 业务校验钩子
*/
private void validateBeforeDelete(List<Device> 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<CreatedResource> 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);
}
}

View File

@ -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;
/**
* 高德猎鹰轨迹服务 工具类
* <p>
* 优化点:
* 1. 引入 Slf4j 增加日志记录
* 2. 使用 UriComponentsBuilder 处理 URL 拼接和参数编码
* 3. 统一异常处理和请求封装
* 4. 提取常量
* </p>
* 官网文档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<String, Object> 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<UriComponentsBuilder> 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;
}
}

View File

@ -48,8 +48,8 @@
r.event_time, r.event_address,
r.create_time
FROM device_fence_access_record r
LEFT JOIN device_geo_fence f ON r.fence_id = f.id
LEFT JOIN device d ON r.device_id = d.id
INNER JOIN device_geo_fence f ON r.fence_id = f.id
INNER JOIN device d ON r.device_id = d.id
<where>
<if test="bo.fenceId != null">
AND r.fence_id = #{bo.fenceId}

View File

@ -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 @@
<if test="criteria.params.beginTime != null and criteria.params.endTime != null">
and da.create_time between #{criteria.params.beginTime} and #{criteria.params.endTime}
</if>
<if test="criteria.sid != null">
and d.sid = #{criteria.sid}
</if>
<if test="criteria.isTid == true">
and d.tid is not null
</if>
<if test="criteria.isTid == false">
and d.tid is null
</if>
<!-- 管理员可以查看所有设备,普通用户只能查看自己的设备 -->
<if test="criteria.isAdmin != true">
AND da.assignee_id = #{criteria.currentOwnerId}
@ -97,6 +106,46 @@
ORDER BY create_time DESC
</select>
<!-- 分页查询设备终端 -->
<select id="findAllTerminal" resultType="com.fuyuanshen.equipment.domain.Device">
SELECT *
FROM (
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.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.create_time AS create_time,
ROW_NUMBER() OVER (PARTITION BY d.id ORDER BY da.create_time DESC) AS rn
FROM device d
LEFT JOIN device_type t ON d.device_type = t.id
LEFT JOIN device_type_grants dg ON dg.device_type_id = t.id
LEFT JOIN device_assignments da ON da.device_id = d.id
LEFT JOIN device_fence_terminal dft ON dft.device_id = d.id
where d.sid = #{criteria.sid}
<!-- 时间范围等其他条件保持原样 -->
<if test="criteria.deviceName != null and criteria.deviceName.trim() != ''">
and d.device_name like concat('%', TRIM(#{criteria.deviceName}), '%')
</if>
<if test="criteria.deviceImei != null and criteria.deviceImei.trim() != ''">
and d.device_imei like concat('%', TRIM(#{criteria.deviceImei}), '%')
</if>
<if test="criteria.isTid == true">
and dft.fence_id = #{criteria.fenceId}
</if>
<if test="criteria.isTid == false">
and (dft.fence_id is null or dft.fence_id != #{criteria.fenceId})
</if>
<!-- 管理员可以查看所有设备,普通用户只能查看自己的设备 -->
<if test="criteria.isAdmin != true">
AND da.assignee_id = #{criteria.currentOwnerId}
AND dg.customer_id = #{criteria.currentOwnerId}
</if>
) AS ranked
WHERE rn = 1
ORDER BY create_time DESC
</select>
<select id="findAllDevices" resultType="com.fuyuanshen.equipment.domain.Device">
select
@ -266,7 +315,7 @@
d.binding_status,
d.online_status,
c.binding_time,
d.create_time,
d.create_time,d.sid,d.tid,d.trid,
ROW_NUMBER() OVER (PARTITION BY d.id ORDER BY d.id) AS row_num
from device d
inner join device_type dt on d.device_type = dt.id

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.fuyuanshen.equipment.mapper.DeviceGeoFenceMapper">
</mapper>