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.
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.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
Create a JUnit 4 test that fixes the time-zone, track and credential-encoding behavior:
@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?"));
}
Run:
$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.
Use Lombok @Data, @Builder, @NoArgsConstructor, @AllArgsConstructor and class/field @Schema descriptions. The request fields are exactly:
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<String> timeRange and String rtspUrl. HikNvrPlaybackResponse contains int code, String message, boolean hasRecord, and List<HikNvrPlaybackRecord> records, with a factory that always returns code 0 and a non-null list. HikNvrPlaybackErrorResponse contains int code and String message.
Implement HikPlaybackUrlBuilder.build(request, start, end) with:
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.
Run mvn -Dtest=HikPlaybackUrlBuilderTest test.
Expected: both tests pass with zero failures.
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
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:
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:
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.
Run mvn -Dtest=HikNvrPlaybackServiceTest test.
Expected: test compilation fails because HikNvrPlaybackService and HikNvrPlaybackException do not exist.
HikNvrPlaybackService.search must:
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:
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();
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:
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.
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.
Run:
mvn -Dtest=HikNvrPlaybackServiceTest,HikPlaybackUrlBuilderTest test
Expected: all service and URL tests pass; Mockito verifies the find handle is closed.
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
Configure standalone MockMvc with the controller and advice. Assert a valid POST returns status 200 and JSON paths:
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.
Run mvn -Dtest=HikNvrPlaybackControllerTest test.
Expected: test compilation fails because the controller and handler do not exist.
Create a controller with:
@RestController
@RequestMapping("/hik/nvr/playback")
@Tag(name = "海康NVR回放", description = "查询海康NVR历史录像并生成RTSP回放地址")
public class HikNvrPlaybackController {
@PostMapping("/search")
public ResponseEntity<HikNvrPlaybackResponse> 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.
Run mvn -Dtest=HikNvrPlaybackControllerTest test.
Expected: all controller tests pass.
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:
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.
Run mvn -Dtest=ApiDocumentationCoverageTest test.
Expected: all coverage assertions pass with 8 controllers and 36 handler methods.
Files:
Verify all files created or modified in Tasks 1–3.
Run:
$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.
Run:
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.
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.
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.