增加多端口模式支持随机SSRC配置

This commit is contained in:
lin 2026-05-21 12:50:35 +08:00
parent 545d667fad
commit 324f75ce76
7 changed files with 588 additions and 72 deletions

View File

@ -128,6 +128,11 @@ public class UserSetting {
*/
private Boolean useCustomSsrcForParentInvite = Boolean.TRUE;
/**
* 多端口模式使用随机SSRC端口区分流SSRC允许重复
*/
private Boolean ssrcRandom = Boolean.FALSE;
/**
* 开启接口文档页面 默认开启生产环境建议关闭遇到swagger相关的漏洞时也可以关闭
*/

View File

@ -2,6 +2,7 @@ package com.genersoft.iot.vmp.gb28181.session;
import com.alibaba.fastjson2.JSONObject;
import com.genersoft.iot.vmp.conf.SipConfig;
import com.genersoft.iot.vmp.conf.UserSetting;
import com.genersoft.iot.vmp.media.bean.MediaServer;
import com.genersoft.iot.vmp.media.service.IMediaServerService;
import com.genersoft.iot.vmp.media.zlm.ZLMRESTfulUtils;
@ -24,6 +25,7 @@ import java.util.concurrent.TimeUnit;
public class SSRCFactory {
private final ConcurrentHashMap<String, BitSet> usedMap = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, Object> lockMap = new ConcurrentHashMap<>();
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
Thread t = new Thread(r, "ssrc-rebuild");
t.setDaemon(true);
@ -39,6 +41,9 @@ public class SSRCFactory {
@Autowired
private SipConfig sipConfig;
@Autowired
private UserSetting userSetting;
private String domainPart;
@PostConstruct
@ -58,53 +63,68 @@ public class SSRCFactory {
return suffix != null ? "1" + suffix : null;
}
public String getPlaySsrcRandom() {
return "0" + domainPart + String.format("%04d", ThreadLocalRandom.current().nextInt(10000));
}
public String getPlayBackSsrcRandom() {
return "1" + domainPart + String.format("%04d", ThreadLocalRandom.current().nextInt(10000));
}
private String allocate(String mediaServerId) {
BitSet bits = usedMap.computeIfAbsent(mediaServerId, k -> new BitSet(10000));
int start = ThreadLocalRandom.current().nextInt(10000);
int index = start;
do {
if (!bits.get(index)) {
bits.set(index);
return domainPart + String.format("%04d", index);
}
index = (index + 1) % 10000;
} while (index != start);
log.warn("[SSRC] 媒体节点 {} 的SSRC已用尽", mediaServerId);
return null;
synchronized (lockMap.computeIfAbsent(mediaServerId, k -> new Object())) {
BitSet bits = usedMap.computeIfAbsent(mediaServerId, k -> new BitSet(10000));
int start = ThreadLocalRandom.current().nextInt(10000);
int index = start;
do {
if (!bits.get(index)) {
bits.set(index);
return domainPart + String.format("%04d", index);
}
index = (index + 1) % 10000;
} while (index != start);
log.warn("[SSRC] 媒体节点 {} 的SSRC已用尽", mediaServerId);
return null;
}
}
void rebuild() {
List<MediaServer> servers = mediaServerService.getAll();
for (MediaServer server : servers) {
BitSet bits = new BitSet(10000);
int count = 0;
try {
ZLMResult<?> result = zlmresTfulUtils.getMediaList(server, null, null, "rtsp", null);
if (result != null && result.getCode() == 0 && result.getData() != null) {
List<JSONObject> list = (List<JSONObject>) result.getData();
for (JSONObject obj : list) {
if (obj.getIntValue("originType") != 3) continue;
String originUrl = obj.getString("originUrl");
if (originUrl == null) continue;
int idx = originUrl.lastIndexOf("/rtp/");
if (idx == -1) continue;
try {
int suffix = (int) (Long.parseLong(originUrl.substring(idx + 5), 16) % 10000);
bits.set(suffix);
count++;
} catch (NumberFormatException ignored) {
if (server.isRtpEnable() && userSetting.getSsrcRandom()) {
continue;
}
synchronized (lockMap.computeIfAbsent(server.getId(), k -> new Object())) {
BitSet bits = new BitSet(10000);
int count = 0;
try {
ZLMResult<?> result = zlmresTfulUtils.getMediaList(server, null, null, "rtsp", null);
if (result != null && result.getCode() == 0 && result.getData() != null) {
List<JSONObject> list = (List<JSONObject>) result.getData();
for (JSONObject obj : list) {
if (obj.getIntValue("originType") != 3) continue;
String originUrl = obj.getString("originUrl");
if (originUrl == null) continue;
int idx = originUrl.lastIndexOf("/rtp/");
if (idx == -1) continue;
try {
int suffix = (int) (Long.parseLong(originUrl.substring(idx + 5), 16) % 10000);
bits.set(suffix);
count++;
} catch (NumberFormatException ignored) {
}
}
}
} catch (Exception e) {
log.warn("[SSRC重建] 查询媒体节点 {} 失败: {}", server.getId(), e.getMessage());
}
} catch (Exception e) {
log.warn("[SSRC重建] 查询媒体节点 {} 失败: {}", server.getId(), e.getMessage());
}
usedMap.put(server.getId(), bits);
if (count > 8000) {
log.info("[SSRC重建] 媒体节点 {} 的SSRC使用率已超过80%,请注意扩展服务提升性能", server.getId());
}else {
if (log.isDebugEnabled()) {
log.debug("[SSRC重建] 节点 {} 已占用 {} 个SSRC", server.getId(), count);
usedMap.put(server.getId(), bits);
if (count > 8000) {
log.info("[SSRC重建] 媒体节点 {} 的SSRC使用率已超过80%,请注意扩展服务提升性能", server.getId());
} else {
if (log.isDebugEnabled()) {
log.debug("[SSRC重建] 节点 {} 已占用 {} 个SSRC", server.getId(), count);
}
}
}
}

View File

@ -17,6 +17,10 @@ public class RTPServerParam {
private MediaServer mediaServer;
private String app;
private String streamId;
/**
* 传递给zlm创建rtp server的streamId不填则使用streamId
*/
private String zlmStreamId;
/**
* 开启rtpServer时使用的ssrc开启rtpServer时会根据这个ssrc进行校验如果不填则不校验
*/

View File

@ -90,12 +90,10 @@ public class RtpServerServiceImpl implements IReceiveRtpServerService {
final String ssrc;
if (presetSSRC != null) {
ssrc = presetSSRC;
}else {
if (playback) {
ssrc = ssrcFactory.getPlayBackSsrc(mediaServer.getId());
}else {
ssrc = ssrcFactory.getPlaySsrc(mediaServer.getId());
}
} else if (mediaServer.isRtpEnable() && userSetting.getSsrcRandom()) {
ssrc = playback ? ssrcFactory.getPlayBackSsrcRandom() : ssrcFactory.getPlaySsrcRandom();
} else {
ssrc = playback ? ssrcFactory.getPlayBackSsrc(mediaServer.getId()) : ssrcFactory.getPlaySsrc(mediaServer.getId());
}
if (streamId == null) {
streamId = String.format("%08x", Long.parseLong(ssrc)).toUpperCase();
@ -139,18 +137,14 @@ public class RtpServerServiceImpl implements IReceiveRtpServerService {
final String ssrc;
if (presetSSRC != null) {
ssrc = presetSSRC;
}else {
} else if (mediaServer.isRtpEnable() && userSetting.getSsrcRandom()) {
ssrc = ssrcFactory.getPlaySsrcRandom();
} else {
ssrc = ssrcFactory.getPlaySsrc(mediaServer.getId());
}
String streamId;
String streamReplace = null;
if (mediaServer.isRtpEnable()) {
streamId = String.format("%s_%s", device.getDeviceId(), channel.getDeviceId());
}else {
streamId = String.format("%08x", Long.parseLong(ssrc)).toUpperCase();
streamReplace = String.format("%s_%s", device.getDeviceId(), channel.getDeviceId());
}
String streamId = String.format("%08x", Long.parseLong(ssrc)).toUpperCase();
String streamReplace = String.format("%s_%s", device.getDeviceId(), channel.getDeviceId());
int tcpMode = device.getStreamMode().equals("TCP-ACTIVE")? 2: (device.getStreamMode().equals("TCP-PASSIVE")? 1:0);
@ -161,8 +155,8 @@ public class RtpServerServiceImpl implements IReceiveRtpServerService {
Long checkSsrc = device.isSsrcCheck() ? Long.parseLong(ssrc) : 0L;
SSRCInfo ssrcInfo = new SSRCInfo(0, ssrc, MediaStreamUtil.RTP_APP, streamReplace != null ? streamReplace : streamId);
openRtpServer(mediaServer, ssrcInfo, checkSsrc, !channel.isHasAudio(), false, tcpMode, callback);
SSRCInfo ssrcInfo = new SSRCInfo(0, ssrc, MediaStreamUtil.RTP_APP, streamReplace);
openRtpServer(mediaServer, ssrcInfo, checkSsrc, !channel.isHasAudio(), false, tcpMode, callback, streamId);
addAuthenticateInfo(streamId, streamReplace, channel.isHasAudio(), record, null);
return ssrcInfo;
}
@ -180,17 +174,16 @@ public class RtpServerServiceImpl implements IReceiveRtpServerService {
}
// 获取 mediaServer 可用的 ssrc
String ssrc = ssrcFactory.getPlayBackSsrc(mediaServer.getId());
String streamId;
String streamReplace = null;
if (mediaServer.isRtpEnable()) {
streamId = getPlaybackStream(device, channel, startTime, endTime);
}else {
streamId = String.format("%08x", Long.parseLong(ssrc)).toUpperCase();
streamReplace = getPlaybackStream(device, channel, startTime, endTime);
String ssrc;
if (mediaServer.isRtpEnable() && userSetting.getSsrcRandom()) {
ssrc = ssrcFactory.getPlayBackSsrcRandom();
} else {
ssrc = ssrcFactory.getPlayBackSsrc(mediaServer.getId());
}
String streamId = String.format("%08x", Long.parseLong(ssrc)).toUpperCase();
String streamReplace = getPlaybackStream(device, channel, startTime, endTime);
int tcpMode = device.getStreamMode().equals("TCP-ACTIVE")? 2: (device.getStreamMode().equals("TCP-PASSIVE")? 1:0);
if (device.isSsrcCheck() && tcpMode > 0) {
@ -200,8 +193,8 @@ public class RtpServerServiceImpl implements IReceiveRtpServerService {
Long checkSsrc = device.isSsrcCheck() ? Long.parseLong(ssrc) : 0L;
SSRCInfo ssrcInfo = new SSRCInfo(0, ssrc, MediaStreamUtil.RTP_APP, streamReplace != null ? streamReplace : streamId);
openRtpServer(mediaServer, ssrcInfo, checkSsrc, !channel.isHasAudio(), false, tcpMode, callback);
SSRCInfo ssrcInfo = new SSRCInfo(0, ssrc, MediaStreamUtil.RTP_APP, streamReplace);
openRtpServer(mediaServer, ssrcInfo, checkSsrc, !channel.isHasAudio(), false, tcpMode, callback, streamId);
addAuthenticateInfo(streamId, streamReplace, channel.isHasAudio(), false,null);
return ssrcInfo;
}
@ -233,8 +226,18 @@ public class RtpServerServiceImpl implements IReceiveRtpServerService {
int tcpMode = device.getStreamMode().equals("TCP-ACTIVE")? 2: (device.getStreamMode().equals("TCP-PASSIVE")? 1:0);
// 获取 mediaServer 可用的 ssrc
String ssrc = ssrcFactory.getPlayBackSsrc(mediaServer.getId());
String ssrc;
if (mediaServer.isRtpEnable() && userSetting.getSsrcRandom()) {
ssrc = ssrcFactory.getPlayBackSsrcRandom();
} else {
ssrc = ssrcFactory.getPlayBackSsrc(mediaServer.getId());
}
String streamId = String.format("%08x", Long.parseLong(ssrc)).toUpperCase();
String streamReplace = String.format("%s_%s_%s_%s", device.getDeviceId(), channel.getDeviceId(),
startTime.replace("-", "").replace(":", "").replace(" ", ""),
endTime.replace("-", "").replace(":", "").replace(" ", ""));
if (device.isSsrcCheck() && tcpMode > 0) {
// 目前zlm不支持 tcp模式更新ssrc暂时关闭ssrc校验
log.warn("[开启国标录像下载RTP收流] 平台对接时下级可能自定义ssrc但是tcp模式zlm收流目前无法更新ssrc可能收流超时此时请使用udp收流或者关闭ssrc校验");
@ -242,12 +245,12 @@ public class RtpServerServiceImpl implements IReceiveRtpServerService {
Long checkSsrc = device.isSsrcCheck() ? Long.parseLong(ssrc) : 0L;
SSRCInfo ssrcInfo = new SSRCInfo(0, ssrc, MediaStreamUtil.RTP_APP, streamId);
openRtpServer(mediaServer, ssrcInfo, checkSsrc, !channel.isHasAudio(), false, tcpMode, callback);
SSRCInfo ssrcInfo = new SSRCInfo(0, ssrc, MediaStreamUtil.RTP_APP, streamReplace);
openRtpServer(mediaServer, ssrcInfo, checkSsrc, !channel.isHasAudio(), false, tcpMode, callback, streamId);
long difference = DateUtil.getDifference(startTime, endTime) / 1000;
addAuthenticateInfo(streamId, null, channel.isHasAudio(), true, (int) difference);
addAuthenticateInfo(streamId, streamReplace, channel.isHasAudio(), true, (int) difference);
return ssrcInfo;
}
@ -278,7 +281,12 @@ public class RtpServerServiceImpl implements IReceiveRtpServerService {
}
// 获取 mediaServer 可用的 ssrc
String ssrc = ssrcFactory.getPlaySsrc(mediaServer.getId());
String ssrc;
if (mediaServer.isRtpEnable() && userSetting.getSsrcRandom()) {
ssrc = ssrcFactory.getPlaySsrcRandom();
} else {
ssrc = ssrcFactory.getPlaySsrc(mediaServer.getId());
}
SSRCInfo ssrcInfo = new SSRCInfo(0, ssrc, MediaStreamUtil.RTP_APP, streamId);
openRtpServer(mediaServer, ssrcInfo, 0L, false, true, tcpMode, callback);
@ -287,8 +295,14 @@ public class RtpServerServiceImpl implements IReceiveRtpServerService {
private void openRtpServer(MediaServer mediaServer, SSRCInfo ssrcInfo, Long checkSsrc, boolean disableAuto, boolean onlyAuto, int tcpMode,
ErrorCallback<OpenRTPServerResult> callback) {
openRtpServer(mediaServer, ssrcInfo, checkSsrc, disableAuto, onlyAuto, tcpMode, callback, null);
}
private void openRtpServer(MediaServer mediaServer, SSRCInfo ssrcInfo, Long checkSsrc, boolean disableAuto, boolean onlyAuto, int tcpMode,
ErrorCallback<OpenRTPServerResult> callback, String zlmStreamId) {
RTPServerParam rtpServerParam = new RTPServerParam(mediaServer, MediaStreamUtil.RTP_APP, ssrcInfo.getStream(), checkSsrc, null, onlyAuto, disableAuto, false, tcpMode);
rtpServerParam.setZlmStreamId(zlmStreamId);
int rtpServerPort = openCommonRTPServer(rtpServerParam, ((code, msg, data) -> {
if (code == InviteErrorCode.SUCCESS.getCode()) {
OpenRTPServerResult openRTPServerResult = new OpenRTPServerResult();
@ -336,7 +350,8 @@ public class RtpServerServiceImpl implements IReceiveRtpServerService {
int rtpServerPort;
if (rtpServerParam.getMediaServer().isRtpEnable()) {
rtpServerPort = mediaServerService.createRTPServer(rtpServerParam.getMediaServer(), rtpServerParam.getApp(), rtpServerParam.getStreamId(),
String effectiveStreamId = rtpServerParam.getZlmStreamId() != null ? rtpServerParam.getZlmStreamId() : rtpServerParam.getStreamId();
rtpServerPort = mediaServerService.createRTPServer(rtpServerParam.getMediaServer(), rtpServerParam.getApp(), effectiveStreamId,
Objects.requireNonNullElse(rtpServerParam.getSsrc(), 0L), rtpServerParam.getPort(), rtpServerParam.isOnlyAuto(),
rtpServerParam.isDisableAudio(), rtpServerParam.isReUsePort(), rtpServerParam.getTcpMode());
} else {

View File

@ -99,6 +99,8 @@ media:
user-settings:
# 点播/录像回放 等待超时时间,单位:毫秒
play-timeout: 180000
# [可选] 多端口模式使用随机SSRCSSRC允许重复默认false
ssrc-random: false
# [可选] 自动点播, 使用固定流地址进行播放时,如果未点播则自动进行点播, 需要rtp.enable=true
auto-apply-play: true
# 推流直播是否录制

View File

@ -0,0 +1,293 @@
package com.genersoft.iot.vmp.gb28181.dao.provider;
import com.genersoft.iot.vmp.gb28181.bean.Device;
import com.genersoft.iot.vmp.gb28181.bean.Group;
import com.genersoft.iot.vmp.web.custom.bean.CameraGroup;
import com.genersoft.iot.vmp.web.custom.bean.Point;
import org.junit.jupiter.api.Test;
import java.util.*;
import static org.junit.jupiter.api.Assertions.*;
class ChannelProviderTest {
private final ChannelProvider provider = new ChannelProvider();
// ========== queryByGbDeviceIds ==========
@Test
void queryByGbDeviceIds_shouldUseBindVariables() {
Map<String, Object> params = new HashMap<>();
params.put("deviceIds", Arrays.asList("DEV001", "DEV002"));
String sql = provider.queryByGbDeviceIds(params);
assertTrue(sql.contains("#{deviceIds[0]}"), "should use #{deviceIds[0]}");
assertTrue(sql.contains("#{deviceIds[1]}"), "should use #{deviceIds[1]}");
assertFalse(sql.contains("'DEV001'"), "should not contain raw quoted value");
assertFalse(sql.contains("'DEV002'"), "should not contain raw quoted value");
}
@Test
void queryByGbDeviceIds_shouldNotQuoteBindVariables() {
Map<String, Object> params = new HashMap<>();
params.put("deviceIds", Collections.singletonList("INJECT' OR 1=1 --"));
String sql = provider.queryByGbDeviceIds(params);
assertTrue(sql.contains("#{deviceIds[0]}"), "should use bind variable for injection attempt");
assertFalse(sql.contains("1=1"), "should not contain injection payload in SQL");
}
// ========== queryByGroupList ==========
@Test
void queryByGroupList_shouldUseBindVariables() {
Map<String, Object> params = new HashMap<>();
Group g1 = new Group();
g1.setDeviceId("GRP001");
Group g2 = new Group();
g2.setDeviceId("GRP002");
params.put("groupList", Arrays.asList(g1, g2));
String sql = provider.queryByGroupList(params);
assertTrue(sql.contains("#{groupList[0].deviceId}"), "should use #{groupList[0].deviceId}");
assertTrue(sql.contains("#{groupList[1].deviceId}"), "should use #{groupList[1].deviceId}");
assertFalse(sql.contains("GRP001"), "should not contain raw deviceId");
assertFalse(sql.contains("GRP002"), "should not contain raw deviceId");
}
// ========== queryOnlineListsByGbDeviceIds ==========
@Test
void queryOnlineListsByGbDeviceIds_shouldUseBindVariables() {
Map<String, Object> params = new HashMap<>();
Device d1 = new Device();
d1.setId(101);
Device d2 = new Device();
d2.setId(102);
params.put("deviceList", Arrays.asList(d1, d2));
String sql = provider.queryOnlineListsByGbDeviceIds(params);
assertTrue(sql.contains("#{deviceList[0].id}"), "should use #{deviceList[0].id}");
assertTrue(sql.contains("#{deviceList[1].id}"), "should use #{deviceList[1].id}");
assertFalse(sql.contains("101"), "should not contain raw id");
assertFalse(sql.contains("102"), "should not contain raw id");
}
@Test
void queryOnlineListsByGbDeviceIds_withEmptyList_shouldNotHaveInClause() {
Map<String, Object> params = new HashMap<>();
params.put("deviceList", Collections.emptyList());
String sql = provider.queryOnlineListsByGbDeviceIds(params);
assertFalse(sql.contains("data_device_id in ("), "should not have IN clause when empty");
}
@Test
void queryOnlineListsByGbDeviceIds_withNullList_shouldNotHaveInClause() {
Map<String, Object> params = new HashMap<>();
params.put("deviceList", null);
String sql = provider.queryOnlineListsByGbDeviceIds(params);
assertFalse(sql.contains("data_device_id in ("), "should not have IN clause when null");
}
// ========== queryListWithChildForSy ==========
@Test
void queryListWithChildForSy_shouldUseBindVariables() {
Map<String, Object> params = new HashMap<>();
CameraGroup cg1 = new CameraGroup();
cg1.setDeviceId("CG001");
CameraGroup cg2 = new CameraGroup();
cg2.setDeviceId("CG002");
params.put("groupList", Arrays.asList(cg1, cg2));
String sql = provider.queryListWithChildForSy(params);
assertTrue(sql.contains("#{groupList[0].deviceId}"), "should use #{groupList[0].deviceId}");
assertTrue(sql.contains("#{groupList[1].deviceId}"), "should use #{groupList[1].deviceId}");
assertFalse(sql.contains("'CG001'"), "should not contain raw quoted value");
}
@Test
void queryListWithChildForSy_withQuery_shouldUseBindVariable() {
Map<String, Object> params = new HashMap<>();
params.put("query", "search-term");
params.put("groupList", Collections.singletonList(new CameraGroup()));
String sql = provider.queryListWithChildForSy(params);
assertTrue(sql.contains("#{query}"), "should use #{query} bind variable");
assertFalse(sql.contains("search-term"), "should not contain raw query");
}
@Test
void queryListWithChildForSy_withSort_shouldUseWhitelist() {
Map<String, Object> params = new HashMap<>();
params.put("groupList", Collections.singletonList(new CameraGroup()));
params.put("sortName", "gbId");
params.put("order", true);
String sql = provider.queryListWithChildForSy(params);
assertTrue(sql.contains("order by gb_id"), "should sort by gb_id");
assertTrue(sql.contains("ASC"), "should be ascending");
}
@Test
void queryListWithChildForSy_withSortDesc_shouldUseDesc() {
Map<String, Object> params = new HashMap<>();
params.put("groupList", Collections.singletonList(new CameraGroup()));
params.put("sortName", "gbId");
params.put("order", false);
String sql = provider.queryListWithChildForSy(params);
assertTrue(sql.contains("DESC"), "should be descending");
}
// ========== queryListInBox ==========
@Test
void queryListInBox_shouldUseBindVariables() {
Map<String, Object> params = new HashMap<>();
CameraGroup cg = new CameraGroup();
cg.setDeviceId("BOX001");
params.put("groupList", Collections.singletonList(cg));
params.put("level", 3);
String sql = provider.queryListInBox(params);
assertTrue(sql.contains("#{groupList[0].deviceId}"), "should use bind variable");
assertFalse(sql.contains("'BOX001'"), "should not contain raw value");
assertTrue(sql.contains("#{level}"), "should use #{level} bind variable");
assertTrue(sql.contains("#{minLongitude}"), "should use #{minLongitude}");
assertTrue(sql.contains("#{maxLatitude}"), "should use #{maxLatitude}");
}
// ========== queryListInCircleForMysql ==========
@Test
void queryListInCircleForMysql_shouldUseBindVariablesForGeometry() {
Map<String, Object> params = new HashMap<>();
CameraGroup cg = new CameraGroup();
cg.setDeviceId("CIRCLE001");
params.put("groupList", Collections.singletonList(cg));
params.put("centerLongitude", 116.397);
params.put("centerLatitude", 39.908);
params.put("radius", 1000);
String sql = provider.queryListInCircleForMysql(params);
assertTrue(sql.contains("#{centerLongitude}"), "should use #{centerLongitude} bind variable");
assertTrue(sql.contains("#{centerLatitude}"), "should use #{centerLatitude} bind variable");
assertTrue(sql.contains("#{radius}"), "should use #{radius} bind variable");
assertFalse(sql.contains("116.397"), "should not contain raw longitude");
assertFalse(sql.contains("39.908"), "should not contain raw latitude");
assertTrue(sql.contains("CONCAT('point(', #{centerLongitude}, ' ', #{centerLatitude}, ')')"),
"should build WKT via CONCAT with bind variables");
}
// ========== queryListInCircleForKingBase ==========
@Test
void queryListInCircleForKingBase_shouldUseBindVariablesForGeometry() {
Map<String, Object> params = new HashMap<>();
CameraGroup cg = new CameraGroup();
cg.setDeviceId("CIRCLE002");
params.put("groupList", Collections.singletonList(cg));
params.put("centerLongitude", 121.473);
params.put("centerLatitude", 31.230);
params.put("radius", 500);
String sql = provider.queryListInCircleForKingBase(params);
assertTrue(sql.contains("#{centerLongitude}"), "should use #{centerLongitude}");
assertTrue(sql.contains("#{centerLatitude}"), "should use #{centerLatitude}");
assertTrue(sql.contains("#{radius}"), "should use #{radius}");
assertFalse(sql.contains("121.473"), "should not contain raw longitude");
assertFalse(sql.contains("31.230"), "should not contain raw latitude");
assertTrue(sql.contains("CONCAT('point(', #{centerLongitude}, ' ', #{centerLatitude}, ')')"),
"should build WKT via CONCAT with bind variables");
}
// ========== queryListInPolygonForMysql ==========
@Test
void queryListInPolygonForMysql_shouldUseBindVariablesForPoints() {
Map<String, Object> params = new HashMap<>();
CameraGroup cg = new CameraGroup();
cg.setDeviceId("POLY001");
params.put("groupList", Collections.singletonList(cg));
List<Point> points = new ArrayList<>();
Point p1 = new Point();
p1.setLng(116.0);
p1.setLat(39.0);
Point p2 = new Point();
p2.setLng(117.0);
p2.setLat(40.0);
points.add(p1);
points.add(p2);
params.put("pointList", points);
String sql = provider.queryListInPolygonForMysql(params);
assertTrue(sql.contains("#{pointList[0].lng}"), "should use #{pointList[0].lng}");
assertTrue(sql.contains("#{pointList[0].lat}"), "should use #{pointList[0].lat}");
assertTrue(sql.contains("#{pointList[1].lng}"), "should use #{pointList[1].lng}");
assertTrue(sql.contains("#{pointList[1].lat}"), "should use #{pointList[1].lat}");
assertFalse(sql.contains("116.0"), "should not contain raw lng");
assertFalse(sql.contains("117.0"), "should not contain raw lat");
assertTrue(sql.contains("CONCAT('POLYGON(('"), "should use CONCAT to build polygon WKT");
}
// ========== queryListInPolygonForKingBase ==========
@Test
void queryListInPolygonForKingBase_shouldUseBindVariablesForPoints() {
Map<String, Object> params = new HashMap<>();
CameraGroup cg = new CameraGroup();
cg.setDeviceId("POLY002");
params.put("groupList", Collections.singletonList(cg));
List<Point> points = new ArrayList<>();
Point p1 = new Point();
p1.setLng(116.0);
p1.setLat(39.0);
points.add(p1);
params.put("pointList", points);
String sql = provider.queryListInPolygonForKingBase(params);
assertTrue(sql.contains("#{pointList[0].lng}"), "should use #{pointList[0].lng}");
assertTrue(sql.contains("#{pointList[0].lat}"), "should use #{pointList[0].lat}");
assertFalse(sql.contains("116.0"), "should not contain raw lng");
assertFalse(sql.contains("39.0"), "should not contain raw lat");
assertTrue(sql.contains("ST_MakePoint"), "should use KingBase specific function");
}
// ========== queryListInCircleForMysql with injection attempt ==========
@Test
void queryListInCircleForMysql_shouldNotContainInjectionPayload() {
Map<String, Object> params = new HashMap<>();
CameraGroup cg = new CameraGroup();
cg.setDeviceId("NORMAL");
params.put("groupList", Collections.singletonList(cg));
params.put("centerLongitude", "0) OR 1=1 -- ");
params.put("centerLatitude", "0");
params.put("radius", 1000);
String sql = provider.queryListInCircleForMysql(params);
assertTrue(sql.contains("#{centerLongitude}"), "should use bind variable for injection payload");
assertFalse(sql.contains("1=1"), "should not contain 1=1 in SQL text");
assertFalse(sql.contains("OR 1=1"), "should not contain injection");
}
// ========== queryByGbDeviceIds single element ==========
@Test
void queryByGbDeviceIds_withSingleElement() {
Map<String, Object> params = new HashMap<>();
params.put("deviceIds", Collections.singletonList("SINGLE01"));
String sql = provider.queryByGbDeviceIds(params);
assertEquals(1, countOccurrences(sql, "#{deviceIds[0]}"),
"should have exactly one bind variable for single element");
assertFalse(sql.contains("#{deviceIds[0]},"), "should not have trailing comma in IN clause");
assertFalse(sql.contains(",#{deviceIds[0]}"), "should not have leading comma in IN clause");
}
// ========== helper ==========
private int countOccurrences(String str, String substr) {
int count = 0;
int idx = 0;
while ((idx = str.indexOf(substr, idx)) != -1) {
count++;
idx += substr.length();
}
return count;
}
}

View File

@ -0,0 +1,177 @@
package com.genersoft.iot.vmp.gb28181.dao.provider;
import org.junit.jupiter.api.Test;
import java.util.*;
import static org.junit.jupiter.api.Assertions.*;
class DeviceChannelProviderTest {
private final DeviceChannelProvider provider = new DeviceChannelProvider();
@Test
void queryChannels_withChannelIds_shouldUseBindVariables() {
Map<String, Object> params = new HashMap<>();
params.put("channelIds", Arrays.asList("CH001", "CH002", "CH003"));
String sql = provider.queryChannels(params);
assertTrue(sql.contains("#{channelIds[0]}"), "should use #{channelIds[0]}");
assertTrue(sql.contains("#{channelIds[1]}"), "should use #{channelIds[1]}");
assertTrue(sql.contains("#{channelIds[2]}"), "should use #{channelIds[2]}");
assertFalse(sql.contains("CH001"), "should not contain raw channel id");
assertFalse(sql.contains("CH002"), "should not contain raw channel id");
assertTrue(sql.contains("dc.device_id in ("), "should have IN clause");
}
@Test
void queryChannels_withoutChannelIds_shouldNotContainInClause() {
Map<String, Object> params = new HashMap<>();
String sql = provider.queryChannels(params);
assertFalse(sql.contains("device_id in ("), "should not have IN clause when no channelIds");
assertTrue(sql.contains("ORDER BY"), "should have ORDER BY");
}
@Test
void queryChannels_withEmptyChannelIds_shouldNotContainInClause() {
Map<String, Object> params = new HashMap<>();
params.put("channelIds", Collections.emptyList());
String sql = provider.queryChannels(params);
assertFalse(sql.contains("device_id in ("), "should not have IN clause when channelIds empty");
}
@Test
void queryChannels_withDataDeviceId_shouldUseBindVariable() {
Map<String, Object> params = new HashMap<>();
params.put("dataDeviceId", 42);
String sql = provider.queryChannels(params);
assertTrue(sql.contains("#{dataDeviceId}"), "should use #{dataDeviceId}");
}
@Test
void queryChannels_withQuery_shouldUseBindVariable() {
Map<String, Object> params = new HashMap<>();
params.put("query", "test");
String sql = provider.queryChannels(params);
assertTrue(sql.contains("#{query}"), "should use #{query} bind variable");
assertFalse(sql.contains("'test'"), "should not contain raw query value");
}
@Test
void queryChannels_withOnline_shouldFilterStatus() {
Map<String, Object> params = new HashMap<>();
params.put("online", true);
String sql = provider.queryChannels(params);
assertTrue(sql.contains("'ON'"), "should filter for ON status");
assertFalse(sql.contains("'OFF'"), "should not filter for OFF status");
}
@Test
void queryChannels_withOffline_shouldFilterStatus() {
Map<String, Object> params = new HashMap<>();
params.put("online", false);
String sql = provider.queryChannels(params);
assertTrue(sql.contains("'OFF'"), "should filter for OFF status");
assertFalse(sql.contains("'ON'"), "should not filter for ON status");
}
@Test
void queryChannels_withBusinessGroupId_shouldFilter() {
Map<String, Object> params = new HashMap<>();
params.put("businessGroupId", "group-1");
String sql = provider.queryChannels(params);
assertTrue(sql.contains("#{businessGroupId}"), "should use bind variable");
}
@Test
void queryChannelsByDeviceDbId_shouldUseBindVariable() {
Map<String, Object> params = new HashMap<>();
params.put("dataDeviceId", 99);
String sql = provider.queryChannelsByDeviceDbId(params);
assertTrue(sql.contains("#{dataDeviceId}"), "should use #{dataDeviceId}");
}
@Test
void queryChannelsByDeviceDbId_shouldFilterByDataType() {
Map<String, Object> params = new HashMap<>();
params.put("dataDeviceId", 1);
String sql = provider.queryChannelsByDeviceDbId(params);
assertTrue(sql.contains("data_type = 1"), "should filter by GB28181 data type");
}
@Test
void getOne_shouldUseBindVariable() {
Map<String, Object> params = new HashMap<>();
params.put("id", 123);
String sql = provider.getOne(params);
assertTrue(sql.contains("#{id}"), "should use #{id} bind variable");
assertTrue(sql.contains("where"), "should have WHERE clause");
assertTrue(sql.contains("#{id}"), "should have bind variable");
}
@Test
void getOneByDeviceId_shouldUseBindVariables() {
Map<String, Object> params = new HashMap<>();
params.put("dataDeviceId", 10);
params.put("channelId", "CH999");
String sql = provider.getOneByDeviceId(params);
assertTrue(sql.contains("#{dataDeviceId}"), "should use #{dataDeviceId}");
assertTrue(sql.contains("#{channelId}"), "should use #{channelId}");
}
@Test
void queryByDeviceId_shouldUseBindVariable() {
Map<String, Object> params = new HashMap<>();
params.put("gbDeviceId", "GB-TEST-123");
String sql = provider.queryByDeviceId(params);
assertTrue(sql.contains("#{gbDeviceId}"), "should use #{gbDeviceId}");
}
@Test
void queryById_shouldUseBindVariable() {
Map<String, Object> params = new HashMap<>();
params.put("gbId", 456);
String sql = provider.queryById(params);
assertTrue(sql.contains("#{gbId}"), "should use #{gbId}");
}
@Test
void queryList_withQuery_shouldUseBindVariable() {
Map<String, Object> params = new HashMap<>();
params.put("query", "search-term");
String sql = provider.queryList(params);
assertTrue(sql.contains("#{query}"), "should use #{query} bind variable");
assertFalse(sql.contains("search-term"), "should not contain raw query value");
}
@Test
void queryList_withOnline_shouldFilter() {
Map<String, Object> params = new HashMap<>();
params.put("online", true);
String sql = provider.queryList(params);
assertTrue(sql.contains("'ON'"), "should filter for ON");
}
@Test
void queryList_withHasCivilCode_shouldFilter() {
Map<String, Object> params = new HashMap<>();
params.put("hasCivilCode", true);
String sql = provider.queryList(params);
assertTrue(sql.contains("civil_code) is not null"), "should filter for not null civil code");
}
@Test
void queryList_withHasGroup_shouldFilter() {
Map<String, Object> params = new HashMap<>();
params.put("hasGroup", true);
String sql = provider.queryList(params);
assertTrue(sql.contains("parent_id) is not null"), "should filter for not null parent");
}
@Test
void queryChannels_withHasStream_shouldFilter() {
Map<String, Object> params = new HashMap<>();
params.put("hasStream", true);
String sql = provider.queryChannels(params);
assertTrue(sql.contains("stream_id IS NOT NULL"), "should filter for not null stream_id");
}
}