|
|
|
|
@ -0,0 +1,287 @@
|
|
|
|
|
package org.dromara.rfid.service.impl;
|
|
|
|
|
|
|
|
|
|
import cn.hutool.core.collection.CollUtil;
|
|
|
|
|
import cn.hutool.core.date.DateUtil;
|
|
|
|
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
|
|
|
|
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
|
|
|
|
import lombok.RequiredArgsConstructor;
|
|
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
|
|
import org.dromara.rfid.domain.RfidDevice;
|
|
|
|
|
import org.dromara.rfid.domain.RfidLocation;
|
|
|
|
|
import org.dromara.rfid.domain.vo.DashboardVO;
|
|
|
|
|
import org.dromara.rfid.domain.vo.RfidDeviceVo;
|
|
|
|
|
import org.dromara.rfid.domain.vo.RfidLocationVo;
|
|
|
|
|
import org.dromara.rfid.domain.vo.RfidReadRecordVo;
|
|
|
|
|
import org.dromara.rfid.helper.RfidReadRecordTableHelper;
|
|
|
|
|
import org.dromara.rfid.mapper.RfidDeviceMapper;
|
|
|
|
|
import org.dromara.rfid.mapper.RfidLocationMapper;
|
|
|
|
|
import org.dromara.rfid.mapper.RfidReadRecordMapper;
|
|
|
|
|
import org.dromara.rfid.service.IDashboardService;
|
|
|
|
|
import org.springframework.stereotype.Service;
|
|
|
|
|
|
|
|
|
|
import java.time.LocalDate;
|
|
|
|
|
import java.time.LocalDateTime;
|
|
|
|
|
import java.time.format.DateTimeFormatter;
|
|
|
|
|
import java.util.*;
|
|
|
|
|
import java.util.stream.Collectors;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 看板数据服务实现类
|
|
|
|
|
* <p>
|
|
|
|
|
* 提供三个独立接口:
|
|
|
|
|
* <ul>
|
|
|
|
|
* <li>实时统计接口(按秒刷新):顶部统计 + 告警列表</li>
|
|
|
|
|
* <li>设备状态接口(定时刷新):设备树 + 设备最新读取记录</li>
|
|
|
|
|
* <li>成功率趋势接口(按小时统计):成功率折线图</li>
|
|
|
|
|
* </ul>
|
|
|
|
|
* </p>
|
|
|
|
|
*
|
|
|
|
|
* @author zch
|
|
|
|
|
* @date 2025-11-26
|
|
|
|
|
*/
|
|
|
|
|
@Slf4j
|
|
|
|
|
@RequiredArgsConstructor
|
|
|
|
|
@Service
|
|
|
|
|
public class DashboardServiceImpl implements IDashboardService {
|
|
|
|
|
|
|
|
|
|
private final RfidDeviceMapper deviceMapper;
|
|
|
|
|
private final RfidLocationMapper locationMapper;
|
|
|
|
|
private final RfidReadRecordMapper readRecordMapper;
|
|
|
|
|
|
|
|
|
|
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 【接口1】获取实时统计数据(按秒刷新)
|
|
|
|
|
*/
|
|
|
|
|
@Override
|
|
|
|
|
public DashboardVO.RealtimeStats getRealtimeStats(Integer alarmLimit) {
|
|
|
|
|
DashboardVO.RealtimeStats realtimeStats = new DashboardVO.RealtimeStats();
|
|
|
|
|
|
|
|
|
|
// 获取统计概览
|
|
|
|
|
realtimeStats.setOverview(getOverview());
|
|
|
|
|
|
|
|
|
|
// 获取告警统计列表
|
|
|
|
|
realtimeStats.setAlarmStats(getAlarmStats(alarmLimit));
|
|
|
|
|
|
|
|
|
|
return realtimeStats;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 获取统计概览
|
|
|
|
|
*/
|
|
|
|
|
@Override
|
|
|
|
|
public DashboardVO.StatisticsOverview getOverview() {
|
|
|
|
|
DashboardVO.StatisticsOverview overview = new DashboardVO.StatisticsOverview();
|
|
|
|
|
|
|
|
|
|
// 统计设备总数
|
|
|
|
|
Long deviceTotal = deviceMapper.selectCount(Wrappers.lambdaQuery(RfidDevice.class)
|
|
|
|
|
.eq(RfidDevice::getIsMarked, "1"));
|
|
|
|
|
overview.setDeviceTotal(deviceTotal);
|
|
|
|
|
|
|
|
|
|
// 统计在线数量
|
|
|
|
|
Long onlineCount = deviceMapper.selectCount(Wrappers.lambdaQuery(RfidDevice.class)
|
|
|
|
|
.eq(RfidDevice::getIsMarked, "1")
|
|
|
|
|
.eq(RfidDevice::getOnlineStatus, "1"));
|
|
|
|
|
overview.setOnlineCount(onlineCount);
|
|
|
|
|
|
|
|
|
|
// 统计离线数量
|
|
|
|
|
Long offlineCount = deviceMapper.selectCount(Wrappers.lambdaQuery(RfidDevice.class)
|
|
|
|
|
.eq(RfidDevice::getIsMarked, "1")
|
|
|
|
|
.eq(RfidDevice::getOnlineStatus, "0"));
|
|
|
|
|
overview.setOfflineCount(offlineCount);
|
|
|
|
|
|
|
|
|
|
// 统计告警数量(当天告警设备数)
|
|
|
|
|
Long alarmCount = deviceMapper.selectCount(Wrappers.lambdaQuery(RfidDevice.class)
|
|
|
|
|
.eq(RfidDevice::getIsMarked, "1")
|
|
|
|
|
.eq(RfidDevice::getAlarmStatus, "1"));
|
|
|
|
|
overview.setAlarmCount(alarmCount);
|
|
|
|
|
|
|
|
|
|
return overview;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 获取位置树(含设备信息)
|
|
|
|
|
* <p>
|
|
|
|
|
* 返回完整的位置树结构,位置类型为 3 时表示设备节点,关联设备信息。
|
|
|
|
|
* 不返回读取记录数据,具体记录由 WebSocket 实时推送。
|
|
|
|
|
* </p>
|
|
|
|
|
*/
|
|
|
|
|
@Override
|
|
|
|
|
public List<DashboardVO.LocationTreeNode> getLocationTree() {
|
|
|
|
|
// 1. 查询所有已标识的位置
|
|
|
|
|
QueryWrapper<RfidLocation> locationWrapper = Wrappers.query(RfidLocation.class);
|
|
|
|
|
locationWrapper.eq("t.is_marked", "1");
|
|
|
|
|
locationWrapper.orderByAsc("t.parent_id", "t.id");
|
|
|
|
|
List<RfidLocationVo> locations = locationMapper.selectCustomRfidLocationVoList(locationWrapper);
|
|
|
|
|
|
|
|
|
|
if (CollUtil.isEmpty(locations)) {
|
|
|
|
|
return Collections.emptyList();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2. 查询所有已标识的设备,构建 locationId -> 设备列表 的映射
|
|
|
|
|
QueryWrapper<RfidDevice> deviceWrapper = Wrappers.query(RfidDevice.class);
|
|
|
|
|
deviceWrapper.eq("t.is_marked", "1");
|
|
|
|
|
List<RfidDeviceVo> devices = deviceMapper.selectCustomRfidDeviceVoList(deviceWrapper);
|
|
|
|
|
|
|
|
|
|
Map<Long, List<RfidDeviceVo>> devicesByLocation = devices.stream()
|
|
|
|
|
.filter(d -> d.getLocationId() != null)
|
|
|
|
|
.collect(Collectors.groupingBy(RfidDeviceVo::getLocationId));
|
|
|
|
|
|
|
|
|
|
// 3. 构建位置树节点
|
|
|
|
|
List<DashboardVO.LocationTreeNode> allNodes = new ArrayList<>();
|
|
|
|
|
Map<Long, DashboardVO.LocationTreeNode> nodeMap = new HashMap<>();
|
|
|
|
|
|
|
|
|
|
for (RfidLocationVo loc : locations) {
|
|
|
|
|
DashboardVO.LocationTreeNode node = new DashboardVO.LocationTreeNode();
|
|
|
|
|
node.setId(loc.getId());
|
|
|
|
|
node.setLocationCode(loc.getLocationCode());
|
|
|
|
|
node.setLocationAlias(loc.getLocationAlias());
|
|
|
|
|
node.setLocationType(loc.getLocationType());
|
|
|
|
|
node.setParentId(loc.getParentId());
|
|
|
|
|
node.setChildren(new ArrayList<>());
|
|
|
|
|
|
|
|
|
|
// 如果是工位(locationType = 3),关联设备信息
|
|
|
|
|
if ("3".equals(loc.getLocationType())) {
|
|
|
|
|
List<RfidDeviceVo> locationDevices = devicesByLocation.get(loc.getId());
|
|
|
|
|
if (CollUtil.isNotEmpty(locationDevices)) {
|
|
|
|
|
// 取第一个设备(通常一个工位对应一个设备)
|
|
|
|
|
RfidDeviceVo device = locationDevices.get(0);
|
|
|
|
|
node.setDeviceId(device.getId());
|
|
|
|
|
node.setDeviceCode(device.getDeviceCode());
|
|
|
|
|
node.setDeviceName(device.getDeviceName());
|
|
|
|
|
node.setOnlineStatus(device.getOnlineStatus());
|
|
|
|
|
node.setAlarmStatus(device.getAlarmStatus());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
allNodes.add(node);
|
|
|
|
|
nodeMap.put(loc.getId(), node);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 4. 构建树形结构
|
|
|
|
|
List<DashboardVO.LocationTreeNode> rootNodes = new ArrayList<>();
|
|
|
|
|
for (DashboardVO.LocationTreeNode node : allNodes) {
|
|
|
|
|
Long parentId = node.getParentId();
|
|
|
|
|
if (parentId == null || parentId == 0L) {
|
|
|
|
|
// 顶级节点
|
|
|
|
|
rootNodes.add(node);
|
|
|
|
|
} else {
|
|
|
|
|
// 挂载到父节点
|
|
|
|
|
DashboardVO.LocationTreeNode parent = nodeMap.get(parentId);
|
|
|
|
|
if (parent != null) {
|
|
|
|
|
parent.getChildren().add(node);
|
|
|
|
|
} else {
|
|
|
|
|
// 父节点不存在,作为顶级节点处理
|
|
|
|
|
rootNodes.add(node);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return rootNodes;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 获取成功率趋势
|
|
|
|
|
* <p>
|
|
|
|
|
* 按小时统计指定日期(今日/昨日)的读取成功率,用于前端折线图展示。
|
|
|
|
|
* 这里会保证返回 0-23 点共 24 条数据,即使某些小时没有任何读取记录,也会返回该时间点(成功率为 null)。
|
|
|
|
|
* </p>
|
|
|
|
|
*
|
|
|
|
|
* @param type 类型:today-今日(默认),yesterday-昨日
|
|
|
|
|
* @return 24 小时的成功率趋势列表
|
|
|
|
|
*/
|
|
|
|
|
@Override
|
|
|
|
|
public List<DashboardVO.SuccessRateTrend> getSuccessRateTrends(String type) {
|
|
|
|
|
LocalDate today = LocalDate.now();
|
|
|
|
|
LocalDate yesterday = today.minusDays(1);
|
|
|
|
|
|
|
|
|
|
Map<String, Double> todayRateMap = buildSuccessRateMap(today);
|
|
|
|
|
Map<String, Double> yesterdayRateMap = buildSuccessRateMap(yesterday);
|
|
|
|
|
|
|
|
|
|
List<DashboardVO.SuccessRateTrend> trends = new ArrayList<>();
|
|
|
|
|
|
|
|
|
|
for (int hour = 0; hour < 24; hour++) {
|
|
|
|
|
String timePoint = String.format("%02d:00", hour);
|
|
|
|
|
DashboardVO.SuccessRateTrend trend = new DashboardVO.SuccessRateTrend();
|
|
|
|
|
trend.setTimePoint(timePoint);
|
|
|
|
|
trend.setSuccessRate(todayRateMap.getOrDefault(timePoint, null));
|
|
|
|
|
trend.setYesterdaySuccessRate(yesterdayRateMap.getOrDefault(timePoint, null));
|
|
|
|
|
trends.add(trend);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return trends;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Map<String, Double> buildSuccessRateMap(LocalDate targetDate) {
|
|
|
|
|
String tableName = RfidReadRecordTableHelper.getTableName(targetDate);
|
|
|
|
|
String startTime = targetDate.atStartOfDay().format(TIME_FORMATTER);
|
|
|
|
|
String endTime = targetDate.atTime(23, 59, 59).format(TIME_FORMATTER);
|
|
|
|
|
|
|
|
|
|
List<Map<String, Object>> results;
|
|
|
|
|
try {
|
|
|
|
|
results = readRecordMapper.selectSuccessRateByHour(tableName, startTime, endTime);
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
log.warn("查询成功率趋势失败,表可能不存在: {}", tableName);
|
|
|
|
|
results = Collections.emptyList();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Map<String, Double> rateMap = new HashMap<>();
|
|
|
|
|
if (CollUtil.isNotEmpty(results)) {
|
|
|
|
|
for (Map<String, Object> row : results) {
|
|
|
|
|
String timePoint = (String) row.get("timePoint");
|
|
|
|
|
if (timePoint == null) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
Object rateObj = row.get("successRate");
|
|
|
|
|
Double rate = rateObj != null ? Double.parseDouble(rateObj.toString()) : null;
|
|
|
|
|
rateMap.put(timePoint, rate);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return rateMap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 获取告警统计列表
|
|
|
|
|
* <p>
|
|
|
|
|
* 当 limit 为 null 时不限制数量,返回全部告警记录。
|
|
|
|
|
* </p>
|
|
|
|
|
*/
|
|
|
|
|
@Override
|
|
|
|
|
public List<DashboardVO.AlarmStatVO> getAlarmStats(Integer limit) {
|
|
|
|
|
// limit 为 null 时不限制数量
|
|
|
|
|
String tableName = RfidReadRecordTableHelper.getTodayTableName();
|
|
|
|
|
|
|
|
|
|
List<RfidReadRecordVo> alarmRecords;
|
|
|
|
|
try {
|
|
|
|
|
alarmRecords = readRecordMapper.selectAlarmRecordList(tableName, limit);
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
log.warn("查询告警记录失败,表可能不存在: {}", tableName);
|
|
|
|
|
alarmRecords = Collections.emptyList();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return alarmRecords.stream().map(record -> {
|
|
|
|
|
DashboardVO.AlarmStatVO alarmStat = new DashboardVO.AlarmStatVO();
|
|
|
|
|
alarmStat.setAlarmTime(record.getRecordTime() != null
|
|
|
|
|
? DateUtil.format(record.getRecordTime(), "MM-dd HH:mm")
|
|
|
|
|
: null);
|
|
|
|
|
alarmStat.setDeviceName(record.getDeviceName());
|
|
|
|
|
alarmStat.setLocation(record.getLocationAlias());
|
|
|
|
|
alarmStat.setAlarmLevel(record.getAlarmLevel());
|
|
|
|
|
alarmStat.setAlarmAction(record.getAlarmAction());
|
|
|
|
|
return alarmStat;
|
|
|
|
|
}).collect(Collectors.toList());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 获取完整看板数据
|
|
|
|
|
*/
|
|
|
|
|
@Override
|
|
|
|
|
public DashboardVO getDashboardData(Long locationId) {
|
|
|
|
|
DashboardVO dashboard = new DashboardVO();
|
|
|
|
|
dashboard.setOverview(getOverview());
|
|
|
|
|
dashboard.setLocationTree(getLocationTree());
|
|
|
|
|
dashboard.setSuccessRateTrends(getSuccessRateTrends("today"));
|
|
|
|
|
dashboard.setAlarmStats(getAlarmStats(null));
|
|
|
|
|
return dashboard;
|
|
|
|
|
}
|
|
|
|
|
}
|