首页统计接口开发

websocket模块优化
This commit is contained in:
2026-04-15 17:08:18 +08:00
parent 7210c13ae1
commit b2a2784724
19 changed files with 827 additions and 279 deletions

View File

@@ -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<Long> deptIds = SpringUtils.getBean(ISysDeptService.class)
.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.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<String, Object> 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<String, Object> claims)
{
String token = Jwts.builder()
private String createToken(Map<String, Object> 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;
}
}

View File

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

View File

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

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

@@ -34,6 +34,10 @@ public class MqttSensorDevice extends BaseEntity
@Excel(name = "所属部门ID")
private Long deptId;
/** 所属部门名称 */
@Excel(name = "所属部门名称")
private String deptName;
/** 状态0正常 1停用 */
@Excel(name = "状态", readConverterExp = "0=正常,1=停用")
private String status;
@@ -92,6 +96,16 @@ public class MqttSensorDevice extends BaseEntity
return deptId;
}
public void setDeptName(String deptName)
{
this.deptName = deptName;
}
public String getDeptName()
{
return deptName;
}
public void setStatus(String status)
{
this.status = status;
@@ -115,18 +129,19 @@ 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("status", getStatus())
.append("remark", getRemark())
.append("createBy", getCreateBy())
.append("createTime", getCreateTime())
.append("updateBy", getUpdateBy())
.append("updateTime", getUpdateTime())
.append("isDelete", getIsDelete())
.toString();
}
}

View File

@@ -32,6 +32,10 @@ public class MqttTopicConfig extends BaseEntity
@Excel(name = "部门ID")
private Long deptId;
/** 所属部门名称 */
@Excel(name = "部门名称")
private String deptName;
/** 🔥 数据权限部门ID集合 */
private List<Long> 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<Long> 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())

View File

@@ -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,8 @@ public interface MqttSensorDeviceMapper
*/
MqttSensorDevice selectByDevEui(@Param("devEui") String devEui);
/**
* 设备统计(按权限)
*/
Map<String, Object> countDeviceStat(@Param("deptIds") List<Long> deptIds);
}

View File

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

@@ -106,9 +106,7 @@ public class SmartSocketHandler {
);
// 没变化直接返回(不写事件、不推送)
if (!changed) {
return;
}
// 状态没变化也继续向前端推送周期数据,事件入库仍在推送后去重。
// ================== 查询部门 ==================
String deptName = null;
@@ -135,6 +133,10 @@ public class SmartSocketHandler {
}
// ================== 事件记录(只在变化时) ==================
if (!changed) {
return;
}
MqttSensorEvent event = new MqttSensorEvent();
event.setDeviceId(device.getId());
event.setDeptId(device.getDeptId());

View File

@@ -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<String, Object> getAlarmStat();
/**
* 告警趋势近7天
*/
List<Map<String, Object>> getAlarmTrend();
/**
* 告警类型占比
*/
List<Map<String, Object>> getAlarmTypeStat();
}

View File

@@ -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;
@@ -101,4 +104,41 @@ public class MqttSensorEventServiceImpl implements IMqttSensorEventService
{
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

@@ -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<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();
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<SysDept> deptList = deptMapper.selectDeptAndChildren(deptId, null);
Set<Long> 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);
}
}
}

View File

@@ -175,4 +175,89 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
WHERE FIND_IN_SET(#{deptId}, ancestors)
</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>

View File

@@ -1,45 +1,79 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.shzg.project.worn.mapper.MqttSensorDeviceMapper">
<!-- ================= resultMap ================= -->
<resultMap type="MqttSensorDevice" id="MqttSensorDeviceResult">
<result property="id" column="id" />
<result property="devEui" column="dev_eui" />
<result property="deviceName" column="device_name" />
<result property="deviceType" column="device_type" />
<result property="deptId" column="dept_id" />
<result property="status" column="status" />
<result property="remark" column="remark" />
<result property="createBy" column="create_by" />
<result property="createTime" column="create_time" />
<result property="updateBy" column="update_by" />
<result property="updateTime" column="update_time" />
<result property="isDelete" column="is_delete" />
<result property="id" column="id"/>
<result property="devEui" column="dev_eui"/>
<result property="deviceName" column="device_name"/>
<result property="deviceType" column="device_type"/>
<result property="deptId" column="dept_id"/>
<result property="deptName" column="dept_name"/>
<result property="status" column="status"/>
<result property="remark" column="remark"/>
<result property="createBy" column="create_by"/>
<result property="createTime" column="create_time"/>
<result property="updateBy" column="update_by"/>
<result property="updateTime" column="update_time"/>
<result property="isDelete" column="is_delete"/>
</resultMap>
<!-- ================= 公共查询SQL ================= -->
<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.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>
<!-- ================= 查询列表 ================= -->
<select id="selectMqttSensorDeviceList" parameterType="MqttSensorDevice" resultMap="MqttSensorDeviceResult">
<include refid="selectMqttSensorDeviceVo"/>
<where>
<if test="devEui != null and devEui != ''"> and dev_eui = #{devEui}</if>
<if test="deviceName != null and deviceName != ''"> and device_name like concat('%', #{deviceName}, '%')</if>
<if test="deviceType != null and deviceType != ''"> and device_type = #{deviceType}</if>
<if test="deptId != null "> and dept_id = #{deptId}</if>
<if test="status != null and status != ''"> and status = #{status}</if>
<if test="isDelete != null and isDelete != ''"> and is_delete = #{isDelete}</if>
<if test="devEui != null and devEui != ''">
and d.dev_eui = #{devEui}
</if>
<if test="deviceName != null and deviceName != ''">
and d.device_name like concat('%', #{deviceName}, '%')
</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="status != null and status != ''">
and d.status = #{status}
</if>
<if test="isDelete != null and isDelete != ''">
and d.is_delete = #{isDelete}
</if>
</where>
</select>
<!-- ================= 根据ID查询 ================= -->
<select id="selectMqttSensorDeviceById" parameterType="Long" resultMap="MqttSensorDeviceResult">
<include refid="selectMqttSensorDeviceVo"/>
where id = #{id}
where d.id = #{id}
</select>
<!-- ================= 新增 ================= -->
<insert id="insertMqttSensorDevice" parameterType="MqttSensorDevice" useGeneratedKeys="true" keyProperty="id">
insert into mqtt_sensor_device
<trim prefix="(" suffix=")" suffixOverrides=",">
@@ -54,7 +88,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="updateBy != null">update_by,</if>
<if test="updateTime != null">update_time,</if>
<if test="isDelete != null">is_delete,</if>
</trim>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="devEui != null and devEui != ''">#{devEui},</if>
<if test="deviceName != null">#{deviceName},</if>
@@ -67,9 +101,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="updateBy != null">#{updateBy},</if>
<if test="updateTime != null">#{updateTime},</if>
<if test="isDelete != null">#{isDelete},</if>
</trim>
</trim>
</insert>
<!-- ================= 修改 ================= -->
<update id="updateMqttSensorDevice" parameterType="MqttSensorDevice">
update mqtt_sensor_device
<trim prefix="SET" suffixOverrides=",">
@@ -88,6 +123,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
where id = #{id}
</update>
<!-- ================= 删除 ================= -->
<delete id="deleteMqttSensorDeviceById" parameterType="Long">
delete from mqtt_sensor_device where id = #{id}
</delete>
@@ -99,10 +135,32 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</foreach>
</delete>
<!-- ================= 根据DevEUI查询 ================= -->
<select id="selectByDevEui" parameterType="String" resultMap="MqttSensorDeviceResult">
<include refid="selectMqttSensorDeviceVo"/>
WHERE dev_eui = #{devEui}
AND is_delete = '0'
LIMIT 1
where d.dev_eui = #{devEui}
and d.is_delete = '0'
limit 1
</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>
</mapper>

View File

@@ -115,4 +115,71 @@
</foreach>
</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>

View File

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