feat(aiot): 告警图片代理 + 告警列表页面

后端:
- IAiAlertService / AiAlertServiceImpl: 新增 proxyAlertImage()
  支持 COS object key(通过 CosUtil 生成 presigned URL)和完整 URL
- AiAlertController: 新增 GET /api/ai/alert/image 图片代理端点
- WebSecurityConfig: 白名单加 /api/ai/alert/image

前端:
- 新建 aiAlert.js API(列表查询、删除、统计、图片 URL 构造)
- 新建 alertList/index.vue 告警列表页面
  · 分页表格 + 类型/时间筛选
  · 缩略图通过 WVP 图片代理显示
  · 详情弹窗 + 删除功能
- router/index.js: 添加告警记录路由

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-03 20:12:19 +08:00
parent 80f0275216
commit 90e9c1c896
7 changed files with 346 additions and 0 deletions

View File

@@ -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<byte[]> 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 告警上报")

View File

@@ -21,4 +21,12 @@ public interface IAiAlertService {
Map<String, Object> statistics(String startTime);
void updateDuration(String alertId, double durationMinutes);
/**
* 代理获取告警图片(通过 COS presigned URL 下载后返回字节)
*
* @param imagePath COS 对象路径
* @return JPEG 图片字节,失败返回 null
*/
byte[] proxyAlertImage(String imagePath);
}

View File

@@ -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;
}
// 如果是完整 URLhttps://),直接下载
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;
}
}
}

View File

@@ -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()) {