功能:边缘设备心跳 HTTP 端点 + stream_count/config_version 字段

- 新增 POST /api/ai/device/heartbeat 端点,接收边缘端心跳上报
- 心跳端点加入安全白名单(无需认证)
- AiEdgeDevice 新增 streamCount、configVersion 字段
- Mapper INSERT/UPDATE 同步新增字段
- Service 从心跳 payload 提取并保存新字段
- 数据库升级 SQL 添加 stream_count、config_version 列

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-17 17:47:02 +08:00
parent a879487551
commit 38b6a20e45
6 changed files with 37 additions and 2 deletions

View File

@@ -31,6 +31,12 @@ public class AiEdgeDevice {
@Schema(description = "流统计信息JSON")
private String streamStats;
@Schema(description = "活跃视频流数量")
private Integer streamCount;
@Schema(description = "当前配置版本")
private String configVersion;
@Schema(description = "更新时间")
private String updatedAt;
}

View File

@@ -56,4 +56,21 @@ public class AiEdgeDeviceController {
public WVPResult<Map<String, Object>> statistics() {
return WVPResult.success(edgeDeviceService.getStatistics());
}
@Operation(summary = "边缘设备心跳上报")
@PostMapping("/heartbeat")
public WVPResult<String> heartbeat(@RequestBody String payload) {
try {
com.alibaba.fastjson2.JSONObject json = com.alibaba.fastjson2.JSON.parseObject(payload);
String deviceId = json.getString("device_id");
if (deviceId == null || deviceId.isEmpty()) {
return WVPResult.fail(400, "device_id 不能为空");
}
edgeDeviceService.saveOrUpdateHeartbeat(deviceId, payload);
return WVPResult.success("ok");
} catch (Exception e) {
log.error("[AiEdgeDevice] 心跳上报处理失败", e);
return WVPResult.fail(500, "心跳处理失败: " + e.getMessage());
}
}
}

View File

@@ -9,15 +9,16 @@ import java.util.List;
public interface AiEdgeDeviceMapper {
@Insert("INSERT INTO wvp_ai_edge_device (device_id, status, last_heartbeat, uptime_seconds, " +
"frames_processed, alerts_generated, stream_stats, updated_at) " +
"frames_processed, alerts_generated, stream_stats, stream_count, config_version, updated_at) " +
"VALUES (#{deviceId}, #{status}, #{lastHeartbeat}, #{uptimeSeconds}, " +
"#{framesProcessed}, #{alertsGenerated}, #{streamStats}, #{updatedAt})")
"#{framesProcessed}, #{alertsGenerated}, #{streamStats}, #{streamCount}, #{configVersion}, #{updatedAt})")
@Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id")
int add(AiEdgeDevice device);
@Update("UPDATE wvp_ai_edge_device SET status=#{status}, last_heartbeat=#{lastHeartbeat}, " +
"uptime_seconds=#{uptimeSeconds}, frames_processed=#{framesProcessed}, " +
"alerts_generated=#{alertsGenerated}, stream_stats=#{streamStats}, " +
"stream_count=#{streamCount}, config_version=#{configVersion}, " +
"updated_at=#{updatedAt} WHERE device_id=#{deviceId}")
int updateByDeviceId(AiEdgeDevice device);

View File

@@ -46,6 +46,8 @@ public class AiEdgeDeviceServiceImpl implements IAiEdgeDeviceService {
device.setAlertsGenerated(status != null ? status.getLong("alerts_generated") : null);
device.setStreamStats(status != null && status.containsKey("stream_stats") ?
status.getJSONObject("stream_stats").toJSONString() : null);
device.setStreamCount(status != null ? status.getInteger("stream_count") : null);
device.setConfigVersion(status != null ? status.getString("config_version") : null);
device.setUpdatedAt(now);
deviceMapper.add(device);
log.info("[AiEdgeDevice] 新设备上线: deviceId={}", deviceId);
@@ -57,6 +59,8 @@ public class AiEdgeDeviceServiceImpl implements IAiEdgeDeviceService {
device.setAlertsGenerated(status != null ? status.getLong("alerts_generated") : null);
device.setStreamStats(status != null && status.containsKey("stream_stats") ?
status.getJSONObject("stream_stats").toJSONString() : null);
device.setStreamCount(status != null ? status.getInteger("stream_count") : null);
device.setConfigVersion(status != null ? status.getString("config_version") : null);
device.setUpdatedAt(now);
deviceMapper.updateByDeviceId(device);
log.debug("[AiEdgeDevice] 心跳更新: deviceId={}", deviceId);

View File

@@ -108,6 +108,7 @@ public class WebSecurityConfig {
defaultExcludes.add("/api/ai/alert/edge/**");
defaultExcludes.add("/api/ai/alert/image");
defaultExcludes.add("/api/ai/device/edge/**");
defaultExcludes.add("/api/ai/device/heartbeat");
if (userSetting.getInterfaceAuthentication() && !userSetting.getInterfaceAuthenticationExcludes().isEmpty()) {
defaultExcludes.addAll(userSetting.getInterfaceAuthenticationExcludes());

View File

@@ -53,10 +53,16 @@ CREATE TABLE IF NOT EXISTS wvp_ai_edge_device (
frames_processed BIGINT NULL COMMENT '已处理帧数',
alerts_generated BIGINT NULL COMMENT '已生成告警数',
stream_stats TEXT NULL COMMENT 'JSON流统计',
stream_count INT NULL COMMENT '活跃视频流数量',
config_version VARCHAR(64) NULL COMMENT '当前配置版本',
updated_at VARCHAR(50) NULL COMMENT '更新时间',
UNIQUE KEY uk_device_id (device_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='边缘设备状态';
-- 升级wvp_ai_edge_device 新增字段(已有表执行)
ALTER TABLE wvp_ai_edge_device ADD COLUMN IF NOT EXISTS stream_count INT NULL COMMENT '活跃视频流数量' AFTER stream_stats;
ALTER TABLE wvp_ai_edge_device ADD COLUMN IF NOT EXISTS config_version VARCHAR(64) NULL COMMENT '当前配置版本' AFTER stream_count;
-- 4. 算法参数模板表
CREATE TABLE IF NOT EXISTS wvp_ai_algo_template (
id INT PRIMARY KEY AUTO_INCREMENT,