package com.inspect.nvr.hik.service; import com.inspect.nvr.hik.domain.HikNvrPtzMode; import com.inspect.nvr.hik.domain.HikNvrUiActionRequest; import com.inspect.nvr.hik.domain.HikNvrUiActionResponse; import com.inspect.nvr.hikVision.utils.jna.HCNetSDK; import com.inspect.nvr.domain.Infrared.NvrInfo; import com.inspect.nvr.ivs.mapper.IvsCameraMapper; import com.inspect.nvr.ivs.pojo.IvsCamera; import com.inspect.nvr.service.HikCameraService; import com.inspect.nvr.service.HikLoginService; import com.inspect.nvr.service.HikLoginSession; import com.sun.jna.Pointer; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.test.util.ReflectionTestUtils; import java.io.ByteArrayOutputStream; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.Map; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) public class HikNvrUiActionServiceTest { @Mock private HikLoginService loginService; @Mock private HikCameraService cameraService; @Mock private HCNetSDK sdk; @Mock private IvsCameraMapper ivsCameraMapper; private HikNvrUiActionService service; @Before public void setUp() { // 构造完全来自数据库的 NVR 连接对象,服务不再依赖固定页面配置。 NvrInfo databaseNvr = new NvrInfo(); databaseNvr.setNvrName("测试 NVR"); databaseNvr.setNvrIp("192.168.1.249"); databaseNvr.setServerPort(8000); databaseNvr.setRtspPort(554); databaseNvr.setHttpPort(80); databaseNvr.setAccount("admin"); databaseNvr.setPassword("secret"); databaseNvr.setStartChannel(1); service = spy(new HikNvrUiActionService()); ReflectionTestUtils.setField(service, "boundNvrInfo", databaseNvr); ReflectionTestUtils.setField(service, "loginService", loginService); ReflectionTestUtils.setField(service, "cameraService", cameraService); ReflectionTestUtils.setField(service, "sdk", sdk); ReflectionTestUtils.setField(service, "ivsCameraMapper", ivsCameraMapper); // 清理静态通道状态,避免不同测试之间复用上一次 PTZ 动作的停止命令。 Map channelStates = (Map) ReflectionTestUtils.getField( HikNvrUiActionService.class, "PTZ_CHANNEL_STATES"); if (channelStates != null) { channelStates.clear(); } when(loginService.loginSession(any())).thenReturn( new HikLoginSession(7, 0, 1, 8, 33)); } @Test public void callsSdkPresetWithoutIsapiRoundTrip() { when(sdk.NET_DVR_PTZPreset_Other( eq(7), eq(40), eq(HCNetSDK.GOTO_PRESET), eq(5))).thenReturn(true); HikNvrUiActionResponse response = service.preset(HikNvrUiActionRequest.builder() .channel(8) .preset(5) .build()); assertTrue(response.isSuccess()); assertEquals("已调用预置位 5(SDK)", response.getMessage()); assertEquals(Integer.valueOf(40), response.getSdkChannel()); verify(sdk).NET_DVR_PTZPreset_Other(7, 40, HCNetSDK.GOTO_PRESET, 5); } @Test public void sendsPtzThroughSdkWithConfiguredSpeed() { when(sdk.NET_DVR_PTZControlWithSpeed_Other( eq(7), eq(40), eq(HCNetSDK.PAN_LEFT), eq(0), eq(6))).thenReturn(true); HikNvrUiActionResponse response = service.ptz(HikNvrUiActionRequest.builder() .channel(8) .action("LEFT") .speed(6) .build()); assertTrue(response.isSuccess()); assertEquals(Integer.valueOf(40), response.getSdkChannel()); verify(sdk).NET_DVR_PTZControlWithSpeed_Other(7, 40, HCNetSDK.PAN_LEFT, 0, 6); } @Test public void sendsPointPtzStartAndAutoStopAfterOneSecond() throws Exception { when(sdk.NET_DVR_PTZControlWithSpeed_Other( eq(7), eq(40), eq(HCNetSDK.PAN_LEFT), eq(0), eq(6))).thenReturn(true); when(sdk.NET_DVR_PTZControlWithSpeed_Other( eq(7), eq(40), eq(HCNetSDK.PAN_LEFT), eq(1), eq(6))).thenReturn(true); HikNvrUiActionResponse response = service.ptz( HikNvrUiActionRequest.builder() .channel(8) .action("LEFT") .speed(6) .build(), HikNvrPtzMode.POINT); assertTrue(response.isSuccess()); assertTrue(response.getMessage().contains("1秒后自动停止")); verify(sdk).NET_DVR_PTZControlWithSpeed_Other(7, 40, HCNetSDK.PAN_LEFT, 0, 6); verify(sdk, timeout(2500)).NET_DVR_PTZControlWithSpeed_Other( 7, 40, HCNetSDK.PAN_LEFT, 1, 6); } @Test public void explicitStopCancelsPendingPointStop() throws Exception { when(sdk.NET_DVR_PTZControlWithSpeed_Other( eq(7), eq(40), eq(HCNetSDK.PAN_LEFT), eq(0), eq(6))).thenReturn(true); when(sdk.NET_DVR_PTZControlWithSpeed_Other( eq(7), eq(40), eq(HCNetSDK.PAN_LEFT), eq(1), eq(6))).thenReturn(true); service.ptz(HikNvrUiActionRequest.builder() .channel(8) .action("LEFT") .speed(6) .build(), HikNvrPtzMode.POINT); service.ptz(HikNvrUiActionRequest.builder() .channel(8) .action("STOP") .speed(6) .build()); Thread.sleep(1200L); verify(sdk, times(1)).NET_DVR_PTZControlWithSpeed_Other( 7, 40, HCNetSDK.PAN_LEFT, 1, 6); } /** 验证光圈动作会映射为海康光圈放大命令并携带设备速度。 */ @Test public void sendsLensCommandThroughSdkWithMappedCommand() { when(sdk.NET_DVR_PTZControlWithSpeed_Other( eq(7), eq(40), eq(HCNetSDK.IRIS_OPEN), eq(0), eq(7))).thenReturn(true); HikNvrUiActionResponse response = service.ptz(HikNvrUiActionRequest.builder() .channel(8) .action("IRIS_OPEN") .speed(7) .build()); assertTrue(response.isSuccess()); verify(sdk).NET_DVR_PTZControlWithSpeed_Other( 7, 40, HCNetSDK.IRIS_OPEN, 0, 7); } /** 验证停止请求会复用最近一次启动的聚焦命令,而不是使用方向命令占位值。 */ @Test public void stopsThePreviouslyStartedLensCommand() { when(sdk.NET_DVR_PTZControlWithSpeed_Other( eq(7), eq(40), eq(HCNetSDK.FOCUS_NEAR), eq(0), eq(5))).thenReturn(true); when(sdk.NET_DVR_PTZControlWithSpeed_Other( eq(7), eq(40), eq(HCNetSDK.FOCUS_NEAR), eq(1), eq(5))).thenReturn(true); service.ptz(HikNvrUiActionRequest.builder() .channel(8) .action("FOCUS_NEAR") .speed(5) .build()); HikNvrUiActionResponse response = service.ptz(HikNvrUiActionRequest.builder() .channel(8) .action("STOP") .speed(4) .build()); assertTrue(response.isSuccess()); verify(sdk).NET_DVR_PTZControlWithSpeed_Other( 7, 40, HCNetSDK.FOCUS_NEAR, 1, 5); } /** 验证雨刷打开请求会调用海康辅助设备控制接口。 */ @Test public void controlsWiperThroughAuxiliarySdkCommand() { when(sdk.NET_DVR_PTZControl_Other( eq(7), eq(40), eq(HCNetSDK.WIPER_PWRON), eq(0))).thenReturn(true); HikNvrUiActionResponse response = service.auxiliaryControl(8, 1, true); assertTrue(response.isSuccess()); verify(sdk).NET_DVR_PTZControl_Other(7, 40, HCNetSDK.WIPER_PWRON, 0); } /** 验证起始位调用会构造远程控制结构体并发送给海康 SDK。 */ @Test public void callsConfiguredInitialPositionThroughRemoteControl() { when(sdk.NET_DVR_RemoteControl( eq(7), eq(HCNetSDK.NET_DVR_PTZ_INITIALPOSITIONCTRL), any(Pointer.class), anyInt())).thenReturn(true); HikNvrUiActionResponse response = service.gotoInitialPosition(8); assertTrue(response.isSuccess()); assertEquals(Integer.valueOf(40), response.getSdkChannel()); verify(sdk).NET_DVR_RemoteControl( eq(7), eq(HCNetSDK.NET_DVR_PTZ_INITIALPOSITIONCTRL), any(Pointer.class), anyInt()); } @Test public void savesUnnamedPresetThroughSdkWithoutHttpNameSync() { when(sdk.NET_DVR_PTZPreset_Other( eq(7), eq(40), eq(HCNetSDK.SET_PRESET), eq(6))).thenReturn(true); HikNvrUiActionResponse response = service.createPreset(HikNvrUiActionRequest.builder() .channel(8) .preset(6) .build()); assertTrue(response.isSuccess()); assertEquals("已保存预置位 6(SDK)", response.getMessage()); assertEquals(Integer.valueOf(40), response.getSdkChannel()); verify(sdk).NET_DVR_PTZPreset_Other(7, 40, HCNetSDK.SET_PRESET, 6); } @Test public void savesPresetNameThroughCameraSdkAndNvrIsapi() { when(sdk.NET_DVR_PTZPreset_Other( eq(7), eq(40), eq(HCNetSDK.SET_PRESET), eq(6))).thenReturn(true); when(sdk.NET_DVR_SetDVRConfig( eq(7), eq(HCNetSDK.NET_DVR_SET_PRESET_NAME), eq(40), any(Pointer.class), anyInt())).thenReturn(true); doNothing().when(service).setPresetByIsapi(eq(8), eq(6), eq("测试大门")); HikNvrUiActionResponse response = service.createPreset(HikNvrUiActionRequest.builder() .channel(8) .preset(6) .presetName("测试大门") .build()); assertTrue(response.isSuccess()); assertEquals("已保存预置位 6(SDK),名称:测试大门", response.getMessage()); verify(sdk).NET_DVR_SetDVRConfig( eq(7), eq(HCNetSDK.NET_DVR_SET_PRESET_NAME), eq(40), any(Pointer.class), anyInt()); } @Test public void prefersClientDemoDirectCameraTargetWhenConfigured() { // 直连目标只从当前 NVR 的 ivs_camera 记录读取。 IvsCamera camera = new IvsCamera(); camera.setCode("22078710632508667008#database-domain"); camera.setDevIP("192.168.1.254"); when(ivsCameraMapper.selectByNvrIp("192.168.1.249")) .thenReturn(java.util.Collections.singletonList(camera)); doReturn(true).when(service).isCameraReachable(any(IvsCamera.class)); when(loginService.loginSession(any(NvrInfo.class))).thenAnswer(invocation -> { NvrInfo info = invocation.getArgument(0); if ("192.168.1.254".equals(info.getNvrIp())) { return new HikLoginSession(9, 1, 1, 0, 0); } return new HikLoginSession(7, 0, 1, 8, 33); }); when(sdk.NET_DVR_PTZPreset_Other( eq(9), eq(1), eq(HCNetSDK.GOTO_PRESET), eq(5))).thenReturn(true); HikNvrUiActionResponse response = service.preset(HikNvrUiActionRequest.builder() .channel(8) .preset(5) .build()); assertTrue(response.isSuccess()); assertEquals(Integer.valueOf(1), response.getSdkChannel()); verify(sdk).NET_DVR_PTZPreset_Other(9, 1, HCNetSDK.GOTO_PRESET, 5); verify(loginService).loginSession(any(NvrInfo.class)); } @Test public void decodesPresetNameUsingItsActualEncoding() { String utf8Name = ReflectionTestUtils.invokeMethod( service, "decodePresetName", "正确".getBytes(StandardCharsets.UTF_8)); String gbkName = ReflectionTestUtils.invokeMethod( service, "decodePresetName", "预置点 3".getBytes(Charset.forName("GBK"))); assertEquals("正确", utf8Name); assertEquals("预置点 3", gbkName); } @Test public void normalizesMixedPresetXmlWithoutChangingUtf8Names() throws Exception { ByteArrayOutputStream xml = new ByteArrayOutputStream(); xml.write(("" + "1").getBytes(StandardCharsets.UTF_8)); xml.write("正确".getBytes(StandardCharsets.UTF_8)); xml.write(("3") .getBytes(StandardCharsets.UTF_8)); xml.write("预置点 3".getBytes(Charset.forName("GBK"))); xml.write("" .getBytes(StandardCharsets.UTF_8)); String normalized = ReflectionTestUtils.invokeMethod( service, "normalizePresetXml", xml.toByteArray()); assertTrue(normalized.contains("正确")); assertTrue(normalized.contains("预置点 3")); } @Test public void buildsPresetNamePayloadAsUtf8() { String xml = ReflectionTestUtils.invokeMethod( service, "buildPresetXml", 6, "测试大门"); byte[] wireBytes = xml.getBytes(StandardCharsets.UTF_8); assertTrue(new String(wireBytes, StandardCharsets.UTF_8) .contains("测试大门")); assertEquals(-1, new String(wireBytes, Charset.forName("GBK")) .indexOf("测试大门")); } }