Merge remote-tracking branch 'origin/master'

This commit is contained in:
2026-04-20 09:12:57 +08:00
29 changed files with 1236 additions and 350 deletions

View File

@@ -3,6 +3,7 @@ package com.shzg.common.utils;
import com.shzg.common.utils.spring.SpringUtils; import com.shzg.common.utils.spring.SpringUtils;
import com.shzg.project.system.service.ISysDeptService; import com.shzg.project.system.service.ISysDeptService;
import java.util.ArrayList;
import java.util.List; import java.util.List;
/** /**
@@ -17,7 +18,21 @@ public class DeptScopeUtils
{ {
Long deptId = SecurityUtils.getDeptId(); Long deptId = SecurityUtils.getDeptId();
return SpringUtils.getBean(ISysDeptService.class) // 查询子部门
List<Long> deptIds = SpringUtils.getBean(ISysDeptService.class)
.selectDeptAndChildIds(deptId); .selectDeptAndChildIds(deptId);
// 防止返回null
if (deptIds == null)
{
deptIds = new ArrayList<>();
}
if (!deptIds.contains(deptId))
{
deptIds.add(deptId);
}
return deptIds;
} }
} }

View File

@@ -4,11 +4,7 @@ import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import javax.servlet.http.HttpServletRequest; 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.CacheConstants;
import com.shzg.common.constant.Constants; import com.shzg.common.constant.Constants;
import com.shzg.common.utils.ServletUtils; 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.common.utils.uuid.IdUtils;
import com.shzg.framework.redis.RedisCache; import com.shzg.framework.redis.RedisCache;
import com.shzg.framework.security.LoginUser; import com.shzg.framework.security.LoginUser;
import io.jsonwebtoken.Claims; import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts; import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.SignatureAlgorithm;
/** import org.slf4j.Logger;
* token验证处理 import org.slf4j.LoggerFactory;
* import org.springframework.beans.factory.annotation.Autowired;
* @author ruoyi import org.springframework.beans.factory.annotation.Value;
*/ import org.springframework.stereotype.Component;
@Component @Component
public class TokenService public class TokenService {
{
private static final Logger log = LoggerFactory.getLogger(TokenService.class); private static final Logger log = LoggerFactory.getLogger(TokenService.class);
// 令牌自定义标识
@Value("${token.header}") @Value("${token.header}")
private String header; private String header;
// 令牌秘钥
@Value("${token.secret}") @Value("${token.secret}")
private String secret; private String secret;
// 令牌有效期默认30分钟
@Value("${token.expireTime}") @Value("${token.expireTime}")
private int expireTime; private int expireTime;
protected static final long MILLIS_SECOND = 1000; protected static final long MILLIS_SECOND = 1000;
protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND; protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND;
private static final Long MILLIS_MINUTE_TWENTY = 20 * 60 * 1000L; private static final Long MILLIS_MINUTE_TWENTY = 20 * 60 * 1000L;
@Autowired @Autowired
private RedisCache redisCache; private RedisCache redisCache;
/** public LoginUser getLoginUser(HttpServletRequest request) {
* 获取用户身份信息
*
* @return 用户信息
*/
public LoginUser getLoginUser(HttpServletRequest request)
{
// 获取请求携带的令牌
String token = getToken(request); String token = getToken(request);
if (StringUtils.isNotEmpty(token)) if (StringUtils.isNotEmpty(token)) {
{ try {
try
{
Claims claims = parseToken(token); Claims claims = parseToken(token);
// 解析对应的权限以及用户信息
String uuid = (String) claims.get(Constants.LOGIN_USER_KEY); String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
String userKey = getTokenKey(uuid); String userKey = getTokenKey(uuid);
LoginUser user = redisCache.getCacheObject(userKey); return redisCache.getCacheObject(userKey);
return user; } catch (Exception e) {
}
catch (Exception e)
{
log.error("获取用户信息异常'{}'", e.getMessage()); log.error("获取用户信息异常'{}'", e.getMessage());
} }
} }
return null; return null;
} }
/** public LoginUser getLoginUserByToken(String token) {
* 设置用户身份信息
*/ if (StringUtils.isEmpty(token)) {
public void setLoginUser(LoginUser loginUser) return null;
{ }
if (StringUtils.isNotNull(loginUser) && StringUtils.isNotEmpty(loginUser.getToken()))
{ 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); refreshToken(loginUser);
} }
} }
/** public void delLoginUser(String token) {
* 删除用户身份信息 if (StringUtils.isNotEmpty(token)) {
*/
public void delLoginUser(String token)
{
if (StringUtils.isNotEmpty(token))
{
String userKey = getTokenKey(token); String userKey = getTokenKey(token);
redisCache.deleteObject(userKey); redisCache.deleteObject(userKey);
} }
} }
/** public String createToken(LoginUser loginUser) {
* 创建令牌
*
* @param loginUser 用户信息
* @return 令牌
*/
public String createToken(LoginUser loginUser)
{
String token = IdUtils.fastUUID(); String token = IdUtils.fastUUID();
loginUser.setToken(token); loginUser.setToken(token);
setUserAgent(loginUser); setUserAgent(loginUser);
@@ -121,46 +120,26 @@ public class TokenService
Map<String, Object> claims = new HashMap<>(); Map<String, Object> claims = new HashMap<>();
claims.put(Constants.LOGIN_USER_KEY, token); claims.put(Constants.LOGIN_USER_KEY, token);
claims.put(Constants.JWT_USERNAME, loginUser.getUsername()); claims.put(Constants.JWT_USERNAME, loginUser.getUsername());
return createToken(claims); return createToken(claims);
} }
/** public void verifyToken(LoginUser loginUser) {
* 验证令牌有效期相差不足20分钟自动刷新缓存
*
* @param loginUser 登录信息
* @return 令牌
*/
public void verifyToken(LoginUser loginUser)
{
long expireTime = loginUser.getExpireTime(); long expireTime = loginUser.getExpireTime();
long currentTime = System.currentTimeMillis(); long currentTime = System.currentTimeMillis();
if (expireTime - currentTime <= MILLIS_MINUTE_TWENTY) if (expireTime - currentTime <= MILLIS_MINUTE_TWENTY) {
{
refreshToken(loginUser); refreshToken(loginUser);
} }
} }
/** public void refreshToken(LoginUser loginUser) {
* 刷新令牌有效期
*
* @param loginUser 登录信息
*/
public void refreshToken(LoginUser loginUser)
{
loginUser.setLoginTime(System.currentTimeMillis()); loginUser.setLoginTime(System.currentTimeMillis());
loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE); loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
// 根据uuid将loginUser缓存
String userKey = getTokenKey(loginUser.getToken()); String userKey = getTokenKey(loginUser.getToken());
redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES); redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
} }
/** public void setUserAgent(LoginUser loginUser) {
* 设置用户代理信息
*
* @param loginUser 登录信息
*/
public void setUserAgent(LoginUser loginUser)
{
String userAgent = ServletUtils.getRequest().getHeader("User-Agent"); String userAgent = ServletUtils.getRequest().getHeader("User-Agent");
String ip = IpUtils.getIpAddr(); String ip = IpUtils.getIpAddr();
loginUser.setIpaddr(ip); loginUser.setIpaddr(ip);
@@ -169,64 +148,34 @@ public class TokenService
loginUser.setOs(UserAgentUtils.getOperatingSystem(userAgent)); loginUser.setOs(UserAgentUtils.getOperatingSystem(userAgent));
} }
/** private String createToken(Map<String, Object> claims) {
* 从数据声明生成令牌 return Jwts.builder()
*
* @param claims 数据声明
* @return 令牌
*/
private String createToken(Map<String, Object> claims)
{
String token = Jwts.builder()
.setClaims(claims) .setClaims(claims)
.signWith(SignatureAlgorithm.HS512, secret).compact(); .signWith(SignatureAlgorithm.HS512, secret)
return token; .compact();
} }
/** private Claims parseToken(String token) {
* 从令牌中获取数据声明
*
* @param token 令牌
* @return 数据声明
*/
private Claims parseToken(String token)
{
return Jwts.parser() return Jwts.parser()
.setSigningKey(secret) .setSigningKey(secret)
.parseClaimsJws(token) .parseClaimsJws(token)
.getBody(); .getBody();
} }
/** public String getUsernameFromToken(String token) {
* 从令牌中获取用户名
*
* @param token 令牌
* @return 用户名
*/
public String getUsernameFromToken(String token)
{
Claims claims = parseToken(token); Claims claims = parseToken(token);
return claims.getSubject(); return claims.getSubject();
} }
/** private String getToken(HttpServletRequest request) {
* 获取请求token
*
* @param request
* @return token
*/
private String getToken(HttpServletRequest request)
{
String token = request.getHeader(header); 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, ""); token = token.replace(Constants.TOKEN_PREFIX, "");
} }
return token; return token;
} }
private String getTokenKey(String uuid) private String getTokenKey(String uuid) {
{
return CacheConstants.LOGIN_TOKEN_KEY + uuid; return CacheConstants.LOGIN_TOKEN_KEY + uuid;
} }
} }

View File

@@ -129,4 +129,5 @@ public class SysDeptController extends BaseController
deptService.checkDeptDataScope(deptId); deptService.checkDeptDataScope(deptId);
return toAjax(deptService.deleteDeptById(deptId)); return toAjax(deptService.deleteDeptById(deptId));
} }
} }

View File

@@ -1,6 +1,8 @@
package com.shzg.project.system.mapper; package com.shzg.project.system.mapper;
import java.util.List; import java.util.List;
import java.util.Map;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
import com.shzg.project.system.domain.SysDept; import com.shzg.project.system.domain.SysDept;
@@ -132,4 +134,19 @@ public interface SysDeptMapper
* @return 部门ID集合 * @return 部门ID集合
*/ */
List<Long> selectDeptAndChildIds(Long deptId); List<Long> selectDeptAndChildIds(Long deptId);
/**
* 项目总数(按权限)
*/
int countProjectByDeptIds(@Param("deptIds") List<Long> deptIds);
/**
* 仓库总数(按权限)
*/
int countWarehouseByDeptIds(@Param("deptIds") List<Long> deptIds);
/**
* 仓库列表(按权限)
*/
List<Map<String, Object>> selectWarehouseList(@Param("deptIds") List<Long> deptIds);
} }

View File

@@ -1,6 +1,8 @@
package com.shzg.project.system.service; package com.shzg.project.system.service;
import java.util.List; import java.util.List;
import java.util.Map;
import com.shzg.framework.web.domain.TreeSelect; import com.shzg.framework.web.domain.TreeSelect;
import com.shzg.project.system.domain.SysDept; import com.shzg.project.system.domain.SysDept;
@@ -137,4 +139,20 @@ public interface ISysDeptService
* @return 部门ID列表 * @return 部门ID列表
*/ */
List<Long> selectDeptAndChildIds(Long deptId); List<Long> selectDeptAndChildIds(Long deptId);
/**
* 首页统计
*/
Map<String, Object> getHomeStat();
/**
* 设备统计
*/
Map<String, Object> getDeviceStat();
/**
* 仓库运行状态
*/
List<Map<String, Object>> selectWarehouseList();
} }

View File

@@ -1,12 +1,12 @@
package com.shzg.project.system.service.impl; package com.shzg.project.system.service.impl;
import java.util.ArrayList; import java.util.*;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import com.shzg.common.utils.DeptScopeUtils;
import com.shzg.project.system.domain.SysUser; import com.shzg.project.system.domain.SysUser;
import com.shzg.project.system.mapper.SysUserMapper; import com.shzg.project.system.mapper.SysUserMapper;
import com.shzg.project.worn.mapper.MqttSensorDeviceMapper;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import com.shzg.common.constant.UserConstants; import com.shzg.common.constant.UserConstants;
@@ -40,6 +40,10 @@ public class SysDeptServiceImpl implements ISysDeptService
@Autowired @Autowired
private SysUserMapper userMapper; private SysUserMapper userMapper;
@Autowired
private MqttSensorDeviceMapper deviceMapper;
/** /**
* 查询部门管理数据 * 查询部门管理数据
* *
@@ -360,4 +364,52 @@ public class SysDeptServiceImpl implements ISysDeptService
{ {
return deptMapper.selectDeptAndChildIds(deptId); return deptMapper.selectDeptAndChildIds(deptId);
} }
@Override
public Map<String, Object> getHomeStat()
{
Map<String, Object> map = new HashMap<>();
// 当前用户可见部门范围
List<Long> 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<String, Object> getDeviceStat()
{
Map<String, Object> map = new HashMap<>();
// 当前用户权限范围(核心)
List<Long> deptIds = DeptScopeUtils.getDeptScope();
Map<String, Object> 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<Map<String, Object>> selectWarehouseList()
{
// 当前用户权限范围
List<Long> deptIds = DeptScopeUtils.getDeptScope();
return deptMapper.selectWarehouseList(deptIds);
}
} }

View File

@@ -13,7 +13,7 @@ public class SocketController {
private IMqttSocketService mqttSocketService; private IMqttSocketService mqttSocketService;
/** /**
* 控制插座开关 * 控制智能排风插座开关
*/ */
@PostMapping("/control") @PostMapping("/control")
public AjaxResult control(@RequestParam String devEui, public AjaxResult control(@RequestParam String devEui,

View File

@@ -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<String, Object> data = deptService.getHomeStat();
return AjaxResult.success(data);
}
/**
* 设备统计(总数 / 在线 / 离线)
*/
@GetMapping("/device/stat")
public AjaxResult deviceStat()
{
Map<String, Object> data = deptService.getDeviceStat();
return AjaxResult.success(data);
}
/**
* 告警统计(今日告警 / 未处理告警)
*/
@GetMapping("/alarm/stat")
public AjaxResult alarmStat()
{
Map<String, Object> 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<Map<String, Object>> list = deptService.selectWarehouseList();
return AjaxResult.success(list);
}
}

View File

@@ -19,21 +19,29 @@ public class MqttSensorDevice extends BaseEntity
private Long id; private Long id;
/** 设备唯一标识DevEUI */ /** 设备唯一标识DevEUI */
@Excel(name = "设备唯一标识", readConverterExp = "D=evEUI") @Excel(name = "设备唯一标识")
private String devEui; private String devEui;
/** 设备名称 */ /** 设备名称 */
@Excel(name = "设备名称") @Excel(name = "设备名称")
private String deviceName; private String deviceName;
/** 设备类型smoke / env */ /** 设备类型smoke / env / socket / door 等 */
@Excel(name = "设备类型", readConverterExp = "s=moke,/=,e=nv") @Excel(name = "设备类型")
private String deviceType; private String deviceType;
/** 所属部门ID */ /** 所属部门ID */
@Excel(name = "所属部门ID") @Excel(name = "所属部门ID")
private Long deptId; private Long deptId;
/** 所属部门名称 */
@Excel(name = "所属部门名称")
private String deptName;
/** 🔥 上报周期(分钟) */
@Excel(name = "上报周期(分钟)")
private Integer reportIntervalMinute;
/** 状态0正常 1停用 */ /** 状态0正常 1停用 */
@Excel(name = "状态", readConverterExp = "0=正常,1=停用") @Excel(name = "状态", readConverterExp = "0=正常,1=停用")
private String status; private String status;
@@ -92,6 +100,26 @@ public class MqttSensorDevice extends BaseEntity
return deptId; return deptId;
} }
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) public void setStatus(String status)
{ {
this.status = status; this.status = status;
@@ -120,6 +148,8 @@ public class MqttSensorDevice extends BaseEntity
.append("deviceName", getDeviceName()) .append("deviceName", getDeviceName())
.append("deviceType", getDeviceType()) .append("deviceType", getDeviceType())
.append("deptId", getDeptId()) .append("deptId", getDeptId())
.append("deptName", getDeptName())
.append("reportIntervalMinute", getReportIntervalMinute())
.append("status", getStatus()) .append("status", getStatus())
.append("remark", getRemark()) .append("remark", getRemark())
.append("createBy", getCreateBy()) .append("createBy", getCreateBy())

View File

@@ -32,6 +32,10 @@ public class MqttTopicConfig extends BaseEntity
@Excel(name = "部门ID") @Excel(name = "部门ID")
private Long deptId; private Long deptId;
/** 所属部门名称 */
@Excel(name = "部门名称")
private String deptName;
/** 🔥 数据权限部门ID集合 */ /** 🔥 数据权限部门ID集合 */
private List<Long> deptIds; private List<Long> deptIds;
@@ -91,6 +95,16 @@ public class MqttTopicConfig extends BaseEntity
return deptId; return deptId;
} }
public void setDeptName(String deptName)
{
this.deptName = deptName;
}
public String getDeptName()
{
return deptName;
}
public void setDeptIds(List<Long> deptIds) public void setDeptIds(List<Long> deptIds)
{ {
this.deptIds = deptIds; this.deptIds = deptIds;
@@ -149,6 +163,7 @@ public class MqttTopicConfig extends BaseEntity
.append("project", getProject()) .append("project", getProject())
.append("warehouse", getWarehouse()) .append("warehouse", getWarehouse())
.append("deptId", getDeptId()) .append("deptId", getDeptId())
.append("deptName", getDeptName())
.append("deptIds", getDeptIds()) .append("deptIds", getDeptIds())
.append("topicUp", getTopicUp()) .append("topicUp", getTopicUp())
.append("topicDownPrefix", getTopicDownPrefix()) .append("topicDownPrefix", getTopicDownPrefix())

View File

@@ -1,6 +1,8 @@
package com.shzg.project.worn.mapper; package com.shzg.project.worn.mapper;
import java.util.List; import java.util.List;
import java.util.Map;
import com.shzg.project.worn.domain.MqttSensorDevice; import com.shzg.project.worn.domain.MqttSensorDevice;
import io.lettuce.core.dynamic.annotation.Param; import io.lettuce.core.dynamic.annotation.Param;
@@ -67,4 +69,23 @@ public interface MqttSensorDeviceMapper
*/ */
MqttSensorDevice selectByDevEui(@Param("devEui") String devEui); MqttSensorDevice selectByDevEui(@Param("devEui") String devEui);
/**
* 设备统计(按权限)
*/
Map<String, Object> countDeviceStat(@Param("deptIds") List<Long> deptIds);
/**
* 更新设备运行状态0在线 1离线
*
* @param device 设备
* @return 结果
*/
int updateRuntimeStatus(MqttSensorDevice device);
/**
* 查询超时离线设备
*
* @return 设备列表
*/
List<MqttSensorDevice> selectOfflineDeviceList();
} }

View File

@@ -1,7 +1,10 @@
package com.shzg.project.worn.mapper; package com.shzg.project.worn.mapper;
import java.util.List; import java.util.List;
import java.util.Map;
import com.shzg.project.worn.domain.MqttSensorEvent; import com.shzg.project.worn.domain.MqttSensorEvent;
import io.lettuce.core.dynamic.annotation.Param;
/** /**
* 设备事件Mapper接口 * 设备事件Mapper接口
@@ -58,4 +61,19 @@ public interface MqttSensorEventMapper
* @return 结果 * @return 结果
*/ */
public int deleteMqttSensorEventByIds(Long[] ids); public int deleteMqttSensorEventByIds(Long[] ids);
/**
* 告警统计(今日 / 未处理)- 按权限
*/
Map<String, Object> countAlarmStat(@Param("deptIds") List<Long> deptIds);
/**
* 告警趋势(按权限)
*/
List<Map<String, Object>> selectAlarmTrend(@Param("deptIds") List<Long> deptIds);
/**
* 告警类型占比(按权限)
*/
List<Map<String, Object>> selectAlarmTypeStat(@Param("deptIds") List<Long> deptIds);
} }

View File

@@ -9,6 +9,7 @@ import com.shzg.project.worn.domain.MqttSensorEvent;
import com.shzg.project.worn.service.IDeviceStatusService; import com.shzg.project.worn.service.IDeviceStatusService;
import com.shzg.project.worn.service.IMqttSensorDataService; import com.shzg.project.worn.service.IMqttSensorDataService;
import com.shzg.project.worn.service.IMqttSensorEventService; import com.shzg.project.worn.service.IMqttSensorEventService;
import com.shzg.project.worn.unit.DeviceStatusUtil;
import com.shzg.project.worn.websocket.manager.WebSocketSessionManager; import com.shzg.project.worn.websocket.manager.WebSocketSessionManager;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@@ -35,10 +36,12 @@ public class DoorSensorHandler {
@Autowired @Autowired
private ISysDeptService deptService; private ISysDeptService deptService;
// 状态服务Redis去重
@Autowired @Autowired
private IDeviceStatusService deviceStatusService; private IDeviceStatusService deviceStatusService;
@Autowired
private DeviceStatusUtil deviceStatusUtil;
public void handle(MqttSensorDevice device, String topic, String payload) { public void handle(MqttSensorDevice device, String topic, String payload) {
// ========================= // =========================
@@ -67,7 +70,7 @@ public class DoorSensorHandler {
devEui, device.getDeptId(), doorStatus, tamperStatus, battery); devEui, device.getDeptId(), doorStatus, tamperStatus, battery);
// ========================= // =========================
// 2⃣ 数据入库(始终入库) // 2⃣ 数据入库
// ========================= // =========================
MqttSensorData data = new MqttSensorData(); MqttSensorData data = new MqttSensorData();
data.setDeviceId(device.getId()); data.setDeviceId(device.getId());
@@ -100,32 +103,12 @@ public class DoorSensorHandler {
} }
// ========================= // =========================
// 4Redis去重核心 // 4恢复在线
// ========================= // =========================
boolean changed = deviceStatusService.isStatusChanged( deviceStatusUtil.handleOnline(device);
device.getId(),
"door",
status
);
// 状态没变化,直接返回(不产生事件、不推送)
if (!changed) {
return;
}
// ========================= // =========================
// 5事件处理(只在变化时) // 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⃣ 查询部门名称
// ========================= // =========================
String deptName = null; String deptName = null;
SysDept dept = deptService.selectDeptById(device.getDeptId()); SysDept dept = deptService.selectDeptById(device.getDeptId());
@@ -134,7 +117,7 @@ public class DoorSensorHandler {
} }
// ========================= // =========================
// 7️⃣ WebSocket推送只推变化 // 6️⃣ WebSocket推送周期上报也要推
// ========================= // =========================
JSONObject ws = new JSONObject(); JSONObject ws = new JSONObject();
ws.put("type", "door"); ws.put("type", "door");
@@ -150,6 +133,30 @@ public class DoorSensorHandler {
sessionManager.sendToDept(device.getDeptId(), ws.toJSONString()); 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) { } catch (Exception e) {
log.error("[DOOR] 处理异常 payload={}", payload, e); log.error("[DOOR] 处理异常 payload={}", payload, e);
} }

View File

@@ -5,6 +5,7 @@ import com.shzg.project.system.domain.SysDept;
import com.shzg.project.system.service.ISysDeptService; import com.shzg.project.system.service.ISysDeptService;
import com.shzg.project.worn.domain.*; import com.shzg.project.worn.domain.*;
import com.shzg.project.worn.service.*; import com.shzg.project.worn.service.*;
import com.shzg.project.worn.unit.DeviceStatusUtil;
import com.shzg.project.worn.websocket.manager.WebSocketSessionManager; import com.shzg.project.worn.websocket.manager.WebSocketSessionManager;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@@ -35,10 +36,13 @@ public class EnvSensorHandler {
@Autowired @Autowired
private ISysDeptService deptService; private ISysDeptService deptService;
// 状态服务Redis
@Autowired @Autowired
private IDeviceStatusService deviceStatusService; private IDeviceStatusService deviceStatusService;
// ✅ 新增
@Autowired
private DeviceStatusUtil deviceStatusUtil;
public void handle(MqttSensorDevice device, String topic, String payload) { public void handle(MqttSensorDevice device, String topic, String payload) {
if (device == null) { if (device == null) {
@@ -59,13 +63,24 @@ public class EnvSensorHandler {
TopicInfo topicInfo = parseTopic(topic); TopicInfo topicInfo = parseTopic(topic);
// =========================
// 1⃣ 入库 // 1⃣ 入库
// =========================
saveData(device, topic, payload, topicInfo, v); saveData(device, topic, payload, topicInfo, v);
// 2⃣ 推送(周期数据必须推) // =========================
// 2⃣ 恢复在线
// =========================
deviceStatusUtil.handleOnline(device);
// =========================
// 3⃣ 推送(周期数据必须推)
// =========================
pushWebSocket(device, v); pushWebSocket(device, v);
// 3⃣ 事件检测(带去重) // =========================
// 4⃣ 事件检测(带去重)
// =========================
handleEvent(device, v); handleEvent(device, v);
} }
@@ -143,9 +158,6 @@ public class EnvSensorHandler {
return "normal"; return "normal";
} }
/**
* 使用Redis去重
*/
private void changeStatus(MqttSensorDevice device, private void changeStatus(MqttSensorDevice device,
String metric, String metric,
String newStatus, String newStatus,

View File

@@ -9,6 +9,7 @@ import com.shzg.project.worn.domain.MqttSensorEvent;
import com.shzg.project.worn.service.IDeviceStatusService; import com.shzg.project.worn.service.IDeviceStatusService;
import com.shzg.project.worn.service.IMqttSensorDataService; import com.shzg.project.worn.service.IMqttSensorDataService;
import com.shzg.project.worn.service.IMqttSensorEventService; import com.shzg.project.worn.service.IMqttSensorEventService;
import com.shzg.project.worn.unit.DeviceStatusUtil;
import com.shzg.project.worn.websocket.manager.WebSocketSessionManager; import com.shzg.project.worn.websocket.manager.WebSocketSessionManager;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@@ -35,10 +36,12 @@ public class SmartSocketHandler {
@Autowired @Autowired
private ISysDeptService deptService; private ISysDeptService deptService;
// 状态去重服务
@Autowired @Autowired
private IDeviceStatusService deviceStatusService; private IDeviceStatusService deviceStatusService;
@Autowired
private DeviceStatusUtil deviceStatusUtil;
public void handle(MqttSensorDevice device, String topic, String payload) { public void handle(MqttSensorDevice device, String topic, String payload) {
// ================== 基础校验 ================== // ================== 基础校验 ==================
@@ -82,7 +85,7 @@ public class SmartSocketHandler {
return; return;
} }
// ================== 入库(始终入库 ================== // ================== 入库(有效数据 ==================
MqttSensorData data = new MqttSensorData(); MqttSensorData data = new MqttSensorData();
data.setDeviceId(device.getId()); data.setDeviceId(device.getId());
data.setDeptId(device.getDeptId()); data.setDeptId(device.getDeptId());
@@ -95,6 +98,9 @@ public class SmartSocketHandler {
dataService.insertMqttSensorData(data); dataService.insertMqttSensorData(data);
// ================== 恢复在线 ==================
deviceStatusUtil.handleOnline(device);
// ================== 状态字符串 ================== // ================== 状态字符串 ==================
String statusStr = (switchStatus == 1 ? "on" : "off"); String statusStr = (switchStatus == 1 ? "on" : "off");
@@ -105,11 +111,6 @@ public class SmartSocketHandler {
statusStr statusStr
); );
// 没变化直接返回(不写事件、不推送)
if (!changed) {
return;
}
// ================== 查询部门 ================== // ================== 查询部门 ==================
String deptName = null; String deptName = null;
SysDept dept = deptService.selectDeptById(device.getDeptId()); SysDept dept = deptService.selectDeptById(device.getDeptId());
@@ -117,7 +118,7 @@ public class SmartSocketHandler {
deptName = dept.getDeptName(); deptName = dept.getDeptName();
} }
// ================== WebSocket推送(只推变化) ================== // ================== WebSocket推送 ==================
try { try {
JSONObject msg = new JSONObject(); JSONObject msg = new JSONObject();
msg.put("type", "socket"); msg.put("type", "socket");
@@ -135,6 +136,10 @@ public class SmartSocketHandler {
} }
// ================== 事件记录(只在变化时) ================== // ================== 事件记录(只在变化时) ==================
if (!changed) {
return;
}
MqttSensorEvent event = new MqttSensorEvent(); MqttSensorEvent event = new MqttSensorEvent();
event.setDeviceId(device.getId()); event.setDeviceId(device.getId());
event.setDeptId(device.getDeptId()); event.setDeptId(device.getDeptId());

View File

@@ -9,6 +9,7 @@ import com.shzg.project.worn.domain.MqttSensorEvent;
import com.shzg.project.worn.service.IDeviceStatusService; import com.shzg.project.worn.service.IDeviceStatusService;
import com.shzg.project.worn.service.IMqttSensorDataService; import com.shzg.project.worn.service.IMqttSensorDataService;
import com.shzg.project.worn.service.IMqttSensorEventService; import com.shzg.project.worn.service.IMqttSensorEventService;
import com.shzg.project.worn.unit.DeviceStatusUtil;
import com.shzg.project.worn.websocket.manager.WebSocketSessionManager; import com.shzg.project.worn.websocket.manager.WebSocketSessionManager;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@@ -18,7 +19,7 @@ import java.math.BigDecimal;
import java.util.Date; import java.util.Date;
/** /**
* 烟雾传感器 Handler * 烟雾传感器 Handler(最终生产版)
*/ */
@Slf4j @Slf4j
@Component @Component
@@ -39,6 +40,9 @@ public class SmokeSensorHandler {
@Autowired @Autowired
private IDeviceStatusService deviceStatusService; private IDeviceStatusService deviceStatusService;
@Autowired
private DeviceStatusUtil deviceStatusUtil;
public void handle(MqttSensorDevice device, String topic, String payload) { public void handle(MqttSensorDevice device, String topic, String payload) {
if (device == null) { if (device == null) {
@@ -85,7 +89,7 @@ public class SmokeSensorHandler {
deptName = dept.getDeptName(); deptName = dept.getDeptName();
} }
// ================== 数据入库(始终入库) ================== // ================== 数据入库 ==================
MqttSensorData data = new MqttSensorData(); MqttSensorData data = new MqttSensorData();
data.setDeviceId(device.getId()); data.setDeviceId(device.getId());
data.setDeptId(device.getDeptId()); data.setDeptId(device.getDeptId());
@@ -106,7 +110,10 @@ public class SmokeSensorHandler {
dataService.insertMqttSensorData(data); dataService.insertMqttSensorData(data);
// ================== 先判断状态 ================== // ================== 🔥 恢复在线 ==================
deviceStatusUtil.handleOnline(device);
// ================== 状态判断 ==================
String newStatus; String newStatus;
String eventType; String eventType;
String desc; String desc;
@@ -147,10 +154,8 @@ public class SmokeSensorHandler {
); );
// ================== WebSocket推送 ================== // ================== WebSocket推送 ==================
// 烟感这里建议周期数据继续推,事件去重即可
pushWebSocket(device, deptName, event, battery, concentration, temperature, newStatus); pushWebSocket(device, deptName, event, battery, concentration, temperature, newStatus);
// 状态没变化,不重复写事件
if (!changed) { if (!changed) {
return; return;
} }
@@ -158,13 +163,18 @@ public class SmokeSensorHandler {
// ================== 事件入库 ================== // ================== 事件入库 ==================
insertEvent(device, eventType, desc, level); 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={}", log.info("[SMOKE] 状态变化 deviceId={}, status={}, eventType={}",
device.getId(), newStatus, eventType); device.getId(), newStatus, eventType);
} }
/**
* WebSocket推送
*/
private void pushWebSocket(MqttSensorDevice device, private void pushWebSocket(MqttSensorDevice device,
String deptName, String deptName,
String event, String event,
@@ -197,9 +207,6 @@ public class SmokeSensorHandler {
} }
} }
/**
* 事件入库
*/
private void insertEvent(MqttSensorDevice device, private void insertEvent(MqttSensorDevice device,
String type, String type,
String desc, String desc,

View File

@@ -9,6 +9,7 @@ import com.shzg.project.worn.domain.MqttSensorEvent;
import com.shzg.project.worn.service.IDeviceStatusService; import com.shzg.project.worn.service.IDeviceStatusService;
import com.shzg.project.worn.service.IMqttSensorDataService; import com.shzg.project.worn.service.IMqttSensorDataService;
import com.shzg.project.worn.service.IMqttSensorEventService; import com.shzg.project.worn.service.IMqttSensorEventService;
import com.shzg.project.worn.unit.DeviceStatusUtil;
import com.shzg.project.worn.websocket.manager.WebSocketSessionManager; import com.shzg.project.worn.websocket.manager.WebSocketSessionManager;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@@ -33,10 +34,12 @@ public class WaterSensorHandler {
@Autowired @Autowired
private ISysDeptService deptService; private ISysDeptService deptService;
// Redis状态去重
@Autowired @Autowired
private IDeviceStatusService deviceStatusService; private IDeviceStatusService deviceStatusService;
@Autowired
private DeviceStatusUtil deviceStatusUtil;
public void handle(MqttSensorDevice device, String topic, String payload) { public void handle(MqttSensorDevice device, String topic, String payload) {
if (device == null) return; if (device == null) return;
@@ -57,15 +60,18 @@ public class WaterSensorHandler {
deptName = dept.getDeptName(); deptName = dept.getDeptName();
} }
// ================== 入库(始终入库) ================== // ================== 入库 ==================
saveData(device, topic, payload, topicInfo, info); saveData(device, topic, payload, topicInfo, info);
// ================== 恢复在线 ==================
deviceStatusUtil.handleOnline(device);
// ================== 状态计算 ================== // ================== 状态计算 ==================
String status = (info.getWater() != null && info.getWater() == 1) String status = (info.getWater() != null && info.getWater() == 1)
? "alarm" ? "alarm"
: "normal"; : "normal";
// ================== WebSocket(周期数据推送) ================== // ================== WebSocket ==================
pushWebSocket(device, deptName, info, status); pushWebSocket(device, deptName, info, status);
// ================== Redis去重 ================== // ================== Redis去重 ==================
@@ -89,7 +95,6 @@ public class WaterSensorHandler {
log.info("[WATER] 状态变化 deviceId={}, status={}", device.getId(), status); log.info("[WATER] 状态变化 deviceId={}, status={}", device.getId(), status);
} }
// ================== WebSocket ================== // ================== WebSocket ==================
private void pushWebSocket(MqttSensorDevice device, private void pushWebSocket(MqttSensorDevice device,
String deptName, String deptName,
@@ -112,8 +117,7 @@ public class WaterSensorHandler {
sessionManager.sendToDept(device.getDeptId(), msg.toJSONString()); sessionManager.sendToDept(device.getDeptId(), msg.toJSONString());
} }
// ================== 事件 ==================
// ================== 事件写入 ==================
private void triggerEvent(MqttSensorDevice device, private void triggerEvent(MqttSensorDevice device,
String type, String type,
String level, String level,
@@ -132,7 +136,6 @@ public class WaterSensorHandler {
eventService.insertMqttSensorEvent(event); eventService.insertMqttSensorEvent(event);
// 推送告警
JSONObject msg = new JSONObject(); JSONObject msg = new JSONObject();
msg.put("type", "alarm"); msg.put("type", "alarm");
msg.put("deviceId", device.getId()); msg.put("deviceId", device.getId());
@@ -148,7 +151,6 @@ public class WaterSensorHandler {
sessionManager.sendToDept(device.getDeptId(), msg.toJSONString()); sessionManager.sendToDept(device.getDeptId(), msg.toJSONString());
} }
// ================== 入库 ================== // ================== 入库 ==================
private void saveData(MqttSensorDevice device, private void saveData(MqttSensorDevice device,
String topic, String topic,
@@ -174,7 +176,6 @@ public class WaterSensorHandler {
sensorDataService.insertMqttSensorData(data); sensorDataService.insertMqttSensorData(data);
} }
// ================== 工具 ================== // ================== 工具 ==================
private JSONObject parseJson(String payload) { private JSONObject parseJson(String payload) {
try { try {
@@ -230,7 +231,6 @@ public class WaterSensorHandler {
} }
} }
private static class TopicInfo { private static class TopicInfo {
String project; String project;
String warehouse; String warehouse;

View File

@@ -65,4 +65,13 @@ public interface IMqttSensorDeviceService
* @return * @return
*/ */
MqttSensorDevice selectByDevEui(String devEui); MqttSensorDevice selectByDevEui(String devEui);
/**
* 更新设备在线状态0在线 1离线
*
* @param deviceId 设备ID
* @param status 状态
* @return 结果
*/
int updateRuntimeStatus(Long deviceId, String status);
} }

View File

@@ -1,6 +1,8 @@
package com.shzg.project.worn.service; package com.shzg.project.worn.service;
import java.util.List; import java.util.List;
import java.util.Map;
import com.shzg.project.worn.domain.MqttSensorEvent; import com.shzg.project.worn.domain.MqttSensorEvent;
/** /**
@@ -58,4 +60,20 @@ public interface IMqttSensorEventService
* @return 结果 * @return 结果
*/ */
public int deleteMqttSensorEventById(Long id); public int deleteMqttSensorEventById(Long id);
/**
* 告警统计
*/
Map<String, Object> getAlarmStat();
/**
* 告警趋势近7天
*/
List<Map<String, Object>> getAlarmTrend();
/**
* 告警类型占比
*/
List<Map<String, Object>> getAlarmTypeStat();
} }

View File

@@ -127,4 +127,19 @@ public class MqttSensorDeviceServiceImpl implements IMqttSensorDeviceService
return mqttSensorDeviceMapper.selectByDevEui(devEui.toLowerCase()); 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);
}
} }

View File

@@ -1,6 +1,9 @@
package com.shzg.project.worn.service.impl; package com.shzg.project.worn.service.impl;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import com.shzg.common.utils.DateUtils; import com.shzg.common.utils.DateUtils;
import com.shzg.common.utils.DeptScopeUtils; import com.shzg.common.utils.DeptScopeUtils;
import com.shzg.common.utils.SecurityUtils; import com.shzg.common.utils.SecurityUtils;
@@ -101,4 +104,41 @@ public class MqttSensorEventServiceImpl implements IMqttSensorEventService
{ {
return mqttSensorEventMapper.deleteMqttSensorEventById(id); return mqttSensorEventMapper.deleteMqttSensorEventById(id);
} }
@Autowired
private MqttSensorEventMapper eventMapper;
@Override
public Map<String, Object> getAlarmStat()
{
// 当前用户权限范围
List<Long> deptIds = DeptScopeUtils.getDeptScope();
Map<String, Object> stat = eventMapper.countAlarmStat(deptIds);
Map<String, Object> 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<Map<String, Object>> getAlarmTrend()
{
// 当前用户权限范围
List<Long> deptIds = DeptScopeUtils.getDeptScope();
return eventMapper.selectAlarmTrend(deptIds);
}
@Override
public List<Map<String, Object>> getAlarmTypeStat()
{
// 当前用户权限范围
List<Long> deptIds = DeptScopeUtils.getDeptScope();
return eventMapper.selectAlarmTypeStat(deptIds);
}
} }

View File

@@ -10,6 +10,7 @@ import com.shzg.project.worn.service.*;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.Base64;
import java.util.Date; import java.util.Date;
import java.util.UUID; import java.util.UUID;
@@ -33,24 +34,42 @@ public class MqttSocketServiceImpl implements IMqttSocketService {
// ================== 1⃣ 查设备 ================== // ================== 1⃣ 查设备 ==================
MqttSensorDevice device = deviceService.selectByDevEui(devEui); MqttSensorDevice device = deviceService.selectByDevEui(devEui);
if (device == null) { if (device == null) {
throw new RuntimeException("设备不存在: " + devEui); throw new RuntimeException("设备不存在: " + devEui);
} }
// ================== 2⃣ 查Topic配置 ================== // ================== 2⃣ 查Topic ==================
MqttTopicConfig config = topicConfigService.selectByDeptId(device.getDeptId()); MqttTopicConfig config = topicConfigService.selectByDeptId(device.getDeptId());
if (config == null) { if (config == null) {
throw new RuntimeException("未配置MQTT主题deptId=" + device.getDeptId()); throw new RuntimeException("未配置MQTT主题deptId=" + device.getDeptId());
} }
// ================== 3⃣ 拼接topic ==================
String topic = config.getTopicDownPrefix() + "/" + devEui.toLowerCase(); String topic = config.getTopicDownPrefix() + "/" + devEui.toLowerCase();
// ================== 4️⃣ 构造指令 ================== // ================== 3️⃣ 生成requestId ==================
String base64 = on ? "CAEA/w==" : "CAAA/w==";
String requestId = UUID.randomUUID().toString(); 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(); JSONObject json = new JSONObject();
json.put("confirmed", true); json.put("confirmed", true);
json.put("fport", 85); json.put("fport", 85);
@@ -59,13 +78,13 @@ public class MqttSocketServiceImpl implements IMqttSocketService {
String payload = json.toJSONString(); String payload = json.toJSONString();
// ================== 5写指令记录(发送前) ================== // ================== 5记录指令 ==================
MqttSensorCommand cmd = new MqttSensorCommand(); MqttSensorCommand cmd = new MqttSensorCommand();
cmd.setDeviceId(device.getId()); cmd.setDeviceId(device.getId());
cmd.setTopic(topic); cmd.setTopic(topic);
cmd.setCommand(on ? "ON" : "OFF"); cmd.setCommand(on ? "ON" : "OFF");
cmd.setPayload(payload); cmd.setPayload(payload);
cmd.setStatus("0"); // 0=待确认 cmd.setStatus("0"); // 待确认
cmd.setSendTime(new Date()); cmd.setSendTime(new Date());
cmd.setIsDelete("0"); cmd.setIsDelete("0");
cmd.setCreateTime(DateUtils.getNowDate()); cmd.setCreateTime(DateUtils.getNowDate());
@@ -75,9 +94,25 @@ public class MqttSocketServiceImpl implements IMqttSocketService {
// ================== 6⃣ 发送MQTT ================== // ================== 6⃣ 发送MQTT ==================
mqttPublishClient.publish(topic, payload); mqttPublishClient.publish(topic, payload);
// ================== 7⃣ 日志 ================== // ================== 7打印日志 ==================
System.out.println("[SOCKET] 指令发送 devEui=" + devEui + System.out.println("[SOCKET] 指令发送 devEui=" + devEui +
", type=" + deviceType +
", status=" + (on ? "ON" : "OFF") + ", status=" + (on ? "ON" : "OFF") +
", requestId=" + requestId); ", 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;
}
} }

View File

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

View File

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

View File

@@ -1,5 +1,7 @@
package com.shzg.project.worn.websocket.endpoint; 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.domain.SysDept;
import com.shzg.project.system.mapper.SysDeptMapper; import com.shzg.project.system.mapper.SysDeptMapper;
import com.shzg.project.worn.websocket.domain.WsUserInfo; 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.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; 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 javax.websocket.server.ServerEndpoint;
import java.io.IOException; import java.io.IOException;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.net.URLDecoder;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
@@ -23,6 +30,7 @@ public class WornWebSocketServer {
private static WebSocketSessionManager sessionManager; private static WebSocketSessionManager sessionManager;
private static SysDeptMapper deptMapper; private static SysDeptMapper deptMapper;
private static TokenService tokenService;
@Autowired @Autowired
public void setSessionManager(WebSocketSessionManager manager) { public void setSessionManager(WebSocketSessionManager manager) {
@@ -34,112 +42,164 @@ public class WornWebSocketServer {
WornWebSocketServer.deptMapper = mapper; WornWebSocketServer.deptMapper = mapper;
} }
@Autowired
public void setTokenService(TokenService service) {
WornWebSocketServer.tokenService = service;
}
@OnOpen @OnOpen
public void onOpen(Session session) { public void onOpen(Session session) {
log.info("[WebSocket] 连接建立 sessionId={}", session.getId()); log.info("[WebSocket] 连接建立 sessionId={}", session.getId());
String query = session.getQueryString(); String query = session.getQueryString();
if (query == null || !query.contains("token=")) {
if (query == null || !query.contains("deptId")) { closeSession(session, "missing token");
closeSession(session, "missing deptId");
return; return;
} }
Long deptId;
try { try {
deptId = parseDeptId(query); // ===== 1. 解析token =====
} catch (Exception e) { String token = parseToken(query);
closeSession(session, "invalid deptId"); if (token == null || token.trim().isEmpty()) {
closeSession(session, "invalid token");
return; return;
} }
List<SysDept> deptList = deptMapper.selectDeptAndChildren(deptId, null); // ===== 2. 获取登录用户 =====
LoginUser loginUser = tokenService.getLoginUserByToken(token);
Set<Long> deptIds = new HashSet<>(); if (loginUser == null) {
for (SysDept dept : deptList) { closeSession(session, "invalid token");
deptIds.add(dept.getDeptId()); 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<Long> deptIds = new HashSet<>();
if (isAdmin) {
// 🔥 管理员:全部部门
SysDept queryDept = new SysDept();
List<SysDept> 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<SysDept> 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(); WsUserInfo userInfo = new WsUserInfo();
userInfo.setUserId(1L); userInfo.setUserId(userId);
userInfo.setUserName("test"); userInfo.setUserName(userName);
userInfo.setAdmin(false); userInfo.setAdmin(isAdmin);
userInfo.setDeptIds(deptIds); userInfo.setDeptIds(deptIds);
// ===== 获取IP(新增)===== // ===== 5. 获取IP =====
String ip = getIp(session); String ip = getIp(session);
// 存入 session给 manager 用)
session.getUserProperties().put("ip", ip); session.getUserProperties().put("ip", ip);
// ===== 注册连接(关键:接入限流)===== // ===== 6. 注册连接 =====
boolean success = sessionManager.register(session, userInfo); boolean success = sessionManager.register(session, userInfo);
if (!success) { if (!success) {
log.warn("[WebSocket] 连接被拒绝限流sessionId={}, ip={}", session.getId(), ip); log.warn("[WebSocket] 连接被拒绝限流sessionId={}, ip={}", session.getId(), ip);
closeSession(session, "too many connections"); closeSession(session, "too many connections");
return; return;
} }
log.info("[WebSocket] 注册成功 sessionId={}, ip={}, deptIds={}", log.info("[WebSocket] 注册成功 sessionId={}, userId={}, userName={}, ip={}, deptIds={}",
session.getId(), ip, deptIds); session.getId(), userId, userName, ip, deptIds);
} catch (Exception e) {
log.error("[WebSocket] 连接异常 sessionId={}", session.getId(), e);
closeSession(session, "error");
}
} }
@OnClose @OnClose
public void onClose(Session session) { public void onClose(Session session) {
sessionManager.remove(session); sessionManager.remove(session);
log.info("[WebSocket] 连接关闭 sessionId={}", session.getId());
} }
@OnError @OnError
public void onError(Session session, Throwable error) { public void onError(Session session, Throwable error) {
if (session != null) { if (session != null) {
sessionManager.remove(session); 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("&"); String[] params = query.split("&");
for (String param : params) { for (String param : params) {
if (param.startsWith("deptId=")) { if (param.startsWith("token=")) {
return Long.parseLong(param.substring(7)); String token = param.substring(6);
token = URLDecoder.decode(token, "UTF-8");
if (token.startsWith("Bearer ")) {
token = token.substring(7);
} }
return token;
}
}
throw new IllegalArgumentException("token not found");
} }
throw new IllegalArgumentException("deptId not found");
}
/**
* 获取客户端IP
*/
private String getIp(Session session) { private String getIp(Session session) {
try { try {
// 标准方式(部分容器支持)
Object addr = session.getUserProperties().get("javax.websocket.endpoint.remoteAddress"); Object addr = session.getUserProperties().get("javax.websocket.endpoint.remoteAddress");
if (addr instanceof InetSocketAddress) { if (addr instanceof InetSocketAddress) {
return ((InetSocketAddress) addr).getAddress().getHostAddress(); InetSocketAddress inetSocketAddress = (InetSocketAddress) addr;
if (inetSocketAddress.getAddress() != null) {
return inetSocketAddress.getAddress().getHostAddress();
}
}
} catch (Exception e) {
log.warn("[WebSocket] 获取客户端IP失败 sessionId={}", session.getId(), e);
} }
} catch (Exception ignored) {}
// fallback
return "unknown"; return "unknown";
} }
private void closeSession(Session session, String reason) { private void closeSession(Session session, String reason) {
try { try {
if (session != null && session.isOpen()) {
session.close(new CloseReason( session.close(new CloseReason(
CloseReason.CloseCodes.CANNOT_ACCEPT, CloseReason.CloseCodes.CANNOT_ACCEPT,
reason reason
)); ));
}
} catch (IOException e) { } catch (IOException e) {
log.error("关闭失败", e); log.error("[WebSocket] 关闭连接失败 sessionId={}", session != null ? session.getId() : "null", e);
} }
} }
} }

View File

@@ -175,4 +175,89 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
WHERE FIND_IN_SET(#{deptId}, ancestors) WHERE FIND_IN_SET(#{deptId}, ancestors)
</select> </select>
<select id="countProjectByDeptIds" resultType="int">
SELECT COUNT(1)
FROM sys_dept d
WHERE d.del_flag = '0'
AND d.status = '0'
AND d.parent_id != 0
AND EXISTS (
SELECT 1 FROM sys_dept c
WHERE c.parent_id = d.dept_id
AND c.del_flag = '0'
)
<!-- ✅ 权限范围 -->
AND d.dept_id IN
<foreach collection="deptIds" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>
<select id="countWarehouseByDeptIds" resultType="int">
SELECT COUNT(1)
FROM sys_dept d
WHERE d.del_flag = '0'
AND d.status = '0'
AND NOT EXISTS (
SELECT 1 FROM sys_dept c
WHERE c.parent_id = d.dept_id
AND c.del_flag = '0'
)
AND d.dept_id IN
<foreach collection="deptIds" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>
<select id="selectWarehouseList" resultType="map">
SELECT
w.dept_id AS deptId,
w.dept_name AS warehouseName,
/* 上级(项目) */
p.dept_name AS parentName,
/* 在线率 */
CASE
WHEN COUNT(d.id) = 0 THEN 0
ELSE ROUND(
SUM(CASE WHEN d.status = 0 THEN 1 ELSE 0 END) * 1.0 / COUNT(d.id) * 100,
1
)
END AS onlineRate
FROM sys_dept w
/* 上级 */
LEFT JOIN sys_dept p
ON w.parent_id = p.dept_id
/* 设备 */
LEFT JOIN mqtt_sensor_device d
ON d.dept_id = w.dept_id
AND d.is_delete = 0
WHERE w.del_flag = '0'
/* ✅ 只查仓库(叶子节点) */
AND NOT EXISTS (
SELECT 1 FROM sys_dept c
WHERE c.parent_id = w.dept_id
AND c.del_flag = '0'
)
/* ✅ 权限控制(核心) */
AND w.dept_id IN
<foreach collection="deptIds" item="id" open="(" separator="," close=")">
#{id}
</foreach>
GROUP BY w.dept_id
ORDER BY w.dept_id DESC
</select>
</mapper> </mapper>

View File

@@ -2,14 +2,18 @@
<!DOCTYPE mapper <!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd"> "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.shzg.project.worn.mapper.MqttSensorDeviceMapper"> <mapper namespace="com.shzg.project.worn.mapper.MqttSensorDeviceMapper">
<!-- ================= resultMap ================= -->
<resultMap type="MqttSensorDevice" id="MqttSensorDeviceResult"> <resultMap type="MqttSensorDevice" id="MqttSensorDeviceResult">
<result property="id" column="id"/> <result property="id" column="id"/>
<result property="devEui" column="dev_eui"/> <result property="devEui" column="dev_eui"/>
<result property="deviceName" column="device_name"/> <result property="deviceName" column="device_name"/>
<result property="deviceType" column="device_type"/> <result property="deviceType" column="device_type"/>
<result property="deptId" column="dept_id"/> <result property="deptId" column="dept_id"/>
<result property="deptName" column="dept_name"/>
<result property="reportIntervalMinute" column="report_interval_minute"/>
<result property="status" column="status"/> <result property="status" column="status"/>
<result property="remark" column="remark"/> <result property="remark" column="remark"/>
<result property="createBy" column="create_by"/> <result property="createBy" column="create_by"/>
@@ -19,27 +23,62 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="isDelete" column="is_delete"/> <result property="isDelete" column="is_delete"/>
</resultMap> </resultMap>
<!-- ================= 公共查询SQL ================= -->
<sql id="selectMqttSensorDeviceVo"> <sql id="selectMqttSensorDeviceVo">
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
</sql> </sql>
<!-- ================= 查询列表 ================= -->
<select id="selectMqttSensorDeviceList" parameterType="MqttSensorDevice" resultMap="MqttSensorDeviceResult"> <select id="selectMqttSensorDeviceList" parameterType="MqttSensorDevice" resultMap="MqttSensorDeviceResult">
<include refid="selectMqttSensorDeviceVo"/> <include refid="selectMqttSensorDeviceVo"/>
<where> <where>
<if test="devEui != null and devEui != ''"> and dev_eui = #{devEui}</if> <if test="devEui != null and devEui != ''">
<if test="deviceName != null and deviceName != ''"> and device_name like concat('%', #{deviceName}, '%')</if> and d.dev_eui = #{devEui}
<if test="deviceType != null and deviceType != ''"> and device_type = #{deviceType}</if> </if>
<if test="deptId != null "> and dept_id = #{deptId}</if> <if test="deviceName != null and deviceName != ''">
<if test="status != null and status != ''"> and status = #{status}</if> and d.device_name like concat('%', #{deviceName}, '%')
<if test="isDelete != null and isDelete != ''"> and is_delete = #{isDelete}</if> </if>
<if test="deviceType != null and deviceType != ''">
and d.device_type = #{deviceType}
</if>
<if test="deptId != null">
and d.dept_id = #{deptId}
</if>
<if test="reportIntervalMinute != null">
and d.report_interval_minute = #{reportIntervalMinute}
</if>
<if test="status != null and status != ''">
and d.status = #{status}
</if>
<if test="isDelete != null and isDelete != ''">
and d.is_delete = #{isDelete}
</if>
</where> </where>
</select> </select>
<!-- ================= 根据ID查询 ================= -->
<select id="selectMqttSensorDeviceById" parameterType="Long" resultMap="MqttSensorDeviceResult"> <select id="selectMqttSensorDeviceById" parameterType="Long" resultMap="MqttSensorDeviceResult">
<include refid="selectMqttSensorDeviceVo"/> <include refid="selectMqttSensorDeviceVo"/>
where id = #{id} where d.id = #{id}
</select> </select>
<!-- ================= 新增 ================= -->
<insert id="insertMqttSensorDevice" parameterType="MqttSensorDevice" useGeneratedKeys="true" keyProperty="id"> <insert id="insertMqttSensorDevice" parameterType="MqttSensorDevice" useGeneratedKeys="true" keyProperty="id">
insert into mqtt_sensor_device insert into mqtt_sensor_device
<trim prefix="(" suffix=")" suffixOverrides=","> <trim prefix="(" suffix=")" suffixOverrides=",">
@@ -47,6 +86,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="deviceName != null">device_name,</if> <if test="deviceName != null">device_name,</if>
<if test="deviceType != null">device_type,</if> <if test="deviceType != null">device_type,</if>
<if test="deptId != null">dept_id,</if> <if test="deptId != null">dept_id,</if>
<if test="reportIntervalMinute != null">report_interval_minute,</if>
<if test="status != null">status,</if> <if test="status != null">status,</if>
<if test="remark != null">remark,</if> <if test="remark != null">remark,</if>
<if test="createBy != null">create_by,</if> <if test="createBy != null">create_by,</if>
@@ -60,6 +100,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="deviceName != null">#{deviceName},</if> <if test="deviceName != null">#{deviceName},</if>
<if test="deviceType != null">#{deviceType},</if> <if test="deviceType != null">#{deviceType},</if>
<if test="deptId != null">#{deptId},</if> <if test="deptId != null">#{deptId},</if>
<if test="reportIntervalMinute != null">#{reportIntervalMinute},</if>
<if test="status != null">#{status},</if> <if test="status != null">#{status},</if>
<if test="remark != null">#{remark},</if> <if test="remark != null">#{remark},</if>
<if test="createBy != null">#{createBy},</if> <if test="createBy != null">#{createBy},</if>
@@ -70,6 +111,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</trim> </trim>
</insert> </insert>
<!-- ================= 修改 ================= -->
<update id="updateMqttSensorDevice" parameterType="MqttSensorDevice"> <update id="updateMqttSensorDevice" parameterType="MqttSensorDevice">
update mqtt_sensor_device update mqtt_sensor_device
<trim prefix="SET" suffixOverrides=","> <trim prefix="SET" suffixOverrides=",">
@@ -77,6 +119,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="deviceName != null">device_name = #{deviceName},</if> <if test="deviceName != null">device_name = #{deviceName},</if>
<if test="deviceType != null">device_type = #{deviceType},</if> <if test="deviceType != null">device_type = #{deviceType},</if>
<if test="deptId != null">dept_id = #{deptId},</if> <if test="deptId != null">dept_id = #{deptId},</if>
<if test="reportIntervalMinute != null">report_interval_minute = #{reportIntervalMinute},</if>
<if test="status != null">status = #{status},</if> <if test="status != null">status = #{status},</if>
<if test="remark != null">remark = #{remark},</if> <if test="remark != null">remark = #{remark},</if>
<if test="createBy != null">create_by = #{createBy},</if> <if test="createBy != null">create_by = #{createBy},</if>
@@ -88,6 +131,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
where id = #{id} where id = #{id}
</update> </update>
<update id="updateRuntimeStatus" parameterType="MqttSensorDevice">
update mqtt_sensor_device
set status = #{status},
update_time = #{updateTime}
where id = #{id}
</update>
<!-- ================= 删除 ================= -->
<delete id="deleteMqttSensorDeviceById" parameterType="Long"> <delete id="deleteMqttSensorDeviceById" parameterType="Long">
delete from mqtt_sensor_device where id = #{id} delete from mqtt_sensor_device where id = #{id}
</delete> </delete>
@@ -99,10 +150,55 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</foreach> </foreach>
</delete> </delete>
<!-- ================= 根据DevEUI查询 ================= -->
<select id="selectByDevEui" parameterType="String" resultMap="MqttSensorDeviceResult"> <select id="selectByDevEui" parameterType="String" resultMap="MqttSensorDeviceResult">
<include refid="selectMqttSensorDeviceVo"/> <include refid="selectMqttSensorDeviceVo"/>
WHERE dev_eui = #{devEui} where d.dev_eui = #{devEui}
AND is_delete = '0' and d.is_delete = '0'
LIMIT 1 limit 1
</select> </select>
<select id="countDeviceStat" resultType="map">
SELECT
COUNT(1) AS total,
COALESCE(SUM(CASE WHEN status = 0 THEN 1 ELSE 0 END),0) AS online,
COALESCE(SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END),0) AS offline,
CASE
WHEN COUNT(1) = 0 THEN 0
ELSE ROUND(
SUM(CASE WHEN status = 0 THEN 1 ELSE 0 END) / COUNT(1) * 100,
1
)
END AS onlineRate
FROM mqtt_sensor_device
WHERE is_delete = 0
AND dept_id IN
<foreach collection="deptIds" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>
<select id="selectOfflineDeviceList" resultMap="MqttSensorDeviceResult">
<include refid="selectMqttSensorDeviceVo"/>
where d.is_delete = '0'
and d.status = '0'
and (
exists (
select 1
from mqtt_sensor_data sd
where sd.device_id = d.id
and sd.is_delete = '0'
)
and (
select max(sd.create_time)
from mqtt_sensor_data sd
where sd.device_id = d.id
and sd.is_delete = '0'
) &lt; DATE_SUB(
NOW(),
INTERVAL (IFNULL(d.report_interval_minute, 10) * 3) MINUTE
)
)
</select>
</mapper> </mapper>

View File

@@ -115,4 +115,71 @@
</foreach> </foreach>
</delete> </delete>
<select id="countAlarmStat" resultType="map">
SELECT
/* 今日告警次数:今天新增的 alarm 事件数 */
(
SELECT COUNT(1)
FROM mqtt_sensor_event
WHERE is_delete = 0
AND event_type = 'alarm'
AND DATE(create_time) = CURDATE()
AND dept_id IN
<foreach collection="deptIds" item="id" open="(" separator="," close=")">
#{id}
</foreach>
) AS today,
/* 未处理告警数:每个设备最新事件仍然是 alarm 的数量 */
(
SELECT COUNT(1)
FROM mqtt_sensor_event e
INNER JOIN (
SELECT device_id, MAX(create_time) AS max_time
FROM mqtt_sensor_event
WHERE is_delete = 0
AND dept_id IN
<foreach collection="deptIds" item="id" open="(" separator="," close=")">
#{id}
</foreach>
GROUP BY device_id
) t
ON e.device_id = t.device_id
AND e.create_time = t.max_time
WHERE e.is_delete = 0
AND e.event_type = 'alarm'
) AS unhandled
</select>
<select id="selectAlarmTrend" resultType="map">
SELECT
DATE(create_time) AS date,
COUNT(1) AS count
FROM mqtt_sensor_event
WHERE is_delete = 0
AND event_type = 'alarm'
AND create_time >= DATE_SUB(CURDATE(), INTERVAL 6 DAY)
AND dept_id IN
<foreach collection="deptIds" item="id" open="(" separator="," close=")">
#{id}
</foreach>
GROUP BY DATE(create_time)
ORDER BY date
</select>
<select id="selectAlarmTypeStat" resultType="map">
SELECT
event_desc AS name,
COUNT(1) AS value
FROM mqtt_sensor_event
WHERE is_delete = 0
AND event_type = 'alarm'
AND dept_id IN
<foreach collection="deptIds" item="id" open="(" separator="," close=")">
#{id}
</foreach>
GROUP BY event_desc
ORDER BY value DESC
</select>
</mapper> </mapper>

View File

@@ -2,6 +2,7 @@
<!DOCTYPE mapper <!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd"> "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.shzg.project.worn.mapper.MqttTopicConfigMapper"> <mapper namespace="com.shzg.project.worn.mapper.MqttTopicConfigMapper">
<!-- ================== ResultMap ================== --> <!-- ================== ResultMap ================== -->
@@ -10,6 +11,7 @@
<result property="project" column="project" /> <result property="project" column="project" />
<result property="warehouse" column="warehouse" /> <result property="warehouse" column="warehouse" />
<result property="deptId" column="dept_id" /> <result property="deptId" column="dept_id" />
<result property="deptName" column="dept_name" /> <!-- ✅新增 -->
<result property="topicUp" column="topic_up" /> <result property="topicUp" column="topic_up" />
<result property="topicDownPrefix" column="topic_down_prefix" /> <result property="topicDownPrefix" column="topic_down_prefix" />
<result property="status" column="status" /> <result property="status" column="status" />
@@ -21,26 +23,53 @@
<result property="isDelete" column="is_delete" /> <result property="isDelete" column="is_delete" />
</resultMap> </resultMap>
<!-- ================== 基础查询SQL ================== --> <!-- ================== 基础查询SQL(连表) ================== -->
<sql id="selectMqttTopicConfigVo"> <sql id="selectMqttTopicConfigVo">
select id, project, warehouse, dept_id, topic_up, topic_down_prefix, select
status, remark, create_by, create_time, update_by, update_time, is_delete t.id,
from mqtt_topic_config 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
</sql> </sql>
<!-- ================== 列表查询(支持数据隔离) ================== --> <!-- ================== 列表查询(支持数据隔离) ================== -->
<select id="selectMqttTopicConfigList" parameterType="MqttTopicConfig" resultMap="MqttTopicConfigResult"> <select id="selectMqttTopicConfigList" parameterType="MqttTopicConfig" resultMap="MqttTopicConfigResult">
<include refid="selectMqttTopicConfigVo"/> <include refid="selectMqttTopicConfigVo"/>
<where> <where>
<if test="project != null and project != ''">and project = #{project}</if> <if test="project != null and project != ''">
<if test="warehouse != null and warehouse != ''">and warehouse = #{warehouse}</if> and t.project = #{project}
<if test="topicUp != null and topicUp != ''">and topic_up = #{topicUp}</if> </if>
<if test="topicDownPrefix != null and topicDownPrefix != ''">and topic_down_prefix = #{topicDownPrefix}</if> <if test="warehouse != null and warehouse != ''">
<if test="status != null and status != ''">and status = #{status}</if> and t.warehouse = #{warehouse}
<if test="isDelete != null and isDelete != ''">and is_delete = #{isDelete}</if> </if>
<if test="topicUp != null and topicUp != ''">
and t.topic_up = #{topicUp}
</if>
<if test="topicDownPrefix != null and topicDownPrefix != ''">
and t.topic_down_prefix = #{topicDownPrefix}
</if>
<if test="status != null and status != ''">
and t.status = #{status}
</if>
<if test="isDelete != null and isDelete != ''">
and t.is_delete = #{isDelete}
</if>
<!-- 🔥 数据权限 -->
<if test="deptIds != null and deptIds.size() > 0"> <if test="deptIds != null and deptIds.size() > 0">
and dept_id in and t.dept_id in
<foreach collection="deptIds" item="deptId" open="(" separator="," close=")"> <foreach collection="deptIds" item="deptId" open="(" separator="," close=")">
#{deptId} #{deptId}
</foreach> </foreach>
@@ -51,20 +80,20 @@
<!-- ================== 根据ID查询 ================== --> <!-- ================== 根据ID查询 ================== -->
<select id="selectMqttTopicConfigById" parameterType="Long" resultMap="MqttTopicConfigResult"> <select id="selectMqttTopicConfigById" parameterType="Long" resultMap="MqttTopicConfigResult">
<include refid="selectMqttTopicConfigVo"/> <include refid="selectMqttTopicConfigVo"/>
where id = #{id} where t.id = #{id}
</select> </select>
<!-- ================== 查询启用的TopicMQTT用 ================== --> <!-- ================== 查询启用的TopicMQTT用 ================== -->
<select id="selectEnabledMqttTopicConfigList" resultMap="MqttTopicConfigResult"> <select id="selectEnabledMqttTopicConfigList" parameterType="MqttTopicConfig" resultMap="MqttTopicConfigResult">
<include refid="selectMqttTopicConfigVo"/> <include refid="selectMqttTopicConfigVo"/>
where status = '0' where t.status = '0'
and is_delete = '0' and t.is_delete = '0'
and topic_up is not null and t.topic_up is not null
and topic_up != '' and t.topic_up != ''
<!-- 🔥 防止跨项目订阅 --> <!-- 🔥 数据隔离 -->
<if test="deptIds != null and deptIds.size() > 0"> <if test="deptIds != null and deptIds.size() > 0">
and dept_id in and t.dept_id in
<foreach collection="deptIds" item="deptId" open="(" separator="," close=")"> <foreach collection="deptIds" item="deptId" open="(" separator="," close=")">
#{deptId} #{deptId}
</foreach> </foreach>
@@ -122,12 +151,11 @@
where id = #{id} where id = #{id}
</update> </update>
<!-- ================== 删除(单个) ================== --> <!-- ================== 删除 ================== -->
<delete id="deleteMqttTopicConfigById" parameterType="Long"> <delete id="deleteMqttTopicConfigById" parameterType="Long">
delete from mqtt_topic_config where id = #{id} delete from mqtt_topic_config where id = #{id}
</delete> </delete>
<!-- ================== 批量删除================== -->
<delete id="deleteMqttTopicConfigByIds"> <delete id="deleteMqttTopicConfigByIds">
delete from mqtt_topic_config where id in delete from mqtt_topic_config where id in
<foreach item="id" collection="array" open="(" separator="," close=")"> <foreach item="id" collection="array" open="(" separator="," close=")">
@@ -135,11 +163,12 @@
</foreach> </foreach>
</delete> </delete>
<select id="selectByDeptId" resultMap="MqttTopicConfigResult"> <!-- ================== 根据部门查询 ================== -->
<select id="selectByDeptId" parameterType="Long" resultMap="MqttTopicConfigResult">
<include refid="selectMqttTopicConfigVo"/> <include refid="selectMqttTopicConfigVo"/>
where dept_id = #{deptId} where t.dept_id = #{deptId}
and status = '0' and t.status = '0'
and is_delete = '0' and t.is_delete = '0'
limit 1 limit 1
</select> </select>