# Hikvision NVR Playback Search Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Add a documented HTTP endpoint that queries a Hikvision NVR for actual recording coverage and returns Beijing-time timestamp ranges plus credential-bearing RTSP playback URLs. **Architecture:** A controller under `com.inspect.nvr.hik` delegates to a focused service. The service reuses `HikLoginService`, queries recordings with `NET_DVR_FindFile_V40`/`NET_DVR_FindNextFile_V40`, clips and merges actual file intervals, and delegates URL construction to a pure `HikPlaybackUrlBuilder`. Controller advice converts validation failures to HTTP 400 and device/SDK failures to HTTP 502. **Tech Stack:** Java 8, Spring Boot 2.3.4, Hikvision HCNetSDK through JNA, Lombok, springdoc-openapi 1.8.0, JUnit 4, Mockito, Spring MockMvc. **Execution constraint:** Keep all changes local. Do not stage or commit any file. --- ## File map **Create:** - `src/main/java/com/inspect/nvr/hik/domain/HikNvrPlaybackRequest.java` — HTTP input and Swagger field contract. - `src/main/java/com/inspect/nvr/hik/domain/HikNvrPlaybackRecord.java` — one actual continuous range and its RTSP URL. - `src/main/java/com/inspect/nvr/hik/domain/HikNvrPlaybackResponse.java` — successful search response. - `src/main/java/com/inspect/nvr/hik/domain/HikNvrPlaybackErrorResponse.java` — parameter or SDK error response. - `src/main/java/com/inspect/nvr/hik/exception/HikNvrPlaybackException.java` — status and SDK error carrier. - `src/main/java/com/inspect/nvr/hik/service/HikPlaybackUrlBuilder.java` — pure Beijing-to-UTC RTSP URL builder. - `src/main/java/com/inspect/nvr/hik/service/HikNvrPlaybackService.java` — validation, SDK query and interval processing. - `src/main/java/com/inspect/nvr/hik/controller/HikNvrPlaybackController.java` — `POST /hik/nvr/playback/search`. - `src/main/java/com/inspect/nvr/hik/controller/HikNvrPlaybackExceptionHandler.java` — scoped HTTP error mapping. - `src/test/java/com/inspect/nvr/hik/service/HikPlaybackUrlBuilderTest.java` — URL and timestamp tests. - `src/test/java/com/inspect/nvr/hik/service/HikNvrPlaybackServiceTest.java` — SDK state and interval tests. - `src/test/java/com/inspect/nvr/hik/controller/HikNvrPlaybackControllerTest.java` — HTTP response tests. **Modify:** - `src/test/java/com/inspect/nvr/config/ApiDocumentationCoverageTest.java` — include the new controller, handler and schemas. ## Task 1: RTSP URL contract and domain models **Files:** - Test: `src/test/java/com/inspect/nvr/hik/service/HikPlaybackUrlBuilderTest.java` - Create: `src/main/java/com/inspect/nvr/hik/domain/HikNvrPlaybackRequest.java` - Create: `src/main/java/com/inspect/nvr/hik/domain/HikNvrPlaybackRecord.java` - Create: `src/main/java/com/inspect/nvr/hik/domain/HikNvrPlaybackResponse.java` - Create: `src/main/java/com/inspect/nvr/hik/domain/HikNvrPlaybackErrorResponse.java` - Create: `src/main/java/com/inspect/nvr/hik/service/HikPlaybackUrlBuilder.java` - [ ] **Step 1: Write the failing URL builder test** Create a JUnit 4 test that fixes the time-zone, track and credential-encoding behavior: ```java @Test public void buildsMainStreamPlaybackUrlUsingUtcQueryTimes() { HikNvrPlaybackRequest request = request("admin", "p@ss word", 1, 1); String url = builder.build( request, LocalDateTime.of(2026, 7, 13, 12, 30, 0), LocalDateTime.of(2026, 7, 13, 13, 0, 0)); assertEquals("rtsp://admin:p%40ss%20word@192.168.1.100:554/Streaming/tracks/101" + "?starttime=20260713T043000Z&endtime=20260713T050000Z", url); } @Test public void buildsSubStreamTrackForNvrChannelThirtyThree() { HikNvrPlaybackRequest request = request("operator", "secret", 33, 2); String url = builder.build( request, LocalDateTime.of(2026, 7, 13, 12, 0, 0), LocalDateTime.of(2026, 7, 13, 13, 0, 0)); assertTrue(url.contains("/Streaming/tracks/3302?")); } ``` - [ ] **Step 2: Run the URL test and verify RED** Run: ```powershell $env:JAVA_HOME='C:\Program Files\Java\jdk1.8.0_191' mvn -Dtest=HikPlaybackUrlBuilderTest test ``` Expected: test compilation fails because `HikPlaybackUrlBuilder` and the new domain classes do not exist. - [ ] **Step 3: Implement the four documented domain models** Use Lombok `@Data`, `@Builder`, `@NoArgsConstructor`, `@AllArgsConstructor` and class/field `@Schema` descriptions. The request fields are exactly: ```java private String nvrIp; private Integer sdkPort; private Integer rtspPort; private Integer channel; private Integer streamType; private String username; @Schema(description = "NVR登录密码", accessMode = Schema.AccessMode.WRITE_ONLY) private String password; private String startTime; private String endTime; ``` `HikNvrPlaybackRecord` contains `List timeRange` and `String rtspUrl`. `HikNvrPlaybackResponse` contains `int code`, `String message`, `boolean hasRecord`, and `List records`, with a factory that always returns code `0` and a non-null list. `HikNvrPlaybackErrorResponse` contains `int code` and `String message`. - [ ] **Step 4: Implement the pure URL builder** Implement `HikPlaybackUrlBuilder.build(request, start, end)` with: ```java private static final ZoneId BEIJING_ZONE = ZoneId.of("Asia/Shanghai"); private static final DateTimeFormatter RTSP_TIME = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss'Z'").withZone(ZoneOffset.UTC); int trackId = request.getChannel() * 100 + request.getStreamType(); String utcStart = RTSP_TIME.format(start.atZone(BEIJING_ZONE).toInstant()); String utcEnd = RTSP_TIME.format(end.atZone(BEIJING_ZONE).toInstant()); ``` Percent-encode every UTF-8 user-info byte except ASCII letters, digits, `-`, `.`, `_`, and `~`. Assemble the URL without logging it. - [ ] **Step 5: Run the URL test and verify GREEN** Run `mvn -Dtest=HikPlaybackUrlBuilderTest test`. Expected: both tests pass with zero failures. ## Task 2: SDK recording search and interval merging **Files:** - Test: `src/test/java/com/inspect/nvr/hik/service/HikNvrPlaybackServiceTest.java` - Create: `src/main/java/com/inspect/nvr/hik/exception/HikNvrPlaybackException.java` - Create: `src/main/java/com/inspect/nvr/hik/service/HikNvrPlaybackService.java` - [ ] **Step 1: Write failing service tests for partial coverage, gaps and cleanup** Use Mockito to return a login ID of `7`, find handle `88`, and populate `NET_DVR_FINDDATA_V40` outputs. The main behavior test must feed these file intervals: ```text 2026-07-13 12:20:00 - 12:40:00 2026-07-13 12:30:00 - 12:50:00 2026-07-13 12:55:00 - 13:10:00 ``` For a request from `12:00:00` to `13:00:00`, assert exactly: ```java assertEquals(Arrays.asList("20260713122000", "20260713125000"), response.getRecords().get(0).getTimeRange()); assertEquals(Arrays.asList("20260713125500", "20260713130000"), response.getRecords().get(1).getTimeRange()); assertTrue(response.isHasRecord()); verify(hcNetSDK).NET_DVR_FindClose_V30(88); ``` Add separate tests that assert: - `NET_DVR_FILE_NOFIND` produces `hasRecord=false` and an empty list. - `NET_DVR_ISFINDING` followed by success is retried and returns data. - `NET_DVR_FILE_EXCEPTION` throws `HikNvrPlaybackException` with the value returned by `NET_DVR_GetLastError` and still closes handle `88`. - an invalid end time is rejected before `HikLoginService.login` is invoked. - [ ] **Step 2: Run the service test and verify RED** Run `mvn -Dtest=HikNvrPlaybackServiceTest test`. Expected: test compilation fails because `HikNvrPlaybackService` and `HikNvrPlaybackException` do not exist. - [ ] **Step 3: Implement validation and SDK condition creation** `HikNvrPlaybackService.search` must: ```java LocalDateTime requestStart = parseBeijing(request.getStartTime()); LocalDateTime requestEnd = parseBeijing(request.getEndTime()); if (!requestEnd.isAfter(requestStart)) { throw HikNvrPlaybackException.badRequest("结束时间必须晚于开始时间"); } ``` Validate nonblank NVR IP, username and password; ports in `1..65535`; channel greater than zero; stream type in `{1, 2}`; and reject URL delimiter characters in the NVR address. Build `NvrInfo`, call `HikLoginService.login`, and create a `NET_DVR_FILECOND_V40` with: ```java condition.lChannel = request.getChannel(); condition.dwFileType = 0xff; condition.dwIsLocked = 0xff; condition.byFindType = 0; condition.byQuickSearch = 0; condition.byStreamType = request.getStreamType() == 1 ? (byte) 3 : (byte) 1; condition.struStartTime = toSdkTime(requestStart); condition.struStopTime = toSdkTime(requestEnd); condition.write(); ``` - [ ] **Step 4: Implement the bounded SDK result loop and guaranteed close** Call `NET_DVR_FindFile_V40`; a negative handle becomes an HTTP-502 exception with `NET_DVR_GetLastError()`. In a `try/finally`, loop over `NET_DVR_FindNextFile_V40`: ```java switch (status) { case HCNetSDK.NET_DVR_FILE_SUCCESS: rawRanges.add(rangeFrom(findData)); findingAttempts = 0; break; case HCNetSDK.NET_DVR_ISFINDING: waitForSearch(++findingAttempts); break; case HCNetSDK.NET_DVR_FILE_NOFIND: case HCNetSDK.NET_DVR_NOMOREFILE: return buildResponse(request, requestStart, requestEnd, rawRanges); default: throw sdkFailure("查询海康NVR录像失败", hcNetSDK.NET_DVR_GetLastError()); } ``` Limit `NET_DVR_ISFINDING` to 100 attempts with a 50 ms wait, restore the interrupt flag when interrupted, and close with `NET_DVR_FindClose_V30(findHandle)` in `finally`. - [ ] **Step 5: Implement clipping, sorting and exact-adjacency merging** For each returned interval, use `max(fileStart, requestStart)` and `min(fileEnd, requestEnd)`, discard empty intervals, sort by start, and merge only when `next.start <= current.end`. Format response pairs using `yyyyMMddHHmmss`, and build one RTSP URL per merged interval. - [ ] **Step 6: Run service and URL tests and verify GREEN** Run: ```powershell mvn -Dtest=HikNvrPlaybackServiceTest,HikPlaybackUrlBuilderTest test ``` Expected: all service and URL tests pass; Mockito verifies the find handle is closed. ## Task 3: HTTP endpoint, error mapping and Swagger coverage **Files:** - Test: `src/test/java/com/inspect/nvr/hik/controller/HikNvrPlaybackControllerTest.java` - Create: `src/main/java/com/inspect/nvr/hik/controller/HikNvrPlaybackController.java` - Create: `src/main/java/com/inspect/nvr/hik/controller/HikNvrPlaybackExceptionHandler.java` - Modify: `src/test/java/com/inspect/nvr/config/ApiDocumentationCoverageTest.java` - [ ] **Step 1: Write failing MockMvc tests** Configure standalone MockMvc with the controller and advice. Assert a valid POST returns status 200 and JSON paths: ```java andExpect(jsonPath("$.hasRecord").value(true)) andExpect(jsonPath("$.records[0].timeRange[0]").value("20260713123000")) andExpect(jsonPath("$.records[0].timeRange[1]").value("20260713130000")) andExpect(jsonPath("$.records[0].rtspUrl").value(expectedUrl)); ``` Make the mocked service throw `HikNvrPlaybackException.badRequest("结束时间必须晚于开始时间")` and assert HTTP 400. Make it throw an SDK exception with code `7` and assert HTTP 502 plus `$.code == 7`. - [ ] **Step 2: Run the controller test and verify RED** Run `mvn -Dtest=HikNvrPlaybackControllerTest test`. Expected: test compilation fails because the controller and handler do not exist. - [ ] **Step 3: Implement the documented controller and scoped advice** Create a controller with: ```java @RestController @RequestMapping("/hik/nvr/playback") @Tag(name = "海康NVR回放", description = "查询海康NVR历史录像并生成RTSP回放地址") public class HikNvrPlaybackController { @PostMapping("/search") public ResponseEntity search( @io.swagger.v3.oas.annotations.parameters.RequestBody( description = "海康NVR历史录像查询参数", required = true) @RequestBody HikNvrPlaybackRequest request) { return ResponseEntity.ok(playbackService.search(request)); } } ``` Add Chinese `@Operation` and `@ApiResponses` for 200, 400 and 502 with concrete response schemas. The advice must use `@RestControllerAdvice(assignableTypes = HikNvrPlaybackController.class)` and return `HikNvrPlaybackErrorResponse` without logging credentials or RTSP URLs. - [ ] **Step 4: Run the controller test and verify GREEN** Run `mvn -Dtest=HikNvrPlaybackControllerTest test`. Expected: all controller tests pass. - [ ] **Step 5: Extend documentation coverage test and verify RED** Change the scan root to `com.inspect.nvr`, add `com.inspect.nvr.hik.controller.HikNvrPlaybackController` to expected controllers, change expected handler count from 35 to 36, and add the four new API models to `API_MODEL_TYPES`. Run: ```powershell mvn -Dtest=ApiDocumentationCoverageTest test ``` Expected before all schema annotations are complete: failure identifies any missing model or field description/example. Complete only the reported Swagger metadata until the test passes. Password must remain `WRITE_ONLY` with no example. - [ ] **Step 6: Verify documentation coverage GREEN** Run `mvn -Dtest=ApiDocumentationCoverageTest test`. Expected: all coverage assertions pass with 8 controllers and 36 handler methods. ## Task 4: Full verification and local handoff **Files:** - Verify all files created or modified in Tasks 1–3. - [ ] **Step 1: Run the full Maven test suite** Run: ```powershell $env:JAVA_HOME='C:\Program Files\Java\jdk1.8.0_191' $env:Path="$env:JAVA_HOME\bin;$env:Path" mvn test -DskipTests=false ``` Expected: build success with all existing and new tests passing. - [ ] **Step 2: Run scoped whitespace and sensitive logging checks** Run: ```powershell git diff --check -- src/main/java/com/inspect/nvr/hik src/test/java/com/inspect/nvr/hik src/test/java/com/inspect/nvr/config/ApiDocumentationCoverageTest.java rg -n 'log\..*(password|rtspUrl)|toString\(\).*HikNvrPlaybackRequest' src/main/java/com/inspect/nvr/hik ``` Expected: `git diff --check` exits 0 and the sensitive logging search returns no matches. - [ ] **Step 3: Verify existing staged files were not changed** Capture `git diff --cached --name-only` and compare it with the pre-task staged list. Do not run `git add`, `git commit`, branch creation, reset or checkout. - [ ] **Step 4: Report the endpoint contract and limits** Report the local endpoint, example request/response, test count, and the important limit that the URL contains credentials because the user explicitly requested it. State that no files were committed or newly staged.