feat: 新增 TDengine 多数据源与 MQTT v5 客户端支持
- 新增 DataSourceConfig 配置类,支持 MySQL(主)与 TDengine(时序)多数据源,使用 Druid 手动配置以兼容 Spring Boot 4.x
- 重构 MQTT 配置,从 spring-integration-mqtt 迁移至 Paho MQTT v5 原生客户端,实现消息发布/订阅与优雅关闭
- 重构 MqttMessageHandler 与 MqttProducer,适配 v5 客户端回调,优化设备状态消息处理逻辑(online/offline 字段调整)
- 新增设备修改昵称接口(PUT /api/v1/devices/{id})与 TDengine 心跳最后在线时间查询接口
- 设备列表查询填充在线状态(Redis),增强设备管理功能
- 配置缓存预热失败时仅记录日志,避免数据库不可用导致应用启动失败
- 更新开发与生产环境配置,添加 TDengine 数据源及 MQTT 认证信息
This commit is contained in:
@@ -30,6 +30,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
@@ -82,6 +83,18 @@ public class DeviceController {
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@Operation(summary = "修改设备(仅允许修改设备昵称)")
|
||||
@PutMapping("/{id}")
|
||||
public Result<Void> updateDevice(
|
||||
@Parameter(description = "设备ID") @PathVariable Long id,
|
||||
@RequestBody SnDeviceInfo device) {
|
||||
boolean success = deviceService.updateSn(id, device.getSnName());
|
||||
if (!success) {
|
||||
return Result.failed("设备不存在或修改失败");
|
||||
}
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
// ===================== 设备系统信息(分面板独立接口) =====================
|
||||
|
||||
@Operation(summary = "设备基本信息")
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
package com.youlai.boot.device.controller;
|
||||
|
||||
import com.youlai.boot.common.result.Result;
|
||||
import com.youlai.boot.device.model.vo.DeviceHeartbeatVO;
|
||||
import com.youlai.boot.device.model.vo.DeviceOnlineVO;
|
||||
import com.youlai.boot.device.service.DeviceOnlineService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
@@ -15,6 +18,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.sql.Timestamp;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -29,12 +33,21 @@ import java.util.Map;
|
||||
@Tag(name = "设备在线状态")
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/device/online")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
@ConditionalOnProperty(prefix = "mqtt", name = "enabled", havingValue = "true")
|
||||
public class DeviceOnlineController {
|
||||
|
||||
private final DeviceOnlineService deviceOnlineService;
|
||||
|
||||
private final JdbcTemplate tdengineJdbcTemplate;
|
||||
|
||||
public DeviceOnlineController(
|
||||
DeviceOnlineService deviceOnlineService,
|
||||
@Qualifier("tdengineJdbcTemplate") JdbcTemplate tdengineJdbcTemplate) {
|
||||
this.deviceOnlineService = deviceOnlineService;
|
||||
this.tdengineJdbcTemplate = tdengineJdbcTemplate;
|
||||
}
|
||||
|
||||
@Operation(summary = "查询单个设备在线状态")
|
||||
@GetMapping("/status")
|
||||
public Result<DeviceOnlineVO> getOnlineStatus(
|
||||
@@ -49,4 +62,35 @@ public class DeviceOnlineController {
|
||||
return Result.success(deviceOnlineService.getOnlineStatusBySns(sns));
|
||||
}
|
||||
|
||||
@Operation(summary = "通过SN查询设备最后在线时间(TDengine心跳)")
|
||||
@GetMapping("/heartbeat/last")
|
||||
public Result<DeviceHeartbeatVO> getLastHeartbeat(
|
||||
@Parameter(description = "设备序列号") @RequestParam String sn) {
|
||||
if (sn == null || sn.isBlank()) {
|
||||
return Result.failed("设备SN不能为空");
|
||||
}
|
||||
// 从超级表 device_heartbeat 中按 tag sn 取最后一条记录
|
||||
String sql = "SELECT ts, online, ip, rssi, screen_on FROM device_heartbeat WHERE sn = ? ORDER BY ts DESC LIMIT 1";
|
||||
try {
|
||||
List<DeviceHeartbeatVO> list = tdengineJdbcTemplate.query(sql, (rs, rowNum) -> {
|
||||
DeviceHeartbeatVO vo = new DeviceHeartbeatVO();
|
||||
vo.setSn(sn);
|
||||
Timestamp ts = rs.getTimestamp("ts");
|
||||
vo.setLastTime(ts != null ? ts.toLocalDateTime() : null);
|
||||
vo.setOnline(rs.getObject("online") != null ? (rs.getBoolean("online") ? 1 : 0) : null);
|
||||
vo.setIp(rs.getString("ip"));
|
||||
vo.setRssi(rs.getObject("rssi") != null ? rs.getInt("rssi") : null);
|
||||
vo.setScreenOn(rs.getObject("screen_on") != null ? (rs.getBoolean("screen_on") ? 1 : 0) : null);
|
||||
return vo;
|
||||
}, sn);
|
||||
if (list.isEmpty()) {
|
||||
return Result.success(null);
|
||||
}
|
||||
return Result.success(list.get(0));
|
||||
} catch (Exception e) {
|
||||
log.error("查询设备最后在线时间失败, sn={}", sn, e);
|
||||
return Result.failed("查询设备心跳失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.youlai.boot.device.model.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 设备心跳(TDengine)视图对象
|
||||
*
|
||||
* @author TongTongStudio
|
||||
* @since 2026/8/11
|
||||
*/
|
||||
@Schema(description = "设备心跳信息")
|
||||
@Data
|
||||
public class DeviceHeartbeatVO {
|
||||
|
||||
@Schema(description = "设备序列号")
|
||||
private String sn;
|
||||
|
||||
@Schema(description = "最后上报时间戳")
|
||||
private LocalDateTime lastTime;
|
||||
|
||||
@Schema(description = "最后在线状态(1:在线;0:离线)")
|
||||
private Integer online;
|
||||
|
||||
@Schema(description = "设备 IP")
|
||||
private String ip;
|
||||
|
||||
@Schema(description = "信号强度")
|
||||
private Integer rssi;
|
||||
|
||||
@Schema(description = "屏幕状态(1:亮屏;0:熄屏)")
|
||||
private Integer screenOn;
|
||||
|
||||
}
|
||||
@@ -75,6 +75,14 @@ public interface DeviceService extends IService<SnDeviceInfo> {
|
||||
*/
|
||||
boolean addSn(SnDeviceInfo device);
|
||||
|
||||
/**
|
||||
* 更新设备信息(仅允许修改设备昵称)
|
||||
* @param id 设备ID
|
||||
* @param snName 设备昵称
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean updateSn(Long id, String snName);
|
||||
|
||||
/**
|
||||
* 删除设备
|
||||
* @param sn 设备序列号
|
||||
|
||||
@@ -131,6 +131,15 @@ public class DeviceServiceImpl extends ServiceImpl<DeviceMapper, SnDeviceInfo> i
|
||||
DevicePageVO devicePageVO = new DevicePageVO();
|
||||
BeanUtils.copyProperties(device, devicePageVO);
|
||||
|
||||
// 填充设备在线状态(Redis)
|
||||
DeviceOnlineVO online = deviceOnlineService.getOnlineInfo(sn);
|
||||
if (online != null) {
|
||||
devicePageVO.setOnline(online.getOnline());
|
||||
devicePageVO.setLastHeartbeatTime(online.getLastHeartbeatTime());
|
||||
} else {
|
||||
devicePageVO.setOnline(0);
|
||||
}
|
||||
|
||||
return devicePageVO;
|
||||
}
|
||||
|
||||
@@ -202,6 +211,24 @@ public class DeviceServiceImpl extends ServiceImpl<DeviceMapper, SnDeviceInfo> i
|
||||
return saved;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean updateSn(Long id, String snName) {
|
||||
if (id == null) {
|
||||
return false;
|
||||
}
|
||||
SnDeviceInfo device = this.getById(id);
|
||||
if (device == null) {
|
||||
log.warn("更新设备失败,设备不存在, id: {}", id);
|
||||
return false;
|
||||
}
|
||||
device.setSnName(snName);
|
||||
boolean updated = this.updateById(device);
|
||||
if (!updated) {
|
||||
log.error("更新设备失败, id: {}, snName: {}", id, snName);
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deleteSn(String sn) {
|
||||
return false;
|
||||
|
||||
Reference in New Issue
Block a user