From 4d2b7c6adf8770265f24b4b7617a38e4ea5abd99 Mon Sep 17 00:00:00 2001 From: daiyongfei <974332738@qq.com> Date: Sun, 28 Sep 2025 11:53:24 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E8=AE=BE=E5=A4=87=E6=98=AF=E5=90=A6?= =?UTF-8?q?=E6=AD=A3=E5=9C=A8=E5=91=8A=E8=AD=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../equipment/domain/vo/DeviceLocationVo.java | 3 +++ .../service/impl/DeviceServiceImpl.java | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/vo/DeviceLocationVo.java b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/vo/DeviceLocationVo.java index 3f3c876..28bf6bd 100644 --- a/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/vo/DeviceLocationVo.java +++ b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/vo/DeviceLocationVo.java @@ -33,4 +33,7 @@ public class DeviceLocationVo { @Schema(description = "进入的电子围栏信息") private DeviceGeoFence fenceInfo; + @Schema(description = "设备是否正在告警") + private Boolean isAlarming; + } \ 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 2b86b93..5fbe8fa 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 @@ -774,6 +774,8 @@ public class DeviceServiceImpl extends ServiceImpl impleme // 注入电子围栏服务 IDeviceGeoFenceService geoFenceService = SpringUtils.getBean(IDeviceGeoFenceService.class); + // 注入设备告警Mapper + DeviceAlarmMapper deviceAlarmMapper = SpringUtils.getBean(DeviceAlarmMapper.class); for (Device device : devices) { DeviceLocationVo vo = new DeviceLocationVo(); @@ -808,6 +810,23 @@ public class DeviceServiceImpl extends ServiceImpl impleme vo.setInFence(false); } + // 检查设备是否正在告警(只要当前设备处于报警状态,且不是电子围栏告警) + if (StringUtils.isNotBlank(device.getDeviceImei())) { + DeviceAlarmVo latestAlarm = deviceAlarmMapper.selectLatestByDeviceImei(device.getDeviceImei()); + // 判断是否正在告警:未处理的告警(treatmentState=1)且不是电子围栏告警(deviceAction!=3) + if (latestAlarm != null && + latestAlarm.getTreatmentState() != null && + latestAlarm.getTreatmentState() == 1 && + latestAlarm.getDeviceAction() != null && + latestAlarm.getDeviceAction() != 3) { + vo.setIsAlarming(true); + } else { + vo.setIsAlarming(false); + } + } else { + vo.setIsAlarming(false); + } + result.add(vo); } From a4596b9c90b636bef062812e9723cf5ebf240ed6 Mon Sep 17 00:00:00 2001 From: DragonWenLong <552045633@qq.com> Date: Sun, 28 Sep 2025 16:11:34 +0800 Subject: [PATCH 2/2] =?UTF-8?q?feat(device):=20=E5=AE=9E=E7=8E=B0=E8=AE=BE?= =?UTF-8?q?=E5=A4=87=E6=89=B9=E9=87=8F=E6=8E=A7=E5=88=B6=E6=8C=87=E4=BB=A4?= =?UTF-8?q?=E5=8F=91=E9=80=81=E5=8A=9F=E8=83=BD-=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E6=89=B9=E9=87=8F=E5=8F=91=E9=80=81=E8=AE=BE=E5=A4=87=E6=8E=A7?= =?UTF-8?q?=E5=88=B6=E6=8C=87=E4=BB=A4=E6=96=B9=E6=B3=95=20sendCommandBatc?= =?UTF-8?q?h-=20=E6=94=AF=E6=8C=81=E8=AE=BE=E5=A4=87=E7=A6=BB=E7=BA=BF?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E6=A3=80=E6=9F=A5=E5=92=8C=E5=BC=82=E5=B8=B8?= =?UTF-8?q?=E5=A4=84=E7=90=86=20-=20=E6=B7=BB=E5=8A=A0=E8=AE=BE=E5=A4=87?= =?UTF-8?q?=E6=93=8D=E4=BD=9C=E6=97=A5=E5=BF=97=E8=AE=B0=E5=BD=95=E5=92=8C?= =?UTF-8?q?=E6=8A=A5=E8=AD=A6=E5=88=9B=E5=BB=BA-=20=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E8=AE=BE=E5=A4=87SOS=E6=A1=A3=E4=BD=8D=E6=89=B9=E9=87=8F?= =?UTF-8?q?=E8=AE=BE=E7=BD=AE=E6=8E=A5=E5=8F=A3=20-=20=E5=9C=A8=E8=AE=BE?= =?UTF-8?q?=E5=A4=87=E6=8C=87=E4=BB=A4=E5=A4=84=E7=90=86=E4=B8=AD=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E6=B6=88=E6=81=AF=E5=8E=BB=E9=87=8D=E6=9C=BA=E5=88=B6?= =?UTF-8?q?=20-=20=E4=BC=98=E5=8C=96=E8=AE=BE=E5=A4=87=E6=8A=A5=E8=AD=A6?= =?UTF-8?q?=E5=A4=84=E7=90=86=E7=9A=84=E5=88=86=E5=B8=83=E5=BC=8F=E9=94=81?= =?UTF-8?q?=E9=80=BB=E8=BE=91=20-=20=E5=AE=8C=E5=96=84=E8=AE=BE=E5=A4=87?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E8=A7=84=E5=88=99=E4=B8=AD=E7=9A=84=E5=B9=B6?= =?UTF-8?q?=E5=8F=91=E6=8E=A7=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../rule/xinghan/XinghanBootLogoRule.java | 8 +++ .../rule/xinghan/XinghanDeviceDataRule.java | 39 +++++++++-- .../xinghan/XinghanSendAlarmMessageRule.java | 7 ++ .../mqtt/rule/xinghan/XinghanSendMsgRule.java | 8 +++ .../device/DeviceXinghanController.java | 11 +++ .../domain/Dto/DeviceXinghanInstructDto.java | 3 + .../device/DeviceXinghanBizService.java | 70 +++++++++++++++++++ .../web/service/impl/AppSmsAuthStrategy.java | 2 +- 8 files changed, 143 insertions(+), 5 deletions(-) diff --git a/fys-admin/src/main/java/com/fuyuanshen/global/mqtt/rule/xinghan/XinghanBootLogoRule.java b/fys-admin/src/main/java/com/fuyuanshen/global/mqtt/rule/xinghan/XinghanBootLogoRule.java index 2b92071..764e792 100644 --- a/fys-admin/src/main/java/com/fuyuanshen/global/mqtt/rule/xinghan/XinghanBootLogoRule.java +++ b/fys-admin/src/main/java/com/fuyuanshen/global/mqtt/rule/xinghan/XinghanBootLogoRule.java @@ -62,6 +62,14 @@ public class XinghanBootLogoRule implements MqttMessageRule { String respText = payload.getStaPicTrans(); log.warn("设备上报LOGO:{}", respText); + // --- 去重 START --- + String dedupKey = "xd:MSG:LOGO:" + ctx.getDeviceImei() + ":" + respText; + boolean first = RedisUtils.setObjectIfAbsent(dedupKey, "1", Duration.ofSeconds(10)); + if (!first) { + log.warn("重复消息丢弃 {}", dedupKey); + return; + } + // 1. great! —— 成功标记 if ("great!".equalsIgnoreCase(respText)) { RedisUtils.setCacheObject(functionAccessKey, 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 cbcf2ea..8739b3a 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 @@ -28,12 +28,15 @@ import lombok.AllArgsConstructor; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; import org.springframework.stereotype.Component; import org.springframework.beans.factory.annotation.Autowired; import java.time.Duration; import java.util.*; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; import static com.fuyuanshen.common.core.constant.GlobalConstants.FUNCTION_ACCESS_KEY; import static com.fuyuanshen.common.core.constant.GlobalConstants.GLOBAL_REDIS_KEY; @@ -144,6 +147,10 @@ public class XinghanDeviceDataRule implements MqttMessageRule { Long alarmId = RedisUtils.getCacheObject(redisKey); + String lockKey = redisKey + ":lock"; // 分布式锁 key + RedissonClient client = RedisUtils.getClient(); // 唯一用到的“旧”入口 + RLock lock = client.getLock(lockKey); + // ---------- 情况 1:当前正在报警 ---------- if (nowAlarming) { // 已存在未结束报警 -> 什么都不做(同一条报警) @@ -152,10 +159,34 @@ public class XinghanDeviceDataRule implements MqttMessageRule { RedisUtils.setCacheObject(redisKey, alarmId, Duration.ofMinutes(10)); return; } - // 不存在 -> 新建 - DeviceAlarmBo bo = createAlarmBo(deviceImei, type); - deviceAlarmService.insertByBo(bo); - RedisUtils.setCacheObject(redisKey, bo.getId(), Duration.ofMinutes(10)); // 5分钟后结束过期 + // 需要新建,抢锁 + boolean locked = false; + try { + locked = lock.tryLock(3, TimeUnit.SECONDS); // 最多等 3 s + if (!locked) { // 抢不到直接放弃 + return; + } + // 锁内二次校验(double-check) + alarmId = RedisUtils.getCacheObject(redisKey); + if (alarmId != null) { + return; // 并发线程已建好 + } + + // 不存在 -> 新建 + DeviceAlarmBo bo = createAlarmBo(deviceImei, type); + if (bo == null){ + return; + } + deviceAlarmService.insertByBo(bo); + RedisUtils.setCacheObject(redisKey, bo.getId(), Duration.ofMinutes(10)); // 5分钟后结束过期 + }catch (InterruptedException ignore) { + // 立即中断并退出,禁止继续往下走 + Thread.currentThread().interrupt(); + } finally { + if (locked && lock.isHeldByCurrentThread()) { + lock.unlock(); + } + } return; } 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 3a4b368..851d52c 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 @@ -55,6 +55,13 @@ public class XinghanSendAlarmMessageRule implements MqttMessageRule { String respText = payload.getStaBreakNews(); log.info("设备上报紧急通知握手: {} ", respText); + // --- 去重 START --- + String dedupKey = "xd:ALARM:dedup:" + ctx.getDeviceImei() + ":" + respText; + boolean first = RedisUtils.setObjectIfAbsent(dedupKey, "1", Duration.ofSeconds(10)); + if (!first) { + log.warn("重复消息丢弃 {}", dedupKey); + return; + } // 1. cover! —— 成功标记 if ("cover!".equalsIgnoreCase(respText)) { diff --git a/fys-admin/src/main/java/com/fuyuanshen/global/mqtt/rule/xinghan/XinghanSendMsgRule.java b/fys-admin/src/main/java/com/fuyuanshen/global/mqtt/rule/xinghan/XinghanSendMsgRule.java index d68d18e..800a5c4 100644 --- a/fys-admin/src/main/java/com/fuyuanshen/global/mqtt/rule/xinghan/XinghanSendMsgRule.java +++ b/fys-admin/src/main/java/com/fuyuanshen/global/mqtt/rule/xinghan/XinghanSendMsgRule.java @@ -55,6 +55,14 @@ public class XinghanSendMsgRule implements MqttMessageRule { String respText = payload.getStaTexTrans(); log.info("设备上报人员信息: {} ", respText); + // --- 去重 START --- + String dedupKey = "xd:MSG:dedup:" + ctx.getDeviceImei() + ":" + respText; + boolean first = RedisUtils.setObjectIfAbsent(dedupKey, "1", Duration.ofSeconds(10)); + if (!first) { + log.warn("重复消息丢弃 {}", dedupKey); + return; + } + // 1. genius! —— 成功标记 if ("genius!".equalsIgnoreCase(respText)) { RedisUtils.setCacheObject(functionAccess, FunctionAccessStatus.OK.getCode(), Duration.ofSeconds(20)); diff --git a/fys-admin/src/main/java/com/fuyuanshen/web/controller/device/DeviceXinghanController.java b/fys-admin/src/main/java/com/fuyuanshen/web/controller/device/DeviceXinghanController.java index 2f1c324..53d5467 100644 --- a/fys-admin/src/main/java/com/fuyuanshen/web/controller/device/DeviceXinghanController.java +++ b/fys-admin/src/main/java/com/fuyuanshen/web/controller/device/DeviceXinghanController.java @@ -109,6 +109,17 @@ public class DeviceXinghanController extends BaseController { return R.ok(); } + /** + * SOS档位 批量 + * SOS档位,2,1,0, 分别表示红蓝模式/爆闪模式/关闭 + */ + @PostMapping("/SOSGradeSettingsBatch") + public R SOSGradeSettingsBatch(@RequestBody DeviceXinghanInstructDto params) { + // params 转 JSONObject + deviceXinghanBizService.sendCommandBatch(params,"ins_SOSGrade","SOS档位"); + return R.ok(); + } + /** * 静止报警状态 * 静止报警状态,0-未静止报警,1-正在静止报警。 diff --git a/fys-admin/src/main/java/com/fuyuanshen/web/domain/Dto/DeviceXinghanInstructDto.java b/fys-admin/src/main/java/com/fuyuanshen/web/domain/Dto/DeviceXinghanInstructDto.java index 0d3b598..3985ef9 100644 --- a/fys-admin/src/main/java/com/fuyuanshen/web/domain/Dto/DeviceXinghanInstructDto.java +++ b/fys-admin/src/main/java/com/fuyuanshen/web/domain/Dto/DeviceXinghanInstructDto.java @@ -2,6 +2,8 @@ package com.fuyuanshen.web.domain.Dto; import lombok.Data; +import java.util.List; + @Data public class DeviceXinghanInstructDto { private Long deviceId; @@ -12,4 +14,5 @@ public class DeviceXinghanInstructDto { */ private String instructValue; private Boolean isBluetooth = false; + private List deviceIds; } diff --git a/fys-admin/src/main/java/com/fuyuanshen/web/service/device/DeviceXinghanBizService.java b/fys-admin/src/main/java/com/fuyuanshen/web/service/device/DeviceXinghanBizService.java index 617458c..6e53f77 100644 --- a/fys-admin/src/main/java/com/fuyuanshen/web/service/device/DeviceXinghanBizService.java +++ b/fys-admin/src/main/java/com/fuyuanshen/web/service/device/DeviceXinghanBizService.java @@ -28,6 +28,7 @@ import com.fuyuanshen.common.json.utils.JsonUtils; import com.fuyuanshen.common.redis.utils.RedisUtils; import com.fuyuanshen.common.satoken.utils.AppLoginHelper; import com.fuyuanshen.equipment.domain.Device; +import com.fuyuanshen.equipment.domain.DeviceLog; import com.fuyuanshen.equipment.domain.DeviceType; import com.fuyuanshen.equipment.domain.bo.DeviceAlarmBo; import com.fuyuanshen.equipment.domain.dto.AppDeviceSendMsgBo; @@ -531,6 +532,75 @@ public class DeviceXinghanBizService { createAlarm(device.getId(),deviceImei,payloadKey,value); } + /** + * 批量发送设备控制指令 + * + * @param dto 设备ID列表 + * @param payloadKey 指令负载数据的键名 + * @param deviceAction 设备操作类型描述 + */ + @Transactional(rollbackFor = Exception.class) // 1. 事务注解 + public void sendCommandBatch(DeviceXinghanInstructDto dto, String payloadKey, String deviceAction) { + List errorMessages = Collections.synchronizedList(new ArrayList<>()); + int value; + try { + value = Integer.parseInt(dto.getInstructValue()); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("指令值格式不正确,必须为整数。", e); + } + Map> payload = Map.of(payloadKey, List.of(value)); + + // 一次性查询所有设备信息 + List devices = deviceMapper.selectList( + new QueryWrapper().lambda().in(Device::getId, dto.getDeviceIds()) + ); + // 日志信息 + String contentText = resolveGradeDesc(payloadKey, value); + + List logs = new ArrayList<>(); + try { + for (Device device : devices) { + String deviceImei = device.getDeviceImei(); + String deviceName = device.getDeviceName(); + + // 2. 提前进行设备状态检查,逻辑更清晰 + if (isDeviceOffline(deviceImei)) { + throw new ServiceException("设备已断开连接:" + deviceName); + } + + String topic = MqttConstants.GLOBAL_PUB_KEY + deviceImei; + String json = JsonUtils.toJsonString(payload); + + mqttGateway.sendMsgToMqtt(topic, 1, json); + log.info("发送指令成功 => topic:{}, payload:{}", topic, json); + + // 创建设备日志实体 + DeviceLog deviceLog = new DeviceLog(); + deviceLog.setDeviceId(device.getId()); + deviceLog.setDeviceAction(deviceAction); + deviceLog.setContent(contentText); + deviceLog.setCreateBy(AppLoginHelper.getUserId()); + deviceLog.setDeviceName(deviceName); + deviceLog.setCreateTime(new Date()); + logs.add(deviceLog); + + createAlarm(device.getId(), deviceImei, payloadKey, value); + } + deviceLogMapper.insertBatch(logs); + } catch (ServiceException e) { + // 捕获并重新抛出自定义异常,避免内层异常被外层泛化捕获 + log.error("批量发送指令失败: {}", e.getMessage(), e); + throw e; + } catch (Exception e) { + log.error("批量发送指令发生未知错误", e); + throw new ServiceException("批量发送指令失败"); + } + } + + /** + * 检查设备是否离线 + */ + // private boolean isDeviceOffline(String imei) { // // 原方法名语义相反,这里取反,使含义更清晰 // return getDeviceStatus(imei); diff --git a/fys-admin/src/main/java/com/fuyuanshen/web/service/impl/AppSmsAuthStrategy.java b/fys-admin/src/main/java/com/fuyuanshen/web/service/impl/AppSmsAuthStrategy.java index eb9ea8d..862cfbd 100644 --- a/fys-admin/src/main/java/com/fuyuanshen/web/service/impl/AppSmsAuthStrategy.java +++ b/fys-admin/src/main/java/com/fuyuanshen/web/service/impl/AppSmsAuthStrategy.java @@ -54,7 +54,7 @@ public class AppSmsAuthStrategy implements IAuthStrategy { String phonenumber = loginBody.getPhonenumber(); String smsCode = loginBody.getSmsCode(); AppLoginUser loginUser = TenantHelper.dynamic(tenantId, () -> { -// loginService.checkLogin(LoginType.SMS, tenantId, phonenumber, () -> !validateSmsCode(tenantId, phonenumber, smsCode)); + loginService.checkLogin(LoginType.SMS, tenantId, phonenumber, () -> !validateSmsCode(tenantId, phonenumber, smsCode)); AppUserVo user = loadUserByPhonenumber(phonenumber); if (ObjectUtil.isNull(user)) { //新增Appuser