diff --git a/ruoyi-modules/hw-rfid/src/main/java/org/dromara/rfid/controller/DashboardController.java b/ruoyi-modules/hw-rfid/src/main/java/org/dromara/rfid/controller/DashboardController.java
new file mode 100644
index 0000000..c6e1d06
--- /dev/null
+++ b/ruoyi-modules/hw-rfid/src/main/java/org/dromara/rfid/controller/DashboardController.java
@@ -0,0 +1,132 @@
+package org.dromara.rfid.controller;
+
+import lombok.RequiredArgsConstructor;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.web.core.BaseController;
+import org.dromara.rfid.domain.vo.DashboardVO;
+import org.dromara.rfid.service.IDashboardService;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+/**
+ * RFID 监控看板接口
+ *
+ * 提供前端看板定时刷新所需的数据接口,分为三个独立接口:
+ *
+ * - 实时统计接口(按秒刷新):顶部统计 + 告警列表
+ * - 设备状态接口(定时刷新):设备树 + 设备最新读取记录
+ * - 成功率趋势接口(按小时统计):成功率折线图
+ *
+ *
+ *
+ * @author zch
+ * @date 2025-11-26
+ */
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/rfid/dashboard")
+public class DashboardController extends BaseController {
+
+ private final IDashboardService dashboardService;
+
+ /**
+ * 【接口1】实时统计数据(按秒刷新)
+ *
+ * 包含:顶部统计(设备数量、在线数量、离线数量、告警数量)+ 告警统计列表
+ *
+ *
+ * 当前实现:告警列表默认不限制数量,返回全部告警记录,供前端滚动显示。
+ *
+ *
+ * @param alarmLimit 告警列表限制数量(可选,不传则不限制)
+ * @return 实时统计数据
+ */
+ @GetMapping("/realtime")
+ public R getRealtimeStats(
+ @RequestParam(required = false) Integer alarmLimit) {
+ return R.ok(dashboardService.getRealtimeStats(alarmLimit));
+ }
+
+ /**
+ * 【接口2】位置树(含设备信息)
+ *
+ * 返回完整的位置树结构,用于前端构建设备树并滚动显示。
+ *
+ *
+ * 当前实现:
+ *
+ * - 返回所有位置的树形结构;
+ * - 位置类型为 3(工位)时表示设备节点,包含设备ID、编号、名称、在线状态、告警状态;
+ * - 不返回设备的读取记录数据,具体记录由 C# WebSocket 实时推送给前端;
+ * - 前端通过 deviceId 匹配 WebSocket 数据。
+ *
+ *
+ *
+ * @return 位置树列表(顶级节点列表)
+ */
+ @GetMapping("/deviceStatus")
+ public R> getLocationTree() {
+ return R.ok(dashboardService.getLocationTree());
+ }
+
+ /**
+ * 【接口3】成功率趋势(按小时统计)
+ *
+ * 按小时统计读取成功率,用于折线图展示,不需要频繁刷新。
+ * 当前实现:
+ *
+ * - 前端无需传任何查询参数;
+ * - 后端默认以当前日期统计“今日”0-23 点成功率;
+ * - 同时附带“昨日同一小时”的成功率,用于前端对比展示。
+ *
+ *
+ *
+ * @param type 预留参数,当前实现忽略,前端可不传
+ * @return 成功率趋势数据(固定 24 条,按小时排序)
+ */
+ @GetMapping("/successRate")
+ public R> getSuccessRateTrends(
+ @RequestParam(required = false, defaultValue = "today") String type) {
+ return R.ok(dashboardService.getSuccessRateTrends(type));
+ }
+
+ // ==================== 以下为原有接口(保留) ====================
+
+ /**
+ * 获取完整看板数据
+ *
+ * @param locationId 位置ID(可选)
+ * @return 完整看板数据
+ */
+ @GetMapping("/data")
+ public R getDashboardData(
+ @RequestParam(required = false) Long locationId) {
+ return R.ok(dashboardService.getDashboardData(locationId));
+ }
+
+ /**
+ * 获取统计概览
+ *
+ * @return 统计概览数据
+ */
+ @GetMapping("/overview")
+ public R getOverview() {
+ return R.ok(dashboardService.getOverview());
+ }
+
+ /**
+ * 获取告警统计列表
+ *
+ * @param limit 限制数量(默认10条)
+ * @return 告警统计列表
+ */
+ @GetMapping("/alarmStats")
+ public R> getAlarmStats(
+ @RequestParam(required = false, defaultValue = "10") Integer limit) {
+ return R.ok(dashboardService.getAlarmStats(limit));
+ }
+}
diff --git a/ruoyi-modules/hw-rfid/src/main/java/org/dromara/rfid/domain/vo/DashboardVO.java b/ruoyi-modules/hw-rfid/src/main/java/org/dromara/rfid/domain/vo/DashboardVO.java
new file mode 100644
index 0000000..6cc38ba
--- /dev/null
+++ b/ruoyi-modules/hw-rfid/src/main/java/org/dromara/rfid/domain/vo/DashboardVO.java
@@ -0,0 +1,258 @@
+package org.dromara.rfid.domain.vo;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 看板数据 VO
+ *
+ * @author zch
+ * @date 2025-11-26
+ */
+@Data
+public class DashboardVO implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ // ==================== 顶层属性(用于 getDashboardData 返回) ====================
+
+ /**
+ * 统计概览
+ */
+ private StatisticsOverview overview;
+
+ /**
+ * 位置树(含设备信息)
+ */
+ private List locationTree;
+
+ /**
+ * 成功率趋势数据
+ */
+ private List successRateTrends;
+
+ /**
+ * 告警统计列表
+ */
+ private List alarmStats;
+
+ // ==================== 内部类定义 ====================
+
+ /**
+ * 实时统计数据(按秒刷新)
+ *
+ * 包含:顶部统计 + 告警统计列表
+ *
+ */
+ @Data
+ public static class RealtimeStats implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 统计概览
+ */
+ private StatisticsOverview overview;
+
+ /**
+ * 告警统计列表
+ */
+ private List alarmStats;
+ }
+
+ /**
+ * 统计概览
+ */
+ @Data
+ public static class StatisticsOverview implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 设备总数
+ */
+ private Long deviceTotal;
+
+ /**
+ * 在线数量
+ */
+ private Long onlineCount;
+
+ /**
+ * 离线数量
+ */
+ private Long offlineCount;
+
+ /**
+ * 告警数量
+ */
+ private Long alarmCount;
+ }
+
+ /**
+ * 位置树节点
+ *
+ * 用于构建位置树,当 locationType = 3 时表示设备节点,包含设备信息。
+ * 前端通过 deviceId 匹配 WebSocket 实时数据。
+ *
+ */
+ @Data
+ public static class LocationTreeNode implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ // ==================== 位置信息 ====================
+
+ /**
+ * 位置ID
+ */
+ private Long id;
+
+ /**
+ * 位置编号
+ */
+ private String locationCode;
+
+ /**
+ * 位置别名
+ */
+ private String locationAlias;
+
+ /**
+ * 位置类型 (1-车间; 2-工序; 3-工位/设备)
+ */
+ private String locationType;
+
+ /**
+ * 父级位置ID
+ */
+ private Long parentId;
+
+ // ==================== 设备信息(仅当 locationType=3 时有值) ====================
+
+ /**
+ * 设备ID(前端用于匹配 WebSocket 数据)
+ */
+ private Long deviceId;
+
+ /**
+ * 设备编号
+ */
+ private String deviceCode;
+
+ /**
+ * 设备名称
+ */
+ private String deviceName;
+
+ /**
+ * 在线状态 (1-在线; 0-离线)
+ */
+ private String onlineStatus;
+
+ /**
+ * 告警状态 (0-正常; 1-告警)
+ */
+ private String alarmStatus;
+
+ // ==================== 子节点 ====================
+
+ /**
+ * 子节点列表
+ */
+ private List children;
+ }
+
+ // ==================== 以下为已废弃的实体类,保留以备参考 ====================
+
+ /*
+ * 设备状态 VO(已废弃,改用 LocationTreeNode)
+ *
+ @Data
+ public static class DeviceStatusVO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ private Long deviceId;
+ private String deviceCode;
+ private String deviceName;
+ private Long locationId;
+ private String locationAlias;
+ private String onlineStatus;
+ private String alarmStatus;
+ private LatestReadRecord latestRecord;
+ }
+ */
+
+ /*
+ * 最新读取记录(已废弃,读取记录改由 WebSocket 实时推送)
+ *
+ @Data
+ public static class LatestReadRecord implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ private String stationName;
+ private String barcode;
+ private String recordTime;
+ private String readStatus;
+ }
+ */
+
+ /**
+ * 成功率趋势
+ */
+ @Data
+ public static class SuccessRateTrend implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 时间点(小时,如 "09:00")
+ */
+ private String timePoint;
+
+ /**
+ * 成功率(百分比,如 98.5)
+ */
+ private Double successRate;
+
+ private Double yesterdaySuccessRate;
+ }
+
+ /**
+ * 告警统计
+ */
+ @Data
+ public static class AlarmStatVO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 告警时间
+ */
+ private String alarmTime;
+
+ /**
+ * 设备名称
+ */
+ private String deviceName;
+
+ /**
+ * 位置
+ */
+ private String location;
+
+ /**
+ * 告警级别
+ */
+ private String alarmLevel;
+
+ /**
+ * 告警行为
+ */
+ private String alarmAction;
+ }
+}
diff --git a/ruoyi-modules/hw-rfid/src/main/java/org/dromara/rfid/mapper/RfidLocationMapper.java b/ruoyi-modules/hw-rfid/src/main/java/org/dromara/rfid/mapper/RfidLocationMapper.java
index 47a498b..d3f303f 100644
--- a/ruoyi-modules/hw-rfid/src/main/java/org/dromara/rfid/mapper/RfidLocationMapper.java
+++ b/ruoyi-modules/hw-rfid/src/main/java/org/dromara/rfid/mapper/RfidLocationMapper.java
@@ -100,5 +100,16 @@ public interface RfidLocationMapper extends BaseMapperPlus queryWrapper);
+ /**
+ * 批量更新子节点的 ancestors(用于父节点变更时)
+ *
+ * @param parentId 当前节点ID
+ * @param newAncestors 新的祖先路径
+ * @param oldAncestors 旧的祖先路径
+ * @return 影响行数
+ */
+ int updateChildrenAncestors(@Param("parentId") Long parentId,
+ @Param("newAncestors") String newAncestors,
+ @Param("oldAncestors") String oldAncestors);
}
diff --git a/ruoyi-modules/hw-rfid/src/main/java/org/dromara/rfid/service/IDashboardService.java b/ruoyi-modules/hw-rfid/src/main/java/org/dromara/rfid/service/IDashboardService.java
new file mode 100644
index 0000000..2e74d6a
--- /dev/null
+++ b/ruoyi-modules/hw-rfid/src/main/java/org/dromara/rfid/service/IDashboardService.java
@@ -0,0 +1,81 @@
+package org.dromara.rfid.service;
+
+import org.dromara.rfid.domain.vo.DashboardVO;
+
+import java.util.List;
+
+/**
+ * 看板数据服务接口
+ *
+ * 提供三个独立接口:
+ *
+ * - 实时统计接口(按秒刷新):顶部统计 + 告警列表
+ * - 设备状态接口(定时刷新):设备树 + 设备最新读取记录
+ * - 成功率趋势接口(按小时统计):成功率折线图
+ *
+ *
+ *
+ * @author zch
+ * @date 2025-11-26
+ */
+public interface IDashboardService {
+
+ /**
+ * 【接口1】获取实时统计数据(按秒刷新)
+ *
+ * 包含:顶部统计(设备数量、在线数量、离线数量、告警数量)+ 告警统计列表
+ *
+ *
+ * @param alarmLimit 告警列表限制数量
+ * @return 实时统计数据
+ */
+ DashboardVO.RealtimeStats getRealtimeStats(Integer alarmLimit);
+
+ /**
+ * 【接口2】获取位置树(含设备信息)
+ *
+ * 返回完整的位置树结构,用于前端构建设备树并滚动显示。
+ * 位置类型为 3 时表示设备节点,包含设备信息。
+ * 不返回读取记录数据,具体记录由 WebSocket 实时推送。
+ *
+ *
+ * @return 位置树列表(顶级节点列表)
+ */
+ List getLocationTree();
+
+ /**
+ * 【接口3】获取成功率趋势(按小时统计)
+ *
+ * 不需要频繁刷新,当前实现固定返回“今日+昨日”的成功率趋势,用于前端对比展示。
+ *
+ *
+ * @param type 预留参数,当前实现忽略,前端可不传
+ * @return 成功率趋势数据
+ */
+ List getSuccessRateTrends(String type);
+
+ // ==================== 以下为原有接口(保留) ====================
+
+ /**
+ * 获取统计概览
+ *
+ * @return 统计概览数据
+ */
+ DashboardVO.StatisticsOverview getOverview();
+
+ /**
+ * 获取告警统计列表
+ *
+ * @param limit 限制数量
+ * @return 告警统计列表
+ */
+ List getAlarmStats(Integer limit);
+
+ /**
+ * 获取完整看板数据
+ *
+ * @param locationId 位置ID(可选)
+ * @return 完整看板数据
+ */
+ DashboardVO getDashboardData(Long locationId);
+}
diff --git a/ruoyi-modules/hw-rfid/src/main/java/org/dromara/rfid/service/impl/DashboardServiceImpl.java b/ruoyi-modules/hw-rfid/src/main/java/org/dromara/rfid/service/impl/DashboardServiceImpl.java
new file mode 100644
index 0000000..cc10617
--- /dev/null
+++ b/ruoyi-modules/hw-rfid/src/main/java/org/dromara/rfid/service/impl/DashboardServiceImpl.java
@@ -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;
+
+/**
+ * 看板数据服务实现类
+ *
+ * 提供三个独立接口:
+ *
+ * - 实时统计接口(按秒刷新):顶部统计 + 告警列表
+ * - 设备状态接口(定时刷新):设备树 + 设备最新读取记录
+ * - 成功率趋势接口(按小时统计):成功率折线图
+ *
+ *
+ *
+ * @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;
+ }
+
+ /**
+ * 获取位置树(含设备信息)
+ *
+ * 返回完整的位置树结构,位置类型为 3 时表示设备节点,关联设备信息。
+ * 不返回读取记录数据,具体记录由 WebSocket 实时推送。
+ *
+ */
+ @Override
+ public List getLocationTree() {
+ // 1. 查询所有已标识的位置
+ QueryWrapper locationWrapper = Wrappers.query(RfidLocation.class);
+ locationWrapper.eq("t.is_marked", "1");
+ locationWrapper.orderByAsc("t.parent_id", "t.id");
+ List locations = locationMapper.selectCustomRfidLocationVoList(locationWrapper);
+
+ if (CollUtil.isEmpty(locations)) {
+ return Collections.emptyList();
+ }
+
+ // 2. 查询所有已标识的设备,构建 locationId -> 设备列表 的映射
+ QueryWrapper deviceWrapper = Wrappers.query(RfidDevice.class);
+ deviceWrapper.eq("t.is_marked", "1");
+ List devices = deviceMapper.selectCustomRfidDeviceVoList(deviceWrapper);
+
+ Map> devicesByLocation = devices.stream()
+ .filter(d -> d.getLocationId() != null)
+ .collect(Collectors.groupingBy(RfidDeviceVo::getLocationId));
+
+ // 3. 构建位置树节点
+ List allNodes = new ArrayList<>();
+ Map 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 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 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;
+ }
+
+ /**
+ * 获取成功率趋势
+ *
+ * 按小时统计指定日期(今日/昨日)的读取成功率,用于前端折线图展示。
+ * 这里会保证返回 0-23 点共 24 条数据,即使某些小时没有任何读取记录,也会返回该时间点(成功率为 null)。
+ *
+ *
+ * @param type 类型:today-今日(默认),yesterday-昨日
+ * @return 24 小时的成功率趋势列表
+ */
+ @Override
+ public List getSuccessRateTrends(String type) {
+ LocalDate today = LocalDate.now();
+ LocalDate yesterday = today.minusDays(1);
+
+ Map todayRateMap = buildSuccessRateMap(today);
+ Map yesterdayRateMap = buildSuccessRateMap(yesterday);
+
+ List 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 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