fix(aiot): 修复截图代理两个bug:竞态条件+URL双重编码

1. handleCallback先写Redis缓存再complete Future,
   避免等待线程被唤醒后读取缓存为空的竞态条件
2. RestTemplate使用URI.create()传入预签名URL,
   避免对已编码的%3B等字符做二次编码导致COS 403

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-28 14:56:30 +08:00
parent c932c1c7a2
commit 83041e9490

View File

@@ -13,6 +13,7 @@ import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.net.URI;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
@@ -161,6 +162,14 @@ public class AiScreenshotServiceImpl implements IAiScreenshotService {
return;
}
// 先写 Redis 缓存,再唤醒等待线程(避免竞态:线程被唤醒后立即读缓存但缓存还没写入)
String cameraCode = (String) data.get("camera_code");
String status = (String) data.get("status");
if ("ok".equals(status) && cameraCode != null) {
String url = (String) data.get("url");
writeCache(cameraCode, url);
}
CompletableFuture<Map<String, Object>> future = pendingRequests.get(requestId);
if (future != null) {
future.complete(data);
@@ -168,14 +177,6 @@ public class AiScreenshotServiceImpl implements IAiScreenshotService {
} else {
log.warn("[AI截图] 回调未找到对应请求(可能已超时): requestId={}", requestId);
}
// 写入 Redis 缓存(无论 future 是否存在,缓存都应更新)
String cameraCode = (String) data.get("camera_code");
String status = (String) data.get("status");
if ("ok".equals(status) && cameraCode != null) {
String url = (String) data.get("url");
writeCache(cameraCode, url);
}
}
/**
@@ -217,8 +218,9 @@ public class AiScreenshotServiceImpl implements IAiScreenshotService {
return null;
}
// 使用 URI.create 避免 RestTemplate 对已编码的预签名 URL 做二次编码
RestTemplate restTemplate = new RestTemplate();
byte[] imageBytes = restTemplate.getForObject(cosUrl, byte[].class);
byte[] imageBytes = restTemplate.getForObject(URI.create(cosUrl), byte[].class);
log.debug("[AI截图] 代理图片成功: cameraCode={}, size={}", cameraCode, imageBytes != null ? imageBytes.length : 0);
return imageBytes;
} catch (Exception e) {