Compare commits

10 Commits

Author SHA1 Message Date
608aa8449a Merge branch 'main' into dyf-device 2025-09-29 09:26:49 +08:00
dyf
6084f11e62 Merge pull request 'jingquan' (#13) from liwenlong/fys-Multi-tenant:jingquan into main
Reviewed-on: #13
2025-09-29 09:26:04 +08:00
1898fe5db9 Merge branch '6170' into dyf-device 2025-09-28 18:19:21 +08:00
7d56e2e80e 发送信息和告警故障3 2025-09-28 17:18:47 +08:00
233e0e32b0 发送信息和告警故障2 2025-09-28 16:19:28 +08:00
cb2acdb3f4 Merge remote-tracking branch 'liwenlong-fys/jingquan' into jingquan 2025-09-28 16:11:52 +08:00
a4596b9c90 feat(device): 实现设备批量控制指令发送功能- 新增批量发送设备控制指令方法 sendCommandBatch- 支持设备离线状态检查和异常处理
- 添加设备操作日志记录和报警创建- 实现设备SOS档位批量设置接口
- 在设备指令处理中增加消息去重机制
- 优化设备报警处理的分布式锁逻辑
- 完善设备数据规则中的并发控制
2025-09-28 16:11:34 +08:00
5230a95865 自动报警 2025-09-28 15:28:19 +08:00
461fd9364c Merge remote-tracking branch 'upstream/6170' into 6170 2025-09-27 15:40:39 +08:00
ad81647939 发送信息和告警故障 2025-09-27 15:40:31 +08:00
20 changed files with 240 additions and 40 deletions

View File

@ -73,9 +73,10 @@ public class AppDeviceShareService {
private static void buildDeviceStatus(AppDeviceShareVo item) {
// 设备在线状态
String onlineStatus = RedisUtils.getCacheObject(GLOBAL_REDIS_KEY + DEVICE_KEY_PREFIX + item.getDeviceImei() + DeviceRedisKeyConstants.DEVICE_ONLINE_STATUS_KEY_PREFIX);
if (StringUtils.isNotBlank(onlineStatus)) {
if("1".equals(onlineStatus)){
item.setOnlineStatus(1);
}else if("2".equals(onlineStatus)){
item.setOnlineStatus(2);
}else{
item.setOnlineStatus(0);
}

View File

@ -48,17 +48,24 @@ public class ReceiverMessageHandler implements MessageHandler {
}
String[] subStr = receivedTopic.split("/");
String deviceImei = subStr[1];
String state = payloadDict.getStr("state");
Object[] convertArr = ImageToCArrayConverter.convertByteStringToMixedObjectArray(state);
if(StringUtils.isNotBlank(deviceImei)){
String arr1 = convertArr[0].toString();
String arr2 = convertArr[1].toString();
if("12".equals(arr1) && "0".equals(arr2)){
return;
}else{
String queueKey = MqttMessageQueueConstants.MQTT_MESSAGE_QUEUE_KEY;
String dedupKey = MqttMessageQueueConstants.MQTT_MESSAGE_DEDUP_KEY;
RedisUtils.offerDeduplicated(queueKey,dedupKey,deviceImei, Duration.ofHours(24));
//在线状态
String deviceOnlineStatusRedisKey = GlobalConstants.GLOBAL_REDIS_KEY+ DEVICE_KEY_PREFIX+ deviceImei + DeviceRedisKeyConstants.DEVICE_ONLINE_STATUS_KEY_PREFIX ;
RedisUtils.setCacheObject(deviceOnlineStatusRedisKey, "1", Duration.ofSeconds(120));
RedisUtils.setCacheObject(deviceOnlineStatusRedisKey, "1", Duration.ofSeconds(303));
}
}
String state = payloadDict.getStr("state");
Object[] convertArr = ImageToCArrayConverter.convertByteStringToMixedObjectArray(state);
if (convertArr.length > 0) {
Byte val1 = (Byte) convertArr[0];

View File

@ -57,12 +57,18 @@ public class BjqAlarmRule implements MqttMessageRule {
if (StringUtils.isNotBlank(convertValue)) {
// 将设备状态信息存储到Redis中
String deviceRedisKey = GlobalConstants.GLOBAL_REDIS_KEY + DeviceRedisKeyConstants.DEVICE_KEY_PREFIX + context.getDeviceImei() + DEVICE_ALARM_KEY_PREFIX;
if ("1".equals(convertValue)) {
// 存储到Redis
RedisUtils.setCacheObject(deviceRedisKey, convertValue);
String sendMessageIng = GLOBAL_REDIS_KEY+ DEVICE_KEY_PREFIX + context.getDeviceImei() + ":messageSending";
RedisUtils.setCacheObject(sendMessageIng, "1", Duration.ofDays(1));
}else if ("0".equals(convertValue)){
String sendMessageIng = GLOBAL_REDIS_KEY+ DEVICE_KEY_PREFIX + context.getDeviceImei() + ":messageSending";
RedisUtils.deleteObject(sendMessageIng);
}
}
RedisUtils.setCacheObject(functionAccess, FunctionAccessStatus.OK.getCode(), Duration.ofSeconds(20));
// 保存告警信息
String deviceImei = context.getDeviceImei();
// 设备告警状态 0:解除告警 1:报警产生

View File

@ -1,8 +1,11 @@
package com.fuyuanshen.global.mqtt.rule.bjq;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.fuyuanshen.common.core.constant.GlobalConstants;
import com.fuyuanshen.common.core.utils.StringUtils;
import com.fuyuanshen.common.redis.utils.RedisUtils;
import com.fuyuanshen.equipment.domain.Device;
import com.fuyuanshen.equipment.service.DeviceService;
import com.fuyuanshen.global.mqtt.base.MqttMessageRule;
import com.fuyuanshen.global.mqtt.base.MqttRuleContext;
import com.fuyuanshen.global.mqtt.constants.DeviceRedisKeyConstants;
@ -16,6 +19,7 @@ import java.time.Duration;
import java.util.concurrent.CompletableFuture;
import static com.fuyuanshen.common.core.constant.GlobalConstants.FUNCTION_ACCESS_KEY;
import static com.fuyuanshen.common.core.constant.GlobalConstants.GLOBAL_REDIS_KEY;
import static com.fuyuanshen.global.mqtt.constants.DeviceRedisKeyConstants.*;
/**
@ -26,6 +30,8 @@ import static com.fuyuanshen.global.mqtt.constants.DeviceRedisKeyConstants.*;
@Slf4j
public class BjqModeRule implements MqttMessageRule {
private final DeviceService deviceService;
@Override
public String getCommandType() {
return LightingCommandTypeConstants.LIGHT_MODE;
@ -40,9 +46,30 @@ public class BjqModeRule implements MqttMessageRule {
String mainLightMode = convertArr[1].toString();
String batteryRemainingTime = convertArr[2].toString();
if(StringUtils.isNotBlank(mainLightMode)){
log.info("设备离线mainLightMode{}",mainLightMode);
if("0".equals(mainLightMode)){
//设备离线
String deviceOnlineStatusRedisKey = GlobalConstants.GLOBAL_REDIS_KEY+ DEVICE_KEY_PREFIX+ context.getDeviceImei() + DeviceRedisKeyConstants.DEVICE_ONLINE_STATUS_KEY_PREFIX ;
RedisUtils.setCacheObject(deviceOnlineStatusRedisKey, "0", Duration.ofSeconds(60*15));
RedisUtils.setCacheObject(deviceOnlineStatusRedisKey, "0");
String sendMessageIng = GLOBAL_REDIS_KEY+ DEVICE_KEY_PREFIX + context.getDeviceImei() + ":messageSending";
String messageSendingValue = RedisUtils.getCacheObject(sendMessageIng);
if("1".equals(messageSendingValue)){
//设置为故障状态
RedisUtils.setCacheObject(deviceOnlineStatusRedisKey, "2");
UpdateWrapper<Device> updateWrapper = new UpdateWrapper<>();
updateWrapper.eq("device_imei", context.getDeviceImei());
updateWrapper.set("online_status", 2);
deviceService.update(updateWrapper);
RedisUtils.deleteObject(sendMessageIng);
// 解除告警
String deviceRedisKey = GlobalConstants.GLOBAL_REDIS_KEY + DeviceRedisKeyConstants.DEVICE_KEY_PREFIX + context.getDeviceImei() + DEVICE_ALARM_KEY_PREFIX;
if(RedisUtils.getCacheObject(deviceRedisKey) != null){
RedisUtils.deleteObject(deviceRedisKey);
}
}
}
// 发送设备状态和位置信息到Redis
syncSendDeviceDataToRedisWithFuture(context.getDeviceImei(),mainLightMode);

View File

@ -43,12 +43,23 @@ public class BjqSendMessageRule implements MqttMessageRule {
public void execute(MqttRuleContext context) {
String functionAccess = FUNCTION_ACCESS_KEY + context.getDeviceImei();
try {
Byte val2 = (Byte) context.getConvertArr()[1];
// Byte val2 = (Byte) context.getConvertArr()[1];
String val2Str = context.getConvertArr()[1].toString();
int val2 = Integer.parseInt(val2Str);
System.out.println("收到设备信息命令:"+val2);
if (val2 == 100) {
RedisUtils.setCacheObject(functionAccess, FunctionAccessStatus.OK.getCode(), Duration.ofSeconds(20));
return;
}
if(val2==200){
String sendMessageIng = GLOBAL_REDIS_KEY+ DEVICE_KEY_PREFIX + context.getDeviceImei() + ":messageSending";
RedisUtils.deleteObject(sendMessageIng);
return;
}
String data = RedisUtils.getCacheObject(GLOBAL_REDIS_KEY+ DEVICE_KEY_PREFIX + context.getDeviceImei() + ":app_send_message_data");
if (StringUtils.isEmpty(data)) {
return;
@ -61,7 +72,7 @@ public class BjqSendMessageRule implements MqttMessageRule {
ArrayList<Integer> intData = new ArrayList<>();
intData.add(6);
intData.add((int) val2);
intData.add(val2);
ImageToCArrayConverter.buildArr(convertHexToDecimal(specificChunk), intData);
intData.add(0);
intData.add(0);

View File

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

View File

@ -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;
}
// 需要新建,抢锁
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;
}

View File

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

View File

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

View File

@ -109,6 +109,17 @@ public class DeviceXinghanController extends BaseController {
return R.ok();
}
/**
* SOS档位 批量
* SOS档位2,1,0, 分别表示红蓝模式/爆闪模式/关闭
*/
@PostMapping("/SOSGradeSettingsBatch")
public R<Void> SOSGradeSettingsBatch(@RequestBody DeviceXinghanInstructDto params) {
// params 转 JSONObject
deviceXinghanBizService.sendCommandBatch(params,"ins_SOSGrade","SOS档位");
return R.ok();
}
/**
* 静止报警状态
* 静止报警状态0-未静止报警1-正在静止报警。

View File

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

View File

@ -108,7 +108,8 @@ public class DeviceBJQBizService {
log.info("发送信息设备发送信息失败:{}" ,deviceId);
throw new ServiceException("发送指令失败");
}
//发送消息
messageSending(device.getDeviceImei());
}
@ -526,8 +527,9 @@ public class DeviceBJQBizService {
log.info("设备发送告警信息信息失败:{}" ,deviceId);
throw new ServiceException("设备发送告警信息信息失败");
}
messageSending(device.getDeviceImei());
}
} catch (Exception e){
e.printStackTrace();
throw new ServiceException("发送告警信息指令失败");
@ -535,6 +537,11 @@ public class DeviceBJQBizService {
return 1;
}
private void messageSending(String deviceImei){
String sendMessageIng = GLOBAL_REDIS_KEY+ DEVICE_KEY_PREFIX + deviceImei + ":messageSending";
RedisUtils.setCacheObject(sendMessageIng, "1", Duration.ofDays(1));
}
private boolean getDeviceStatus(String deviceImei) {
String deviceOnlineStatusRedisKey = GlobalConstants.GLOBAL_REDIS_KEY+ DEVICE_KEY_PREFIX+ deviceImei + DeviceRedisKeyConstants.DEVICE_ONLINE_STATUS_KEY_PREFIX ;
return RedisUtils.getCacheObject(deviceOnlineStatusRedisKey) == null;

View File

@ -90,9 +90,10 @@ public class DeviceBizService {
//设备在线状态
String onlineStatus = RedisUtils.getCacheObject(GLOBAL_REDIS_KEY+ DEVICE_KEY_PREFIX+ item.getDeviceImei() + DeviceRedisKeyConstants.DEVICE_ONLINE_STATUS_KEY_PREFIX);
if(StringUtils.isNotBlank(onlineStatus)){
if("1".equals(onlineStatus)){
item.setOnlineStatus(1);
}else if("2".equals(onlineStatus)){
item.setOnlineStatus(2);
}else{
item.setOnlineStatus(0);
}
@ -131,9 +132,10 @@ public class DeviceBizService {
//设备在线状态
String onlineStatus = RedisUtils.getCacheObject(GLOBAL_REDIS_KEY+ DEVICE_KEY_PREFIX+ item.getDeviceImei() + DeviceRedisKeyConstants.DEVICE_ONLINE_STATUS_KEY_PREFIX);
if(StringUtils.isNotBlank(onlineStatus)){
if("1".equals(onlineStatus)){
item.setOnlineStatus(1);
}else if("2".equals(onlineStatus)){
item.setOnlineStatus(2);
}else{
item.setOnlineStatus(0);
}

View File

@ -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<String> errorMessages = Collections.synchronizedList(new ArrayList<>());
int value;
try {
value = Integer.parseInt(dto.getInstructValue());
} catch (NumberFormatException e) {
throw new IllegalArgumentException("指令值格式不正确,必须为整数。", e);
}
Map<String, List<Integer>> payload = Map.of(payloadKey, List.of(value));
// 一次性查询所有设备信息
List<Device> devices = deviceMapper.selectList(
new QueryWrapper<Device>().lambda().in(Device::getId, dto.getDeviceIds())
);
// 日志信息
String contentText = resolveGradeDesc(payloadKey, value);
List<DeviceLog> 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);

View File

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

View File

@ -301,7 +301,7 @@ file:
mqtt:
username: admin
password: #YtvpSfCNG
url: tcp://47.120.79.150:2883
url: tcp://www.cnxhyc.com:2883
subClientId: fys_subClient
subTopic: A/#
pubTopic: B/#

View File

@ -4,6 +4,7 @@ import lombok.Data;
/**
* 报警信息
* 0-强制报警1-撞击闯入2-自动报警3-电子围栏告警
*
* @author: 默苍璃
* @date: 2025-09-0114:24
@ -21,7 +22,6 @@ public class AlarmInformationVo {
*/
private Integer processingAlarm = 0;
/**
* 今日报警总数
*/
@ -32,7 +32,6 @@ public class AlarmInformationVo {
*/
private Integer processingAlarmToday = 0;
/**
* 强制报警
*/
@ -44,12 +43,12 @@ public class AlarmInformationVo {
private Integer intrusionImpact = 0;
/**
* 动报警
* 动报警
*/
private Integer alarmManual = 0;
private Integer alarmAuto = 0;
/**
*
* 电子围栏告警
*/
private Integer fenceElectronic = 0;

View File

@ -43,10 +43,10 @@ public class AlarmStatisticsVo implements Serializable {
private Integer intrusionImpactAlarms = 0;
/**
* 动报警数量
* 动报警数量
* device_action = 2
*/
private Integer manualAlarms = 0;
private Integer autoAlarms = 0;
/**
* 电子围栏告警数量

View File

@ -76,7 +76,7 @@
(SELECT COUNT(1) FROM device_alarm WHERE treatment_state = 0) AS processedAlarms,
(SELECT COUNT(1) FROM device_alarm WHERE device_action = 0) AS forcedAlarms,
(SELECT COUNT(1) FROM device_alarm WHERE device_action = 1) AS intrusionImpactAlarms,
(SELECT COUNT(1) FROM device_alarm WHERE device_action = 2) AS manualAlarms,
(SELECT COUNT(1) FROM device_alarm WHERE device_action = 2) AS autoAlarms,
(SELECT COUNT(1) FROM device_alarm WHERE device_action = 3) AS geoFenceAlarms
</select>

View File

@ -241,9 +241,11 @@
<select id="queryWebDeviceList" resultType="com.fuyuanshen.equipment.domain.vo.WebDeviceVo">
select * from (select d.id, d.device_name,
d.device_mac,
d.device_type,
d.device_sn,
d.device_imei,
d.device_pic,
d.group_id,
dt.type_name,
dt.communication_mode,
d.bluetooth_name,
@ -404,7 +406,7 @@
SELECT COUNT (1)
FROM device_alarm
WHERE device_action = 2
) AS alarmManual
) AS alarmAuto
, (
SELECT COUNT (1)
FROM device_alarm