From 4596a61ba8a49ab318e3e53537bd5949e2bf1be7 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Mon, 29 Dec 2025 16:06:32 +0800 Subject: [PATCH] =?UTF-8?q?feat(ExtraDevice):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=A4=96=E9=83=A8=E8=AE=BE=E5=A4=87=E7=AE=A1=E7=90=86=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 创建了 ExtraDeviceController 提供分页查询外部设备列表的API接口 - 新增 ExtraDeviceService 和 ExtraDeviceServiceImpl 实现设备查询逻辑 - 添加 ExtraDevicePageQueryReq 和 ExtraDeviceRespVO 请求响应数据模型 - 扩展 ExtraDeviceMapper 支持分页查询外部设备列表 - 实现景区名称填充和设备在线状态判断功能 - 集成 Redis 获取设备心跳时间判断在线状态 - 添加了完整的参数校验和异常处理机制 --- .../controller/pc/ExtraDeviceController.java | 44 ++++++ .../ycwl/basic/mapper/ExtraDeviceMapper.java | 9 ++ .../req/ExtraDevicePageQueryReq.java | 24 +++ .../extraDevice/resp/ExtraDeviceRespVO.java | 53 +++++++ .../basic/service/pc/ExtraDeviceService.java | 17 +++ .../pc/impl/ExtraDeviceServiceImpl.java | 140 ++++++++++++++++++ .../resources/mapper/ExtraDeviceMapper.xml | 18 ++- 7 files changed, 304 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/ycwl/basic/controller/pc/ExtraDeviceController.java create mode 100644 src/main/java/com/ycwl/basic/model/pc/extraDevice/req/ExtraDevicePageQueryReq.java create mode 100644 src/main/java/com/ycwl/basic/model/pc/extraDevice/resp/ExtraDeviceRespVO.java create mode 100644 src/main/java/com/ycwl/basic/service/pc/ExtraDeviceService.java create mode 100644 src/main/java/com/ycwl/basic/service/pc/impl/ExtraDeviceServiceImpl.java diff --git a/src/main/java/com/ycwl/basic/controller/pc/ExtraDeviceController.java b/src/main/java/com/ycwl/basic/controller/pc/ExtraDeviceController.java new file mode 100644 index 00000000..aea55a1c --- /dev/null +++ b/src/main/java/com/ycwl/basic/controller/pc/ExtraDeviceController.java @@ -0,0 +1,44 @@ +package com.ycwl.basic.controller.pc; + +import com.github.pagehelper.PageInfo; +import com.ycwl.basic.model.pc.extraDevice.req.ExtraDevicePageQueryReq; +import com.ycwl.basic.model.pc.extraDevice.resp.ExtraDeviceRespVO; +import com.ycwl.basic.service.pc.ExtraDeviceService; +import com.ycwl.basic.utils.ApiResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 外部设备管理控制器 + */ +@Slf4j +@RestController +@RequestMapping("/api/extra_device/v1") +@RequiredArgsConstructor +public class ExtraDeviceController { + + private final ExtraDeviceService extraDeviceService; + + /** + * 分页查询外部设备列表 + * + * @param req 查询请求参数,包含scenicId(可选)、pageNum、pageSize + * @return 分页查询结果,包含设备ID、景区ID、景区名称、设备名称、标识、状态、心跳时间、在线状态 + */ + @PostMapping("/page") + public ApiResponse> page(@RequestBody ExtraDevicePageQueryReq req) { + log.info("分页查询外部设备列表, scenicId: {}, pageNum: {}, pageSize: {}", + req.getScenicId(), req.getPageNum(), req.getPageSize()); + + PageInfo pageInfo = extraDeviceService.pageQuery(req); + + log.info("外部设备列表查询完成, total: {}, pages: {}", + pageInfo.getTotal(), pageInfo.getPages()); + + return ApiResponse.success(pageInfo); + } +} diff --git a/src/main/java/com/ycwl/basic/mapper/ExtraDeviceMapper.java b/src/main/java/com/ycwl/basic/mapper/ExtraDeviceMapper.java index 59cc2e3d..8d53d7c9 100644 --- a/src/main/java/com/ycwl/basic/mapper/ExtraDeviceMapper.java +++ b/src/main/java/com/ycwl/basic/mapper/ExtraDeviceMapper.java @@ -1,11 +1,20 @@ package com.ycwl.basic.mapper; import com.ycwl.basic.model.pc.device.resp.DeviceRespVO; +import com.ycwl.basic.model.pc.extraDevice.resp.ExtraDeviceRespVO; import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; import java.util.List; @Mapper public interface ExtraDeviceMapper { List listExtraDeviceByScenicId(Long scenicId); + + /** + * 分页查询外部设备列表 + * @param scenicId 景区ID (可选) + * @return 外部设备列表 + */ + List pageQuery(@Param("scenicId") Long scenicId); } diff --git a/src/main/java/com/ycwl/basic/model/pc/extraDevice/req/ExtraDevicePageQueryReq.java b/src/main/java/com/ycwl/basic/model/pc/extraDevice/req/ExtraDevicePageQueryReq.java new file mode 100644 index 00000000..3febdc81 --- /dev/null +++ b/src/main/java/com/ycwl/basic/model/pc/extraDevice/req/ExtraDevicePageQueryReq.java @@ -0,0 +1,24 @@ +package com.ycwl.basic.model.pc.extraDevice.req; + +import lombok.Data; + +/** + * 外部设备分页查询请求 + */ +@Data +public class ExtraDevicePageQueryReq { + /** + * 景区ID (支持 Long 或字符串格式的Long) + */ + private Long scenicId; + + /** + * 页码,默认1 + */ + private Integer pageNum = 1; + + /** + * 每页大小,默认20 + */ + private Integer pageSize = 20; +} diff --git a/src/main/java/com/ycwl/basic/model/pc/extraDevice/resp/ExtraDeviceRespVO.java b/src/main/java/com/ycwl/basic/model/pc/extraDevice/resp/ExtraDeviceRespVO.java new file mode 100644 index 00000000..2515c0f5 --- /dev/null +++ b/src/main/java/com/ycwl/basic/model/pc/extraDevice/resp/ExtraDeviceRespVO.java @@ -0,0 +1,53 @@ +package com.ycwl.basic.model.pc.extraDevice.resp; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import lombok.Data; + +/** + * 外部设备响应VO + * 对应文档中的 ExtraDeviceResp 结构 + */ +@Data +public class ExtraDeviceRespVO { + /** + * 设备ID (JSON输出为字符串) + */ + private Long id; + + /** + * 景区ID (JSON输出为字符串) + */ + private Long scenicId; + + /** + * 景区名称 (从景区服务获取) + */ + private String scenicName; + + /** + * 设备名称 + */ + private String name; + + /** + * 设备标识 + */ + private String ident; + + /** + * 数据库状态 + */ + private Integer status; + + /** + * 心跳时间 (格式: yyyy-MM-dd HH:mm:ss) + */ + private String keepaliveAt; + + /** + * 在线状态: 1=在线, 0=离线 + * 判断逻辑:5分钟内有心跳则在线 + */ + private Integer online; +} diff --git a/src/main/java/com/ycwl/basic/service/pc/ExtraDeviceService.java b/src/main/java/com/ycwl/basic/service/pc/ExtraDeviceService.java new file mode 100644 index 00000000..0a7354a6 --- /dev/null +++ b/src/main/java/com/ycwl/basic/service/pc/ExtraDeviceService.java @@ -0,0 +1,17 @@ +package com.ycwl.basic.service.pc; + +import com.github.pagehelper.PageInfo; +import com.ycwl.basic.model.pc.extraDevice.req.ExtraDevicePageQueryReq; +import com.ycwl.basic.model.pc.extraDevice.resp.ExtraDeviceRespVO; + +/** + * 外部设备服务接口 + */ +public interface ExtraDeviceService { + /** + * 分页查询外部设备列表 + * @param req 查询请求参数 + * @return 分页结果 + */ + PageInfo pageQuery(ExtraDevicePageQueryReq req); +} diff --git a/src/main/java/com/ycwl/basic/service/pc/impl/ExtraDeviceServiceImpl.java b/src/main/java/com/ycwl/basic/service/pc/impl/ExtraDeviceServiceImpl.java new file mode 100644 index 00000000..81825a86 --- /dev/null +++ b/src/main/java/com/ycwl/basic/service/pc/impl/ExtraDeviceServiceImpl.java @@ -0,0 +1,140 @@ +package com.ycwl.basic.service.pc.impl; + +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO; +import com.ycwl.basic.mapper.ExtraDeviceMapper; +import com.ycwl.basic.model.pc.extraDevice.req.ExtraDevicePageQueryReq; +import com.ycwl.basic.model.pc.extraDevice.resp.ExtraDeviceRespVO; +import com.ycwl.basic.repository.ScenicRepository; +import com.ycwl.basic.service.pc.ExtraDeviceService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; + +/** + * 外部设备服务实现 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class ExtraDeviceServiceImpl implements ExtraDeviceService { + + private final ExtraDeviceMapper extraDeviceMapper; + private final ScenicRepository scenicRepository; + private final RedisTemplate redisTemplate; + + /** + * Redis Key前缀:ext_device:online:{ident} + */ + private static final String REDIS_KEY_PREFIX = "ext_device:online:"; + + /** + * 在线判断阈值:5分钟(300秒) + */ + private static final long ONLINE_THRESHOLD_SECONDS = 300; + + /** + * 时间格式:yyyy-MM-dd HH:mm:ss + */ + private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss"; + + @Override + public PageInfo pageQuery(ExtraDevicePageQueryReq req) { + // 参数校验与默认值设置 + if (req.getPageNum() == null || req.getPageNum() < 1) { + req.setPageNum(1); + } + if (req.getPageSize() == null || req.getPageSize() < 1) { + req.setPageSize(20); + } + + // 使用PageHelper进行分页 + PageHelper.startPage(req.getPageNum(), req.getPageSize()); + + // 查询数据库获取设备基础信息 + List devices = extraDeviceMapper.pageQuery(req.getScenicId()); + + // 遍历设备列表,填充景区名称和在线状态 + for (ExtraDeviceRespVO device : devices) { + // 1. 填充景区名称 + fillScenicName(device); + + // 2. 填充在线状态和心跳时间 + fillOnlineStatus(device); + } + + return new PageInfo<>(devices); + } + + /** + * 填充景区名称 + * 从景区服务获取景区信息,优先从远程服务获取,失败则降级读取缓存 + */ + private void fillScenicName(ExtraDeviceRespVO device) { + try { + if (device.getScenicId() != null) { + ScenicV2DTO scenic = scenicRepository.getScenicBasic(device.getScenicId()); + if (scenic != null && StringUtils.isNotBlank(scenic.getName())) { + device.setScenicName(scenic.getName()); + } else { + device.setScenicName("未知景区"); + } + } else { + device.setScenicName("未知景区"); + } + } catch (Exception e) { + log.warn("获取景区名称失败, scenicId: {}, error: {}", device.getScenicId(), e.getMessage()); + device.setScenicName("未知景区"); + } + } + + /** + * 填充在线状态和心跳时间 + * 从Redis获取设备心跳时间戳,判断是否在线(5分钟内有心跳) + */ + private void fillOnlineStatus(ExtraDeviceRespVO device) { + try { + if (StringUtils.isBlank(device.getIdent())) { + device.setOnline(0); + device.setKeepaliveAt(""); + return; + } + + // 从Redis获取心跳时间戳 + String redisKey = REDIS_KEY_PREFIX + device.getIdent(); + String timestampStr = redisTemplate.opsForValue().get(redisKey); + + if (StringUtils.isBlank(timestampStr) || !StringUtils.isNumeric(timestampStr)) { + // Redis中没有心跳记录或格式不正确 + device.setOnline(0); + device.setKeepaliveAt(""); + return; + } + + // 解析时间戳(秒级) + long keepaliveTimestamp = Long.parseLong(timestampStr); + long currentTimestamp = System.currentTimeMillis() / 1000; + + // 判断是否在线(5分钟内有心跳) + boolean isOnline = (currentTimestamp - keepaliveTimestamp) < ONLINE_THRESHOLD_SECONDS; + device.setOnline(isOnline ? 1 : 0); + + // 格式化心跳时间 + Date keepaliveDate = new Date(keepaliveTimestamp * 1000); + SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT); + device.setKeepaliveAt(sdf.format(keepaliveDate)); + + } catch (Exception e) { + log.warn("获取设备在线状态失败, ident: {}, error: {}", device.getIdent(), e.getMessage()); + device.setOnline(0); + device.setKeepaliveAt(""); + } + } +} diff --git a/src/main/resources/mapper/ExtraDeviceMapper.xml b/src/main/resources/mapper/ExtraDeviceMapper.xml index a7a7d888..9b945aad 100644 --- a/src/main/resources/mapper/ExtraDeviceMapper.xml +++ b/src/main/resources/mapper/ExtraDeviceMapper.xml @@ -4,8 +4,24 @@ + + \ No newline at end of file