From 184bb863b0743afb125b04394a3dfd56b1a0321e Mon Sep 17 00:00:00 2001 From: 16337 <1633794139@qq.com> Date: Mon, 9 Feb 2026 10:24:47 +0800 Subject: [PATCH] =?UTF-8?q?fix(aiot):=20=E4=BF=AE=E5=A4=8D=20WVP=20?= =?UTF-8?q?=E8=AE=A4=E8=AF=81=E6=9C=BA=E5=88=B6=EF=BC=8C=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E7=99=BB=E5=BD=95=E5=92=8C=20token=20?= =?UTF-8?q?=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - WVP 使用独立 JWT 认证(access-token 头),与芋道 Authorization Bearer 不同 - 实现 WVP 自动登录:首次请求时自动调用 /api/user/login 获取 token - 缓存 token 防止重复登录,401 时自动续期 - 响应拦截器自动解包 WVP {code:0, data:...} 格式 - Vite 代理新增 /aiot/device/user 和 /aiot/device/server 路由规则 - 移除已废弃的 aiot/video 代理规则 Co-Authored-By: Claude Opus 4.6 --- apps/web-antd/src/api/aiot/request.ts | 101 ++++++++++++++++++++++---- apps/web-antd/vite.config.mts | 33 ++++++--- 2 files changed, 109 insertions(+), 25 deletions(-) diff --git a/apps/web-antd/src/api/aiot/request.ts b/apps/web-antd/src/api/aiot/request.ts index 638d33689..3ed7ee954 100644 --- a/apps/web-antd/src/api/aiot/request.ts +++ b/apps/web-antd/src/api/aiot/request.ts @@ -1,34 +1,109 @@ /** * WVP 视频平台专用请求客户端 * - * WVP 返回原始 JSON(非芋道的 {code:0, data:...} 格式), - * 需要跳过芋道默认的响应拦截器。 + * WVP 使用独立的 JWT 认证系统(access-token 头), + * 与芋道前端的 Authorization Bearer 认证不同。 + * 本模块实现自动登录 WVP、缓存 token、401 自动续期。 */ import { useAppConfig } from '@vben/hooks'; import { preferences } from '@vben/preferences'; import { RequestClient } from '@vben/request'; -import { useAccessStore } from '@vben/stores'; const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD); -function createWvpRequestClient(baseURL: string) { - const client = new RequestClient({ - baseURL, - }); +// ==================== WVP Token 管理 ==================== - // 请求头:携带 token 用于 Vite 代理鉴权透传 +/** WVP 默认账号(开发环境使用,生产环境应通过环境变量配置) */ +const WVP_USERNAME = 'admin'; +const WVP_PASSWORD_MD5 = '21232f297a57a5a743894a0e4a801fc3'; // admin 的 MD5 + +let wvpAccessToken: null | string = null; +let tokenPromise: null | Promise = null; + +/** 登录 WVP 获取 access-token */ +async function loginToWvp(): Promise { + const url = + `${apiURL}/aiot/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 { + 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 accessStore = useAccessStore(); - const token = accessStore.accessToken; - if (token) { - config.headers.Authorization = `Bearer ${token}`; - } + 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; } diff --git a/apps/web-antd/vite.config.mts b/apps/web-antd/vite.config.mts index 2c2376fe1..934c84184 100644 --- a/apps/web-antd/vite.config.mts +++ b/apps/web-antd/vite.config.mts @@ -8,6 +8,7 @@ export default defineConfig(async () => { allowedHosts: true, proxy: { // ==================== AIoT 统一路由 ==================== + // aiot/alarm, aiot/edge -> 告警服务 :8000(直通) '/admin-api/aiot/alarm': { changeOrigin: true, @@ -17,36 +18,44 @@ export default defineConfig(async () => { changeOrigin: true, target: 'http://127.0.0.1:8000', }, - // aiot/device/proxy -> WVP :18080(rewrite: /admin-api/aiot/device/proxy -> /api/proxy) - // 摄像头拉流代理接口在 /api/proxy 下,需单独匹配 + + // aiot/device/* -> WVP :18080(按子路径分别 rewrite) + // 注意:更具体的路径必须写在通配路径前面 + + // 摄像头拉流代理: /admin-api/aiot/device/proxy -> /api/proxy '/admin-api/aiot/device/proxy': { changeOrigin: true, target: 'http://127.0.0.1:18080', rewrite: (path: string) => path.replace('/admin-api/aiot/device/proxy', '/api/proxy'), }, - // aiot/device/server -> WVP :18080(rewrite: -> /api/server/media_server) + // WVP 用户认证: /admin-api/aiot/device/user -> /api/user + '/admin-api/aiot/device/user': { + changeOrigin: true, + target: 'http://127.0.0.1:18080', + rewrite: (path: string) => + path.replace('/admin-api/aiot/device/user', '/api/user'), + }, + // 媒体服务器: /admin-api/aiot/device/server -> /api/server/media_server '/admin-api/aiot/device/server': { changeOrigin: true, target: 'http://127.0.0.1:18080', rewrite: (path: string) => - path.replace('/admin-api/aiot/device/server', '/api/server/media_server'), + path.replace( + '/admin-api/aiot/device/server', + '/api/server/media_server', + ), }, - // aiot/device -> WVP :18080(rewrite: /admin-api/aiot/device -> /api/ai) + // ROI/算法/配置等: /admin-api/aiot/device -> /api/ai(通配,放最后) '/admin-api/aiot/device': { changeOrigin: true, target: 'http://127.0.0.1:18080', rewrite: (path: string) => path.replace('/admin-api/aiot/device', '/api/ai'), }, - // aiot/video -> WVP :18080(rewrite: /admin-api/aiot/video -> /api) - '/admin-api/aiot/video': { - changeOrigin: true, - target: 'http://127.0.0.1:18080', - rewrite: (path: string) => - path.replace('/admin-api/aiot/video', '/api'), - }, + // ==================== 系统基础路由 ==================== + // 认证相关接口 -> 告警平台(测试阶段提供模拟认证) '/admin-api/system/auth': { changeOrigin: true,