重构: video 模块走芋道网关统一鉴权,移除独立 WVP JWT 客户端
- api/video/request.ts: 删除自实现的 WVP access-token 登录/缓存/401 续期逻辑,
直接 re-export requestClient(yudao Authorization: Bearer),
wvpRequestClient 名字仅作过渡期别名,TODO 标记后续统一重命名。
- api/video/device/index.ts: 路径从 /video/device/{proxy,user,server,...} 迁移到
后端 @RequestMapping 对齐的 /video/{proxy,ai/*,server/media_server};
删除 getAlertImageUrl(老 WVP 时代产物),删除截图 URL 里的 ?access-token。
- api/video/edge/index.ts: /video/device/device/* -> /video/ai/device/*(对齐 AiEdgeDeviceController)。
- views/video/device/camera: HEAD 探活 URL 同步,补注释说明后端需对
/video/ai/roi/snap/image permitAll 或依赖 session cookie。
- vite.config: 删除 /admin-api/video/device/* 按子路径 rewrite 到 WVP:18080 的规则,
统一走 /admin-api 到芋道网关 48080;去掉 VITE_WVP_USERNAME/PASSWORD_MD5 依赖
(安全改进:凭据不再打进客户端 bundle)。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,16 +1,18 @@
|
|||||||
/**
|
/**
|
||||||
* Video 模块 - 设备管理 API
|
* Video 模块 - 设备管理 API
|
||||||
*
|
*
|
||||||
* 所有请求通过 wvpRequestClient 发送到 WVP 视频平台后端,
|
* 请求走统一的 requestClient(yudao 框架鉴权:Authorization: Bearer)。
|
||||||
* 由 Vite 代理按路径分发并 rewrite:
|
* video 微服务已接入 gateway,路径对齐后端 @RequestMapping:
|
||||||
* /video/device/proxy/* → WVP /api/proxy/*
|
* /video/proxy/* 拉流代理(StreamProxyController)
|
||||||
* /video/device/user/* → WVP /api/user/*
|
* /video/server/* 媒体服务器(ServerController)
|
||||||
* /video/device/server/* → WVP /api/server/media_server/*
|
* /video/ai/camera/* 摄像头
|
||||||
* /video/device/* → WVP /api/ai/*
|
* /video/ai/roi/* ROI 及算法绑定
|
||||||
|
* /video/ai/algorithm/* 算法
|
||||||
|
* /video/ai/config/* 配置推送
|
||||||
*/
|
*/
|
||||||
import { useAppConfig } from '@vben/hooks';
|
import { useAppConfig } from '@vben/hooks';
|
||||||
|
|
||||||
import { getWvpToken, wvpRequestClient } from '#/api/video/request';
|
import { wvpRequestClient } from '#/api/video/request';
|
||||||
|
|
||||||
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
|
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
|
||||||
|
|
||||||
@@ -106,19 +108,19 @@ export function getCameraList(params: {
|
|||||||
pulling?: boolean;
|
pulling?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return wvpRequestClient.get<VideoDeviceApi.PageResult<VideoDeviceApi.Camera>>(
|
return wvpRequestClient.get<VideoDeviceApi.PageResult<VideoDeviceApi.Camera>>(
|
||||||
'/video/device/proxy/list',
|
'/video/proxy/list',
|
||||||
{ params },
|
{ params },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 新增摄像头 */
|
/** 新增摄像头 */
|
||||||
export function addCamera(data: Partial<VideoDeviceApi.Camera>) {
|
export function addCamera(data: Partial<VideoDeviceApi.Camera>) {
|
||||||
return wvpRequestClient.post('/video/device/proxy/add', data);
|
return wvpRequestClient.post('/video/proxy/add', data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 编辑摄像头 */
|
/** 编辑摄像头 */
|
||||||
export function updateCamera(data: Partial<VideoDeviceApi.Camera>) {
|
export function updateCamera(data: Partial<VideoDeviceApi.Camera>) {
|
||||||
return wvpRequestClient.post('/video/device/proxy/update', data);
|
return wvpRequestClient.post('/video/proxy/update', data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 保存摄像头(新增/编辑,有 id 为编辑) */
|
/** 保存摄像头(新增/编辑,有 id 为编辑) */
|
||||||
@@ -128,25 +130,25 @@ export function saveCamera(data: Partial<VideoDeviceApi.Camera>) {
|
|||||||
|
|
||||||
/** 删除摄像头 */
|
/** 删除摄像头 */
|
||||||
export function deleteCamera(id: number) {
|
export function deleteCamera(id: number) {
|
||||||
return wvpRequestClient.delete('/video/device/proxy/delete', {
|
return wvpRequestClient.delete('/video/proxy/delete', {
|
||||||
params: { id },
|
params: { id },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 开始拉流 */
|
/** 开始拉流 */
|
||||||
export function startCamera(id: number) {
|
export function startCamera(id: number) {
|
||||||
return wvpRequestClient.get('/video/device/proxy/start', { params: { id } });
|
return wvpRequestClient.get('/video/proxy/start', { params: { id } });
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 停止拉流 */
|
/** 停止拉流 */
|
||||||
export function stopCamera(id: number) {
|
export function stopCamera(id: number) {
|
||||||
return wvpRequestClient.get('/video/device/proxy/stop', { params: { id } });
|
return wvpRequestClient.get('/video/proxy/stop', { params: { id } });
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 在线媒体服务器列表 */
|
/** 在线媒体服务器列表 */
|
||||||
export function getMediaServerList() {
|
export function getMediaServerList() {
|
||||||
return wvpRequestClient.get<VideoDeviceApi.MediaServer[]>(
|
return wvpRequestClient.get<VideoDeviceApi.MediaServer[]>(
|
||||||
'/video/device/server/online/list',
|
'/video/server/media_server/online/list',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,7 +156,7 @@ export function getMediaServerList() {
|
|||||||
export function getCameraOptions() {
|
export function getCameraOptions() {
|
||||||
return wvpRequestClient.get<
|
return wvpRequestClient.get<
|
||||||
{ cameraCode: string; cameraName: string }[]
|
{ cameraCode: string; cameraName: string }[]
|
||||||
>('/video/device/camera/options');
|
>('/video/ai/camera/options');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== ROI 区域管理 ====================
|
// ==================== ROI 区域管理 ====================
|
||||||
@@ -168,68 +170,71 @@ export function getRoiList(params: {
|
|||||||
query?: string;
|
query?: string;
|
||||||
}) {
|
}) {
|
||||||
return wvpRequestClient.get<VideoDeviceApi.PageResult<VideoDeviceApi.Roi>>(
|
return wvpRequestClient.get<VideoDeviceApi.PageResult<VideoDeviceApi.Roi>>(
|
||||||
'/video/device/roi/list',
|
'/video/ai/roi/list',
|
||||||
{ params },
|
{ params },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** ROI 详情(含算法绑定) */
|
/** ROI 详情(含算法绑定) */
|
||||||
export function getRoiDetail(id: number) {
|
export function getRoiDetail(id: number) {
|
||||||
return wvpRequestClient.get<VideoDeviceApi.Roi>(`/video/device/roi/${id}`);
|
return wvpRequestClient.get<VideoDeviceApi.Roi>(`/video/ai/roi/${id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 某摄像头的所有 ROI */
|
/** 某摄像头的所有 ROI */
|
||||||
export function getRoiByCameraId(cameraId: string) {
|
export function getRoiByCameraId(cameraId: string) {
|
||||||
return wvpRequestClient.get<VideoDeviceApi.Roi[]>(
|
return wvpRequestClient.get<VideoDeviceApi.Roi[]>(
|
||||||
'/video/device/roi/channel',
|
'/video/ai/roi/channel',
|
||||||
{ params: { cameraId } },
|
{ params: { cameraId } },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 保存 ROI(新增/编辑) */
|
/** 保存 ROI(新增/编辑) */
|
||||||
export function saveRoi(data: Partial<VideoDeviceApi.Roi>) {
|
export function saveRoi(data: Partial<VideoDeviceApi.Roi>) {
|
||||||
return wvpRequestClient.post('/video/device/roi/save', data);
|
return wvpRequestClient.post('/video/ai/roi/save', data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 删除 ROI */
|
/** 删除 ROI */
|
||||||
export function deleteRoi(roiId: string) {
|
export function deleteRoi(roiId: string) {
|
||||||
return wvpRequestClient.delete(`/video/device/roi/delete/${roiId}`);
|
return wvpRequestClient.delete(`/video/ai/roi/delete/${roiId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取摄像头截图 URL
|
* 获取摄像头截图 URL
|
||||||
*
|
*
|
||||||
* 非 force 模式:直接返回 /snap/image 代理 URL(无时间戳,浏览器自动缓存)
|
* 非 force:返回代理 URL,浏览器按 URL 缓存。
|
||||||
* force 模式:先触发边缘端截图,再返回带时间戳的代理 URL 破缓存
|
* force:先触发边缘端截图,再返回加时间戳的代理 URL 破缓存。
|
||||||
|
*
|
||||||
|
* 图片鉴权假设(重要):
|
||||||
|
* - 老 WVP 的 ?access-token=xxx 已移除。
|
||||||
|
* - 返回的 URL 会被 <img src> 使用,浏览器不会带上 Authorization 头;
|
||||||
|
* 该端点必须在后端满足以下任一条件之一才能正常显示:
|
||||||
|
* (a) SecurityConfig 里对 /video/ai/roi/snap/image 放行(permitAll);
|
||||||
|
* (b) 依赖 yudao session cookie(非 JWT)做鉴权;
|
||||||
|
* (c) 后端返回预签名的 OSS URL 并由调用方改造成 302 重定向。
|
||||||
|
* - 任何 401/403 不会在这里抛出,调用方需要处理 <img onerror> 以显示占位图。
|
||||||
*/
|
*/
|
||||||
export async function getSnapUrl(cameraCode: string, force = false): Promise<string> {
|
export async function getSnapUrl(cameraCode: string, force = false): Promise<string> {
|
||||||
const token = await getWvpToken();
|
|
||||||
if (force) {
|
if (force) {
|
||||||
// 强制刷新:先触发边缘端截图(等待完成)
|
// 强制刷新:先触发边缘端截图(等待完成)
|
||||||
try {
|
try {
|
||||||
await wvpRequestClient.get('/video/device/roi/snap', {
|
await wvpRequestClient.get('/video/ai/roi/snap', {
|
||||||
params: { cameraCode, force: true },
|
params: { cameraCode, force: true },
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// 截图请求可能超时,但 COS 上可能已有图片,继续返回代理 URL
|
// 截图请求可能超时,但 OSS 上可能已有图片,继续返回代理 URL
|
||||||
// 留一条日志便于图片加载 401/404 时回溯根因
|
|
||||||
if (import.meta.env.DEV) {
|
if (import.meta.env.DEV) {
|
||||||
console.debug('[WVP] force snap 触发失败,继续返回已有代理 URL', err);
|
console.debug('[video] force snap 触发失败,继续返回已有代理 URL', err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 加时间戳破浏览器缓存
|
|
||||||
return (
|
return (
|
||||||
`${apiURL}/video/device/roi/snap/image` +
|
`${apiURL}/video/ai/roi/snap/image` +
|
||||||
`?cameraCode=${encodeURIComponent(cameraCode)}` +
|
`?cameraCode=${encodeURIComponent(cameraCode)}` +
|
||||||
`&access-token=${encodeURIComponent(token)}` +
|
|
||||||
`&t=${Date.now()}`
|
`&t=${Date.now()}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// 非 force:使用代理端点,不加时间戳,浏览器自动缓存
|
|
||||||
return (
|
return (
|
||||||
`${apiURL}/video/device/roi/snap/image` +
|
`${apiURL}/video/ai/roi/snap/image` +
|
||||||
`?cameraCode=${encodeURIComponent(cameraCode)}` +
|
`?cameraCode=${encodeURIComponent(cameraCode)}`
|
||||||
`&access-token=${encodeURIComponent(token)}`
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,7 +243,7 @@ export async function getSnapUrl(cameraCode: string, force = false): Promise<str
|
|||||||
/** 算法列表 */
|
/** 算法列表 */
|
||||||
export function getAlgorithmList(deviceId?: string) {
|
export function getAlgorithmList(deviceId?: string) {
|
||||||
return wvpRequestClient.get<VideoDeviceApi.Algorithm[]>(
|
return wvpRequestClient.get<VideoDeviceApi.Algorithm[]>(
|
||||||
'/video/device/algorithm/list',
|
'/video/ai/algorithm/list',
|
||||||
{ params: deviceId ? { deviceId } : {} },
|
{ params: deviceId ? { deviceId } : {} },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -246,7 +251,7 @@ export function getAlgorithmList(deviceId?: string) {
|
|||||||
/** 保存算法全局参数 */
|
/** 保存算法全局参数 */
|
||||||
export function saveAlgoGlobalParams(algoCode: string, globalParams: string) {
|
export function saveAlgoGlobalParams(algoCode: string, globalParams: string) {
|
||||||
return wvpRequestClient.post(
|
return wvpRequestClient.post(
|
||||||
`/video/device/algorithm/global-params/${algoCode}`,
|
`/video/ai/algorithm/global-params/${algoCode}`,
|
||||||
{ globalParams },
|
{ globalParams },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -254,7 +259,7 @@ export function saveAlgoGlobalParams(algoCode: string, globalParams: string) {
|
|||||||
/** 批量更新设备算法参数 */
|
/** 批量更新设备算法参数 */
|
||||||
export function updateDeviceAlgoParams(deviceId: string, algoCode: string, params: string) {
|
export function updateDeviceAlgoParams(deviceId: string, algoCode: string, params: string) {
|
||||||
return wvpRequestClient.post(
|
return wvpRequestClient.post(
|
||||||
`/video/device/algorithm/device-binds/${deviceId}/${algoCode}`,
|
`/video/ai/algorithm/device-binds/${deviceId}/${algoCode}`,
|
||||||
{ params },
|
{ params },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -263,12 +268,12 @@ export function updateDeviceAlgoParams(deviceId: string, algoCode: string, param
|
|||||||
|
|
||||||
/** 绑定算法到 ROI */
|
/** 绑定算法到 ROI */
|
||||||
export function bindAlgo(data: { roiId: string; algoCode: string }) {
|
export function bindAlgo(data: { roiId: string; algoCode: string }) {
|
||||||
return wvpRequestClient.post('/video/device/roi/bindAlgo', data);
|
return wvpRequestClient.post('/video/ai/roi/bindAlgo', data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 解绑算法 */
|
/** 解绑算法 */
|
||||||
export function unbindAlgo(bindId: string) {
|
export function unbindAlgo(bindId: string) {
|
||||||
return wvpRequestClient.delete('/video/device/roi/unbindAlgo', {
|
return wvpRequestClient.delete('/video/ai/roi/unbindAlgo', {
|
||||||
params: { bindId },
|
params: { bindId },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -279,14 +284,14 @@ export function updateAlgoParams(data: {
|
|||||||
params?: string;
|
params?: string;
|
||||||
enabled?: number;
|
enabled?: number;
|
||||||
}) {
|
}) {
|
||||||
return wvpRequestClient.post('/video/device/roi/updateAlgoParams', data);
|
return wvpRequestClient.post('/video/ai/roi/updateAlgoParams', data);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 配置推送 ====================
|
// ==================== 配置推送 ====================
|
||||||
|
|
||||||
/** 推送配置到边缘端 */
|
/** 推送配置到边缘端 */
|
||||||
export function pushConfig(cameraId: string) {
|
export function pushConfig(cameraId: string) {
|
||||||
return wvpRequestClient.post('/video/device/config/push', null, {
|
return wvpRequestClient.post('/video/ai/config/push', null, {
|
||||||
params: { cameraId },
|
params: { cameraId },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -294,30 +299,15 @@ export function pushConfig(cameraId: string) {
|
|||||||
/** 一次性推送全部配置到本地Edge */
|
/** 一次性推送全部配置到本地Edge */
|
||||||
export function pushAllConfig() {
|
export function pushAllConfig() {
|
||||||
return wvpRequestClient.post<Record<string, any>>(
|
return wvpRequestClient.post<Record<string, any>>(
|
||||||
'/video/device/config/push-all',
|
'/video/ai/config/push-all',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 导出摄像头配置 JSON */
|
/** 导出摄像头配置 JSON */
|
||||||
export function exportConfig(cameraId: string) {
|
export function exportConfig(cameraId: string) {
|
||||||
return wvpRequestClient.get<Record<string, any>>(
|
return wvpRequestClient.get<Record<string, any>>(
|
||||||
'/video/device/config/export',
|
'/video/ai/config/export',
|
||||||
{ params: { cameraId } },
|
{ params: { cameraId } },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 告警图片代理 ====================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构造告警图片代理 URL(通过 WVP 下载 COS 图片后返回字节流)
|
|
||||||
* @param imagePath COS 对象路径或完整 URL
|
|
||||||
*/
|
|
||||||
export async function getAlertImageUrl(imagePath: string): Promise<string> {
|
|
||||||
if (!imagePath) return '';
|
|
||||||
const token = await getWvpToken();
|
|
||||||
return (
|
|
||||||
`${apiURL}/video/device/alert/image` +
|
|
||||||
`?imagePath=${encodeURIComponent(imagePath)}` +
|
|
||||||
`&access-token=${encodeURIComponent(token)}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -31,27 +31,26 @@ export namespace VideoEdgeApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 边缘设备 API ====================
|
// ==================== 边缘设备 API ====================
|
||||||
// 数据源:WVP 数据库 wvp_ai_edge_device 表
|
// 对应后端 AiEdgeDeviceController @RequestMapping("/video/ai/device")
|
||||||
// 路径经 vite proxy: /admin-api/video/device/* → rewrite → /api/ai/* → WVP:18080
|
|
||||||
|
|
||||||
/** 获取全部边缘设备列表 */
|
/** 获取全部边缘设备列表 */
|
||||||
export function getDeviceList() {
|
export function getDeviceList() {
|
||||||
return wvpRequestClient.get<VideoEdgeApi.Device[]>(
|
return wvpRequestClient.get<VideoEdgeApi.Device[]>(
|
||||||
'/video/device/device/list',
|
'/video/ai/device/list',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 分页查询边缘设备列表 */
|
/** 分页查询边缘设备列表 */
|
||||||
export function getDevicePage(params: PageParam) {
|
export function getDevicePage(params: PageParam) {
|
||||||
return wvpRequestClient.get<PageResult<VideoEdgeApi.Device>>(
|
return wvpRequestClient.get<PageResult<VideoEdgeApi.Device>>(
|
||||||
'/video/device/device/page',
|
'/video/ai/device/page',
|
||||||
{ params },
|
{ params },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 获取设备详情 */
|
/** 获取设备详情 */
|
||||||
export function getDevice(deviceId: string) {
|
export function getDevice(deviceId: string) {
|
||||||
return wvpRequestClient.get<VideoEdgeApi.Device>('/video/device/device/get', {
|
return wvpRequestClient.get<VideoEdgeApi.Device>('/video/ai/device/get', {
|
||||||
params: { deviceId },
|
params: { deviceId },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -59,6 +58,6 @@ export function getDevice(deviceId: string) {
|
|||||||
/** 获取设备统计 */
|
/** 获取设备统计 */
|
||||||
export function getDeviceStatistics() {
|
export function getDeviceStatistics() {
|
||||||
return wvpRequestClient.get<VideoEdgeApi.DeviceStatistics>(
|
return wvpRequestClient.get<VideoEdgeApi.DeviceStatistics>(
|
||||||
'/video/device/device/statistics',
|
'/video/ai/device/statistics',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,118 +1,12 @@
|
|||||||
/**
|
/**
|
||||||
* WVP 视频平台专用请求客户端
|
* Video 模块请求客户端
|
||||||
*
|
*
|
||||||
* WVP 使用独立的 JWT 认证系统(access-token 头),
|
* 历史:原 WVP 独立服务有自己的 JWT + access-token 认证;
|
||||||
* 与芋道前端的 Authorization Bearer 认证不同。
|
* 已迁移到 viewsh-module-video 微服务下,走框架统一鉴权
|
||||||
* 本模块实现自动登录 WVP、缓存 token、401 自动续期。
|
* (yudao Authorization: Bearer,由 #/api/request 的拦截器统一注入)。
|
||||||
*/
|
|
||||||
import { useAppConfig } from '@vben/hooks';
|
|
||||||
import { preferences } from '@vben/preferences';
|
|
||||||
import { RequestClient } from '@vben/request';
|
|
||||||
|
|
||||||
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
|
|
||||||
|
|
||||||
// ==================== WVP Token 管理 ====================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* WVP 默认账号
|
|
||||||
*
|
*
|
||||||
* 注意:前端任何 WVP 账号/密码都会打进 bundle,对懂行的人等于明文。
|
* TODO(video-rename): 这是过渡期的命名别名。后续把 `#/api/video/**` 下所有
|
||||||
* 长期方案应由后端代理 WVP 鉴权,不要把账号分发到客户端。
|
* `wvpRequestClient` 的调用点改为直接 import `requestClient`,然后删掉本文件。
|
||||||
* 作为过渡,至少走环境变量让生产可覆盖。
|
* 保留只是为了降低此次迁移的一次性改动量。
|
||||||
*/
|
*/
|
||||||
const WVP_USERNAME = import.meta.env.VITE_WVP_USERNAME || 'admin';
|
export { requestClient as wvpRequestClient } from '#/api/request';
|
||||||
const WVP_PASSWORD_MD5 =
|
|
||||||
import.meta.env.VITE_WVP_PASSWORD_MD5 ||
|
|
||||||
'21232f297a57a5a743894a0e4a801fc3'; // 'admin' 的 MD5,仅作兜底
|
|
||||||
|
|
||||||
let wvpAccessToken: null | string = null;
|
|
||||||
let tokenPromise: null | Promise<string> = null;
|
|
||||||
|
|
||||||
/** 登录 WVP 获取 access-token */
|
|
||||||
async function loginToWvp(): Promise<string> {
|
|
||||||
const url =
|
|
||||||
`${apiURL}/video/device/user/login` +
|
|
||||||
`?username=${encodeURIComponent(WVP_USERNAME)}` +
|
|
||||||
`&password=${encodeURIComponent(WVP_PASSWORD_MD5)}`;
|
|
||||||
|
|
||||||
const res = await fetch(url);
|
|
||||||
const json = await res.json();
|
|
||||||
|
|
||||||
if (json.code !== 0 || !json.data?.accessToken) {
|
|
||||||
throw new Error(json.msg || 'WVP 登录失败');
|
|
||||||
}
|
|
||||||
|
|
||||||
wvpAccessToken = json.data.accessToken;
|
|
||||||
return wvpAccessToken!;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 获取有效的 WVP token(自动登录,防止并发重复登录) */
|
|
||||||
export async function getWvpToken(): Promise<string> {
|
|
||||||
if (wvpAccessToken) {
|
|
||||||
return wvpAccessToken;
|
|
||||||
}
|
|
||||||
if (!tokenPromise) {
|
|
||||||
tokenPromise = loginToWvp().finally(() => {
|
|
||||||
tokenPromise = null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return tokenPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 清除缓存的 token(用于 401 后重新登录) */
|
|
||||||
function clearWvpToken() {
|
|
||||||
wvpAccessToken = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== 请求客户端 ====================
|
|
||||||
|
|
||||||
function createWvpRequestClient(baseURL: string) {
|
|
||||||
const client = new RequestClient({ baseURL });
|
|
||||||
|
|
||||||
// 请求拦截器:注入 WVP access-token
|
|
||||||
client.addRequestInterceptor({
|
|
||||||
fulfilled: async (config) => {
|
|
||||||
const token = await getWvpToken();
|
|
||||||
config.headers['access-token'] = token;
|
|
||||||
config.headers['Accept-Language'] = preferences.app.locale;
|
|
||||||
return config;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// 响应拦截器:处理 WVP 响应格式 + 401 自动续期
|
|
||||||
client.addResponseInterceptor({
|
|
||||||
fulfilled: (response) => {
|
|
||||||
const data = response.data;
|
|
||||||
|
|
||||||
// WVP 标准格式 { code: 0, msg: "成功", data: ... }
|
|
||||||
if (data && typeof data === 'object' && 'code' in data) {
|
|
||||||
if (data.code === 0) {
|
|
||||||
return data.data;
|
|
||||||
}
|
|
||||||
return Promise.reject(new Error(data.msg || '请求失败'));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 非标准格式直接返回
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
rejected: async (error) => {
|
|
||||||
// 401 → 清除 token 并重试一次
|
|
||||||
if (error?.response?.status === 401 && error.config) {
|
|
||||||
clearWvpToken();
|
|
||||||
try {
|
|
||||||
const token = await getWvpToken();
|
|
||||||
error.config.headers['access-token'] = token;
|
|
||||||
const response = await client.instance.request(error.config);
|
|
||||||
return response;
|
|
||||||
} catch {
|
|
||||||
return Promise.reject(new Error('WVP 认证失败,请检查服务状态'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Promise.reject(error);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return client;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const wvpRequestClient = createWvpRequestClient(apiURL);
|
|
||||||
|
|||||||
@@ -146,9 +146,12 @@ async function loadCameraStatus() {
|
|||||||
for (const cam of cameraList.value) {
|
for (const cam of cameraList.value) {
|
||||||
const cameraCode = cam.cameraCode;
|
const cameraCode = cam.cameraCode;
|
||||||
if (!cameraCode) continue;
|
if (!cameraCode) continue;
|
||||||
// 使用 fetch HEAD 请求检测截图是否可用(/snap/image 已免认证)
|
// 通过 HEAD 探测截图是否可用。
|
||||||
|
// 前置条件:后端对 /video/ai/roi/snap/image 放行(SecurityConfig permitAll)
|
||||||
|
// 或依赖 session cookie —— fetch 默认不会带 Authorization 头。
|
||||||
|
// 401/403 会被视作"离线",与 4xx/5xx 行为一致,对端上逻辑透明。
|
||||||
try {
|
try {
|
||||||
const url = `${apiURL}/video/device/roi/snap/image?cameraCode=${encodeURIComponent(cameraCode)}`;
|
const url = `${apiURL}/video/ai/roi/snap/image?cameraCode=${encodeURIComponent(cameraCode)}`;
|
||||||
const res = await fetch(url, { method: 'HEAD' });
|
const res = await fetch(url, { method: 'HEAD' });
|
||||||
cameraStatus.value = { ...cameraStatus.value, [cameraCode]: res.ok };
|
cameraStatus.value = { ...cameraStatus.value, [cameraCode]: res.ok };
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -34,43 +34,9 @@ export default defineConfig(async () => {
|
|||||||
target: 'http://127.0.0.1:8000',
|
target: 'http://127.0.0.1:8000',
|
||||||
},
|
},
|
||||||
|
|
||||||
// video/device/* -> WVP :18080(按子路径分别 rewrite)
|
|
||||||
// 注意:更具体的路径必须写在通配路径前面
|
|
||||||
|
|
||||||
// 摄像头拉流代理: /admin-api/video/device/proxy -> /api/proxy
|
|
||||||
'/admin-api/video/device/proxy': {
|
|
||||||
changeOrigin: true,
|
|
||||||
target: 'http://127.0.0.1:18080',
|
|
||||||
rewrite: (path: string) =>
|
|
||||||
path.replace('/admin-api/video/device/proxy', '/api/proxy'),
|
|
||||||
},
|
|
||||||
// WVP 用户认证: /admin-api/video/device/user -> /api/user
|
|
||||||
'/admin-api/video/device/user': {
|
|
||||||
changeOrigin: true,
|
|
||||||
target: 'http://127.0.0.1:18080',
|
|
||||||
rewrite: (path: string) =>
|
|
||||||
path.replace('/admin-api/video/device/user', '/api/user'),
|
|
||||||
},
|
|
||||||
// 媒体服务器: /admin-api/video/device/server -> /api/server/media_server
|
|
||||||
'/admin-api/video/device/server': {
|
|
||||||
changeOrigin: true,
|
|
||||||
target: 'http://127.0.0.1:18080',
|
|
||||||
rewrite: (path: string) =>
|
|
||||||
path.replace(
|
|
||||||
'/admin-api/video/device/server',
|
|
||||||
'/api/server/media_server',
|
|
||||||
),
|
|
||||||
},
|
|
||||||
// ROI/算法/配置等: /admin-api/video/device -> /api/ai(通配,放最后)
|
|
||||||
'/admin-api/video/device': {
|
|
||||||
changeOrigin: true,
|
|
||||||
target: 'http://127.0.0.1:18080',
|
|
||||||
rewrite: (path: string) =>
|
|
||||||
path.replace('/admin-api/video/device', '/api/ai'),
|
|
||||||
},
|
|
||||||
|
|
||||||
// ==================== 芋道主平台 ====================
|
// ==================== 芋道主平台 ====================
|
||||||
// 所有 system/*、infra/* 等基础接口 -> 芋道后端 48080
|
// 所有 system/*、infra/*、video/device/* 等业务接口 -> 芋道网关 48080
|
||||||
|
// video 微服务已挂到 gateway,不再直连 WVP :18080
|
||||||
'/admin-api': {
|
'/admin-api': {
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
rewrite: (path) => path.replace(/^\/admin-api/, ''),
|
rewrite: (path) => path.replace(/^\/admin-api/, ''),
|
||||||
|
|||||||
Reference in New Issue
Block a user