diff --git a/src/main/java/com/genersoft/iot/vmp/aiot/controller/AiRoiController.java b/src/main/java/com/genersoft/iot/vmp/aiot/controller/AiRoiController.java index 9f51963fa..1b3b6604a 100644 --- a/src/main/java/com/genersoft/iot/vmp/aiot/controller/AiRoiController.java +++ b/src/main/java/com/genersoft/iot/vmp/aiot/controller/AiRoiController.java @@ -11,6 +11,8 @@ import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -95,4 +97,17 @@ public class AiRoiController { String requestId = (String) body.get("request_id"); screenshotService.handleCallback(requestId, body); } + + @Operation(summary = "截图图片代理(服务端从 COS 下载后返回)") + @GetMapping("/snap/image") + public ResponseEntity getSnapImage(@RequestParam String cameraCode) { + byte[] image = screenshotService.proxyScreenshotImage(cameraCode); + if (image == null) { + return ResponseEntity.notFound().build(); + } + return ResponseEntity.ok() + .contentType(MediaType.IMAGE_JPEG) + .header("Cache-Control", "max-age=60") + .body(image); + } } diff --git a/src/main/java/com/genersoft/iot/vmp/aiot/service/IAiScreenshotService.java b/src/main/java/com/genersoft/iot/vmp/aiot/service/IAiScreenshotService.java index 566c36801..f318ad9f5 100644 --- a/src/main/java/com/genersoft/iot/vmp/aiot/service/IAiScreenshotService.java +++ b/src/main/java/com/genersoft/iot/vmp/aiot/service/IAiScreenshotService.java @@ -20,4 +20,12 @@ public interface IAiScreenshotService { * @param data 回调数据 {request_id, camera_code, status, url/message} */ void handleCallback(String requestId, Map data); + + /** + * 代理获取截图图片(服务端从 COS 下载后返回字节) + * + * @param cameraCode 摄像头编码 + * @return JPEG 图片字节,缓存不存在或下载失败返回 null + */ + byte[] proxyScreenshotImage(String cameraCode); } diff --git a/src/main/java/com/genersoft/iot/vmp/aiot/service/impl/AiScreenshotServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/aiot/service/impl/AiScreenshotServiceImpl.java index ebffc20f8..d8e4a224b 100644 --- a/src/main/java/com/genersoft/iot/vmp/aiot/service/impl/AiScreenshotServiceImpl.java +++ b/src/main/java/com/genersoft/iot/vmp/aiot/service/impl/AiScreenshotServiceImpl.java @@ -11,6 +11,8 @@ import org.springframework.data.redis.connection.stream.RecordId; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.HashMap; @@ -53,7 +55,7 @@ public class AiScreenshotServiceImpl implements IAiScreenshotService { try { JSONObject cached = JSON.parseObject(cacheJson); result.put("status", "ok"); - result.put("url", cached.getString("url")); + result.put("url", "/api/ai/roi/snap/image?cameraCode=" + cameraCode); result.put("cached", true); log.info("[AI截图] 命中缓存: cameraCode={}", cameraCode); return result; @@ -98,7 +100,7 @@ public class AiScreenshotServiceImpl implements IAiScreenshotService { String status = (String) callbackData.get("status"); result.put("status", status); if ("ok".equals(status)) { - result.put("url", callbackData.get("url")); + result.put("url", "/api/ai/roi/snap/image?cameraCode=" + cameraCode); } else { result.put("message", callbackData.get("message")); } @@ -112,7 +114,7 @@ public class AiScreenshotServiceImpl implements IAiScreenshotService { JSONObject res = JSON.parseObject(resultJson); result.put("status", res.getString("status")); if ("ok".equals(res.getString("status"))) { - result.put("url", res.getString("url")); + result.put("url", "/api/ai/roi/snap/image?cameraCode=" + cameraCode); // 降级成功,写入缓存 writeCache(cameraCode, res.getString("url")); } else { @@ -132,7 +134,7 @@ public class AiScreenshotServiceImpl implements IAiScreenshotService { try { JSONObject cached = JSON.parseObject(staleCache); result.put("status", "ok"); - result.put("url", cached.getString("url")); + result.put("url", "/api/ai/roi/snap/image?cameraCode=" + cameraCode); result.put("stale", true); return result; } catch (Exception ignored) { @@ -198,4 +200,30 @@ public class AiScreenshotServiceImpl implements IAiScreenshotService { String time = now.format(DateTimeFormatter.ofPattern("HH-mm-ss")); return String.format("snapshots/%s/%s/%s_%s.jpg", cameraCode, date, time, requestId); } + + @Override + public byte[] proxyScreenshotImage(String cameraCode) { + String cacheJson = stringRedisTemplate.opsForValue().get(SNAP_CACHE_KEY_PREFIX + cameraCode); + if (cacheJson == null) { + log.warn("[AI截图] 代理图片: 缓存不存在 cameraCode={}", cameraCode); + return null; + } + + try { + JSONObject cached = JSON.parseObject(cacheJson); + String cosUrl = cached.getString("url"); + if (cosUrl == null || cosUrl.isEmpty()) { + log.warn("[AI截图] 代理图片: 缓存中无 URL cameraCode={}", cameraCode); + return null; + } + + RestTemplate restTemplate = new RestTemplate(); + byte[] imageBytes = restTemplate.getForObject(cosUrl, byte[].class); + log.debug("[AI截图] 代理图片成功: cameraCode={}, size={}", cameraCode, imageBytes != null ? imageBytes.length : 0); + return imageBytes; + } catch (Exception e) { + log.error("[AI截图] 代理下载图片失败: cameraCode={}, error={}", cameraCode, e.getMessage()); + return null; + } + } } diff --git a/src/main/java/com/genersoft/iot/vmp/conf/security/WebSecurityConfig.java b/src/main/java/com/genersoft/iot/vmp/conf/security/WebSecurityConfig.java index 6fe07106c..3ce708936 100644 --- a/src/main/java/com/genersoft/iot/vmp/conf/security/WebSecurityConfig.java +++ b/src/main/java/com/genersoft/iot/vmp/conf/security/WebSecurityConfig.java @@ -103,6 +103,7 @@ public class WebSecurityConfig { defaultExcludes.add("/api/jt1078/snap"); defaultExcludes.add("/api/ai/roi/snap"); defaultExcludes.add("/api/ai/roi/snap/callback"); + defaultExcludes.add("/api/ai/roi/snap/image"); defaultExcludes.add("/api/ai/camera/get"); if (userSetting.getInterfaceAuthentication() && !userSetting.getInterfaceAuthenticationExcludes().isEmpty()) {