refactor(video): P2 应用层时间字段 varchar → datetime / String → LocalDateTime

SQL 类型优化(应用层生成的业务时间改为标准 datetime):
- video_ai_edge_device: last_heartbeat / updated_at
- video_ai_config_log: updated_at
- video_ai_config_snapshot: created_at
- video_ai_algorithm: sync_time
- video_ai_alert: received_at
- video_media_server: last_keepalive_time

DO 字段 String → LocalDateTime 对齐:AiEdgeDevice / AiConfigLog /
AiConfigSnapshot / AiAlgorithm / AiAlert / MediaServer

调用点适配:
- Service 层 new Date() 字符串格式化赋值统一改为 LocalDateTime.now()
- AiAlertController.edgeReport / MqttService.handleAlert 新增
  parseEventTime / parseTimestamp 私有方法,外部字符串防御性解析

保留 varchar(本批次暂不动,SIP 协议原生字符串,改动涉及 72+ 处
Mapper 与 SIP 处理器,单独迭代):
- video_device.register_time / keepalive_time
- video_device_channel.end_time / gps_time
- video_device_mobile_position.time
- video_stream_push.push_time

编译通过(mvn compile BUILD SUCCESS)。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
lzh
2026-04-21 12:32:23 +08:00
parent 64dcdd8d4c
commit 0ca1adf2a4
16 changed files with 78 additions and 42 deletions

View File

@@ -533,7 +533,7 @@ CREATE TABLE `video_media_server` (
`transcode_suffix` varchar(255) NULL COMMENT '转码指令后缀',
`server_id` varchar(50) NULL COMMENT '对应信令服务器ID',
`status` tinyint(1) NOT NULL DEFAULT 0 COMMENT '媒体服务器在线状态0离线,1在线',
`last_keepalive_time` varchar(50) NULL COMMENT '最近心跳时间',
`last_keepalive_time` datetime NULL COMMENT '最近心跳时间',
`creator` varchar(64) NOT NULL DEFAULT '' COMMENT '创建者',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` varchar(64) NOT NULL DEFAULT '' COMMENT '更新者',
@@ -708,7 +708,7 @@ CREATE TABLE `video_ai_alert` (
`image_path` varchar(255) NULL COMMENT '告警图片路径',
`duration_minutes` double NULL COMMENT '持续时长(分钟)',
`extra_data` text NULL COMMENT 'JSON 扩展',
`received_at` varchar(50) NOT NULL COMMENT '接收时间',
`received_at` datetime NULL COMMENT '接收时间',
`creator` varchar(64) NOT NULL DEFAULT '' COMMENT '创建者',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` varchar(64) NOT NULL DEFAULT '' COMMENT '更新者',
@@ -731,14 +731,14 @@ CREATE TABLE `video_ai_edge_device` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`device_id` varchar(64) NOT NULL COMMENT '设备唯一ID',
`status` varchar(20) NOT NULL DEFAULT 'offline' COMMENT '状态online/offline',
`last_heartbeat` varchar(50) NULL COMMENT '最后心跳时间',
`last_heartbeat` datetime NULL COMMENT '最后心跳时间',
`uptime_seconds` bigint NULL COMMENT '运行时长(秒)',
`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 '更新时间',
`updated_at` datetime NULL COMMENT '更新时间',
`creator` varchar(64) NOT NULL DEFAULT '' COMMENT '创建者',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` varchar(64) NOT NULL DEFAULT '' COMMENT '更新者',
@@ -819,7 +819,7 @@ CREATE TABLE `video_ai_config_log` (
`old_value` text NULL COMMENT '变更前 JSON',
`new_value` text NULL COMMENT '变更后 JSON',
`updated_by` varchar(100) NULL COMMENT '操作人',
`updated_at` varchar(50) NULL COMMENT '操作时间',
`updated_at` datetime NULL COMMENT '操作时间',
`creator` varchar(64) NOT NULL DEFAULT '' COMMENT '创建者',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` varchar(64) NOT NULL DEFAULT '' COMMENT '更新者',
@@ -846,7 +846,7 @@ CREATE TABLE `video_ai_config_snapshot` (
`change_type` varchar(20) NOT NULL COMMENT 'CREATE | UPDATE | DELETE | ROLLBACK | PUSH',
`change_desc` varchar(255) NULL COMMENT '变更描述',
`created_by` varchar(64) NULL COMMENT '操作人',
`created_at` varchar(50) NOT NULL COMMENT '创建时间',
`created_at` datetime NULL COMMENT '创建时间',
`creator` varchar(64) NOT NULL DEFAULT '' COMMENT '创建者',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` varchar(64) NOT NULL DEFAULT '' COMMENT '更新者',
@@ -897,7 +897,7 @@ CREATE TABLE `video_ai_algorithm` (
`global_params` text NULL COMMENT '用户自定义的全局默认参数 JSON',
`description` varchar(500) NULL COMMENT '描述',
`is_active` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否可用',
`sync_time` varchar(50) NULL COMMENT '最后同步时间',
`sync_time` datetime NULL COMMENT '最后同步时间',
`creator` varchar(64) NOT NULL DEFAULT '' COMMENT '创建者',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` varchar(64) NOT NULL DEFAULT '' COMMENT '更新者',

View File

@@ -9,6 +9,8 @@ import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
@Data
@TableName("video_ai_alert")
@EqualsAndHashCode(callSuper = true)
@@ -64,5 +66,5 @@ public class AiAlert extends ProjectBaseDO {
private String extraData;
@Schema(description = "接收时间")
private String receivedAt;
private LocalDateTime receivedAt;
}

View File

@@ -8,6 +8,8 @@ import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
@Data
@TableName("video_ai_algorithm")
@EqualsAndHashCode(callSuper = true)
@@ -40,5 +42,5 @@ public class AiAlgorithm extends TenantBaseDO {
private String globalParams;
@Schema(description = "最后同步时间")
private String syncTime;
private LocalDateTime syncTime;
}

View File

@@ -8,6 +8,8 @@ import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
@Data
@TableName("video_ai_config_log")
@EqualsAndHashCode(callSuper = true)
@@ -34,5 +36,5 @@ public class AiConfigLog extends ProjectBaseDO {
private String updatedBy;
@Schema(description = "操作时间")
private String updatedAt;
private LocalDateTime updatedAt;
}

View File

@@ -8,6 +8,8 @@ import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
@Data
@TableName("video_ai_config_snapshot")
@EqualsAndHashCode(callSuper = true)
@@ -43,5 +45,5 @@ public class AiConfigSnapshot extends ProjectBaseDO {
private String createdBy;
@Schema(description = "创建时间(业务字段,记录快照生成时间)")
private String createdAt;
private LocalDateTime createdAt;
}

View File

@@ -8,6 +8,8 @@ import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
@Data
@TableName("video_ai_edge_device")
@EqualsAndHashCode(callSuper = true)
@@ -25,7 +27,7 @@ public class AiEdgeDevice extends ProjectBaseDO {
private String status;
@Schema(description = "最后心跳时间")
private String lastHeartbeat;
private LocalDateTime lastHeartbeat;
@Schema(description = "运行时长(秒)")
private Long uptimeSeconds;
@@ -46,5 +48,5 @@ public class AiEdgeDevice extends ProjectBaseDO {
private String configVersion;
@Schema(description = "更新时间(业务字段)")
private String updatedAt;
private LocalDateTime updatedAt;
}

View File

@@ -13,6 +13,8 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Map;
@@ -92,7 +94,7 @@ public class AiAlertController {
alert.setRoiId((String) body.get("scene_id"));
alert.setAlertType((String) body.get("algorithm_code"));
alert.setImagePath((String) body.get("snapshot_url"));
alert.setReceivedAt((String) body.get("event_time"));
alert.setReceivedAt(parseEventTime((String) body.get("event_time")));
Object conf = body.get("confidence_score");
if (conf instanceof Number) {
@@ -124,4 +126,21 @@ public class AiAlertController {
log.info("[AiAlert] Edge 告警结束: alarmId={}, durationMin={}", alarmId, minutes);
}
}
/**
* 解析外部上报的事件时间字符串为 LocalDateTime解析失败返回 null由 save() 兜底为当前时间)
* 支持格式yyyy-MM-dd HH:mm:ss 或 yyyy-MM-dd'T'HH:mm:ss
*/
private static LocalDateTime parseEventTime(String s) {
if (s == null || s.isEmpty()) {
return null;
}
try {
String normalized = s.replace('T', ' ');
return LocalDateTime.parse(normalized, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
} catch (Exception e) {
log.warn("[AiAlert] 无法解析事件时间: {}", s);
return null;
}
}
}

View File

@@ -7,6 +7,7 @@ import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
import java.time.LocalDateTime;
import java.util.List;
@Mapper
@@ -40,5 +41,5 @@ public interface AiAlgorithmMapper extends BaseMapperX<AiAlgorithm> {
}
@Update("UPDATE video_ai_algorithm SET is_active=#{isActive}, update_time=#{updateTime} WHERE id=#{id} AND deleted=0")
int updateActive(@Param("id") Long id, @Param("isActive") Integer isActive, @Param("updateTime") String updateTime);
int updateActive(@Param("id") Long id, @Param("isActive") Integer isActive, @Param("updateTime") LocalDateTime updateTime);
}

View File

@@ -9,6 +9,7 @@ import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import java.time.LocalDateTime;
import java.util.List;
@Mapper
@@ -33,7 +34,7 @@ public interface AiEdgeDeviceMapper extends BaseMapperX<AiEdgeDevice> {
@Update("UPDATE video_ai_edge_device SET status='offline', updated_at=#{now} " +
"WHERE status='online' AND last_heartbeat < #{threshold} AND deleted=0")
int markOffline(@Param("threshold") String threshold, @Param("now") String now);
int markOffline(@Param("threshold") LocalDateTime threshold, @Param("now") LocalDateTime now);
@Update("UPDATE video_ai_edge_device SET deleted=1 WHERE device_id != #{keepDeviceId} AND deleted=0")
int deleteAllExcept(@Param("keepDeviceId") String keepDeviceId);

View File

@@ -18,6 +18,8 @@ import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.function.BiConsumer;
@Slf4j
@@ -145,8 +147,8 @@ public class MqttService {
alert.setMessage(json.getString("message"));
alert.setImagePath(json.getString("image_path"));
alert.setDurationMinutes(json.getDouble("duration_minutes"));
// timestamp → receivedAt
alert.setReceivedAt(json.getString("timestamp"));
// timestamp → receivedAt(防御性解析,失败则由 save() 兜底为当前时间)
alert.setReceivedAt(parseTimestamp(json.getString("timestamp")));
// 其余字段放入extraData
JSONObject extra = new JSONObject();
@@ -235,6 +237,22 @@ public class MqttService {
return client != null && client.isConnected();
}
/**
* 解析 MQTT 上报的时间戳字符串为 LocalDateTime解析失败返回 null由 save() 兜底为当前时间)
* 支持格式yyyy-MM-dd HH:mm:ss 或 yyyy-MM-dd'T'HH:mm:ss
*/
private static LocalDateTime parseTimestamp(String s) {
if (s == null || s.isEmpty()) {
return null;
}
try {
String normalized = s.replace('T', ' ');
return LocalDateTime.parse(normalized, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
} catch (Exception e) {
return null;
}
}
@PreDestroy
public void destroy() {
if (client != null && client.isConnected()) {

View File

@@ -14,7 +14,6 @@ import org.springframework.web.client.RestTemplate;
import java.net.URI;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
@Slf4j
@@ -27,12 +26,10 @@ public class AiAlertServiceImpl implements IAiAlertService {
@Autowired
private CosUtil cosUtil;
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Override
public void save(AiAlert alert) {
if (alert.getReceivedAt() == null) {
alert.setReceivedAt(LocalDateTime.now().format(FORMATTER));
alert.setReceivedAt(LocalDateTime.now());
}
// 防止重复插入(轻量查询,不做 JOIN
if (alertMapper.countByAlertId(alert.getAlertId()) > 0) {

View File

@@ -12,7 +12,6 @@ import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -30,8 +29,6 @@ public class AiAlgorithmServiceImpl implements IAiAlgorithmService {
@Autowired
private IAiConfigLogService configLogService;
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
/**
* 预置算法定义,启动时自动校正数据库中的乱码数据
* 注意:仅包含边缘端实际实现的算法
@@ -96,7 +93,7 @@ public class AiAlgorithmServiceImpl implements IAiAlgorithmService {
@Override
public void toggleActive(Long id, Integer isActive) {
String now = LocalDateTime.now().format(FORMATTER);
LocalDateTime now = LocalDateTime.now();
algorithmMapper.updateActive(id, isActive, now);
configLogService.addLog("ALGORITHM", String.valueOf(id),
null, "{\"is_active\":" + isActive + "}", null);
@@ -109,7 +106,7 @@ public class AiAlgorithmServiceImpl implements IAiAlgorithmService {
log.warn("AI服务未启用跳过同步");
return;
}
String now = LocalDateTime.now().format(FORMATTER);
LocalDateTime now = LocalDateTime.now();
try {
RestTemplate restTemplate = new RestTemplate();
String url = aiServiceConfig.getUrl() + "/api/algorithms";

View File

@@ -11,7 +11,6 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
@Slf4j
@@ -21,8 +20,6 @@ public class AiConfigLogServiceImpl implements IAiConfigLogService {
@Autowired
private AiConfigLogMapper configLogMapper;
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Override
public void addLog(String configType, String configId, String oldValue, String newValue, String updatedBy) {
AiConfigLog logEntry = new AiConfigLog();
@@ -31,7 +28,7 @@ public class AiConfigLogServiceImpl implements IAiConfigLogService {
logEntry.setOldValue(oldValue);
logEntry.setNewValue(newValue);
logEntry.setUpdatedBy(updatedBy);
logEntry.setUpdatedAt(LocalDateTime.now().format(FORMATTER));
logEntry.setUpdatedAt(LocalDateTime.now());
configLogMapper.add(logEntry);
}

View File

@@ -20,7 +20,6 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
@Slf4j
@@ -43,8 +42,6 @@ public class AiConfigSnapshotServiceImpl implements IAiConfigSnapshotService {
@Autowired
private IAiRedisConfigService redisConfigService;
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Override
public AiConfigSnapshot createSnapshot(String scopeType, String scopeId, String cameraId,
String snapshot, String changeType, String changeDesc, String createdBy) {
@@ -58,7 +55,7 @@ public class AiConfigSnapshotServiceImpl implements IAiConfigSnapshotService {
s.setChangeType(changeType);
s.setChangeDesc(changeDesc);
s.setCreatedBy(createdBy);
s.setCreatedAt(LocalDateTime.now().format(FORMATTER));
s.setCreatedAt(LocalDateTime.now());
snapshotMapper.add(s);
log.info("[AiSnapshot] 创建快照: scope={}/{}, version={}, type={}", scopeType, scopeId, s.getVersion(), changeType);
return s;

View File

@@ -14,7 +14,6 @@ import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -26,11 +25,9 @@ public class AiEdgeDeviceServiceImpl implements IAiEdgeDeviceService {
@Autowired
private AiEdgeDeviceMapper deviceMapper;
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Override
public void saveOrUpdateHeartbeat(String deviceId, String payload) {
String now = LocalDateTime.now().format(FORMATTER);
LocalDateTime now = LocalDateTime.now();
try {
JSONObject json = JSON.parseObject(payload);
@@ -84,8 +81,8 @@ public class AiEdgeDeviceServiceImpl implements IAiEdgeDeviceService {
@Override
@Scheduled(fixedRate = 90000) // 每90秒检查一次
public void checkOffline() {
String now = LocalDateTime.now().format(FORMATTER);
String threshold = LocalDateTime.now().minusSeconds(90).format(FORMATTER);
LocalDateTime now = LocalDateTime.now();
LocalDateTime threshold = LocalDateTime.now().minusSeconds(90);
int count = deviceMapper.markOffline(threshold, now);
if (count > 0) {
log.warn("[AiEdgeDevice] 标记{}台设备为离线", count);

View File

@@ -10,6 +10,8 @@ import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.util.ObjectUtils;
import java.time.LocalDateTime;
@Schema(description = "流媒体服务信息")
@Data
@TableName("video_media_server")
@@ -95,7 +97,7 @@ public class MediaServer extends BaseDO {
private int recordAssistPort;
@Schema(description = "上次心跳时间")
private String lastKeepaliveTime;
private LocalDateTime lastKeepaliveTime;
@Schema(description = "是否是默认ZLM")
private boolean defaultServer;