From 9e3a406c687e2145c09e26cf2ea87de62919343d Mon Sep 17 00:00:00 2001 From: 16337 <1633794139@qq.com> Date: Tue, 10 Mar 2026 16:29:03 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9A=E6=88=AA=E5=9B=BE?= =?UTF-8?q?=E4=BB=A3=E7=90=86=E4=B8=8B=E8=BD=BD=E9=AA=8C=E8=AF=81=20+=20HE?= =?UTF-8?q?AD=E8=AF=B7=E6=B1=82=E6=94=AF=E6=8C=81=20+=20=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E6=A0=8F=E4=BD=8D=E7=BD=AE=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 后端修复: 1. proxyScreenshotImage 优化 - 直接从DB获取cos_key生成presigned URL - 移除过期的Redis缓存URL优先逻辑(避免使用过期URL) - 添加JPEG魔数验证,确保返回有效图片数据 - 空数据或无效数据返回null而非损坏数据 2. /snap/image 端点支持 HEAD 请求 - HEAD请求只检查图片是否存在,返回Content-Length - GET请求返回完整图片数据 - 用于前端快速检测截图可用性 前端修复: 1. 状态栏位置调整 - 列顺序改为:应用名 -> 流ID -> 状态 -> 拉流地址 -> ROI 2. 状态检测优化 - 使用 HEAD 请求替代 GET,避免下载完整图片 - 使用 fetch API 直接发送 HEAD 请求 Co-Authored-By: Claude Opus 4.6 --- .../vmp/aiot/controller/AiRoiController.java | 18 +++++++- .../service/impl/AiScreenshotServiceImpl.java | 44 +++++++------------ 2 files changed, 34 insertions(+), 28 deletions(-) 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={}",