From 291d0be1c91794a444348023124b760d062c12f9 Mon Sep 17 00:00:00 2001 From: Simon Date: Thu, 13 Aug 2026 14:35:11 +0800 Subject: [PATCH] =?UTF-8?q?[BUG-=E9=80=9A=E7=94=A8-001]=20=E4=B8=89?= =?UTF-8?q?=E7=AB=AF=20dev=20=E6=8E=A2=E6=B5=8B=E5=AE=88=E5=8D=AB=20+=20ax?= =?UTF-8?q?ios=20case=20403=20=E6=96=87=E6=A1=88=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. src/frontend-agent/src/views/Login.vue - 加回 DEV_USERS 常量 + checkDevMode() + devLogin() - onMounted 第一行加 if (import.meta.env.DEV) checkDevMode() - 模板加 "测试账号登录" 分区 (v-if="devModeEnabled") - dev 探测代码包在守卫中 → Vite build 时 tree shaking 剔除 2. src/frontend-admin/src/views/Login.vue - 同上(admin 端,token 用 admin_token / adminStore) 3. src/frontend-agent/src/api/index.ts (case 403) - 文案: 拒绝访问 → 拒绝访问: ${method} ${url} - 含具体请求 method+URL 便于 4xx 排查 4. src/frontend-admin/src/api/index.ts - 同上 Vite tree shaking 实测: pnpm build 后 dist 0 处 /dev/health(生产环境不触发 403)。 开发环境 (pnpm dev) 完整保留,E2E 自动化测试 (Playwright/Cypress) 可用。 BUG-通用-001 修复。 --- src/frontend-admin/src/api/index.ts | 6 +- src/frontend-admin/src/views/Login.vue | 104 +++++++++++++++++++++++++ src/frontend-agent/src/api/index.ts | 6 +- src/frontend-agent/src/views/Login.vue | 97 +++++++++++++++++++++++ 4 files changed, 211 insertions(+), 2 deletions(-) diff --git a/src/frontend-admin/src/api/index.ts b/src/frontend-admin/src/api/index.ts index 7529b1c..7456c1a 100644 --- a/src/frontend-admin/src/api/index.ts +++ b/src/frontend-admin/src/api/index.ts @@ -87,6 +87,9 @@ apiClient.interceptors.response.use( if (error.response) { // 服务器返回了错误状态码 code = error.response.status + // P0-NEW13: 提取请求 method + URL 便于 4xx/5xx 排查 + const reqMethod = (error.response.config?.method || '').toUpperCase() + const reqUrl = error.response.config?.url || '' switch (error.response.status) { case 401: message = '未授权,请重新登录' @@ -94,7 +97,8 @@ apiClient.interceptors.response.use( localStorage.removeItem('admin_token') break case 403: - message = '拒绝访问' + // 含具体 method+URL,便于排查(生产环境 dev_mock 端点会被 nginx 拦截返 403) + message = `拒绝访问: ${reqMethod} ${reqUrl}` break case 404: message = '请求的资源不存在' diff --git a/src/frontend-admin/src/views/Login.vue b/src/frontend-admin/src/views/Login.vue index abbb26c..77df9f9 100644 --- a/src/frontend-admin/src/views/Login.vue +++ b/src/frontend-admin/src/views/Login.vue @@ -51,6 +51,31 @@

{{ loadingText }}

+ + +
+ + + + +
@@ -61,6 +86,7 @@ import { useRouter } from 'vue-router' import { ElMessage } from 'element-plus' import { Loading, CircleCheckFilled, Headset } from '@element-plus/icons-vue' import { getQrcode, getScanStatus, handleOAuthCallback, type QrcodeResponse, type ScanStatusResponse } from '@/api/auth' +import apiClient from '@/api' import { useAdminStore } from '@/stores/admin' const router = useRouter() @@ -94,6 +120,80 @@ let pollTimer: ReturnType | null = null /** 当前扫码登录票据 */ let currentTicket = '' +// -------------------------------------------------------------------------- +// 自动化测试支持(dev_mock 一键登录) +// -------------------------------------------------------------------------- +// ⚠️ P0-NEW13: 以下 dev 探测代码必须用 import.meta.env.DEV 守卫, +// 否则 vite build 会把 /api/dev/health 编译进生产 dist,触发 axios 拦截器 403 → +// ElMessage.error("拒绝访问") 红色弹窗(2026-08-13 故障根因)。 +// 开发环境(pnpm dev):保留 → 可用预设测试账号一键登录,E2E 自动化可用 +// 生产构建(pnpm build):Vite 自动 dead-code elimination → 代码不进 dist + +/** 预设 dev 测试账号清单(与后端 PRESET_DEV_USERS 对齐子集) */ +const DEV_USERS: Array<{ userid: string; label: string; role: string }> = [ + { userid: 'dev-user-001', label: '员工(user)', role: 'user' }, + { userid: 'dev-agent-001', label: '坐席(agent)', role: 'agent' }, + { userid: 'dev-admin-001', label: '管理员(admin)', role: 'admin' }, +] + +/** dev_mock 模式是否启用(后端 app_env!=production 且 dev_mode=true) */ +const devModeEnabled = ref(false) + +/** 当前选中的 dev 测试账号 */ +const selectedDevUser = ref('dev-admin-001') + +/** dev 一键登录加载中 */ +const devLoginLoading = ref(false) + +/** + * 探测后端 dev_mock 模式是否启用 + * 仅在 import.meta.env.DEV=true 时调用 + */ +async function checkDevMode(): Promise { + try { + const res = await apiClient.get('/dev/health', { timeout: 3000 }) + if (res != null && (res as any).dev_mode) { + devModeEnabled.value = true + } + } catch { + devModeEnabled.value = false + } +} + +/** + * 一键登录当前选中的 dev 测试账号 + * 绕过企微扫码直接拿 token,用于 E2E 自动化测试 + */ +async function devLogin(): Promise { + const target = DEV_USERS.find((u) => u.userid === selectedDevUser.value) + if (!target) return + devLoginLoading.value = true + try { + const res = await apiClient.get('/dev/login', { + params: { userid: target.userid, name: target.label, role: target.role }, + }) + // admin 端 token 持久化(与 processOAuthCallback 一致) + localStorage.setItem('admin_token', res.token) + adminStore.token = res.token + localStorage.setItem('admin_user_id', target.userid) + adminStore.adminUserId = target.userid + const adminInfoData = { + user_id: target.userid, + name: target.label, + status: 'online', + } + adminStore.adminInfo = adminInfoData + localStorage.setItem('admin_info', JSON.stringify(adminInfoData)) + ElMessage.success('登录成功') + router.push('/') + } catch (error) { + console.error('测试登录失败:', error) + ElMessage.error('测试登录失败,请确认内网环境') + } finally { + devLoginLoading.value = false + } +} + // -------------------------------------------------------------------------- // 方法 // -------------------------------------------------------------------------- @@ -298,6 +398,10 @@ async function redirectToOAuth(): Promise { // -------------------------------------------------------------------------- onMounted(async () => { + // P0-NEW13: 仅在 dev 模式下探测 /dev/health(生产构建被 Vite 剔除,不会发请求) + if (import.meta.env.DEV) { + checkDevMode() + } // 检查 URL 中的 code 参数(OAuth2 回调) const urlParams = new URLSearchParams(window.location.search) const code = urlParams.get('code') diff --git a/src/frontend-agent/src/api/index.ts b/src/frontend-agent/src/api/index.ts index 7b86bef..853b0e6 100644 --- a/src/frontend-agent/src/api/index.ts +++ b/src/frontend-agent/src/api/index.ts @@ -225,6 +225,9 @@ apiClient.interceptors.response.use( if (error.response) { // 服务器返回了错误状态码 code = error.response.status + // P0-NEW13: 提取请求 method + URL 便于 4xx/5xx 排查时一目了然(曾因 /api/dev/health 在生产触发 403 弹窗排查费时) + const reqMethod = (error.response.config?.method || '').toUpperCase() + const reqUrl = error.response.config?.url || '' switch (error.response.status) { case 401: // AUTH-P0-02: 先尝试静默刷新 Token,成功则重放请求 @@ -232,7 +235,8 @@ apiClient.interceptors.response.use( // 不显示通用提示,因为会自动处理 break case 403: - message = '拒绝访问' + // 含具体 method+URL,便于排查(生产环境 dev_mock 端点会被 nginx 拦截返 403) + message = `拒绝访问: ${reqMethod} ${reqUrl}` break case 404: message = '请求的资源不存在' diff --git a/src/frontend-agent/src/views/Login.vue b/src/frontend-agent/src/views/Login.vue index e830cf3..73d62b0 100644 --- a/src/frontend-agent/src/views/Login.vue +++ b/src/frontend-agent/src/views/Login.vue @@ -50,6 +50,32 @@

{{ loadingText }}

+ + + + @@ -60,6 +86,7 @@ import { useRouter } from 'vue-router' import { ElMessage } from 'element-plus' import { Loading, CircleCheckFilled } from '@element-plus/icons-vue' import { getQrcode, getScanStatus, handleOAuthCallback, type QrcodeResponse, type ScanStatusResponse } from '@/api/auth' +import apiClient from '@/api' import { useAgentStore } from '@/stores/agent' const router = useRouter() @@ -96,6 +123,72 @@ let pollTimer: ReturnType | null = null /** 当前扫码登录票据 */ let currentTicket = '' +// -------------------------------------------------------------------------- +// 自动化测试支持(dev_mock 一键登录) +// -------------------------------------------------------------------------- +// ⚠️ P0-NEW13: 以下 dev 探测代码必须用 import.meta.env.DEV 守卫, +// 否则 vite build 会把 /api/dev/health 编译进生产 dist,触发 axios 拦截器 403 → +// ElMessage.error("拒绝访问") 红色弹窗(2026-08-13 故障根因)。 +// 开发环境(pnpm dev):保留 → 可用预设测试账号一键登录,E2E 自动化可用 +// 生产构建(pnpm build):Vite 自动 dead-code elimination → 代码不进 dist + +/** 预设 dev 测试账号清单(与后端 PRESET_DEV_USERS 对齐子集) */ +const DEV_USERS: Array<{ userid: string; label: string; role: string }> = [ + { userid: 'dev-user-001', label: '员工(user)', role: 'user' }, + { userid: 'dev-agent-001', label: '坐席(agent)', role: 'agent' }, + { userid: 'dev-admin-001', label: '管理员(admin)', role: 'admin' }, +] + +/** dev_mock 模式是否启用(后端 app_env!=production 且 dev_mode=true) */ +const devModeEnabled = ref(false) + +/** 当前选中的 dev 测试账号 */ +const selectedDevUser = ref('dev-agent-001') + +/** dev 一键登录加载中 */ +const devLoginLoading = ref(false) + +/** + * 探测后端 dev_mock 模式是否启用 + * 仅在 import.meta.env.DEV=true 时调用(vite dev 模式或 dev 构建) + */ +async function checkDevMode(): Promise { + try { + const res = await apiClient.get('/dev/health', { timeout: 3000 }) + if (res != null && (res as any).dev_mode) { + devModeEnabled.value = true + } + } catch { + // 403/网络错误都视为 dev_mode 未启用,静默忽略 + devModeEnabled.value = false + } +} + +/** + * 一键登录当前选中的 dev 测试账号 + * 绕过企微扫码直接拿 token,用于 E2E 自动化测试 + */ +async function devLogin(): Promise { + const target = DEV_USERS.find((u) => u.userid === selectedDevUser.value) + if (!target) return + devLoginLoading.value = true + try { + const res = await apiClient.get('/dev/login', { + params: { userid: target.userid, name: target.label, role: target.role }, + }) + await handleLoginSuccess(res.token, { + userid: target.userid, + name: target.label, + is_new_user: false, + } as any) + } catch (error) { + console.error('测试登录失败:', error) + ElMessage.error('测试登录失败,请确认内网环境') + } finally { + devLoginLoading.value = false + } +} + // -------------------------------------------------------------------------- // 方法 // -------------------------------------------------------------------------- @@ -296,6 +389,10 @@ async function redirectToOAuth(): Promise { // -------------------------------------------------------------------------- onMounted(async () => { + // P0-NEW13: 仅在 dev 模式下探测 /dev/health(生产构建被 Vite 剔除,不会发请求) + if (import.meta.env.DEV) { + checkDevMode() + } // 检查 URL 中的 code 参数(OAuth2 回调) const urlParams = new URLSearchParams(window.location.search) const code = urlParams.get('code')