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】位置树(含设备信息) + *

+ * 返回完整的位置树结构,用于前端构建设备树并滚动显示。 + *

+ *

+ * 当前实现: + *

+ *

+ * + * @return 位置树列表(顶级节点列表) + */ + @GetMapping("/deviceStatus") + public R> getLocationTree() { + return R.ok(dashboardService.getLocationTree()); + } + + /** + * 【接口3】成功率趋势(按小时统计) + *

+ * 按小时统计读取成功率,用于折线图展示,不需要频繁刷新。 + * 当前实现: + *

+ *

+ * + * @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> results; + try { + results = readRecordMapper.selectSuccessRateByHour(tableName, startTime, endTime); + } catch (Exception e) { + log.warn("查询成功率趋势失败,表可能不存在: {}", tableName); + results = Collections.emptyList(); + } + + Map rateMap = new HashMap<>(); + if (CollUtil.isNotEmpty(results)) { + for (Map 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; + } + + /** + * 获取告警统计列表 + *

+ * 当 limit 为 null 时不限制数量,返回全部告警记录。 + *

+ */ + @Override + public List getAlarmStats(Integer limit) { + // limit 为 null 时不限制数量 + String tableName = RfidReadRecordTableHelper.getTodayTableName(); + + List 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; + } +} diff --git a/ruoyi-modules/hw-rfid/src/main/java/org/dromara/rfid/service/impl/RfidDeviceServiceImpl.java b/ruoyi-modules/hw-rfid/src/main/java/org/dromara/rfid/service/impl/RfidDeviceServiceImpl.java index 4f8fec8..056ec07 100644 --- a/ruoyi-modules/hw-rfid/src/main/java/org/dromara/rfid/service/impl/RfidDeviceServiceImpl.java +++ b/ruoyi-modules/hw-rfid/src/main/java/org/dromara/rfid/service/impl/RfidDeviceServiceImpl.java @@ -11,6 +11,7 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import org.dromara.rfid.domain.bo.RfidDeviceBo; import org.dromara.rfid.domain.vo.RfidDeviceVo; import org.dromara.rfid.domain.RfidDevice; @@ -18,7 +19,9 @@ import org.dromara.rfid.domain.RfidReadRecord; import org.dromara.rfid.mapper.RfidDeviceMapper; import org.dromara.rfid.mapper.RfidReadRecordMapper; import org.dromara.rfid.service.IRfidDeviceService; +import org.dromara.rfid.helper.RfidReadRecordTableHelper; +import java.util.Date; import java.util.List; import java.util.Map; import java.util.Collection; @@ -120,6 +123,7 @@ public class RfidDeviceServiceImpl implements IRfidDeviceService { * @return 是否新增成功 */ @Override + @Transactional(rollbackFor = Exception.class) public Boolean insertByBo(RfidDeviceBo bo) { RfidDevice add = MapstructUtils.convert(bo, RfidDevice.class); validEntityBeforeSave(add); @@ -137,6 +141,7 @@ public class RfidDeviceServiceImpl implements IRfidDeviceService { * @return 是否修改成功 */ @Override + @Transactional(rollbackFor = Exception.class) public Boolean updateByBo(RfidDeviceBo bo) { RfidDevice update = MapstructUtils.convert(bo, RfidDevice.class); validEntityBeforeSave(update); @@ -171,14 +176,23 @@ public class RfidDeviceServiceImpl implements IRfidDeviceService { * @return 是否删除成功 */ @Override + @Transactional(rollbackFor = Exception.class) public Boolean deleteWithValidByIds(Collection ids, Boolean isValid) { if (isValid && ids != null && !ids.isEmpty()) { // 校验是否存在关联的读取记录,防止产生孤儿记录 - boolean existsRecord = rfidReadRecordMapper.existsRfidReadRecord( - Wrappers.lambdaQuery().in(RfidReadRecord::getDeviceId, ids) - ); - if (existsRecord) { - throw new ServiceException("存在关联读取记录的设备,无法删除"); + // 获取最近30天实际存在的分表进行检查 + Date endDate = new Date(); + Date startDate = cn.hutool.core.date.DateUtil.offsetDay(endDate, -30); + List tableNames = RfidReadRecordTableHelper.getExistingTableNames(startDate, endDate); + // 只有存在分表时才进行检查 + if (!tableNames.isEmpty()) { + boolean existsRecord = rfidReadRecordMapper.existsRfidReadRecordMultiTable( + tableNames, + Wrappers.lambdaQuery().in(RfidReadRecord::getDeviceId, ids) + ); + if (existsRecord) { + throw new ServiceException("存在关联读取记录的设备,无法删除"); + } } } return baseMapper.deleteByIds(ids) > 0; diff --git a/ruoyi-modules/hw-rfid/src/main/java/org/dromara/rfid/service/impl/RfidLocationServiceImpl.java b/ruoyi-modules/hw-rfid/src/main/java/org/dromara/rfid/service/impl/RfidLocationServiceImpl.java index e486e91..22f49eb 100644 --- a/ruoyi-modules/hw-rfid/src/main/java/org/dromara/rfid/service/impl/RfidLocationServiceImpl.java +++ b/ruoyi-modules/hw-rfid/src/main/java/org/dromara/rfid/service/impl/RfidLocationServiceImpl.java @@ -7,12 +7,15 @@ import lombok.extern.slf4j.Slf4j; import org.dromara.common.core.exception.ServiceException; import org.dromara.common.core.utils.MapstructUtils; import org.dromara.common.core.utils.StringUtils; +import org.dromara.rfid.domain.RfidDevice; import org.dromara.rfid.domain.RfidLocation; import org.dromara.rfid.domain.bo.RfidLocationBo; import org.dromara.rfid.domain.vo.RfidLocationVo; +import org.dromara.rfid.mapper.RfidDeviceMapper; import org.dromara.rfid.mapper.RfidLocationMapper; import org.dromara.rfid.service.IRfidLocationService; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.util.Collection; import java.util.List; @@ -31,6 +34,8 @@ public class RfidLocationServiceImpl implements IRfidLocationService { private final RfidLocationMapper baseMapper; + private final RfidDeviceMapper rfidDeviceMapper; + /** * 查询位置信息 * @@ -79,6 +84,7 @@ public class RfidLocationServiceImpl implements IRfidLocationService { * @return 是否新增成功 */ @Override + @Transactional(rollbackFor = Exception.class) public Boolean insertByBo(RfidLocationBo bo) { RfidLocation add = MapstructUtils.convert(bo, RfidLocation.class); validEntityBeforeSave(add); @@ -110,6 +116,7 @@ public class RfidLocationServiceImpl implements IRfidLocationService { * @return 是否修改成功 */ @Override + @Transactional(rollbackFor = Exception.class) public Boolean updateByBo(RfidLocationBo bo) { RfidLocation update = MapstructUtils.convert(bo, RfidLocation.class); validEntityBeforeSave(update); @@ -167,21 +174,11 @@ public class RfidLocationServiceImpl implements IRfidLocationService { } /** - * 递归更新所有子节点的ancestors + * 批量更新所有子节点的 ancestors(一次性 SQL,性能更优) */ private void updateChildrenAncestors(Long currentId, String newAncestors, String oldAncestors) { - // 查出所有以旧ancestors开头且包含当前id的子节点 (简单起见,这里仅演示逻辑,实际海量数据需优化) - // RuoYi标准做法通常是查出所有children,然后替换前缀 - // 这里使用数据库函数或全量更新 - // 也可以查出所有直属子节点递归更新 - - List children = baseMapper.selectList(new LambdaQueryWrapper() - .apply("find_in_set({0}, ancestors)", currentId)); - - for (RfidLocation child : children) { - child.setAncestors(child.getAncestors().replaceFirst(oldAncestors, newAncestors)); - baseMapper.updateById(child); - } + // 使用单条 SQL 批量更新所有后代节点的 ancestors,避免逐条更新 + baseMapper.updateChildrenAncestors(currentId, newAncestors, oldAncestors); } /** @@ -207,12 +204,21 @@ public class RfidLocationServiceImpl implements IRfidLocationService { * @return 是否删除成功 */ @Override + @Transactional(rollbackFor = Exception.class) public Boolean deleteWithValidByIds(Collection ids, Boolean isValid) { if(isValid){ for (Long id : ids) { if (baseMapper.exists(new LambdaQueryWrapper().eq(RfidLocation::getParentId, id))) { throw new ServiceException("存在子节点,无法删除"); } + + // 校验是否存在绑定设备,防止删除仍被设备引用的位置 + boolean existsDevice = rfidDeviceMapper.existsRfidDevice( + Wrappers.lambdaQuery().eq(RfidDevice::getLocationId, id) + ); + if (existsDevice) { + throw new ServiceException("存在绑定设备,无法删除"); + } } } return baseMapper.deleteByIds(ids) > 0; diff --git a/ruoyi-modules/hw-rfid/src/main/resources/mapper/rfid/RfidLocationMapper.xml b/ruoyi-modules/hw-rfid/src/main/resources/mapper/rfid/RfidLocationMapper.xml index 4e69cf6..c39ab4f 100644 --- a/ruoyi-modules/hw-rfid/src/main/resources/mapper/rfid/RfidLocationMapper.xml +++ b/ruoyi-modules/hw-rfid/src/main/resources/mapper/rfid/RfidLocationMapper.xml @@ -165,5 +165,11 @@ + + + update rfid_location + set ancestors = REPLACE(ancestors, #{oldAncestors}, #{newAncestors}) + where find_in_set(#{parentId}, ancestors) +