2 Commits fa60f45eb9 ... b272cffa89

Auteur SHA1 Bericht Datum
  liweimin b272cffa89 1、调试宝设备 MQTT 登录在线注册表使用redis维护 2 weken geleden
  liweimin ca4af5374a 1、调试宝设备 MQTT 登录在线注册表 2 weken geleden

+ 5 - 0
ruoyi-common/src/main/java/com/ruoyi/common/constant/CacheConstants.java

@@ -41,4 +41,9 @@ public class CacheConstants
      * 登录账户密码错误次数 redis key
      */
     public static final String PWD_ERR_CNT_KEY = "pwd_err_cnt:";
+
+    /**
+     * 调试宝设备 MQTT 登录在线 redis key 前缀(完整 key:tsb:device:online:{deviceSn})
+     */
+    public static final String TSB_DEVICE_ONLINE_KEY = "tsb:device:online:";
 }

+ 50 - 0
ruoyi-device/src/main/java/com/ruoyi/device/mqtt/enums/DeviceLineStatusEnum.java

@@ -0,0 +1,50 @@
+package com.ruoyi.device.mqtt.enums;
+
+/**
+ * 设备在线状态枚举
+ *
+ * @author lwm
+ **/
+public enum DeviceLineStatusEnum
+{
+    ON_LINE(1, "在线"),
+    OFF_LINE(0, "离线"),
+    ;
+
+    private final Integer status;
+    private final String message;
+
+    DeviceLineStatusEnum(Integer status, String message)
+    {
+        this.status = status;
+        this.message = message;
+    }
+
+    public Integer getStatus()
+    {
+        return status;
+    }
+
+    public String getMessage()
+    {
+        return message;
+    }
+
+    // 判断是否在线
+    public static boolean isOnLine(Integer status)
+    {
+        if (status == null)
+        {
+            return false;
+        }
+        return DeviceLineStatusEnum.ON_LINE.getStatus().equals(status);
+    }
+
+    // 判断是否离线
+    public static boolean isOffLine(Integer status)
+    {
+        return !isOnLine(status);
+    }
+
+
+}

+ 176 - 0
ruoyi-device/src/main/java/com/ruoyi/device/mqtt/handler/DeviceOnlineManager.java

@@ -0,0 +1,176 @@
+package com.ruoyi.device.mqtt.handler;
+
+import com.ruoyi.common.constant.CacheConstants;
+import com.ruoyi.common.core.redis.RedisCache;
+import com.ruoyi.device.mqtt.enums.DeviceLineStatusEnum;
+import com.ruoyi.device.mqtt.vo.DeviceOnlineInfo;
+import jakarta.annotation.Resource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+import java.util.Collection;
+import java.util.Date;
+
+/**
+ * 调试宝设备 MQTT 登录在线注册表(deviceSn → 设备信息/用户信息,Redis String 维护)
+ *
+ * @author lwm
+ */
+@Component
+public class DeviceOnlineManager
+{
+    private static final Logger log = LoggerFactory.getLogger(DeviceOnlineManager.class);
+
+    /** 默认心跳超时(毫秒),超过该时间未收到心跳则判定离线 */
+    public static final long DEFAULT_HEARTBEAT_TIMEOUT_MS = 90_000L;
+
+    @Resource
+    private RedisCache redisCache;
+
+    /**
+     * 登录在线:写入/覆盖设备登录信息,并标记在线、刷新心跳时间
+     *
+     * @param info 设备登录信息
+     */
+    public void loginOnline(DeviceOnlineInfo info)
+    {
+        if (info == null || info.getDeviceSn() == null)
+        {
+            return;
+        }
+        info.setLineStatus(DeviceLineStatusEnum.ON_LINE.getStatus());
+        info.setHeartbeatTime(new Date());
+        saveDeviceLoginInfo(info);
+        log.info("设备登录在线, deviceSn={}, userId={}", info.getDeviceSn(), info.getUserId());
+    }
+
+    /**
+     * 登出离线:保留登录信息,仅标记离线
+     *
+     * @param deviceSn 设备SN码
+     */
+    public void logoutOffline(Long deviceSn)
+    {
+        if (deviceSn == null)
+        {
+            return;
+        }
+        DeviceOnlineInfo info = getDeviceLoginInfo(deviceSn);
+        if (info == null)
+        {
+            log.info("设备登出离线,登录信息不存在, deviceSn={}", deviceSn);
+            return;
+        }
+        info.setLineStatus(DeviceLineStatusEnum.OFF_LINE.getStatus());
+        saveDeviceLoginInfo(info);
+        log.info("设备登出离线, deviceSn={}, userId={}", deviceSn, info.getUserId());
+    }
+
+    /**
+     * 心跳在线:刷新心跳时间,恢复/保持在线状态
+     *
+     * @param deviceSn 设备SN码
+     */
+    public void heartbeatOnline(Long deviceSn)
+    {
+        if (deviceSn == null)
+        {
+            return;
+        }
+        DeviceOnlineInfo info = getDeviceLoginInfo(deviceSn);
+        if (info == null)
+        {
+            log.debug("设备心跳,登录信息不存在, deviceSn={}", deviceSn);
+            return;
+        }
+        info.setLineStatus(DeviceLineStatusEnum.ON_LINE.getStatus());
+        info.setHeartbeatTime(new Date());
+        saveDeviceLoginInfo(info);
+        log.debug("设备心跳在线, deviceSn={}", deviceSn);
+    }
+
+    /**
+     * 长时间未心跳离线:标记离线(由定时扫描或外部检测调用)
+     *
+     * @param deviceSn 设备SN码
+     */
+    public void offlineByHeartbeatTimeout(Long deviceSn)
+    {
+        if (deviceSn == null)
+        {
+            return;
+        }
+        DeviceOnlineInfo info = getDeviceLoginInfo(deviceSn);
+        if (info == null || DeviceLineStatusEnum.isOffLine(info.getLineStatus()))
+        {
+            return;
+        }
+        info.setLineStatus(DeviceLineStatusEnum.OFF_LINE.getStatus());
+        saveDeviceLoginInfo(info);
+        log.info("设备心跳超时离线, deviceSn={}, lastHeartbeat={}", deviceSn, info.getHeartbeatTime());
+    }
+
+    /**
+     * 扫描所有在线设备,将心跳超时的设备标记为离线
+     *
+     */
+    public void scanOfflineByHeartbeatTimeout()
+    {
+        Collection<String> keys = redisCache.keys(CacheConstants.TSB_DEVICE_ONLINE_KEY + "*");
+        if (keys == null || keys.isEmpty())
+        {
+            return;
+        }
+        Date now = new Date();
+        for (String key : keys)
+        {
+            DeviceOnlineInfo info = redisCache.getCacheObject(key);
+            if (info == null || DeviceLineStatusEnum.isOffLine(info.getLineStatus()))
+            {
+                continue;
+            }
+            Date lastHeartbeat = info.getHeartbeatTime();
+            if (lastHeartbeat == null || now.getTime() - lastHeartbeat.getTime() > DEFAULT_HEARTBEAT_TIMEOUT_MS)
+            {
+                offlineByHeartbeatTimeout(info.getDeviceSn());
+            }
+        }
+    }
+
+    /**
+     * 获取设备登录信息(含在线状态与心跳时间)
+     *
+     * @param deviceSn 设备SN码
+     * @return 设备登录信息;未登录过则返回 null
+     */
+    public DeviceOnlineInfo getDeviceLoginInfo(Long deviceSn)
+    {
+        if (deviceSn == null)
+        {
+            return null;
+        }
+        return redisCache.getCacheObject(getDeviceOnlineKey(deviceSn));
+    }
+
+    /**
+     * 设备是否在线
+     *
+     * @param deviceSn 设备SN码
+     */
+    public boolean isOnline(Long deviceSn)
+    {
+        DeviceOnlineInfo info = getDeviceLoginInfo(deviceSn);
+        return info != null && DeviceLineStatusEnum.isOnLine(info.getLineStatus());
+    }
+
+    private void saveDeviceLoginInfo(DeviceOnlineInfo info)
+    {
+        redisCache.setCacheObject(getDeviceOnlineKey(info.getDeviceSn()), info);
+    }
+
+    private String getDeviceOnlineKey(Long deviceSn)
+    {
+        return CacheConstants.TSB_DEVICE_ONLINE_KEY + deviceSn;
+    }
+}

+ 19 - 0
ruoyi-device/src/main/java/com/ruoyi/device/mqtt/handler/decoder/json/service/DeviceLoginService.java

@@ -17,6 +17,8 @@ import com.ruoyi.device.mqtt.handler.decoder.json.IJsonCmdHandler;
 import com.ruoyi.device.mqtt.vo.CommonHeader;
 import com.ruoyi.device.mqtt.vo.CommonTopic;
 import com.ruoyi.device.service.ITsbUserDeviceService;
+import com.ruoyi.device.mqtt.handler.DeviceOnlineManager;
+import com.ruoyi.device.mqtt.vo.DeviceOnlineInfo;
 import com.ruoyi.framework.web.service.SysPermissionService;
 import com.ruoyi.system.service.ISysUserService;
 import jakarta.annotation.Resource;
@@ -43,6 +45,8 @@ public class DeviceLoginService implements IJsonCmdHandler
     private ISysUserService userService;
     @Resource
     private SysPermissionService permissionService;
+    @Resource
+    private DeviceOnlineManager deviceOnlineManager;
 
     @Override
     public BaseJsonBody handle(CommonTopic topic, CommonHeader header, String bodyJson, CmdTypeEnum cmd)
@@ -116,6 +120,21 @@ public class DeviceLoginService implements IJsonCmdHandler
         }
         Set<String> permissions = permissionService.getMenuPermission(user);
         response.setPermissions(permissions);
+
+        // 4、设备 MQTT 登录成功,注册 deviceSn 在线映射
+        DeviceOnlineInfo onlineInfo = new DeviceOnlineInfo();
+        onlineInfo.setUserId(user.getUserId());
+        onlineInfo.setUserName(user.getUserName());
+        onlineInfo.setPhoneNumber(user.getPhonenumber());
+        onlineInfo.setNickName(user.getNickName());
+        onlineInfo.setRoleName(response.getRoleName());
+        onlineInfo.setPermissions(response.getPermissions());
+        onlineInfo.setDeviceId(bindResult.getDeviceId());
+        onlineInfo.setDeviceSn(request.getDeviceSn());
+        onlineInfo.setDeviceType(request.getDeviceType());
+        onlineInfo.setImei(request.getImei());
+        deviceOnlineManager.loginOnline(onlineInfo);
+
         return response;
     }
 

+ 133 - 0
ruoyi-device/src/main/java/com/ruoyi/device/mqtt/vo/DeviceOnlineInfo.java

@@ -0,0 +1,133 @@
+package com.ruoyi.device.mqtt.vo;
+
+import java.util.Date;
+import java.util.Set;
+
+/**
+ * 调试宝设备 MQTT 登录在线信息(以 deviceSn 为 key,因为 deviceSn 是必填的)
+ * 后续使用 deviceSn 获取设备信息/用户信息
+ *
+ * @author lwm
+ */
+public class DeviceOnlineInfo
+{
+    /** 用户信息部分 */
+    private Long userId;
+    private String userName;
+    private String phoneNumber;
+    private String nickName;
+    private String roleName;
+    private Set<String> permissions;
+
+    /** 设备信息部分 */
+    private Long deviceId;
+    private Long deviceSn;
+    private String deviceType;
+    private String imei;
+
+    /**
+     * 在线状态(0:离线;1:在线)
+     */
+    private Integer lineStatus;
+
+    /**
+     * 心跳时间
+     */
+    private Date heartbeatTime;
+
+    public Long getUserId() {
+        return userId;
+    }
+
+    public void setUserId(Long userId) {
+        this.userId = userId;
+    }
+
+    public String getUserName() {
+        return userName;
+    }
+
+    public void setUserName(String userName) {
+        this.userName = userName;
+    }
+
+    public String getPhoneNumber() {
+        return phoneNumber;
+    }
+
+    public void setPhoneNumber(String phoneNumber) {
+        this.phoneNumber = phoneNumber;
+    }
+
+    public String getNickName() {
+        return nickName;
+    }
+
+    public void setNickName(String nickName) {
+        this.nickName = nickName;
+    }
+
+    public String getRoleName() {
+        return roleName;
+    }
+
+    public void setRoleName(String roleName) {
+        this.roleName = roleName;
+    }
+
+    public Set<String> getPermissions() {
+        return permissions;
+    }
+
+    public void setPermissions(Set<String> permissions) {
+        this.permissions = permissions;
+    }
+
+    public Long getDeviceId() {
+        return deviceId;
+    }
+
+    public void setDeviceId(Long deviceId) {
+        this.deviceId = deviceId;
+    }
+
+    public Long getDeviceSn() {
+        return deviceSn;
+    }
+
+    public void setDeviceSn(Long deviceSn) {
+        this.deviceSn = deviceSn;
+    }
+
+    public String getDeviceType() {
+        return deviceType;
+    }
+
+    public void setDeviceType(String deviceType) {
+        this.deviceType = deviceType;
+    }
+
+    public String getImei() {
+        return imei;
+    }
+
+    public void setImei(String imei) {
+        this.imei = imei;
+    }
+
+    public Integer getLineStatus() {
+        return lineStatus;
+    }
+
+    public void setLineStatus(Integer lineStatus) {
+        this.lineStatus = lineStatus;
+    }
+
+    public Date getHeartbeatTime() {
+        return heartbeatTime;
+    }
+
+    public void setHeartbeatTime(Date heartbeatTime) {
+        this.heartbeatTime = heartbeatTime;
+    }
+}