fix(aiot): 回退告警 API 到 Service 后端,修复截图 URL 端点
告警问题: - 告警数据存储在 FastAPI Service 的 alert_platform.db 中 - 之前错误地切换到 wvpRequestClient(WVP 后端的 wvp_ai_alert 表) - 回退到 requestClient + /aiot/alarm/alert/* 路径恢复数据 截图问题: - /snap/image 代理端点在 Redis+DB 都无缓存时返回 404 - 回退到 /snap 端点,利用其 Accept: image/* 自动 302 重定向逻辑 - 保留 force 参数支持手动刷新截图 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,11 @@
|
|||||||
import type { PageParam, PageResult } from '@vben/request';
|
import type { PageParam, PageResult } from '@vben/request';
|
||||||
|
|
||||||
import { wvpRequestClient } from '#/api/aiot/request';
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
export namespace AiotAlarmApi {
|
export namespace AiotAlarmApi {
|
||||||
/** AI 告警 VO */
|
/** AI 告警 VO */
|
||||||
export interface Alert {
|
export interface Alert {
|
||||||
id?: number | string;
|
id?: number | string;
|
||||||
alertId?: string;
|
|
||||||
alarmId?: string;
|
alarmId?: string;
|
||||||
alertNo?: string;
|
alertNo?: string;
|
||||||
cameraId?: string;
|
cameraId?: string;
|
||||||
@@ -24,7 +23,6 @@ export namespace AiotAlarmApi {
|
|||||||
triggerTime?: string;
|
triggerTime?: string;
|
||||||
message?: string;
|
message?: string;
|
||||||
bbox?: string;
|
bbox?: string;
|
||||||
imagePath?: string;
|
|
||||||
snapshotUrl?: string;
|
snapshotUrl?: string;
|
||||||
ossUrl?: string;
|
ossUrl?: string;
|
||||||
status?: string;
|
status?: string;
|
||||||
@@ -36,11 +34,9 @@ export namespace AiotAlarmApi {
|
|||||||
lastFrameTime?: string;
|
lastFrameTime?: string;
|
||||||
workOrderId?: number;
|
workOrderId?: number;
|
||||||
aiAnalysis?: Record<string, any>;
|
aiAnalysis?: Record<string, any>;
|
||||||
receivedAt?: string;
|
|
||||||
createdAt?: string;
|
createdAt?: string;
|
||||||
updatedAt?: string;
|
updatedAt?: string;
|
||||||
logInfo?: AlertLogInfo;
|
logInfo?: AlertLogInfo;
|
||||||
extraData?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 告警日志信息 */
|
/** 告警日志信息 */
|
||||||
@@ -78,20 +74,20 @@ export namespace AiotAlarmApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 告警管理 API(WVP 后端)====================
|
// ==================== 告警管理 API ====================
|
||||||
|
|
||||||
/** 分页查询告警列表 */
|
/** 分页查询告警列表 */
|
||||||
export function getAlertPage(params: PageParam) {
|
export function getAlertPage(params: PageParam) {
|
||||||
return wvpRequestClient.get<PageResult<AiotAlarmApi.Alert>>(
|
return requestClient.get<PageResult<AiotAlarmApi.Alert>>(
|
||||||
'/aiot/device/alert/list',
|
'/aiot/alarm/alert/page',
|
||||||
{ params: { page: params.pageNo, count: params.pageSize, ...params } },
|
{ params },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 获取告警详情 */
|
/** 获取告警详情 */
|
||||||
export function getAlert(id: number | string) {
|
export function getAlert(id: number | string) {
|
||||||
return wvpRequestClient.get<AiotAlarmApi.Alert>(
|
return requestClient.get<AiotAlarmApi.Alert>(
|
||||||
`/aiot/device/alert/${id}`,
|
`/aiot/alarm/alert/get?id=${id}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,22 +97,20 @@ export function handleAlert(
|
|||||||
status: string,
|
status: string,
|
||||||
remark?: string,
|
remark?: string,
|
||||||
) {
|
) {
|
||||||
return wvpRequestClient.put('/aiot/device/alert/handle', null, {
|
return requestClient.put('/aiot/alarm/alert/handle', null, {
|
||||||
params: { id, status, remark },
|
params: { id, status, remark },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 删除告警 */
|
/** 删除告警 */
|
||||||
export function deleteAlert(id: number | string) {
|
export function deleteAlert(id: number | string) {
|
||||||
return wvpRequestClient.delete('/aiot/device/alert/delete', {
|
return requestClient.delete(`/aiot/alarm/alert/delete?id=${id}`);
|
||||||
params: { alertId: id },
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 获取告警统计 */
|
/** 获取告警统计 */
|
||||||
export function getAlertStatistics(startTime?: string, endTime?: string) {
|
export function getAlertStatistics(startTime?: string, endTime?: string) {
|
||||||
return wvpRequestClient.get<AiotAlarmApi.AlertStatistics>(
|
return requestClient.get<AiotAlarmApi.AlertStatistics>(
|
||||||
'/aiot/device/alert/statistics',
|
'/aiot/alarm/alert/statistics',
|
||||||
{ params: { startTime, endTime } },
|
{ params: { startTime, endTime } },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -125,8 +119,8 @@ export function getAlertStatistics(startTime?: string, endTime?: string) {
|
|||||||
|
|
||||||
/** 以摄像头维度获取告警汇总 */
|
/** 以摄像头维度获取告警汇总 */
|
||||||
export function getCameraAlertSummary(params: PageParam) {
|
export function getCameraAlertSummary(params: PageParam) {
|
||||||
return wvpRequestClient.get<PageResult<AiotAlarmApi.CameraAlertSummary>>(
|
return requestClient.get<PageResult<AiotAlarmApi.CameraAlertSummary>>(
|
||||||
'/aiot/device/alert/statistics',
|
'/aiot/alarm/device-summary/page',
|
||||||
{ params },
|
{ params },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -189,27 +189,19 @@ export function deleteRoi(roiId: string) {
|
|||||||
/**
|
/**
|
||||||
* 获取摄像头截图 URL
|
* 获取摄像头截图 URL
|
||||||
*
|
*
|
||||||
* 非 force 模式:直接返回 /snap/image 代理 URL(从 DB 读取持久化截图,不触发 Edge)
|
* /snap 端点会自动处理缓存逻辑:
|
||||||
* force 模式:先触发 Edge 截新图(更新 DB),再返回代理 URL
|
* - 有 Redis 缓存时直接 302 重定向到 COS presigned URL(快)
|
||||||
|
* - 无缓存时触发 Edge 截图,等待完成后重定向(首次较慢)
|
||||||
|
* - force=true 时强制触发 Edge 截新图
|
||||||
*/
|
*/
|
||||||
export async function getSnapUrl(cameraCode: string, force = false): Promise<string> {
|
export async function getSnapUrl(cameraCode: string, force = false): Promise<string> {
|
||||||
const token = await getWvpToken();
|
const token = await getWvpToken();
|
||||||
if (force) {
|
|
||||||
// force 时先触发一次截图请求(确保 Edge 截新图并更新 DB)
|
|
||||||
try {
|
|
||||||
await wvpRequestClient.get('/aiot/device/roi/snap', {
|
|
||||||
params: { cameraCode, force: true },
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
/* 截图请求可能超时,但 DB 会被更新 */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 返回代理 URL(WVP 从 DB 读 cos_key → 生成 presigned URL → 下载返回)
|
|
||||||
return (
|
return (
|
||||||
`${apiURL}/aiot/device/roi/snap/image` +
|
`${apiURL}/aiot/device/roi/snap` +
|
||||||
`?cameraCode=${encodeURIComponent(cameraCode)}` +
|
`?cameraCode=${encodeURIComponent(cameraCode)}` +
|
||||||
`&access-token=${encodeURIComponent(token)}` +
|
`&access-token=${encodeURIComponent(token)}` +
|
||||||
(force ? `&t=${Date.now()}` : '')
|
(force ? `&force=true` : '') +
|
||||||
|
`&t=${Date.now()}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
import type { AiotAlarmApi } from '#/api/aiot/alarm';
|
import type { AiotAlarmApi } from '#/api/aiot/alarm';
|
||||||
|
|
||||||
import { h, onMounted, ref } from 'vue';
|
import { h, ref } from 'vue';
|
||||||
|
|
||||||
import { Page } from '@vben/common-ui';
|
import { Page } from '@vben/common-ui';
|
||||||
|
|
||||||
@@ -10,7 +10,6 @@ import { Button, Image, message, Modal, Tag } from 'ant-design-vue';
|
|||||||
|
|
||||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
import { getAlert, getAlertPage, handleAlert } from '#/api/aiot/alarm';
|
import { getAlert, getAlertPage, handleAlert } from '#/api/aiot/alarm';
|
||||||
import { getAlertImageUrl } from '#/api/aiot/device';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ALERT_LEVEL_OPTIONS,
|
ALERT_LEVEL_OPTIONS,
|
||||||
@@ -22,21 +21,6 @@ import {
|
|||||||
|
|
||||||
defineOptions({ name: 'AiotAlarmList' });
|
defineOptions({ name: 'AiotAlarmList' });
|
||||||
|
|
||||||
/** 告警图片 URL 缓存(imagePath → 代理 URL) */
|
|
||||||
const imageUrlCache = ref<Record<string, string>>({});
|
|
||||||
|
|
||||||
/** 获取告警图片代理 URL(异步构造后缓存) */
|
|
||||||
function getImageProxyUrl(row: AiotAlarmApi.Alert): string {
|
|
||||||
const imagePath = row.imagePath || row.ossUrl || row.snapshotUrl;
|
|
||||||
if (!imagePath) return '';
|
|
||||||
if (imageUrlCache.value[imagePath]) return imageUrlCache.value[imagePath]!;
|
|
||||||
// 异步构造并缓存
|
|
||||||
getAlertImageUrl(imagePath).then((url) => {
|
|
||||||
imageUrlCache.value[imagePath] = url;
|
|
||||||
});
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 格式化持续时长(毫秒 → 可读文本) */
|
/** 格式化持续时长(毫秒 → 可读文本) */
|
||||||
function formatDuration(ms: number | null | undefined): string {
|
function formatDuration(ms: number | null | undefined): string {
|
||||||
// 当 duration_ms 为 null 时,说明告警仍在进行中
|
// 当 duration_ms 为 null 时,说明告警仍在进行中
|
||||||
@@ -263,12 +247,11 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
|||||||
<!-- 截图缩略图列 -->
|
<!-- 截图缩略图列 -->
|
||||||
<template #snapshot="{ row }">
|
<template #snapshot="{ row }">
|
||||||
<Image
|
<Image
|
||||||
v-if="getImageProxyUrl(row)"
|
v-if="row.snapshotUrl || row.ossUrl"
|
||||||
:src="getImageProxyUrl(row)"
|
:src="row.ossUrl || row.snapshotUrl"
|
||||||
:width="40"
|
:width="40"
|
||||||
:height="40"
|
:height="40"
|
||||||
:preview="{ src: getImageProxyUrl(row) }"
|
:preview="{ src: row.ossUrl || row.snapshotUrl }"
|
||||||
:fallback="`data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='40' height='40'><rect width='40' height='40' fill='%23f0f0f0'/><text x='50%25' y='55%25' dominant-baseline='middle' text-anchor='middle' fill='%23bbb' font-size='12'>-</text></svg>`"
|
|
||||||
style="object-fit: cover; border-radius: 4px; cursor: pointer"
|
style="object-fit: cover; border-radius: 4px; cursor: pointer"
|
||||||
/>
|
/>
|
||||||
<span v-else class="text-gray-400">-</span>
|
<span v-else class="text-gray-400">-</span>
|
||||||
@@ -366,16 +349,25 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 告警截图 -->
|
<!-- 告警截图 -->
|
||||||
<div v-if="getImageProxyUrl(currentAlert)">
|
<div v-if="currentAlert.ossUrl || currentAlert.snapshotUrl">
|
||||||
<span class="text-gray-500">告警截图:</span>
|
<span class="text-gray-500">告警截图:</span>
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<Image
|
<Image
|
||||||
:src="getImageProxyUrl(currentAlert)"
|
:src="currentAlert.ossUrl || currentAlert.snapshotUrl"
|
||||||
:width="300"
|
:width="300"
|
||||||
:preview="{ src: getImageProxyUrl(currentAlert) }"
|
:preview="{ src: currentAlert.ossUrl || currentAlert.snapshotUrl }"
|
||||||
:fallback="`data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='300' height='200'><rect width='300' height='200' fill='%23f5f5f5'/><text x='50%25' y='50%25' dominant-baseline='middle' text-anchor='middle' fill='%23bbb' font-size='14'>图片加载失败</text></svg>`"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mt-1">
|
||||||
|
<a
|
||||||
|
:href="currentAlert.ossUrl || currentAlert.snapshotUrl"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="text-blue-500 text-xs hover:underline"
|
||||||
|
>
|
||||||
|
{{ currentAlert.ossUrl || currentAlert.snapshotUrl }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 检测区域 -->
|
<!-- 检测区域 -->
|
||||||
|
|||||||
Reference in New Issue
Block a user