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:
TongTongStudio
2026-08-12 08:26:30 +08:00
parent bc68004c6e
commit 035e21a432
14 changed files with 463 additions and 141 deletions

View File

@@ -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 = "设备基本信息")

View File

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

View File

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

View File

@@ -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 设备序列号

View File

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