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 aa9b29ac0..1410a485a 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 @@ -15,6 +15,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.List; @@ -117,7 +118,22 @@ public class AiRoiController { @Operation(summary = "截图图片代理(服务端从 COS 下载后返回)") @GetMapping("/snap/image") - public ResponseEntity getSnapImage(@RequestParam String cameraCode) { + @RequestMapping(value = "/snap/image", method = {RequestMethod.GET, RequestMethod.HEAD}) + public ResponseEntity getSnapImage(@RequestParam String cameraCode, HttpServletRequest request) { + // HEAD 请求:只检查是否存在,不返回图片数据 + if ("HEAD".equalsIgnoreCase(request.getMethod())) { + byte[] image = screenshotService.proxyScreenshotImage(cameraCode); + if (image == null || image.length == 0) { + return ResponseEntity.notFound().build(); + } + return ResponseEntity.ok() + .contentType(MediaType.IMAGE_JPEG) + .contentLength(image.length) + .header("Cache-Control", "public, max-age=300") + .build(); + } + + // GET 请求:返回图片数据 byte[] image = screenshotService.proxyScreenshotImage(cameraCode); if (image == null) { return ResponseEntity.notFound().build(); 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 2c22fcc9c..ddad80594 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 @@ -258,33 +258,14 @@ public class AiScreenshotServiceImpl implements IAiScreenshotService { @Override public byte[] proxyScreenshotImage(String cameraCode) { - // 1. 先查 Redis 缓存中的 presigned URL(5分钟有效) - String cacheJson = stringRedisTemplate.opsForValue().get(SNAP_CACHE_KEY_PREFIX + cameraCode); - if (cacheJson != null) { - try { - JSONObject cached = JSON.parseObject(cacheJson); - String cosUrl = cached.getString("url"); - if (cosUrl != null && !cosUrl.isEmpty()) { - RestTemplate restTemplate = new RestTemplate(); - byte[] bytes = restTemplate.getForObject(URI.create(cosUrl), byte[].class); - if (bytes != null && bytes.length > 0) { - log.debug("[AI截图] 代理图片(Redis缓存): cameraCode={}, size={}", cameraCode, bytes.length); - return bytes; - } - } - } catch (Exception e) { - log.warn("[AI截图] Redis 缓存 URL 下载失败,尝试 DB: {}", e.getMessage()); - } - } - - // 2. 查 DB 持久化的 cos_key(永不过期) + // 1. 查 DB 持久化的 cos_key(永不过期)- 优先直接操作 COS String cosKey = snapshotMapper.getCosKey(cameraCode); if (cosKey == null) { - log.warn("[AI截图] 代理图片: 无缓存也无持久化记录 cameraCode={}", cameraCode); + log.warn("[AI截图] 代理图片: 无持久化记录 cameraCode={}", cameraCode); return null; } - // 3. 通过 CosUtil 直接生成 presigned URL(无需调 FastAPI) + // 2. 通过 CosUtil 直接生成 presigned URL if (!cosUtil.isAvailable()) { log.warn("[AI截图] COS 客户端未初始化,无法生成 presigned URL"); return null; @@ -297,15 +278,24 @@ public class AiScreenshotServiceImpl implements IAiScreenshotService { } try { - // 4. 下载图片 + // 3. 下载图片 RestTemplate restTemplate = new RestTemplate(); byte[] imageBytes = restTemplate.getForObject(URI.create(presignedUrl), byte[].class); - // 5. 更新 Redis 缓存(加速后续请求) - if (imageBytes != null && imageBytes.length > 0) { - writeCache(cameraCode, presignedUrl); - log.debug("[AI截图] 代理图片(DB→COS): cameraCode={}, size={}", cameraCode, imageBytes.length); + if (imageBytes == null || imageBytes.length == 0) { + log.error("[AI截图] 下载图片为空: cameraCode={}, cosKey={}", cameraCode, cosKey); + return null; } + + // 验证是否为有效的 JPEG 图片(检查魔数) + if (imageBytes.length < 2 || (imageBytes[0] & 0xFF) != 0xFF || (imageBytes[1] & 0xFF) != 0xD8) { + log.error("[AI截图] 下载的数据不是有效的JPEG: cameraCode={}, size={}", cameraCode, imageBytes.length); + return null; + } + + // 4. 更新 Redis 缓存(加速后续请求) + writeCache(cameraCode, presignedUrl); + log.debug("[AI截图] 代理图片成功: cameraCode={}, size={}", cameraCode, imageBytes.length); return imageBytes; } catch (Exception e) { log.error("[AI截图] 通过 DB cos_key 下载图片失败: cameraCode={}, cosKey={}, error={}",