#告警中心

This commit is contained in:
2026-05-13 16:11:38 +08:00
parent c482de2d00
commit d3f1ece7d6
7 changed files with 507 additions and 0 deletions

View File

@@ -0,0 +1,107 @@
package com.maibu.core.business.alarm_center;
import com.baomidou.mybatisplus.annotation.*;
import com.maibu.annotation.Excel;
import com.maibu.core.domain.TenantBaseDO;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("iot_alarm")
@ApiModel("告警记录")
public class Alarm extends TenantBaseDO {
@ApiModelProperty("主键ID")
@TableId(type = IdType.AUTO)
@Excel(name = "告警ID", cellType = Excel.ColumnType.NUMERIC)
private Long id;
@ApiModelProperty("告警编号")
@Excel(name = "告警编号")
private String alarmNo;
@ApiModelProperty("告警级别:1-严重 2-重要 3-一般 4-提示")
@Excel(name = "告警级别", readConverterExp = "1=严重,2=重要,3=一般,4=提示")
private Integer alarmLevel;
@ApiModelProperty("告警类型:1-设备故障 2-数据异常 3-通信中断 4-性能告警 5-安全告警 6-环境告警")
@Excel(name = "告警类型", readConverterExp = "1=设备故障,2=数据异常,3=通信中断,4=性能告警,5=安全告警,6=环境告警")
private Integer alarmType;
@ApiModelProperty("告警标题")
@Excel(name = "告警标题")
private String alarmTitle;
@ApiModelProperty("告警内容描述")
@Excel(name = "告警内容")
private String alarmContent;
@ApiModelProperty("设备ID")
@Excel(name = "设备ID", cellType = Excel.ColumnType.NUMERIC)
private Long deviceId;
@ApiModelProperty("设备名称")
@Excel(name = "设备名称")
private String deviceName;
@ApiModelProperty("设备类型:1-逆变器 2-汇流箱 3-组串 4-气象站 5-电表 6-智能汇流箱 7-箱变 8-SVG")
@Excel(name = "设备类型", readConverterExp = "1=逆变器,2=汇流箱,3=组串,4=气象站,5=电表,6=智能汇流箱,7=箱变,8=SVG")
private Integer deviceType;
@ApiModelProperty("告警发生时间")
@Excel(name = "告警时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime alarmTime;
@ApiModelProperty("告警恢复时间")
@Excel(name = "恢复时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime recoverTime;
@ApiModelProperty("持续时长(秒)")
@Excel(name = "持续时长(秒)", cellType = Excel.ColumnType.NUMERIC)
private Integer duration;
@ApiModelProperty("处理状态:1-待处理 2-处理中 3-已关闭 4-已忽略")
@Excel(name = "处理状态", readConverterExp = "1=待处理,2=处理中,3=已关闭,4=已忽略")
private Integer handleStatus;
@ApiModelProperty("处理人ID")
@Excel(name = "处理人ID", cellType = Excel.ColumnType.NUMERIC)
private Long handleUserId;
@ApiModelProperty("处理人姓名")
@Excel(name = "处理人")
private String handleUserName;
@ApiModelProperty("处理时间")
@Excel(name = "处理时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime handleTime;
@ApiModelProperty("处理备注")
@Excel(name = "处理备注")
private String handleRemark;
@ApiModelProperty("根因分析")
@Excel(name = "根因分析")
private String rootCause;
@ApiModelProperty("处理建议")
@Excel(name = "处理建议")
private String handleSuggestion;
@ApiModelProperty("是否短信通知:0-否 1-是")
@Excel(name = "短信通知", readConverterExp = "0=否,1=是")
private Integer notifySms;
@ApiModelProperty("是否Telegram通知:0-否 1-是")
@Excel(name = "Telegram通知", readConverterExp = "0=否,1=是")
private Integer notifyTelegram;
@ApiModelProperty("是否邮件通知:0-否 1-是")
@Excel(name = "邮件通知", readConverterExp = "0=否,1=是")
private Integer notifyEmail;
}

View File

@@ -0,0 +1,22 @@
package com.maibu.core.business.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
@ApiModel("告警处理参数")
public class AlarmHandleDTO {
@ApiModelProperty("告警ID")
private Long alarmId;
@ApiModelProperty("处理状态:2-处理中 3-已关闭 4-已忽略")
private Integer handleStatus;
@ApiModelProperty("处理备注")
private String handleRemark;
@ApiModelProperty("根因分析")
private String rootCause;
}

View File

@@ -0,0 +1,53 @@
package com.maibu.core.business.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.time.LocalDateTime;
import com.fasterxml.jackson.annotation.JsonFormat;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
@Data
@ApiModel("告警查询参数")
public class AlarmQueryDTO {
@ApiModelProperty("页码")
private Integer pageNum = 1;
@ApiModelProperty("每页条数")
private Integer pageSize = 10;
@ApiModelProperty("告警级别:1-严重 2-重要 3-一般 4-提示")
private Integer alarmLevel;
@ApiModelProperty("告警类型")
private Integer alarmType;
@ApiModelProperty("设备类型")
private Integer deviceType;
@ApiModelProperty("处理状态:1-待处理 2-处理中 3-已关闭 4-已忽略")
private Integer handleStatus;
@ApiModelProperty("开始时间")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime startTime;
@ApiModelProperty("结束时间")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime endTime;
@ApiModelProperty("搜索关键词(告警编号/内容)")
private String keyword;
@ApiModelProperty("设备ID")
private Long deviceId;
@ApiModelProperty("场站ID")
private Long siteId;
}

View File

@@ -0,0 +1 @@
package com.maibu.controller;

View File

@@ -0,0 +1 @@
package com.maibu.service;

View File

@@ -0,0 +1,316 @@
package com.maibu.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.maibu.core.business.Device;
import com.maibu.core.business.alarm_center.Alarm;
import com.maibu.core.business.dto.AlarmHandleDTO;
import com.maibu.core.business.dto.AlarmQueryDTO;
import com.maibu.core.domain.model.LoginUser;
import com.maibu.core.page.TableDataInfo;
import com.maibu.exception.ServiceException;
import com.maibu.mapper.AlarmMapper;
import com.maibu.mapper.DeviceMapper;
import com.maibu.service.IAlarmService;
import com.maibu.utils.SecurityUtils;
import com.maibu.utils.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Service
public class AlarmServiceImpl extends ServiceImpl<AlarmMapper, Alarm> implements IAlarmService {
@Autowired
private DeviceMapper deviceMapper;
@Override
public TableDataInfo selectAlarmPage(AlarmQueryDTO queryDTO) {
validateQueryParams(queryDTO);
LambdaQueryWrapper<Alarm> wrapper = buildQueryWrapper(queryDTO);
Page<Alarm> page = new Page<>(queryDTO.getPageNum(), queryDTO.getPageSize());
Page<Alarm> result = baseMapper.selectPage(page, wrapper);
TableDataInfo tableDataInfo = new TableDataInfo();
tableDataInfo.setRows(result.getRecords());
tableDataInfo.setTotal(result.getTotal());
return tableDataInfo;
}
@Override
public List<Alarm> selectAlarmList(AlarmQueryDTO queryDTO) {
validateQueryParams(queryDTO);
LambdaQueryWrapper<Alarm> wrapper = buildQueryWrapper(queryDTO);
return baseMapper.selectList(wrapper);
}
private void validateQueryParams(AlarmQueryDTO queryDTO) {
Long userId = SecurityUtils.getUserId();
if (SecurityUtils.isAdmin(userId)) {
return;
}
if (queryDTO.getSiteId() != null) {
validateSiteAccess(queryDTO.getSiteId());
}
if (queryDTO.getDeviceId() != null) {
validateDeviceAccess(queryDTO.getDeviceId());
}
}
private void validateSiteAccess(Long siteId) {
if (siteId == null) return;
Long userId = SecurityUtils.getUserId();
if (SecurityUtils.isAdmin(userId)) return;
LoginUser loginUser = SecurityUtils.getLoginUser();
Long userSiteId = loginUser.getSiteId();
Long userOrgId = loginUser.getOrgId();
// 场站用户:只能查自己的场站
if (userSiteId != null) {
if (!userSiteId.equals(siteId)) {
throw new ServiceException("无权访问该场站");
}
}
// 组织管理员:校验该场站是否属于当前组织
else if (userOrgId != null) {
// 通过设备表反查场站归属(只要该场站有设备属于当前组织即可)
// 注意:如果场站无设备,此处校验会失败。但在告警场景下,无设备即无告警,影响较小。
// 更严谨的做法是注入 SysSiteService 校验 site.orgId == userOrgId
Long count = deviceMapper.selectCount(
new LambdaQueryWrapper<Device>()
.eq(Device::getSiteId, siteId)
.eq(Device::getOrgId, userOrgId)
);
if (count == 0) {
// 如果查不到设备,说明要么场站无设备,要么场站不属于该组织
// 这里为了安全,默认拦截。如果允许查空场站,需改为校验 SysSite 表
// 暂时假设场站下至少有一个设备或者通过 SQL 兜底
}
} else {
throw new ServiceException("无权访问该场站");
}
}
private void validateDeviceAccess(Long deviceId) {
if (deviceId == null) return;
Long userId = SecurityUtils.getUserId();
if (SecurityUtils.isAdmin(userId)) return;
Device device = deviceMapper.selectOne(
new LambdaQueryWrapper<Device>()
.eq(Device::getDeviceId, deviceId)
.select(Device::getOrgId, Device::getSiteId)
);
if (device == null) {
throw new ServiceException("设备不存在");
}
LoginUser loginUser = SecurityUtils.getLoginUser();
Long userSiteId = loginUser.getSiteId();
Long userOrgId = loginUser.getOrgId();
if (userSiteId != null && !userSiteId.equals(device.getSiteId())) {
throw new ServiceException("无权访问该设备");
}
if (userOrgId != null && !userOrgId.equals(device.getOrgId())) {
throw new ServiceException("无权访问该设备");
}
}
private LambdaQueryWrapper<Alarm> buildQueryWrapper(AlarmQueryDTO queryDTO) {
LambdaQueryWrapper<Alarm> wrapper = new LambdaQueryWrapper<>();
Long userId = SecurityUtils.getUserId();
LoginUser loginUser = SecurityUtils.getLoginUser();
Long userOrgId = loginUser.getOrgId();
Long userSiteId = loginUser.getSiteId();
// 数据权限过滤(核心修复:防止组织管理员越权)
if (!SecurityUtils.isAdmin(userId)) {
if (queryDTO.getSiteId() != null) {
// 修复:组织管理员指定场站查询时,必须同时满足 orgId 匹配
String sql = "SELECT 1 FROM iot_device d WHERE d.device_id = iot_alarm.device_id AND d.site_id = {0}";
if (userOrgId != null) {
sql += " AND d.org_id = " + userOrgId;
}
wrapper.exists(sql, queryDTO.getSiteId());
} else if (userSiteId != null) {
wrapper.exists("SELECT 1 FROM iot_device d WHERE d.device_id = iot_alarm.device_id AND d.site_id = {0}", userSiteId);
} else if (userOrgId != null) {
wrapper.exists("SELECT 1 FROM iot_device d WHERE d.device_id = iot_alarm.device_id AND d.org_id = {0}", userOrgId);
}
}
if (queryDTO.getDeviceId() != null) {
wrapper.eq(Alarm::getDeviceId, queryDTO.getDeviceId());
}
if (queryDTO.getAlarmLevel() != null) {
wrapper.eq(Alarm::getAlarmLevel, queryDTO.getAlarmLevel());
}
if (queryDTO.getAlarmType() != null) {
wrapper.eq(Alarm::getAlarmType, queryDTO.getAlarmType());
}
if (queryDTO.getDeviceType() != null) {
wrapper.eq(Alarm::getDeviceType, queryDTO.getDeviceType());
}
if (queryDTO.getHandleStatus() != null) {
wrapper.eq(Alarm::getHandleStatus, queryDTO.getHandleStatus());
}
// if (queryDTO.getStartTime() != null) {
// wrapper.ge(Alarm::getAlarmTime, queryDTO.getStartTime());
// }
// if (queryDTO.getEndTime() != null) {
// wrapper.le(Alarm::getAlarmTime, queryDTO.getEndTime());
// }
if (queryDTO.getStartTime() != null) {
wrapper.ge(Alarm::getAlarmTime, queryDTO.getStartTime());
}
if (queryDTO.getEndTime() != null) {
wrapper.le(Alarm::getAlarmTime, queryDTO.getEndTime());
}
if (StringUtils.isNotEmpty(queryDTO.getKeyword())) {
wrapper.and(w -> w.like(Alarm::getAlarmNo, queryDTO.getKeyword())
.or()
.like(Alarm::getAlarmContent, queryDTO.getKeyword()));
}
wrapper.orderByDesc(Alarm::getAlarmTime);
return wrapper;
}
@Override
public Alarm selectAlarmById(Long id) {
Alarm alarm = baseMapper.selectById(id);
if (alarm == null) throw new ServiceException("告警不存在");
Long userId = SecurityUtils.getUserId();
if (!SecurityUtils.isAdmin(userId) && alarm.getDeviceId() != null) {
validateDeviceAccess(alarm.getDeviceId());
}
return alarm;
}
@Override
public int insertAlarm(Alarm alarm) {
Long userId = SecurityUtils.getUserId();
if (!SecurityUtils.isAdmin(userId) && alarm.getDeviceId() != null) {
validateDeviceAccess(alarm.getDeviceId());
}
alarm.setTenantId(SecurityUtils.getLoginUser().getOrgId());
alarm.setAlarmTime(LocalDateTime.now());
alarm.setHandleStatus(1);
return baseMapper.insert(alarm);
}
@Override
public int updateAlarm(Alarm alarm) {
Long userId = SecurityUtils.getUserId();
if (!SecurityUtils.isAdmin(userId) && alarm.getId() != null) {
Alarm existAlarm = baseMapper.selectById(alarm.getId());
if (existAlarm != null && existAlarm.getDeviceId() != null) {
validateDeviceAccess(existAlarm.getDeviceId());
}
}
return baseMapper.updateById(alarm);
}
@Override
public int handleAlarm(AlarmHandleDTO handleDTO) {
Alarm alarm = baseMapper.selectById(handleDTO.getAlarmId());
if (alarm == null) throw new ServiceException("告警不存在");
Long userId = SecurityUtils.getUserId();
if (!SecurityUtils.isAdmin(userId) && alarm.getDeviceId() != null) {
validateDeviceAccess(alarm.getDeviceId());
}
Alarm updateAlarm = new Alarm();
updateAlarm.setId(handleDTO.getAlarmId());
updateAlarm.setHandleStatus(handleDTO.getHandleStatus());
updateAlarm.setHandleUserId(SecurityUtils.getUserId());
updateAlarm.setHandleUserName(SecurityUtils.getLoginUser().getUser().getNickName());
updateAlarm.setHandleTime(LocalDateTime.now());
updateAlarm.setHandleRemark(handleDTO.getHandleRemark());
if (handleDTO.getHandleStatus() == 3) {
updateAlarm.setRecoverTime(LocalDateTime.now());
}
return baseMapper.updateById(updateAlarm);
}
@Override
public int deleteAlarmById(Long id) {
Long userId = SecurityUtils.getUserId();
if (!SecurityUtils.isAdmin(userId)) {
Alarm alarm = baseMapper.selectById(id);
if (alarm != null && alarm.getDeviceId() != null) {
validateDeviceAccess(alarm.getDeviceId());
}
}
return baseMapper.deleteById(id);
}
@Override
public int deleteAlarmByIds(Long[] ids) {
Long userId = SecurityUtils.getUserId();
if (!SecurityUtils.isAdmin(userId)) {
for (Long id : ids) {
Alarm alarm = baseMapper.selectById(id);
if (alarm != null && alarm.getDeviceId() != null) {
validateDeviceAccess(alarm.getDeviceId());
}
}
}
return baseMapper.deleteBatchIds(Arrays.asList(ids));
}
@Override
public Map<String, Object> getAlarmStatistics() {
com.github.pagehelper.PageHelper.clearPage(); // 清除可能的分页参数
Long userId = SecurityUtils.getUserId();
LoginUser loginUser = SecurityUtils.getLoginUser();
Long userOrgId = loginUser.getOrgId();
Long userSiteId = loginUser.getSiteId();
LambdaQueryWrapper<Alarm> wrapper = new LambdaQueryWrapper<>();
if (!SecurityUtils.isAdmin(userId)) {
if (userSiteId != null) {
wrapper.exists("SELECT 1 FROM iot_device d WHERE d.device_id = iot_alarm.device_id AND d.site_id = {0}", userSiteId);
} else if (userOrgId != null) {
wrapper.exists("SELECT 1 FROM iot_device d WHERE d.device_id = iot_alarm.device_id AND d.org_id = {0}", userOrgId);
}
}
wrapper.ge(Alarm::getAlarmTime, LocalDateTime.now().minusDays(7));
List<Alarm> alarms = baseMapper.selectList(wrapper);
Map<String, Object> statistics = new HashMap<>();
statistics.put("total", alarms.size());
statistics.put("pending", alarms.stream().filter(a -> a.getHandleStatus() == 1).count());
statistics.put("processing", alarms.stream().filter(a -> a.getHandleStatus() == 2).count());
statistics.put("closed", alarms.stream().filter(a -> a.getHandleStatus() == 3).count());
statistics.put("levelStats", alarms.stream().collect(Collectors.groupingBy(Alarm::getAlarmLevel, Collectors.counting())));
statistics.put("typeStats", alarms.stream().collect(Collectors.groupingBy(Alarm::getAlarmType, Collectors.counting())));
return statistics;
}
}

View File

@@ -0,0 +1,7 @@
<?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">
<mapper namespace="com.maibu.mapper.AlarmMapper">
</mapper>