修复:截图代理下载验证 + HEAD请求支持 + 状态栏位置调整
后端修复: 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 <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,7 @@ import org.springframework.http.MediaType;
|
|||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -117,7 +118,22 @@ public class AiRoiController {
|
|||||||
|
|
||||||
@Operation(summary = "截图图片代理(服务端从 COS 下载后返回)")
|
@Operation(summary = "截图图片代理(服务端从 COS 下载后返回)")
|
||||||
@GetMapping("/snap/image")
|
@GetMapping("/snap/image")
|
||||||
public ResponseEntity<byte[]> 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);
|
byte[] image = screenshotService.proxyScreenshotImage(cameraCode);
|
||||||
if (image == null) {
|
if (image == null) {
|
||||||
return ResponseEntity.notFound().build();
|
return ResponseEntity.notFound().build();
|
||||||
|
|||||||
@@ -258,33 +258,14 @@ public class AiScreenshotServiceImpl implements IAiScreenshotService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public byte[] proxyScreenshotImage(String cameraCode) {
|
public byte[] proxyScreenshotImage(String cameraCode) {
|
||||||
// 1. 先查 Redis 缓存中的 presigned URL(5分钟有效)
|
// 1. 查 DB 持久化的 cos_key(永不过期)- 优先直接操作 COS
|
||||||
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(永不过期)
|
|
||||||
String cosKey = snapshotMapper.getCosKey(cameraCode);
|
String cosKey = snapshotMapper.getCosKey(cameraCode);
|
||||||
if (cosKey == null) {
|
if (cosKey == null) {
|
||||||
log.warn("[AI截图] 代理图片: 无缓存也无持久化记录 cameraCode={}", cameraCode);
|
log.warn("[AI截图] 代理图片: 无持久化记录 cameraCode={}", cameraCode);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 通过 CosUtil 直接生成 presigned URL(无需调 FastAPI)
|
// 2. 通过 CosUtil 直接生成 presigned URL
|
||||||
if (!cosUtil.isAvailable()) {
|
if (!cosUtil.isAvailable()) {
|
||||||
log.warn("[AI截图] COS 客户端未初始化,无法生成 presigned URL");
|
log.warn("[AI截图] COS 客户端未初始化,无法生成 presigned URL");
|
||||||
return null;
|
return null;
|
||||||
@@ -297,15 +278,24 @@ public class AiScreenshotServiceImpl implements IAiScreenshotService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 4. 下载图片
|
// 3. 下载图片
|
||||||
RestTemplate restTemplate = new RestTemplate();
|
RestTemplate restTemplate = new RestTemplate();
|
||||||
byte[] imageBytes = restTemplate.getForObject(URI.create(presignedUrl), byte[].class);
|
byte[] imageBytes = restTemplate.getForObject(URI.create(presignedUrl), byte[].class);
|
||||||
|
|
||||||
// 5. 更新 Redis 缓存(加速后续请求)
|
if (imageBytes == null || imageBytes.length == 0) {
|
||||||
if (imageBytes != null && imageBytes.length > 0) {
|
log.error("[AI截图] 下载图片为空: cameraCode={}, cosKey={}", cameraCode, cosKey);
|
||||||
writeCache(cameraCode, presignedUrl);
|
return null;
|
||||||
log.debug("[AI截图] 代理图片(DB→COS): cameraCode={}, size={}", cameraCode, imageBytes.length);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 验证是否为有效的 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;
|
return imageBytes;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("[AI截图] 通过 DB cos_key 下载图片失败: cameraCode={}, cosKey={}, error={}",
|
log.error("[AI截图] 通过 DB cos_key 下载图片失败: cameraCode={}, cosKey={}, error={}",
|
||||||
|
|||||||
Reference in New Issue
Block a user