feat(ExtraDevice): 添加外部设备管理功能

- 创建了 ExtraDeviceController 提供分页查询外部设备列表的API接口
- 新增 ExtraDeviceService 和 ExtraDeviceServiceImpl 实现设备查询逻辑
- 添加 ExtraDevicePageQueryReq 和 ExtraDeviceRespVO 请求响应数据模型
- 扩展 ExtraDeviceMapper 支持分页查询外部设备列表
- 实现景区名称填充和设备在线状态判断功能
- 集成 Redis 获取设备心跳时间判断在线状态
- 添加了完整的参数校验和异常处理机制
This commit is contained in:
2025-12-29 16:06:32 +08:00
parent d6780ccb7a
commit 4596a61ba8
7 changed files with 304 additions and 1 deletions

View File

@@ -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<PageInfo<ExtraDeviceRespVO>> page(@RequestBody ExtraDevicePageQueryReq req) {
log.info("分页查询外部设备列表, scenicId: {}, pageNum: {}, pageSize: {}",
req.getScenicId(), req.getPageNum(), req.getPageSize());
PageInfo<ExtraDeviceRespVO> pageInfo = extraDeviceService.pageQuery(req);
log.info("外部设备列表查询完成, total: {}, pages: {}",
pageInfo.getTotal(), pageInfo.getPages());
return ApiResponse.success(pageInfo);
}
}

View File

@@ -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<DeviceRespVO> listExtraDeviceByScenicId(Long scenicId);
/**
* 分页查询外部设备列表
* @param scenicId 景区ID (可选)
* @return 外部设备列表
*/
List<ExtraDeviceRespVO> pageQuery(@Param("scenicId") Long scenicId);
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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<ExtraDeviceRespVO> pageQuery(ExtraDevicePageQueryReq req);
}

View File

@@ -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<String, String> 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<ExtraDeviceRespVO> 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<ExtraDeviceRespVO> 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("");
}
}
}

View File

@@ -4,8 +4,24 @@
<select id="listExtraDeviceByScenicId" resultType="com.ycwl.basic.model.pc.device.resp.DeviceRespVO">
select d.id, d.ident as no, d.scenic_id, d.name, d.status
from extra_device d
where d.scenic_id = #{scenicId}
and d.status = 1
</select>
<select id="pageQuery" resultType="com.ycwl.basic.model.pc.extraDevice.resp.ExtraDeviceRespVO">
select
d.id,
d.scenic_id as scenicId,
d.name,
d.ident,
d.status
from extra_device d
<where>
<if test="scenicId != null">
and d.scenic_id = #{scenicId}
</if>
</where>
order by d.id desc
</select>
</mapper>