diff --git a/src/main/java/com/shzg/common/utils/DeptScopeUtils.java b/src/main/java/com/shzg/common/utils/DeptScopeUtils.java index 607357b..4d9281c 100644 --- a/src/main/java/com/shzg/common/utils/DeptScopeUtils.java +++ b/src/main/java/com/shzg/common/utils/DeptScopeUtils.java @@ -3,6 +3,7 @@ package com.shzg.common.utils; import com.shzg.common.utils.spring.SpringUtils; import com.shzg.project.system.service.ISysDeptService; +import java.util.ArrayList; import java.util.List; /** @@ -17,7 +18,21 @@ public class DeptScopeUtils { Long deptId = SecurityUtils.getDeptId(); - return SpringUtils.getBean(ISysDeptService.class) + // 查询子部门 + List deptIds = SpringUtils.getBean(ISysDeptService.class) .selectDeptAndChildIds(deptId); + + // 防止返回null + if (deptIds == null) + { + deptIds = new ArrayList<>(); + } + + if (!deptIds.contains(deptId)) + { + deptIds.add(deptId); + } + + return deptIds; } } \ No newline at end of file diff --git a/src/main/java/com/shzg/framework/security/service/TokenService.java b/src/main/java/com/shzg/framework/security/service/TokenService.java index 1ca231c..3e4e519 100644 --- a/src/main/java/com/shzg/framework/security/service/TokenService.java +++ b/src/main/java/com/shzg/framework/security/service/TokenService.java @@ -4,11 +4,7 @@ import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; import javax.servlet.http.HttpServletRequest; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; + import com.shzg.common.constant.CacheConstants; import com.shzg.common.constant.Constants; import com.shzg.common.utils.ServletUtils; @@ -19,100 +15,103 @@ import com.shzg.common.utils.ip.IpUtils; import com.shzg.common.utils.uuid.IdUtils; import com.shzg.framework.redis.RedisCache; import com.shzg.framework.security.LoginUser; + import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; -/** - * token验证处理 - * - * @author ruoyi - */ +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + @Component -public class TokenService -{ +public class TokenService { + private static final Logger log = LoggerFactory.getLogger(TokenService.class); - // 令牌自定义标识 @Value("${token.header}") private String header; - // 令牌秘钥 @Value("${token.secret}") private String secret; - // 令牌有效期(默认30分钟) @Value("${token.expireTime}") private int expireTime; protected static final long MILLIS_SECOND = 1000; - protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND; - private static final Long MILLIS_MINUTE_TWENTY = 20 * 60 * 1000L; @Autowired private RedisCache redisCache; - /** - * 获取用户身份信息 - * - * @return 用户信息 - */ - public LoginUser getLoginUser(HttpServletRequest request) - { - // 获取请求携带的令牌 + public LoginUser getLoginUser(HttpServletRequest request) { String token = getToken(request); - if (StringUtils.isNotEmpty(token)) - { - try - { + if (StringUtils.isNotEmpty(token)) { + try { Claims claims = parseToken(token); - // 解析对应的权限以及用户信息 String uuid = (String) claims.get(Constants.LOGIN_USER_KEY); String userKey = getTokenKey(uuid); - LoginUser user = redisCache.getCacheObject(userKey); - return user; - } - catch (Exception e) - { + return redisCache.getCacheObject(userKey); + } catch (Exception e) { log.error("获取用户信息异常'{}'", e.getMessage()); } } return null; } - /** - * 设置用户身份信息 - */ - public void setLoginUser(LoginUser loginUser) - { - if (StringUtils.isNotNull(loginUser) && StringUtils.isNotEmpty(loginUser.getToken())) - { + public LoginUser getLoginUserByToken(String token) { + + if (StringUtils.isEmpty(token)) { + return null; + } + + try { + // 1. 解析JWT + Claims claims = parseToken(token); + + if (claims == null) { + return null; + } + + // 2. 获取uuid(关键) + String uuid = (String) claims.get(Constants.LOGIN_USER_KEY); + + if (StringUtils.isEmpty(uuid)) { + return null; + } + + // 3. 拼Redis key + String userKey = getTokenKey(uuid); + + // 4. 查缓存 + LoginUser loginUser = redisCache.getCacheObject(userKey); + + return loginUser; + + } catch (Exception e) { + log.error("WebSocket获取用户失败: {}", e.getMessage()); + } + + return null; + } + + public void setLoginUser(LoginUser loginUser) { + if (StringUtils.isNotNull(loginUser) && StringUtils.isNotEmpty(loginUser.getToken())) { refreshToken(loginUser); } } - /** - * 删除用户身份信息 - */ - public void delLoginUser(String token) - { - if (StringUtils.isNotEmpty(token)) - { + public void delLoginUser(String token) { + if (StringUtils.isNotEmpty(token)) { String userKey = getTokenKey(token); redisCache.deleteObject(userKey); } } - /** - * 创建令牌 - * - * @param loginUser 用户信息 - * @return 令牌 - */ - public String createToken(LoginUser loginUser) - { + public String createToken(LoginUser loginUser) { String token = IdUtils.fastUUID(); loginUser.setToken(token); setUserAgent(loginUser); @@ -121,46 +120,26 @@ public class TokenService Map claims = new HashMap<>(); claims.put(Constants.LOGIN_USER_KEY, token); claims.put(Constants.JWT_USERNAME, loginUser.getUsername()); + return createToken(claims); } - /** - * 验证令牌有效期,相差不足20分钟,自动刷新缓存 - * - * @param loginUser 登录信息 - * @return 令牌 - */ - public void verifyToken(LoginUser loginUser) - { + public void verifyToken(LoginUser loginUser) { long expireTime = loginUser.getExpireTime(); long currentTime = System.currentTimeMillis(); - if (expireTime - currentTime <= MILLIS_MINUTE_TWENTY) - { + if (expireTime - currentTime <= MILLIS_MINUTE_TWENTY) { refreshToken(loginUser); } } - /** - * 刷新令牌有效期 - * - * @param loginUser 登录信息 - */ - public void refreshToken(LoginUser loginUser) - { + public void refreshToken(LoginUser loginUser) { loginUser.setLoginTime(System.currentTimeMillis()); loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE); - // 根据uuid将loginUser缓存 String userKey = getTokenKey(loginUser.getToken()); redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES); } - /** - * 设置用户代理信息 - * - * @param loginUser 登录信息 - */ - public void setUserAgent(LoginUser loginUser) - { + public void setUserAgent(LoginUser loginUser) { String userAgent = ServletUtils.getRequest().getHeader("User-Agent"); String ip = IpUtils.getIpAddr(); loginUser.setIpaddr(ip); @@ -169,64 +148,34 @@ public class TokenService loginUser.setOs(UserAgentUtils.getOperatingSystem(userAgent)); } - /** - * 从数据声明生成令牌 - * - * @param claims 数据声明 - * @return 令牌 - */ - private String createToken(Map claims) - { - String token = Jwts.builder() + private String createToken(Map claims) { + return Jwts.builder() .setClaims(claims) - .signWith(SignatureAlgorithm.HS512, secret).compact(); - return token; + .signWith(SignatureAlgorithm.HS512, secret) + .compact(); } - /** - * 从令牌中获取数据声明 - * - * @param token 令牌 - * @return 数据声明 - */ - private Claims parseToken(String token) - { + private Claims parseToken(String token) { return Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); } - /** - * 从令牌中获取用户名 - * - * @param token 令牌 - * @return 用户名 - */ - public String getUsernameFromToken(String token) - { + public String getUsernameFromToken(String token) { Claims claims = parseToken(token); return claims.getSubject(); } - /** - * 获取请求token - * - * @param request - * @return token - */ - private String getToken(HttpServletRequest request) - { + private String getToken(HttpServletRequest request) { String token = request.getHeader(header); - if (StringUtils.isNotEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX)) - { + if (StringUtils.isNotEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX)) { token = token.replace(Constants.TOKEN_PREFIX, ""); } return token; } - private String getTokenKey(String uuid) - { + private String getTokenKey(String uuid) { return CacheConstants.LOGIN_TOKEN_KEY + uuid; } -} +} \ No newline at end of file diff --git a/src/main/java/com/shzg/project/system/controller/SysDeptController.java b/src/main/java/com/shzg/project/system/controller/SysDeptController.java index f9a9119..b19b265 100644 --- a/src/main/java/com/shzg/project/system/controller/SysDeptController.java +++ b/src/main/java/com/shzg/project/system/controller/SysDeptController.java @@ -129,4 +129,5 @@ public class SysDeptController extends BaseController deptService.checkDeptDataScope(deptId); return toAjax(deptService.deleteDeptById(deptId)); } + } diff --git a/src/main/java/com/shzg/project/system/mapper/SysDeptMapper.java b/src/main/java/com/shzg/project/system/mapper/SysDeptMapper.java index a45694f..c0d1301 100644 --- a/src/main/java/com/shzg/project/system/mapper/SysDeptMapper.java +++ b/src/main/java/com/shzg/project/system/mapper/SysDeptMapper.java @@ -1,6 +1,8 @@ package com.shzg.project.system.mapper; import java.util.List; +import java.util.Map; + import org.apache.ibatis.annotations.Param; import com.shzg.project.system.domain.SysDept; @@ -132,4 +134,19 @@ public interface SysDeptMapper * @return 部门ID集合 */ List selectDeptAndChildIds(Long deptId); + + /** + * 项目总数(按权限) + */ + int countProjectByDeptIds(@Param("deptIds") List deptIds); + + /** + * 仓库总数(按权限) + */ + int countWarehouseByDeptIds(@Param("deptIds") List deptIds); + + /** + * 仓库列表(按权限) + */ + List> selectWarehouseList(@Param("deptIds") List deptIds); } diff --git a/src/main/java/com/shzg/project/system/service/ISysDeptService.java b/src/main/java/com/shzg/project/system/service/ISysDeptService.java index 52a13cf..d0a2e3a 100644 --- a/src/main/java/com/shzg/project/system/service/ISysDeptService.java +++ b/src/main/java/com/shzg/project/system/service/ISysDeptService.java @@ -1,6 +1,8 @@ package com.shzg.project.system.service; import java.util.List; +import java.util.Map; + import com.shzg.framework.web.domain.TreeSelect; import com.shzg.project.system.domain.SysDept; @@ -137,4 +139,20 @@ public interface ISysDeptService * @return 部门ID列表 */ List selectDeptAndChildIds(Long deptId); + + /** + * 首页统计 + */ + Map getHomeStat(); + + /** + * 设备统计 + */ + Map getDeviceStat(); + + /** + * 仓库运行状态 + */ + List> selectWarehouseList(); + } diff --git a/src/main/java/com/shzg/project/system/service/impl/SysDeptServiceImpl.java b/src/main/java/com/shzg/project/system/service/impl/SysDeptServiceImpl.java index 4815443..c2bb925 100644 --- a/src/main/java/com/shzg/project/system/service/impl/SysDeptServiceImpl.java +++ b/src/main/java/com/shzg/project/system/service/impl/SysDeptServiceImpl.java @@ -1,12 +1,12 @@ package com.shzg.project.system.service.impl; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; +import java.util.*; import java.util.stream.Collectors; +import com.shzg.common.utils.DeptScopeUtils; import com.shzg.project.system.domain.SysUser; import com.shzg.project.system.mapper.SysUserMapper; +import com.shzg.project.worn.mapper.MqttSensorDeviceMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import com.shzg.common.constant.UserConstants; @@ -40,6 +40,10 @@ public class SysDeptServiceImpl implements ISysDeptService @Autowired private SysUserMapper userMapper; + @Autowired + private MqttSensorDeviceMapper deviceMapper; + + /** * 查询部门管理数据 * @@ -360,4 +364,52 @@ public class SysDeptServiceImpl implements ISysDeptService { return deptMapper.selectDeptAndChildIds(deptId); } + + @Override + public Map getHomeStat() + { + Map map = new HashMap<>(); + + // 当前用户可见部门范围 + List deptIds = DeptScopeUtils.getDeptScope(); + + // 项目总数(顶级部门) + int projectCount = deptMapper.countProjectByDeptIds(deptIds); + + // 仓库总数(子部门) + int warehouseCount = deptMapper.countWarehouseByDeptIds(deptIds); + + map.put("projectCount", projectCount); + map.put("warehouseCount", warehouseCount); + + return map; + } + + @Override + public Map getDeviceStat() + { + Map map = new HashMap<>(); + + // 当前用户权限范围(核心) + List deptIds = DeptScopeUtils.getDeptScope(); + + Map stat = deviceMapper.countDeviceStat(deptIds); + + map.put("total", stat.get("total")); + map.put("online", stat.get("online")); + map.put("offline", stat.get("offline")); + map.put("onlineRate", stat.get("onlineRate")); + + return map; + } + + @Override + public List> selectWarehouseList() + { + // 当前用户权限范围 + List deptIds = DeptScopeUtils.getDeptScope(); + + return deptMapper.selectWarehouseList(deptIds); + } + } diff --git a/src/main/java/com/shzg/project/worn/controller/SocketController.java b/src/main/java/com/shzg/project/worn/controller/SocketController.java index 71be864..9cadfc8 100644 --- a/src/main/java/com/shzg/project/worn/controller/SocketController.java +++ b/src/main/java/com/shzg/project/worn/controller/SocketController.java @@ -13,7 +13,7 @@ public class SocketController { private IMqttSocketService mqttSocketService; /** - * 控制插座开关 + * 控制智能排风插座开关 */ @PostMapping("/control") public AjaxResult control(@RequestParam String devEui, diff --git a/src/main/java/com/shzg/project/worn/controller/SysHomeController.java b/src/main/java/com/shzg/project/worn/controller/SysHomeController.java new file mode 100644 index 0000000..3c29ffe --- /dev/null +++ b/src/main/java/com/shzg/project/worn/controller/SysHomeController.java @@ -0,0 +1,83 @@ +package com.shzg.project.worn.controller; + +import com.shzg.framework.web.controller.BaseController; +import com.shzg.framework.web.domain.AjaxResult; +import com.shzg.framework.web.page.TableDataInfo; +import com.shzg.project.system.domain.SysDept; +import com.shzg.project.system.service.ISysDeptService; +import com.shzg.project.worn.service.IMqttSensorEventService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/home") +public class SysHomeController extends BaseController +{ + @Autowired + private ISysDeptService deptService; + + @Autowired + private IMqttSensorEventService eventService; + + /** + * 首页统计 + */ + @GetMapping("/stat") + public AjaxResult stat() + { + Map data = deptService.getHomeStat(); + return AjaxResult.success(data); + } + + /** + * 设备统计(总数 / 在线 / 离线) + */ + @GetMapping("/device/stat") + public AjaxResult deviceStat() + { + Map data = deptService.getDeviceStat(); + return AjaxResult.success(data); + } + + /** + * 告警统计(今日告警 / 未处理告警) + */ + @GetMapping("/alarm/stat") + public AjaxResult alarmStat() + { + Map data = eventService.getAlarmStat(); + return AjaxResult.success(data); + } + + /** + * 告警趋势(近7天) + */ + @GetMapping("/alarm/trend") + public AjaxResult alarmTrend() + { + return AjaxResult.success(eventService.getAlarmTrend()); + } + + /** + * 告警类型占比 + */ + @GetMapping("/alarm/type") + public AjaxResult alarmType() + { + return AjaxResult.success(eventService.getAlarmTypeStat()); + } + + /** + * 仓库运行状态(全部仓库) + */ + @GetMapping("/warehouse/list") + public AjaxResult warehouseList() + { + List> list = deptService.selectWarehouseList(); + return AjaxResult.success(list); + } + +} \ No newline at end of file diff --git a/src/main/java/com/shzg/project/worn/domain/MqttSensorDevice.java b/src/main/java/com/shzg/project/worn/domain/MqttSensorDevice.java index c5743e6..7f3a072 100644 --- a/src/main/java/com/shzg/project/worn/domain/MqttSensorDevice.java +++ b/src/main/java/com/shzg/project/worn/domain/MqttSensorDevice.java @@ -7,7 +7,7 @@ import com.shzg.framework.aspectj.lang.annotation.Excel; /** * MQTT设备对象 mqtt_sensor_device - * + * * @author shzg * @date 2026-04-01 */ @@ -19,21 +19,29 @@ public class MqttSensorDevice extends BaseEntity private Long id; /** 设备唯一标识(DevEUI) */ - @Excel(name = "设备唯一标识", readConverterExp = "D=evEUI") + @Excel(name = "设备唯一标识") private String devEui; /** 设备名称 */ @Excel(name = "设备名称") private String deviceName; - /** 设备类型(smoke / env) */ - @Excel(name = "设备类型", readConverterExp = "s=moke,/=,e=nv") + /** 设备类型(smoke / env / socket / door 等) */ + @Excel(name = "设备类型") private String deviceType; /** 所属部门ID */ @Excel(name = "所属部门ID") private Long deptId; + /** 所属部门名称 */ + @Excel(name = "所属部门名称") + private String deptName; + + /** 🔥 上报周期(分钟) */ + @Excel(name = "上报周期(分钟)") + private Integer reportIntervalMinute; + /** 状态(0正常 1停用) */ @Excel(name = "状态", readConverterExp = "0=正常,1=停用") private String status; @@ -42,72 +50,92 @@ public class MqttSensorDevice extends BaseEntity @Excel(name = "删除标识", readConverterExp = "0=正常,1=删除") private String isDelete; - public void setId(Long id) + public void setId(Long id) { this.id = id; } - public Long getId() + public Long getId() { return id; } - public void setDevEui(String devEui) + public void setDevEui(String devEui) { this.devEui = devEui; } - public String getDevEui() + public String getDevEui() { return devEui; } - public void setDeviceName(String deviceName) + public void setDeviceName(String deviceName) { this.deviceName = deviceName; } - public String getDeviceName() + public String getDeviceName() { return deviceName; } - public void setDeviceType(String deviceType) + public void setDeviceType(String deviceType) { this.deviceType = deviceType; } - public String getDeviceType() + public String getDeviceType() { return deviceType; } - public void setDeptId(Long deptId) + public void setDeptId(Long deptId) { this.deptId = deptId; } - public Long getDeptId() + public Long getDeptId() { return deptId; } - public void setStatus(String status) + public void setDeptName(String deptName) + { + this.deptName = deptName; + } + + public String getDeptName() + { + return deptName; + } + + public void setReportIntervalMinute(Integer reportIntervalMinute) + { + this.reportIntervalMinute = reportIntervalMinute; + } + + public Integer getReportIntervalMinute() + { + return reportIntervalMinute; + } + + public void setStatus(String status) { this.status = status; } - public String getStatus() + public String getStatus() { return status; } - public void setIsDelete(String isDelete) + public void setIsDelete(String isDelete) { this.isDelete = isDelete; } - public String getIsDelete() + public String getIsDelete() { return isDelete; } @@ -115,18 +143,20 @@ public class MqttSensorDevice extends BaseEntity @Override public String toString() { return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) - .append("id", getId()) - .append("devEui", getDevEui()) - .append("deviceName", getDeviceName()) - .append("deviceType", getDeviceType()) - .append("deptId", getDeptId()) - .append("status", getStatus()) - .append("remark", getRemark()) - .append("createBy", getCreateBy()) - .append("createTime", getCreateTime()) - .append("updateBy", getUpdateBy()) - .append("updateTime", getUpdateTime()) - .append("isDelete", getIsDelete()) - .toString(); + .append("id", getId()) + .append("devEui", getDevEui()) + .append("deviceName", getDeviceName()) + .append("deviceType", getDeviceType()) + .append("deptId", getDeptId()) + .append("deptName", getDeptName()) + .append("reportIntervalMinute", getReportIntervalMinute()) + .append("status", getStatus()) + .append("remark", getRemark()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("isDelete", getIsDelete()) + .toString(); } -} +} \ No newline at end of file diff --git a/src/main/java/com/shzg/project/worn/domain/MqttTopicConfig.java b/src/main/java/com/shzg/project/worn/domain/MqttTopicConfig.java index 42bbe0d..787e67e 100644 --- a/src/main/java/com/shzg/project/worn/domain/MqttTopicConfig.java +++ b/src/main/java/com/shzg/project/worn/domain/MqttTopicConfig.java @@ -32,6 +32,10 @@ public class MqttTopicConfig extends BaseEntity @Excel(name = "部门ID") private Long deptId; + /** 所属部门名称 */ + @Excel(name = "部门名称") + private String deptName; + /** 🔥 数据权限(部门ID集合) */ private List deptIds; @@ -91,6 +95,16 @@ public class MqttTopicConfig extends BaseEntity return deptId; } + public void setDeptName(String deptName) + { + this.deptName = deptName; + } + + public String getDeptName() + { + return deptName; + } + public void setDeptIds(List deptIds) { this.deptIds = deptIds; @@ -149,6 +163,7 @@ public class MqttTopicConfig extends BaseEntity .append("project", getProject()) .append("warehouse", getWarehouse()) .append("deptId", getDeptId()) + .append("deptName", getDeptName()) .append("deptIds", getDeptIds()) .append("topicUp", getTopicUp()) .append("topicDownPrefix", getTopicDownPrefix()) diff --git a/src/main/java/com/shzg/project/worn/mapper/MqttSensorDeviceMapper.java b/src/main/java/com/shzg/project/worn/mapper/MqttSensorDeviceMapper.java index 3b4a51b..9c9d17e 100644 --- a/src/main/java/com/shzg/project/worn/mapper/MqttSensorDeviceMapper.java +++ b/src/main/java/com/shzg/project/worn/mapper/MqttSensorDeviceMapper.java @@ -1,6 +1,8 @@ package com.shzg.project.worn.mapper; import java.util.List; +import java.util.Map; + import com.shzg.project.worn.domain.MqttSensorDevice; import io.lettuce.core.dynamic.annotation.Param; @@ -67,4 +69,23 @@ public interface MqttSensorDeviceMapper */ MqttSensorDevice selectByDevEui(@Param("devEui") String devEui); + /** + * 设备统计(按权限) + */ + Map countDeviceStat(@Param("deptIds") List deptIds); + + /** + * 更新设备运行状态(0在线 1离线) + * + * @param device 设备 + * @return 结果 + */ + int updateRuntimeStatus(MqttSensorDevice device); + + /** + * 查询超时离线设备 + * + * @return 设备列表 + */ + List selectOfflineDeviceList(); } diff --git a/src/main/java/com/shzg/project/worn/mapper/MqttSensorEventMapper.java b/src/main/java/com/shzg/project/worn/mapper/MqttSensorEventMapper.java index 7252246..3cc3ccc 100644 --- a/src/main/java/com/shzg/project/worn/mapper/MqttSensorEventMapper.java +++ b/src/main/java/com/shzg/project/worn/mapper/MqttSensorEventMapper.java @@ -1,7 +1,10 @@ package com.shzg.project.worn.mapper; import java.util.List; +import java.util.Map; + import com.shzg.project.worn.domain.MqttSensorEvent; +import io.lettuce.core.dynamic.annotation.Param; /** * 设备事件Mapper接口 @@ -58,4 +61,19 @@ public interface MqttSensorEventMapper * @return 结果 */ public int deleteMqttSensorEventByIds(Long[] ids); + + /** + * 告警统计(今日 / 未处理)- 按权限 + */ + Map countAlarmStat(@Param("deptIds") List deptIds); + + /** + * 告警趋势(按权限) + */ + List> selectAlarmTrend(@Param("deptIds") List deptIds); + + /** + * 告警类型占比(按权限) + */ + List> selectAlarmTypeStat(@Param("deptIds") List deptIds); } diff --git a/src/main/java/com/shzg/project/worn/sensor/mqtt/handler/DoorSensorHandler.java b/src/main/java/com/shzg/project/worn/sensor/mqtt/handler/DoorSensorHandler.java index ad6bc24..c3b9023 100644 --- a/src/main/java/com/shzg/project/worn/sensor/mqtt/handler/DoorSensorHandler.java +++ b/src/main/java/com/shzg/project/worn/sensor/mqtt/handler/DoorSensorHandler.java @@ -9,6 +9,7 @@ import com.shzg.project.worn.domain.MqttSensorEvent; import com.shzg.project.worn.service.IDeviceStatusService; import com.shzg.project.worn.service.IMqttSensorDataService; import com.shzg.project.worn.service.IMqttSensorEventService; +import com.shzg.project.worn.unit.DeviceStatusUtil; import com.shzg.project.worn.websocket.manager.WebSocketSessionManager; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -35,10 +36,12 @@ public class DoorSensorHandler { @Autowired private ISysDeptService deptService; - // 状态服务(Redis去重) @Autowired private IDeviceStatusService deviceStatusService; + @Autowired + private DeviceStatusUtil deviceStatusUtil; + public void handle(MqttSensorDevice device, String topic, String payload) { // ========================= @@ -67,7 +70,7 @@ public class DoorSensorHandler { devEui, device.getDeptId(), doorStatus, tamperStatus, battery); // ========================= - // 2️⃣ 数据入库(始终入库) + // 2️⃣ 数据入库 // ========================= MqttSensorData data = new MqttSensorData(); data.setDeviceId(device.getId()); @@ -100,32 +103,12 @@ public class DoorSensorHandler { } // ========================= - // 4️⃣ Redis去重(核心) + // 4️⃣ 恢复在线 // ========================= - boolean changed = deviceStatusService.isStatusChanged( - device.getId(), - "door", - status - ); - - // 状态没变化,直接返回(不产生事件、不推送) - if (!changed) { - return; - } + deviceStatusUtil.handleOnline(device); // ========================= - // 5️⃣ 事件处理(只在变化时) - // ========================= - if ("open".equals(status)) { - saveEvent(device, "door_open", "门已打开"); - } else if ("close".equals(status)) { - saveEvent(device, "door_close", "门已关闭"); - } else if ("tamper".equals(status)) { - saveEvent(device, "tamper", "设备被拆除"); - } - - // ========================= - // 6️⃣ 查询部门名称 + // 5️⃣ 查询部门名称 // ========================= String deptName = null; SysDept dept = deptService.selectDeptById(device.getDeptId()); @@ -134,7 +117,7 @@ public class DoorSensorHandler { } // ========================= - // 7️⃣ WebSocket推送(只推变化) + // 6️⃣ WebSocket推送(周期上报也要推) // ========================= JSONObject ws = new JSONObject(); ws.put("type", "door"); @@ -150,6 +133,30 @@ public class DoorSensorHandler { sessionManager.sendToDept(device.getDeptId(), ws.toJSONString()); + // ========================= + // 7️⃣ Redis去重 + // ========================= + boolean changed = deviceStatusService.isStatusChanged( + device.getId(), + "door", + status + ); + + if (!changed) { + return; + } + + // ========================= + // 8️⃣ 事件处理 + // ========================= + if ("open".equals(status)) { + saveEvent(device, "door_open", "门已打开"); + } else if ("close".equals(status)) { + saveEvent(device, "door_close", "门已关闭"); + } else if ("tamper".equals(status)) { + saveEvent(device, "tamper", "设备被拆除"); + } + } catch (Exception e) { log.error("[DOOR] 处理异常 payload={}", payload, e); } @@ -179,4 +186,4 @@ public class DoorSensorHandler { eventService.insertMqttSensorEvent(event); } -} \ No newline at end of file +} diff --git a/src/main/java/com/shzg/project/worn/sensor/mqtt/handler/EnvSensorHandler.java b/src/main/java/com/shzg/project/worn/sensor/mqtt/handler/EnvSensorHandler.java index 0cf2b42..b2728fe 100644 --- a/src/main/java/com/shzg/project/worn/sensor/mqtt/handler/EnvSensorHandler.java +++ b/src/main/java/com/shzg/project/worn/sensor/mqtt/handler/EnvSensorHandler.java @@ -5,6 +5,7 @@ import com.shzg.project.system.domain.SysDept; import com.shzg.project.system.service.ISysDeptService; import com.shzg.project.worn.domain.*; import com.shzg.project.worn.service.*; +import com.shzg.project.worn.unit.DeviceStatusUtil; import com.shzg.project.worn.websocket.manager.WebSocketSessionManager; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -35,10 +36,13 @@ public class EnvSensorHandler { @Autowired private ISysDeptService deptService; - // 状态服务(Redis) @Autowired private IDeviceStatusService deviceStatusService; + // ✅ 新增 + @Autowired + private DeviceStatusUtil deviceStatusUtil; + public void handle(MqttSensorDevice device, String topic, String payload) { if (device == null) { @@ -59,13 +63,24 @@ public class EnvSensorHandler { TopicInfo topicInfo = parseTopic(topic); + // ========================= // 1️⃣ 入库 + // ========================= saveData(device, topic, payload, topicInfo, v); - // 2️⃣ 推送(周期数据必须推) + // ========================= + // 2️⃣ 恢复在线 + // ========================= + deviceStatusUtil.handleOnline(device); + + // ========================= + // 3️⃣ 推送(周期数据必须推) + // ========================= pushWebSocket(device, v); - // 3️⃣ 事件检测(带去重) + // ========================= + // 4️⃣ 事件检测(带去重) + // ========================= handleEvent(device, v); } @@ -143,9 +158,6 @@ public class EnvSensorHandler { return "normal"; } - /** - * 使用Redis去重 - */ private void changeStatus(MqttSensorDevice device, String metric, String newStatus, diff --git a/src/main/java/com/shzg/project/worn/sensor/mqtt/handler/SmartSocketHandler.java b/src/main/java/com/shzg/project/worn/sensor/mqtt/handler/SmartSocketHandler.java index 9cea28d..ccbccab 100644 --- a/src/main/java/com/shzg/project/worn/sensor/mqtt/handler/SmartSocketHandler.java +++ b/src/main/java/com/shzg/project/worn/sensor/mqtt/handler/SmartSocketHandler.java @@ -9,6 +9,7 @@ import com.shzg.project.worn.domain.MqttSensorEvent; import com.shzg.project.worn.service.IDeviceStatusService; import com.shzg.project.worn.service.IMqttSensorDataService; import com.shzg.project.worn.service.IMqttSensorEventService; +import com.shzg.project.worn.unit.DeviceStatusUtil; import com.shzg.project.worn.websocket.manager.WebSocketSessionManager; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -35,10 +36,12 @@ public class SmartSocketHandler { @Autowired private ISysDeptService deptService; - // 状态去重服务 @Autowired private IDeviceStatusService deviceStatusService; + @Autowired + private DeviceStatusUtil deviceStatusUtil; + public void handle(MqttSensorDevice device, String topic, String payload) { // ================== 基础校验 ================== @@ -82,7 +85,7 @@ public class SmartSocketHandler { return; } - // ================== 入库(始终入库) ================== + // ================== 入库(有效数据) ================== MqttSensorData data = new MqttSensorData(); data.setDeviceId(device.getId()); data.setDeptId(device.getDeptId()); @@ -95,6 +98,9 @@ public class SmartSocketHandler { dataService.insertMqttSensorData(data); + // ================== 恢复在线 ================== + deviceStatusUtil.handleOnline(device); + // ================== 状态字符串 ================== String statusStr = (switchStatus == 1 ? "on" : "off"); @@ -105,11 +111,6 @@ public class SmartSocketHandler { statusStr ); - // 没变化直接返回(不写事件、不推送) - if (!changed) { - return; - } - // ================== 查询部门 ================== String deptName = null; SysDept dept = deptService.selectDeptById(device.getDeptId()); @@ -117,7 +118,7 @@ public class SmartSocketHandler { deptName = dept.getDeptName(); } - // ================== WebSocket推送(只推变化) ================== + // ================== WebSocket推送 ================== try { JSONObject msg = new JSONObject(); msg.put("type", "socket"); @@ -135,6 +136,10 @@ public class SmartSocketHandler { } // ================== 事件记录(只在变化时) ================== + if (!changed) { + return; + } + MqttSensorEvent event = new MqttSensorEvent(); event.setDeviceId(device.getId()); event.setDeptId(device.getDeptId()); diff --git a/src/main/java/com/shzg/project/worn/sensor/mqtt/handler/SmokeSensorHandler.java b/src/main/java/com/shzg/project/worn/sensor/mqtt/handler/SmokeSensorHandler.java index dc1dfb5..6c129ee 100644 --- a/src/main/java/com/shzg/project/worn/sensor/mqtt/handler/SmokeSensorHandler.java +++ b/src/main/java/com/shzg/project/worn/sensor/mqtt/handler/SmokeSensorHandler.java @@ -9,6 +9,7 @@ import com.shzg.project.worn.domain.MqttSensorEvent; import com.shzg.project.worn.service.IDeviceStatusService; import com.shzg.project.worn.service.IMqttSensorDataService; import com.shzg.project.worn.service.IMqttSensorEventService; +import com.shzg.project.worn.unit.DeviceStatusUtil; import com.shzg.project.worn.websocket.manager.WebSocketSessionManager; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -18,7 +19,7 @@ import java.math.BigDecimal; import java.util.Date; /** - * 烟雾传感器 Handler + * 烟雾传感器 Handler(最终生产版) */ @Slf4j @Component @@ -39,6 +40,9 @@ public class SmokeSensorHandler { @Autowired private IDeviceStatusService deviceStatusService; + @Autowired + private DeviceStatusUtil deviceStatusUtil; + public void handle(MqttSensorDevice device, String topic, String payload) { if (device == null) { @@ -85,7 +89,7 @@ public class SmokeSensorHandler { deptName = dept.getDeptName(); } - // ================== 数据入库(始终入库) ================== + // ================== 数据入库 ================== MqttSensorData data = new MqttSensorData(); data.setDeviceId(device.getId()); data.setDeptId(device.getDeptId()); @@ -106,7 +110,10 @@ public class SmokeSensorHandler { dataService.insertMqttSensorData(data); - // ================== 先判断状态 ================== + // ================== 🔥 恢复在线 ================== + deviceStatusUtil.handleOnline(device); + + // ================== 状态判断 ================== String newStatus; String eventType; String desc; @@ -147,10 +154,8 @@ public class SmokeSensorHandler { ); // ================== WebSocket推送 ================== - // 烟感这里建议周期数据继续推,事件去重即可 pushWebSocket(device, deptName, event, battery, concentration, temperature, newStatus); - // 状态没变化,不重复写事件 if (!changed) { return; } @@ -158,13 +163,18 @@ public class SmokeSensorHandler { // ================== 事件入库 ================== insertEvent(device, eventType, desc, level); + // ================== 🔥 联动控制(核心扩展点) ================== + if ("alarm".equals(newStatus)) { + log.warn("[SMOKE] 触发联动(烟雾报警)deviceId={}", device.getId()); + + // 👉 这里后面接: + // socketService.openFan(device.getDeptId()); + } + log.info("[SMOKE] 状态变化 deviceId={}, status={}, eventType={}", device.getId(), newStatus, eventType); } - /** - * WebSocket推送 - */ private void pushWebSocket(MqttSensorDevice device, String deptName, String event, @@ -197,9 +207,6 @@ public class SmokeSensorHandler { } } - /** - * 事件入库 - */ private void insertEvent(MqttSensorDevice device, String type, String desc, diff --git a/src/main/java/com/shzg/project/worn/sensor/mqtt/handler/WaterSensorHandler.java b/src/main/java/com/shzg/project/worn/sensor/mqtt/handler/WaterSensorHandler.java index df19d05..a1cfc94 100644 --- a/src/main/java/com/shzg/project/worn/sensor/mqtt/handler/WaterSensorHandler.java +++ b/src/main/java/com/shzg/project/worn/sensor/mqtt/handler/WaterSensorHandler.java @@ -9,6 +9,7 @@ import com.shzg.project.worn.domain.MqttSensorEvent; import com.shzg.project.worn.service.IDeviceStatusService; import com.shzg.project.worn.service.IMqttSensorDataService; import com.shzg.project.worn.service.IMqttSensorEventService; +import com.shzg.project.worn.unit.DeviceStatusUtil; import com.shzg.project.worn.websocket.manager.WebSocketSessionManager; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -33,10 +34,12 @@ public class WaterSensorHandler { @Autowired private ISysDeptService deptService; - // Redis状态去重 @Autowired private IDeviceStatusService deviceStatusService; + @Autowired + private DeviceStatusUtil deviceStatusUtil; + public void handle(MqttSensorDevice device, String topic, String payload) { if (device == null) return; @@ -57,15 +60,18 @@ public class WaterSensorHandler { deptName = dept.getDeptName(); } - // ================== 入库(始终入库) ================== + // ================== 入库 ================== saveData(device, topic, payload, topicInfo, info); + // ================== 恢复在线 ================== + deviceStatusUtil.handleOnline(device); + // ================== 状态计算 ================== String status = (info.getWater() != null && info.getWater() == 1) ? "alarm" : "normal"; - // ================== WebSocket(周期数据推送) ================== + // ================== WebSocket ================== pushWebSocket(device, deptName, info, status); // ================== Redis去重 ================== @@ -89,7 +95,6 @@ public class WaterSensorHandler { log.info("[WATER] 状态变化 deviceId={}, status={}", device.getId(), status); } - // ================== WebSocket ================== private void pushWebSocket(MqttSensorDevice device, String deptName, @@ -112,8 +117,7 @@ public class WaterSensorHandler { sessionManager.sendToDept(device.getDeptId(), msg.toJSONString()); } - - // ================== 事件写入 ================== + // ================== 事件 ================== private void triggerEvent(MqttSensorDevice device, String type, String level, @@ -132,7 +136,6 @@ public class WaterSensorHandler { eventService.insertMqttSensorEvent(event); - // 推送告警 JSONObject msg = new JSONObject(); msg.put("type", "alarm"); msg.put("deviceId", device.getId()); @@ -148,7 +151,6 @@ public class WaterSensorHandler { sessionManager.sendToDept(device.getDeptId(), msg.toJSONString()); } - // ================== 入库 ================== private void saveData(MqttSensorDevice device, String topic, @@ -174,7 +176,6 @@ public class WaterSensorHandler { sensorDataService.insertMqttSensorData(data); } - // ================== 工具 ================== private JSONObject parseJson(String payload) { try { @@ -230,7 +231,6 @@ public class WaterSensorHandler { } } - private static class TopicInfo { String project; String warehouse; diff --git a/src/main/java/com/shzg/project/worn/service/IMqttSensorDeviceService.java b/src/main/java/com/shzg/project/worn/service/IMqttSensorDeviceService.java index c2d849e..093a09f 100644 --- a/src/main/java/com/shzg/project/worn/service/IMqttSensorDeviceService.java +++ b/src/main/java/com/shzg/project/worn/service/IMqttSensorDeviceService.java @@ -65,4 +65,13 @@ public interface IMqttSensorDeviceService * @return */ MqttSensorDevice selectByDevEui(String devEui); + + /** + * 更新设备在线状态(0在线 1离线) + * + * @param deviceId 设备ID + * @param status 状态 + * @return 结果 + */ + int updateRuntimeStatus(Long deviceId, String status); } diff --git a/src/main/java/com/shzg/project/worn/service/IMqttSensorEventService.java b/src/main/java/com/shzg/project/worn/service/IMqttSensorEventService.java index 247531d..624e58f 100644 --- a/src/main/java/com/shzg/project/worn/service/IMqttSensorEventService.java +++ b/src/main/java/com/shzg/project/worn/service/IMqttSensorEventService.java @@ -1,6 +1,8 @@ package com.shzg.project.worn.service; import java.util.List; +import java.util.Map; + import com.shzg.project.worn.domain.MqttSensorEvent; /** @@ -58,4 +60,20 @@ public interface IMqttSensorEventService * @return 结果 */ public int deleteMqttSensorEventById(Long id); + + /** + * 告警统计 + */ + Map getAlarmStat(); + + /** + * 告警趋势(近7天) + */ + List> getAlarmTrend(); + + /** + * 告警类型占比 + */ + List> getAlarmTypeStat(); + } diff --git a/src/main/java/com/shzg/project/worn/service/impl/MqttSensorDeviceServiceImpl.java b/src/main/java/com/shzg/project/worn/service/impl/MqttSensorDeviceServiceImpl.java index e57cb61..6caf533 100644 --- a/src/main/java/com/shzg/project/worn/service/impl/MqttSensorDeviceServiceImpl.java +++ b/src/main/java/com/shzg/project/worn/service/impl/MqttSensorDeviceServiceImpl.java @@ -127,4 +127,19 @@ public class MqttSensorDeviceServiceImpl implements IMqttSensorDeviceService return mqttSensorDeviceMapper.selectByDevEui(devEui.toLowerCase()); } + + @Override + public int updateRuntimeStatus(Long deviceId, String status) + { + if (deviceId == null || status == null || status.isEmpty()) { + return 0; + } + + MqttSensorDevice update = new MqttSensorDevice(); + update.setId(deviceId); + update.setStatus(status); + update.setUpdateTime(DateUtils.getNowDate()); + + return mqttSensorDeviceMapper.updateRuntimeStatus(update); + } } diff --git a/src/main/java/com/shzg/project/worn/service/impl/MqttSensorEventServiceImpl.java b/src/main/java/com/shzg/project/worn/service/impl/MqttSensorEventServiceImpl.java index 46483c0..992a516 100644 --- a/src/main/java/com/shzg/project/worn/service/impl/MqttSensorEventServiceImpl.java +++ b/src/main/java/com/shzg/project/worn/service/impl/MqttSensorEventServiceImpl.java @@ -1,6 +1,9 @@ package com.shzg.project.worn.service.impl; +import java.util.HashMap; import java.util.List; +import java.util.Map; + import com.shzg.common.utils.DateUtils; import com.shzg.common.utils.DeptScopeUtils; import com.shzg.common.utils.SecurityUtils; @@ -17,7 +20,7 @@ import com.shzg.project.worn.service.IMqttSensorEventService; * @date 2026-04-01 */ @Service -public class MqttSensorEventServiceImpl implements IMqttSensorEventService +public class MqttSensorEventServiceImpl implements IMqttSensorEventService { @Autowired private MqttSensorEventMapper mqttSensorEventMapper; @@ -101,4 +104,41 @@ public class MqttSensorEventServiceImpl implements IMqttSensorEventService { return mqttSensorEventMapper.deleteMqttSensorEventById(id); } + + @Autowired + private MqttSensorEventMapper eventMapper; + + @Override + public Map getAlarmStat() + { + // 当前用户权限范围 + List deptIds = DeptScopeUtils.getDeptScope(); + + Map stat = eventMapper.countAlarmStat(deptIds); + + Map result = new HashMap<>(); + + result.put("today", stat == null ? 0 : stat.getOrDefault("today", 0)); + result.put("unhandled", stat == null ? 0 : stat.getOrDefault("unhandled", 0)); + + return result; + } + + @Override + public List> getAlarmTrend() + { + // 当前用户权限范围 + List deptIds = DeptScopeUtils.getDeptScope(); + + return eventMapper.selectAlarmTrend(deptIds); + } + + @Override + public List> getAlarmTypeStat() + { + // 当前用户权限范围 + List deptIds = DeptScopeUtils.getDeptScope(); + + return eventMapper.selectAlarmTypeStat(deptIds); + } } diff --git a/src/main/java/com/shzg/project/worn/service/impl/MqttSocketServiceImpl.java b/src/main/java/com/shzg/project/worn/service/impl/MqttSocketServiceImpl.java index 8c7a93e..ba0b0d4 100644 --- a/src/main/java/com/shzg/project/worn/service/impl/MqttSocketServiceImpl.java +++ b/src/main/java/com/shzg/project/worn/service/impl/MqttSocketServiceImpl.java @@ -10,6 +10,7 @@ import com.shzg.project.worn.service.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import java.util.Base64; import java.util.Date; import java.util.UUID; @@ -33,24 +34,42 @@ public class MqttSocketServiceImpl implements IMqttSocketService { // ================== 1️⃣ 查设备 ================== MqttSensorDevice device = deviceService.selectByDevEui(devEui); + if (device == null) { throw new RuntimeException("设备不存在: " + devEui); } - // ================== 2️⃣ 查Topic配置 ================== + // ================== 2️⃣ 查Topic ================== MqttTopicConfig config = topicConfigService.selectByDeptId(device.getDeptId()); + if (config == null) { throw new RuntimeException("未配置MQTT主题,deptId=" + device.getDeptId()); } - // ================== 3️⃣ 拼接topic ================== String topic = config.getTopicDownPrefix() + "/" + devEui.toLowerCase(); - // ================== 4️⃣ 构造指令 ================== - String base64 = on ? "CAEA/w==" : "CAAA/w=="; - + // ================== 3️⃣ 生成requestId ================== String requestId = UUID.randomUUID().toString(); + // ================== 4️⃣ 构造指令 ================== + String base64; + + String deviceType = device.getDeviceType(); + + if ("socket".equalsIgnoreCase(deviceType)) { + // 插座(WS513 / WS515) + base64 = on ? "CAEA/w==" : "CAAA/w=="; + + } else if ("switch".equalsIgnoreCase(deviceType)) { + // 开关(WS503) + // 控制 L1(第一路) + String hex = on ? "0811FF" : "0810FF"; + base64 = Base64.getEncoder().encodeToString(hexStringToBytes(hex)); + + } else { + throw new RuntimeException("未知设备类型: " + deviceType); + } + JSONObject json = new JSONObject(); json.put("confirmed", true); json.put("fport", 85); @@ -59,13 +78,13 @@ public class MqttSocketServiceImpl implements IMqttSocketService { String payload = json.toJSONString(); - // ================== 5️⃣ 写指令记录(发送前) ================== + // ================== 5️⃣ 记录指令 ================== MqttSensorCommand cmd = new MqttSensorCommand(); cmd.setDeviceId(device.getId()); cmd.setTopic(topic); cmd.setCommand(on ? "ON" : "OFF"); cmd.setPayload(payload); - cmd.setStatus("0"); // 0=待确认 + cmd.setStatus("0"); // 待确认 cmd.setSendTime(new Date()); cmd.setIsDelete("0"); cmd.setCreateTime(DateUtils.getNowDate()); @@ -75,9 +94,25 @@ public class MqttSocketServiceImpl implements IMqttSocketService { // ================== 6️⃣ 发送MQTT ================== mqttPublishClient.publish(topic, payload); - // ================== 7️⃣ 日志 ================== + // ================== 7️⃣ 打印日志 ================== System.out.println("[SOCKET] 指令发送 devEui=" + devEui + + ", type=" + deviceType + ", status=" + (on ? "ON" : "OFF") + ", requestId=" + requestId); } + + /** + * hex字符串转byte[] + */ + private byte[] hexStringToBytes(String hex) { + int len = hex.length(); + byte[] data = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + data[i / 2] = (byte) ( + (Character.digit(hex.charAt(i), 16) << 4) + + Character.digit(hex.charAt(i + 1), 16) + ); + } + return data; + } } \ No newline at end of file diff --git a/src/main/java/com/shzg/project/worn/unit/DeviceOfflineCheckTask.java b/src/main/java/com/shzg/project/worn/unit/DeviceOfflineCheckTask.java new file mode 100644 index 0000000..48b14e0 --- /dev/null +++ b/src/main/java/com/shzg/project/worn/unit/DeviceOfflineCheckTask.java @@ -0,0 +1,101 @@ +package com.shzg.project.worn.unit; + +import com.alibaba.fastjson2.JSONObject; +import com.shzg.project.worn.domain.MqttSensorDevice; +import com.shzg.project.worn.mapper.MqttSensorDeviceMapper; +import com.shzg.project.worn.service.IMqttSensorDeviceService; +import com.shzg.project.worn.websocket.manager.WebSocketSessionManager; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * 设备离线巡检任务 + * 规则:按设备 report_interval_minute * 3 动态判定离线 + */ +@Slf4j +@Component +public class DeviceOfflineCheckTask { + + @Autowired + private MqttSensorDeviceMapper deviceMapper; + + @Autowired + private IMqttSensorDeviceService deviceService; + + @Autowired + private MqttDeviceCache deviceCache; + + @Autowired + private WebSocketSessionManager sessionManager; + + /** + * 每5分钟巡检一次 + */ + @Scheduled(initialDelay = 60 * 1000L, fixedDelay = 5 * 60 * 1000L) + public void checkOfflineDevices() { + + List offlineDevices = deviceMapper.selectOfflineDeviceList(); + + if (offlineDevices == null || offlineDevices.isEmpty()) { + return; + } + + boolean cacheChanged = false; + + for (MqttSensorDevice device : offlineDevices) { + + // ✅ 已经是离线状态,跳过(防止重复更新) + if ("1".equals(device.getStatus())) { + continue; + } + + int rows = deviceService.updateRuntimeStatus(device.getId(), "1"); + if (rows <= 0) { + continue; + } + + cacheChanged = true; + + // 推送离线消息 + pushOfflineMessage(device); + + log.info("[DEVICE] 判定离线 deviceId={}, devEui={}, interval={}分钟", + device.getId(), + device.getDevEui(), + device.getReportIntervalMinute()); + } + + // ✅ 只有发生变化才刷新缓存 + if (cacheChanged) { + deviceCache.refresh(); + } + } + + /** + * 推送设备离线消息 + */ + private void pushOfflineMessage(MqttSensorDevice device) { + + if (device == null || device.getDeptId() == null) { + return; + } + + JSONObject msg = new JSONObject(); + msg.put("type", "device_offline"); + msg.put("deviceId", device.getId()); + msg.put("devEui", device.getDevEui()); + msg.put("deviceName", device.getDeviceName()); + msg.put("deviceType", device.getDeviceType()); + msg.put("deptId", device.getDeptId()); + msg.put("deptName", device.getDeptName()); + msg.put("deviceStatus", 1); + msg.put("statusDesc", "离线"); + msg.put("time", System.currentTimeMillis()); + + sessionManager.sendToDept(device.getDeptId(), msg.toJSONString()); + } +} \ No newline at end of file diff --git a/src/main/java/com/shzg/project/worn/unit/DeviceStatusUtil.java b/src/main/java/com/shzg/project/worn/unit/DeviceStatusUtil.java new file mode 100644 index 0000000..6b03362 --- /dev/null +++ b/src/main/java/com/shzg/project/worn/unit/DeviceStatusUtil.java @@ -0,0 +1,81 @@ +package com.shzg.project.worn.unit; + +import com.alibaba.fastjson2.JSONObject; +import com.shzg.common.utils.DateUtils; +import com.shzg.project.worn.domain.MqttSensorDevice; +import com.shzg.project.worn.service.IMqttSensorDeviceService; +import com.shzg.project.worn.unit.MqttDeviceCache; +import com.shzg.project.worn.websocket.manager.WebSocketSessionManager; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * 设备状态工具类(在线 / 离线统一处理) + */ +@Slf4j +@Component +public class DeviceStatusUtil { + + @Autowired + private IMqttSensorDeviceService deviceService; + + @Autowired + private WebSocketSessionManager sessionManager; + + @Autowired + private MqttDeviceCache deviceCache; + + /** + * 🔥 设备恢复在线(只在“有效消息”后调用) + */ + public void handleOnline(MqttSensorDevice device) { + + if (device == null || device.getId() == null) { + return; + } + + // ✅ 已经在线,直接返回 + if ("0".equals(device.getStatus())) { + return; + } + + int rows = deviceService.updateRuntimeStatus(device.getId(), "0"); + if (rows <= 0) { + return; + } + + log.info("[DEVICE] 恢复在线 deviceId={}, devEui={}", + device.getId(), device.getDevEui()); + + // 推送WebSocket + pushOnlineMessage(device); + + // 刷新缓存 + deviceCache.refresh(); + } + + /** + * 推送在线消息 + */ + private void pushOnlineMessage(MqttSensorDevice device) { + + if (device.getDeptId() == null) { + return; + } + + JSONObject msg = new JSONObject(); + msg.put("type", "device_online"); + msg.put("deviceId", device.getId()); + msg.put("devEui", device.getDevEui()); + msg.put("deviceName", device.getDeviceName()); + msg.put("deviceType", device.getDeviceType()); + msg.put("deptId", device.getDeptId()); + msg.put("deptName", device.getDeptName()); + msg.put("deviceStatus", 0); + msg.put("statusDesc", "在线"); + msg.put("time", System.currentTimeMillis()); + + sessionManager.sendToDept(device.getDeptId(), msg.toJSONString()); + } +} \ No newline at end of file diff --git a/src/main/java/com/shzg/project/worn/websocket/endpoint/WornWebSocketServer.java b/src/main/java/com/shzg/project/worn/websocket/endpoint/WornWebSocketServer.java index 479a000..70aec26 100644 --- a/src/main/java/com/shzg/project/worn/websocket/endpoint/WornWebSocketServer.java +++ b/src/main/java/com/shzg/project/worn/websocket/endpoint/WornWebSocketServer.java @@ -1,5 +1,7 @@ package com.shzg.project.worn.websocket.endpoint; +import com.shzg.framework.security.LoginUser; +import com.shzg.framework.security.service.TokenService; import com.shzg.project.system.domain.SysDept; import com.shzg.project.system.mapper.SysDeptMapper; import com.shzg.project.worn.websocket.domain.WsUserInfo; @@ -8,10 +10,15 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; -import javax.websocket.*; +import javax.websocket.CloseReason; +import javax.websocket.OnClose; +import javax.websocket.OnError; +import javax.websocket.OnOpen; +import javax.websocket.Session; import javax.websocket.server.ServerEndpoint; import java.io.IOException; import java.net.InetSocketAddress; +import java.net.URLDecoder; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -23,6 +30,7 @@ public class WornWebSocketServer { private static WebSocketSessionManager sessionManager; private static SysDeptMapper deptMapper; + private static TokenService tokenService; @Autowired public void setSessionManager(WebSocketSessionManager manager) { @@ -34,112 +42,164 @@ public class WornWebSocketServer { WornWebSocketServer.deptMapper = mapper; } + @Autowired + public void setTokenService(TokenService service) { + WornWebSocketServer.tokenService = service; + } + @OnOpen public void onOpen(Session session) { - log.info("[WebSocket] 连接建立 sessionId={}", session.getId()); String query = session.getQueryString(); - - if (query == null || !query.contains("deptId")) { - closeSession(session, "missing deptId"); + if (query == null || !query.contains("token=")) { + closeSession(session, "missing token"); return; } - Long deptId; - try { - deptId = parseDeptId(query); + // ===== 1. 解析token ===== + String token = parseToken(query); + if (token == null || token.trim().isEmpty()) { + closeSession(session, "invalid token"); + return; + } + + // ===== 2. 获取登录用户 ===== + LoginUser loginUser = tokenService.getLoginUserByToken(token); + if (loginUser == null) { + closeSession(session, "invalid token"); + return; + } + + Long userId = loginUser.getUserId(); + Long deptId = loginUser.getDeptId(); + String userName = loginUser.getUsername(); + + boolean isAdmin = false; + if (loginUser.getUser() != null) { + isAdmin = loginUser.getUser().isAdmin(); + } + + if (deptId == null) { + closeSession(session, "deptId is null"); + return; + } + + // ===== 3. 计算部门范围(🔥重点修改)===== + Set deptIds = new HashSet<>(); + + if (isAdmin) { + // 🔥 管理员:全部部门 + SysDept queryDept = new SysDept(); + List allDept = deptMapper.selectDeptList(queryDept); + if (allDept != null && !allDept.isEmpty()) { + for (SysDept d : allDept) { + if (d != null && d.getDeptId() != null) { + deptIds.add(d.getDeptId()); + } + } + } + } else { + // 普通用户:当前部门 + 子部门 + List deptList = deptMapper.selectDeptAndChildren(deptId, null); + + if (deptList != null && !deptList.isEmpty()) { + for (SysDept dept : deptList) { + if (dept != null && dept.getDeptId() != null) { + deptIds.add(dept.getDeptId()); + } + } + } else { + deptIds.add(deptId); + } + } + + // ===== 4. 构造用户信息 ===== + WsUserInfo userInfo = new WsUserInfo(); + userInfo.setUserId(userId); + userInfo.setUserName(userName); + userInfo.setAdmin(isAdmin); + userInfo.setDeptIds(deptIds); + + // ===== 5. 获取IP ===== + String ip = getIp(session); + session.getUserProperties().put("ip", ip); + + // ===== 6. 注册连接 ===== + boolean success = sessionManager.register(session, userInfo); + if (!success) { + log.warn("[WebSocket] 连接被拒绝(限流)sessionId={}, ip={}", session.getId(), ip); + closeSession(session, "too many connections"); + return; + } + + log.info("[WebSocket] 注册成功 sessionId={}, userId={}, userName={}, ip={}, deptIds={}", + session.getId(), userId, userName, ip, deptIds); + } catch (Exception e) { - closeSession(session, "invalid deptId"); - return; + log.error("[WebSocket] 连接异常 sessionId={}", session.getId(), e); + closeSession(session, "error"); } - - List deptList = deptMapper.selectDeptAndChildren(deptId, null); - - Set deptIds = new HashSet<>(); - for (SysDept dept : deptList) { - deptIds.add(dept.getDeptId()); - } - - // ===== 构造用户(当前写死)===== - WsUserInfo userInfo = new WsUserInfo(); - userInfo.setUserId(1L); - userInfo.setUserName("test"); - userInfo.setAdmin(false); - userInfo.setDeptIds(deptIds); - - // ===== 获取IP(新增)===== - String ip = getIp(session); - - // 存入 session(给 manager 用) - session.getUserProperties().put("ip", ip); - - // ===== 注册连接(关键:接入限流)===== - boolean success = sessionManager.register(session, userInfo); - - if (!success) { - log.warn("[WebSocket] 连接被拒绝(限流)sessionId={}, ip={}", session.getId(), ip); - closeSession(session, "too many connections"); - return; - } - - log.info("[WebSocket] 注册成功 sessionId={}, ip={}, deptIds={}", - session.getId(), ip, deptIds); } @OnClose public void onClose(Session session) { sessionManager.remove(session); + log.info("[WebSocket] 连接关闭 sessionId={}", session.getId()); } @OnError public void onError(Session session, Throwable error) { if (session != null) { sessionManager.remove(session); + log.error("[WebSocket] 异常 sessionId={}", session.getId(), error); + } else { + log.error("[WebSocket] 异常", error); } } - private Long parseDeptId(String query) { - + private String parseToken(String query) throws Exception { String[] params = query.split("&"); - for (String param : params) { - if (param.startsWith("deptId=")) { - return Long.parseLong(param.substring(7)); + if (param.startsWith("token=")) { + String token = param.substring(6); + token = URLDecoder.decode(token, "UTF-8"); + + if (token.startsWith("Bearer ")) { + token = token.substring(7); + } + return token; } } - - throw new IllegalArgumentException("deptId not found"); + throw new IllegalArgumentException("token not found"); } - /** - * 获取客户端IP - */ private String getIp(Session session) { - try { - // 标准方式(部分容器支持) Object addr = session.getUserProperties().get("javax.websocket.endpoint.remoteAddress"); - if (addr instanceof InetSocketAddress) { - return ((InetSocketAddress) addr).getAddress().getHostAddress(); + InetSocketAddress inetSocketAddress = (InetSocketAddress) addr; + if (inetSocketAddress.getAddress() != null) { + return inetSocketAddress.getAddress().getHostAddress(); + } } - - } catch (Exception ignored) {} - - // fallback + } catch (Exception e) { + log.warn("[WebSocket] 获取客户端IP失败 sessionId={}", session.getId(), e); + } return "unknown"; } private void closeSession(Session session, String reason) { try { - session.close(new CloseReason( - CloseReason.CloseCodes.CANNOT_ACCEPT, - reason - )); + if (session != null && session.isOpen()) { + session.close(new CloseReason( + CloseReason.CloseCodes.CANNOT_ACCEPT, + reason + )); + } } catch (IOException e) { - log.error("关闭失败", e); + log.error("[WebSocket] 关闭连接失败 sessionId={}", session != null ? session.getId() : "null", e); } } -} \ No newline at end of file +} diff --git a/src/main/resources/mybatis/system/SysDeptMapper.xml b/src/main/resources/mybatis/system/SysDeptMapper.xml index 7545619..b06dfb5 100644 --- a/src/main/resources/mybatis/system/SysDeptMapper.xml +++ b/src/main/resources/mybatis/system/SysDeptMapper.xml @@ -175,4 +175,89 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" WHERE FIND_IN_SET(#{deptId}, ancestors) - \ No newline at end of file + + + + + + + diff --git a/src/main/resources/mybatis/worn/MqttSensorDeviceMapper.xml b/src/main/resources/mybatis/worn/MqttSensorDeviceMapper.xml index 9ba4c9e..47b3431 100644 --- a/src/main/resources/mybatis/worn/MqttSensorDeviceMapper.xml +++ b/src/main/resources/mybatis/worn/MqttSensorDeviceMapper.xml @@ -1,45 +1,84 @@ + PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> + + - - - - - - - - - - - - + + + + + + + + + + + + + + + - select id, dev_eui, device_name, device_type, dept_id, status, remark, create_by, create_time, update_by, update_time, is_delete from mqtt_sensor_device + select + d.id, + d.dev_eui, + d.device_name, + d.device_type, + d.dept_id, + dept.dept_name, + d.report_interval_minute, + d.status, + d.remark, + d.create_by, + d.create_time, + d.update_by, + d.update_time, + d.is_delete + from mqtt_sensor_device d + left join sys_dept dept on d.dept_id = dept.dept_id + + + insert into mqtt_sensor_device @@ -47,6 +86,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" device_name, device_type, dept_id, + report_interval_minute, status, remark, create_by, @@ -54,12 +94,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" update_by, update_time, is_delete, - + #{devEui}, #{deviceName}, #{deviceType}, #{deptId}, + #{reportIntervalMinute}, #{status}, #{remark}, #{createBy}, @@ -67,9 +108,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" #{updateBy}, #{updateTime}, #{isDelete}, - + + update mqtt_sensor_device @@ -77,6 +119,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" device_name = #{deviceName}, device_type = #{deviceType}, dept_id = #{deptId}, + report_interval_minute = #{reportIntervalMinute}, status = #{status}, remark = #{remark}, create_by = #{createBy}, @@ -88,6 +131,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" where id = #{id} + + update mqtt_sensor_device + set status = #{status}, + update_time = #{updateTime} + where id = #{id} + + + delete from mqtt_sensor_device where id = #{id} @@ -99,10 +150,55 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + - \ No newline at end of file + + + + + + diff --git a/src/main/resources/mybatis/worn/MqttSensorEventMapper.xml b/src/main/resources/mybatis/worn/MqttSensorEventMapper.xml index 68da760..c3c7ed0 100644 --- a/src/main/resources/mybatis/worn/MqttSensorEventMapper.xml +++ b/src/main/resources/mybatis/worn/MqttSensorEventMapper.xml @@ -115,4 +115,71 @@ + + + + + + \ No newline at end of file diff --git a/src/main/resources/mybatis/worn/MqttTopicConfigMapper.xml b/src/main/resources/mybatis/worn/MqttTopicConfigMapper.xml index b9411ee..66c290b 100644 --- a/src/main/resources/mybatis/worn/MqttTopicConfigMapper.xml +++ b/src/main/resources/mybatis/worn/MqttTopicConfigMapper.xml @@ -2,6 +2,7 @@ + @@ -10,6 +11,7 @@ + @@ -21,26 +23,53 @@ - + - select id, project, warehouse, dept_id, topic_up, topic_down_prefix, - status, remark, create_by, create_time, update_by, update_time, is_delete - from mqtt_topic_config + select + t.id, + t.project, + t.warehouse, + t.dept_id, + d.dept_name, + t.topic_up, + t.topic_down_prefix, + t.status, + t.remark, + t.create_by, + t.create_time, + t.update_by, + t.update_time, + t.is_delete + from mqtt_topic_config t + left join sys_dept d on t.dept_id = d.dept_id - where id = #{id} + where t.id = #{id} - - where status = '0' - and is_delete = '0' - and topic_up is not null - and topic_up != '' + where t.status = '0' + and t.is_delete = '0' + and t.topic_up is not null + and t.topic_up != '' - + - and dept_id in + and t.dept_id in #{deptId} @@ -122,12 +151,11 @@ where id = #{id} - + delete from mqtt_topic_config where id = #{id} - delete from mqtt_topic_config where id in @@ -135,11 +163,12 @@ - - where dept_id = #{deptId} - and status = '0' - and is_delete = '0' + where t.dept_id = #{deptId} + and t.status = '0' + and t.is_delete = '0' limit 1