diff --git a/src/main/java/com/genersoft/iot/vmp/aiot/controller/AiAlertController.java b/src/main/java/com/genersoft/iot/vmp/aiot/controller/AiAlertController.java index 7b6d8c0ad..a332ee736 100644 --- a/src/main/java/com/genersoft/iot/vmp/aiot/controller/AiAlertController.java +++ b/src/main/java/com/genersoft/iot/vmp/aiot/controller/AiAlertController.java @@ -9,6 +9,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; @@ -60,6 +62,19 @@ public class AiAlertController { return alertService.statistics(startTime); } + @Operation(summary = "告警图片代理(服务端从 COS 下载后返回)") + @GetMapping("/image") + public ResponseEntity getAlertImage(@RequestParam String imagePath) { + byte[] image = alertService.proxyAlertImage(imagePath); + if (image == null) { + return ResponseEntity.notFound().build(); + } + return ResponseEntity.ok() + .contentType(MediaType.IMAGE_JPEG) + .header("Cache-Control", "public, max-age=3600") + .body(image); + } + // ==================== Edge 告警上报 ==================== @Operation(summary = "Edge 告警上报") diff --git a/src/main/java/com/genersoft/iot/vmp/aiot/service/IAiAlertService.java b/src/main/java/com/genersoft/iot/vmp/aiot/service/IAiAlertService.java index d257b18dd..75b088027 100644 --- a/src/main/java/com/genersoft/iot/vmp/aiot/service/IAiAlertService.java +++ b/src/main/java/com/genersoft/iot/vmp/aiot/service/IAiAlertService.java @@ -21,4 +21,12 @@ public interface IAiAlertService { Map statistics(String startTime); void updateDuration(String alertId, double durationMinutes); + + /** + * 代理获取告警图片(通过 COS presigned URL 下载后返回字节) + * + * @param imagePath COS 对象路径 + * @return JPEG 图片字节,失败返回 null + */ + byte[] proxyAlertImage(String imagePath); } diff --git a/src/main/java/com/genersoft/iot/vmp/aiot/service/impl/AiAlertServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/aiot/service/impl/AiAlertServiceImpl.java index 9cb3972a2..126079588 100644 --- a/src/main/java/com/genersoft/iot/vmp/aiot/service/impl/AiAlertServiceImpl.java +++ b/src/main/java/com/genersoft/iot/vmp/aiot/service/impl/AiAlertServiceImpl.java @@ -3,12 +3,15 @@ package com.genersoft.iot.vmp.aiot.service.impl; import com.genersoft.iot.vmp.aiot.bean.AiAlert; import com.genersoft.iot.vmp.aiot.dao.AiAlertMapper; import com.genersoft.iot.vmp.aiot.service.IAiAlertService; +import com.genersoft.iot.vmp.aiot.util.CosUtil; import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageInfo; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; 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.*; @@ -20,6 +23,9 @@ public class AiAlertServiceImpl implements IAiAlertService { @Autowired private AiAlertMapper alertMapper; + @Autowired + private CosUtil cosUtil; + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); @Override @@ -72,4 +78,42 @@ public class AiAlertServiceImpl implements IAiAlertService { public void updateDuration(String alertId, double durationMinutes) { alertMapper.updateDuration(alertId, durationMinutes); } + + @Override + public byte[] proxyAlertImage(String imagePath) { + if (imagePath == null || imagePath.isEmpty()) { + return null; + } + + // 如果是完整 URL(https://),直接下载 + if (imagePath.startsWith("http://") || imagePath.startsWith("https://")) { + try { + RestTemplate restTemplate = new RestTemplate(); + return restTemplate.getForObject(URI.create(imagePath), byte[].class); + } catch (Exception e) { + log.error("[AiAlert] 直接下载告警图片失败: path={}, error={}", imagePath, e.getMessage()); + return null; + } + } + + // COS object key → 生成 presigned URL → 下载 + if (!cosUtil.isAvailable()) { + log.warn("[AiAlert] COS 客户端未初始化,无法代理告警图片"); + return null; + } + + String presignedUrl = cosUtil.generatePresignedUrl(imagePath); + if (presignedUrl == null) { + log.error("[AiAlert] 生成 presigned URL 失败: imagePath={}", imagePath); + return null; + } + + try { + RestTemplate restTemplate = new RestTemplate(); + return restTemplate.getForObject(URI.create(presignedUrl), byte[].class); + } catch (Exception e) { + log.error("[AiAlert] 代理告警图片失败: path={}, error={}", imagePath, 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 5d5208eb5..5de05670e 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 @@ -106,6 +106,7 @@ public class WebSecurityConfig { defaultExcludes.add("/api/ai/roi/snap/image"); defaultExcludes.add("/api/ai/camera/get"); defaultExcludes.add("/api/ai/alert/edge/**"); + defaultExcludes.add("/api/ai/alert/image"); defaultExcludes.add("/api/ai/device/edge/**"); if (userSetting.getInterfaceAuthentication() && !userSetting.getInterfaceAuthenticationExcludes().isEmpty()) { diff --git a/web/src/api/aiAlert.js b/web/src/api/aiAlert.js new file mode 100644 index 000000000..64e1f8a98 --- /dev/null +++ b/web/src/api/aiAlert.js @@ -0,0 +1,51 @@ +import request from '@/utils/request' + +export function queryAlertList(params) { + const { page, count, cameraId, alertType, startTime, endTime } = params + return request({ + method: 'get', + url: '/api/ai/alert/list', + params: { page, count, cameraId, alertType, startTime, endTime } + }) +} + +export function queryAlertDetail(alertId) { + return request({ + method: 'get', + url: `/api/ai/alert/${alertId}` + }) +} + +export function deleteAlert(alertId) { + return request({ + method: 'delete', + url: '/api/ai/alert/delete', + params: { alertId } + }) +} + +export function deleteAlertBatch(alertIds) { + return request({ + method: 'delete', + url: '/api/ai/alert/delete', + params: { alertIds } + }) +} + +export function queryAlertStatistics(startTime) { + return request({ + method: 'get', + url: '/api/ai/alert/statistics', + params: { startTime } + }) +} + +/** + * 构造告警图片代理 URL + * @param {string} imagePath COS 对象路径 + * @returns {string} 图片代理 URL + */ +export function getAlertImageUrl(imagePath) { + if (!imagePath) return '' + return '/api/ai/alert/image?imagePath=' + encodeURIComponent(imagePath) +} diff --git a/web/src/router/index.js b/web/src/router/index.js index 6a8eee5f3..4d50442fe 100644 --- a/web/src/router/index.js +++ b/web/src/router/index.js @@ -233,6 +233,17 @@ export const constantRoutes = [ } ] }, + { + path: '/alertList', + component: Layout, + redirect: '/alertList', + children: [{ + path: '', + name: 'AlertList', + component: () => import('@/views/alertList/index'), + meta: { title: '告警记录', icon: 'alarm' } + }] + }, { path: '/cameraConfig', component: Layout, diff --git a/web/src/views/alertList/index.vue b/web/src/views/alertList/index.vue new file mode 100644 index 000000000..b06a8cf38 --- /dev/null +++ b/web/src/views/alertList/index.vue @@ -0,0 +1,216 @@ + + + + +