功能:摄像头命名改造 — camera_name + 新编码格式
- 新增 camera_name 字段(用户自定义名称,必填)
- camera_code 生成规则改为 CAM{YYYYMMDD}{NNN}(日期+序号)
- INSERT/UPDATE SQL 支持 camera_name
- 新增 /api/ai/camera/options 接口(前端下拉选择用)
- 存量数据迁移脚本:camera_name = app + stream
This commit is contained in:
@@ -10,6 +10,11 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* AI摄像头管理接口
|
||||
*/
|
||||
@@ -38,4 +43,17 @@ public class AiCameraController {
|
||||
|
||||
return WVPResult.success(proxy);
|
||||
}
|
||||
|
||||
@Operation(summary = "获取摄像头选项列表(用于下拉选择)")
|
||||
@GetMapping("/options")
|
||||
public WVPResult<List<Map<String, String>>> getCameraOptions() {
|
||||
List<StreamProxy> list = streamProxyMapper.selectAllCameraOptions();
|
||||
List<Map<String, String>> options = list.stream().map(p -> {
|
||||
Map<String, String> m = new HashMap<>();
|
||||
m.put("cameraCode", p.getCameraCode());
|
||||
m.put("cameraName", p.getCameraName() != null ? p.getCameraName() : p.getApp());
|
||||
return m;
|
||||
}).collect(Collectors.toList());
|
||||
return WVPResult.success(options);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +1,47 @@
|
||||
package com.genersoft.iot.vmp.aiot.util;
|
||||
|
||||
import java.util.UUID;
|
||||
import com.genersoft.iot.vmp.streamProxy.dao.StreamProxyMapper;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
/**
|
||||
* 摄像头编码生成工具类
|
||||
* 从 RTSP URL 中提取 IP/Host 特征,生成可读的 camera_code
|
||||
* 格式:cam_{host下划线分隔}_{4位随机}
|
||||
* 例:rtsp://admin:pwd@192.168.1.100:554/stream → cam_192_168_1_100_a3f1
|
||||
* 格式:CAM{YYYYMMDD}{NNN} 例:CAM20260319001
|
||||
* 日期部分标识创建日期,序号部分为当日递增
|
||||
*/
|
||||
public class CameraCodeUtil {
|
||||
|
||||
private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyyMMdd");
|
||||
|
||||
private CameraCodeUtil() {}
|
||||
|
||||
public static String generate(String srcUrl) {
|
||||
String hostPart = "unknown";
|
||||
if (srcUrl != null && !srcUrl.isEmpty()) {
|
||||
/**
|
||||
* 生成新的 camera_code(需要查询数据库获取当日最大序号)
|
||||
*/
|
||||
public static String generate(StreamProxyMapper mapper) {
|
||||
String dateStr = LocalDate.now().format(DATE_FMT);
|
||||
String prefix = "CAM" + dateStr;
|
||||
|
||||
// 查询当日最大编码
|
||||
String maxCode = mapper.selectMaxCameraCodeByPrefix(prefix);
|
||||
int nextSeq = 1;
|
||||
if (maxCode != null && maxCode.length() > prefix.length()) {
|
||||
try {
|
||||
String urlBody = srcUrl;
|
||||
int schemeEnd = urlBody.indexOf("://");
|
||||
if (schemeEnd >= 0) {
|
||||
urlBody = urlBody.substring(schemeEnd + 3);
|
||||
}
|
||||
int atIndex = urlBody.indexOf('@');
|
||||
if (atIndex >= 0) {
|
||||
urlBody = urlBody.substring(atIndex + 1);
|
||||
}
|
||||
String host = urlBody.split("[:/]")[0];
|
||||
hostPart = host.replace(".", "_");
|
||||
} catch (Exception e) {
|
||||
hostPart = "unknown";
|
||||
nextSeq = Integer.parseInt(maxCode.substring(prefix.length())) + 1;
|
||||
} catch (NumberFormatException e) {
|
||||
nextSeq = 1;
|
||||
}
|
||||
}
|
||||
String suffix = UUID.randomUUID().toString().replace("-", "").substring(0, 4);
|
||||
return "cam_" + hostPart + "_" + suffix;
|
||||
return prefix + String.format("%03d", nextSeq);
|
||||
}
|
||||
|
||||
/**
|
||||
* 兼容旧接口:从 RTSP URL 生成(降级方案)
|
||||
*/
|
||||
public static String generateFromUrl(String srcUrl) {
|
||||
String dateStr = LocalDate.now().format(DATE_FMT);
|
||||
String hash = String.format("%03d", Math.abs(srcUrl.hashCode()) % 999 + 1);
|
||||
return "CAM" + dateStr + hash;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,9 +30,12 @@ public class StreamProxy extends CommonGBChannel {
|
||||
@Schema(description = "流ID")
|
||||
private String stream;
|
||||
|
||||
@Schema(description = "摄像头全局唯一编码(格式:cam_xxxxxxxxxxxx)")
|
||||
@Schema(description = "摄像头全局唯一编码(格式:CAM{YYYYMMDD}{NNN})")
|
||||
private String cameraCode;
|
||||
|
||||
@Schema(description = "摄像头名称(用户自定义)")
|
||||
private String cameraName;
|
||||
|
||||
@Schema(description = "当前拉流使用的流媒体服务ID")
|
||||
private String mediaServerId;
|
||||
|
||||
|
||||
@@ -13,10 +13,10 @@ public interface StreamProxyMapper {
|
||||
|
||||
@Insert("INSERT INTO wvp_stream_proxy (type, app, stream,relates_media_server_id, src_url, " +
|
||||
"timeout, ffmpeg_cmd_key, rtsp_type, enable_audio, enable_mp4, enable, pulling, " +
|
||||
"enable_disable_none_reader, server_id, create_time, camera_code, area_id) VALUES" +
|
||||
"enable_disable_none_reader, server_id, create_time, camera_code, camera_name, area_id) VALUES" +
|
||||
"(#{type}, #{app}, #{stream}, #{relatesMediaServerId}, #{srcUrl}, " +
|
||||
"#{timeout}, #{ffmpegCmdKey}, #{rtspType}, #{enableAudio}, #{enableMp4}, #{enable}, #{pulling}, " +
|
||||
"#{enableDisableNoneReader}, #{serverId}, #{createTime}, #{cameraCode}, #{areaId} )")
|
||||
"#{enableDisableNoneReader}, #{serverId}, #{createTime}, #{cameraCode}, #{cameraName}, #{areaId} )")
|
||||
@Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id")
|
||||
int add(StreamProxy streamProxyDto);
|
||||
|
||||
@@ -34,6 +34,7 @@ public interface StreamProxyMapper {
|
||||
"pulling=#{pulling}, " +
|
||||
"enable_disable_none_reader=#{enableDisableNoneReader}, " +
|
||||
"enable_mp4=#{enableMp4}, " +
|
||||
"camera_name=#{cameraName}, " +
|
||||
"area_id=#{areaId} " +
|
||||
"WHERE id=#{id}")
|
||||
int update(StreamProxy streamProxyDto);
|
||||
@@ -113,4 +114,16 @@ public interface StreamProxyMapper {
|
||||
*/
|
||||
@Update("UPDATE wvp_stream_proxy SET camera_code = #{cameraCode} WHERE id = #{id}")
|
||||
int updateCameraCode(@Param("id") int id, @Param("cameraCode") String cameraCode);
|
||||
|
||||
/**
|
||||
* 查询指定前缀的最大 camera_code(用于生成新编码的序号)
|
||||
*/
|
||||
@Select("SELECT MAX(camera_code) FROM wvp_stream_proxy WHERE camera_code LIKE CONCAT(#{prefix}, '%')")
|
||||
String selectMaxCameraCodeByPrefix(@Param("prefix") String prefix);
|
||||
|
||||
/**
|
||||
* 查询所有摄像头简要信息(用于前端下拉选择)
|
||||
*/
|
||||
@Select("SELECT camera_code, camera_name FROM wvp_stream_proxy WHERE enable = 1 ORDER BY camera_name")
|
||||
List<StreamProxy> selectAllCameraOptions();
|
||||
}
|
||||
|
||||
@@ -151,29 +151,27 @@ public class StreamProxyServiceImpl implements IStreamProxyService {
|
||||
throw new ControllerException(ErrorCode.ERROR100.getCode(), "APP+STREAM已经存在");
|
||||
}
|
||||
|
||||
// 自动生成 camera_code(最多重试3次避免冲突)
|
||||
int retryCount = 0;
|
||||
while (retryCount < 3) {
|
||||
String cameraCode = CameraCodeUtil.generate(streamProxy.getSrcUrl());
|
||||
streamProxy.setCameraCode(cameraCode);
|
||||
// 自动生成 camera_code(日期+序号格式)
|
||||
String cameraCode = CameraCodeUtil.generate(streamProxyMapper);
|
||||
streamProxy.setCameraCode(cameraCode);
|
||||
|
||||
streamProxy.setCreateTime(DateUtil.getNow());
|
||||
streamProxy.setUpdateTime(DateUtil.getNow());
|
||||
// camera_name 必填校验
|
||||
if (streamProxy.getCameraName() == null || streamProxy.getCameraName().trim().isEmpty()) {
|
||||
throw new ControllerException(ErrorCode.ERROR100.getCode(), "摄像头名称不能为空");
|
||||
}
|
||||
|
||||
try {
|
||||
if (streamProxy.getGbDeviceId() != null) {
|
||||
gbChannelService.add(streamProxy.buildCommonGBChannel());
|
||||
}
|
||||
streamProxyMapper.add(streamProxy);
|
||||
streamProxy.setDataType(ChannelDataType.STREAM_PROXY);
|
||||
streamProxy.setDataDeviceId(streamProxy.getId());
|
||||
return;
|
||||
} catch (DuplicateKeyException e) {
|
||||
retryCount++;
|
||||
if (retryCount >= 3) {
|
||||
throw new RuntimeException("生成 camera_code 失败,请重试");
|
||||
}
|
||||
streamProxy.setCreateTime(DateUtil.getNow());
|
||||
streamProxy.setUpdateTime(DateUtil.getNow());
|
||||
|
||||
try {
|
||||
if (streamProxy.getGbDeviceId() != null) {
|
||||
gbChannelService.add(streamProxy.buildCommonGBChannel());
|
||||
}
|
||||
streamProxyMapper.add(streamProxy);
|
||||
streamProxy.setDataType(ChannelDataType.STREAM_PROXY);
|
||||
streamProxy.setDataDeviceId(streamProxy.getId());
|
||||
} catch (DuplicateKeyException e) {
|
||||
throw new RuntimeException("生成 camera_code 失败,请重试");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
16
数据库/aiot/迁移-添加camera_name字段.sql
Normal file
16
数据库/aiot/迁移-添加camera_name字段.sql
Normal file
@@ -0,0 +1,16 @@
|
||||
-- 摄像头命名改造迁移脚本
|
||||
-- 1. 新增 camera_name 字段
|
||||
-- 2. 存量数据:camera_name = CONCAT(app, stream)
|
||||
-- 3. 设置 NOT NULL
|
||||
|
||||
-- Step 1: 添加 camera_name 列
|
||||
ALTER TABLE wvp_stream_proxy ADD COLUMN camera_name VARCHAR(100) NULL AFTER camera_code;
|
||||
|
||||
-- Step 2: 存量数据迁移 — app + stream 合并为 camera_name
|
||||
UPDATE wvp_stream_proxy SET camera_name = CONCAT(IFNULL(app, ''), IFNULL(stream, '')) WHERE camera_name IS NULL;
|
||||
|
||||
-- Step 3: 设置 NOT NULL(确认数据填充后执行)
|
||||
ALTER TABLE wvp_stream_proxy MODIFY COLUMN camera_name VARCHAR(100) NOT NULL DEFAULT '';
|
||||
|
||||
-- 验证
|
||||
SELECT camera_code, camera_name, app, stream FROM wvp_stream_proxy LIMIT 20;
|
||||
Reference in New Issue
Block a user