Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 291d0be1c9 |
@@ -87,6 +87,9 @@ apiClient.interceptors.response.use(
|
|||||||
if (error.response) {
|
if (error.response) {
|
||||||
// 服务器返回了错误状态码
|
// 服务器返回了错误状态码
|
||||||
code = error.response.status
|
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) {
|
switch (error.response.status) {
|
||||||
case 401:
|
case 401:
|
||||||
message = '未授权,请重新登录'
|
message = '未授权,请重新登录'
|
||||||
@@ -94,7 +97,8 @@ apiClient.interceptors.response.use(
|
|||||||
localStorage.removeItem('admin_token')
|
localStorage.removeItem('admin_token')
|
||||||
break
|
break
|
||||||
case 403:
|
case 403:
|
||||||
message = '拒绝访问'
|
// 含具体 method+URL,便于排查(生产环境 dev_mock 端点会被 nginx 拦截返 403)
|
||||||
|
message = `拒绝访问: ${reqMethod} ${reqUrl}`
|
||||||
break
|
break
|
||||||
case 404:
|
case 404:
|
||||||
message = '请求的资源不存在'
|
message = '请求的资源不存在'
|
||||||
|
|||||||
@@ -51,6 +51,31 @@
|
|||||||
<el-icon class="is-loading" size="32"><Loading /></el-icon>
|
<el-icon class="is-loading" size="32"><Loading /></el-icon>
|
||||||
<p>{{ loadingText }}</p>
|
<p>{{ loadingText }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- P0-NEW13: 自动化测试登录入口(仅 dev_mode=true 时显示) -->
|
||||||
|
<div v-if="devModeEnabled" class="dev-login">
|
||||||
|
<el-divider>
|
||||||
|
<span class="test-login-divider-text">测试账号登录</span>
|
||||||
|
</el-divider>
|
||||||
|
<div class="dev-login-row">
|
||||||
|
<el-select v-model="selectedDevUser" size="small" placeholder="选择测试角色" style="width: 180px">
|
||||||
|
<el-option
|
||||||
|
v-for="user in DEV_USERS"
|
||||||
|
:key="user.userid"
|
||||||
|
:label="user.label"
|
||||||
|
:value="user.userid"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
:loading="devLoginLoading"
|
||||||
|
@click="devLogin"
|
||||||
|
>
|
||||||
|
免企微登录
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -61,6 +86,7 @@ import { useRouter } from 'vue-router'
|
|||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { Loading, CircleCheckFilled, Headset } from '@element-plus/icons-vue'
|
import { Loading, CircleCheckFilled, Headset } from '@element-plus/icons-vue'
|
||||||
import { getQrcode, getScanStatus, handleOAuthCallback, type QrcodeResponse, type ScanStatusResponse } from '@/api/auth'
|
import { getQrcode, getScanStatus, handleOAuthCallback, type QrcodeResponse, type ScanStatusResponse } from '@/api/auth'
|
||||||
|
import apiClient from '@/api'
|
||||||
import { useAdminStore } from '@/stores/admin'
|
import { useAdminStore } from '@/stores/admin'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -94,6 +120,80 @@ let pollTimer: ReturnType<typeof setInterval> | null = null
|
|||||||
/** 当前扫码登录票据 */
|
/** 当前扫码登录票据 */
|
||||||
let currentTicket = ''
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
|||||||
// --------------------------------------------------------------------------
|
// --------------------------------------------------------------------------
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
// P0-NEW13: 仅在 dev 模式下探测 /dev/health(生产构建被 Vite 剔除,不会发请求)
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
checkDevMode()
|
||||||
|
}
|
||||||
// 检查 URL 中的 code 参数(OAuth2 回调)
|
// 检查 URL 中的 code 参数(OAuth2 回调)
|
||||||
const urlParams = new URLSearchParams(window.location.search)
|
const urlParams = new URLSearchParams(window.location.search)
|
||||||
const code = urlParams.get('code')
|
const code = urlParams.get('code')
|
||||||
|
|||||||
@@ -225,6 +225,9 @@ apiClient.interceptors.response.use(
|
|||||||
if (error.response) {
|
if (error.response) {
|
||||||
// 服务器返回了错误状态码
|
// 服务器返回了错误状态码
|
||||||
code = error.response.status
|
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) {
|
switch (error.response.status) {
|
||||||
case 401:
|
case 401:
|
||||||
// AUTH-P0-02: 先尝试静默刷新 Token,成功则重放请求
|
// AUTH-P0-02: 先尝试静默刷新 Token,成功则重放请求
|
||||||
@@ -232,7 +235,8 @@ apiClient.interceptors.response.use(
|
|||||||
// 不显示通用提示,因为会自动处理
|
// 不显示通用提示,因为会自动处理
|
||||||
break
|
break
|
||||||
case 403:
|
case 403:
|
||||||
message = '拒绝访问'
|
// 含具体 method+URL,便于排查(生产环境 dev_mock 端点会被 nginx 拦截返 403)
|
||||||
|
message = `拒绝访问: ${reqMethod} ${reqUrl}`
|
||||||
break
|
break
|
||||||
case 404:
|
case 404:
|
||||||
message = '请求的资源不存在'
|
message = '请求的资源不存在'
|
||||||
|
|||||||
@@ -50,6 +50,32 @@
|
|||||||
<el-icon class="is-loading" size="32"><Loading /></el-icon>
|
<el-icon class="is-loading" size="32"><Loading /></el-icon>
|
||||||
<p>{{ loadingText }}</p>
|
<p>{{ loadingText }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- P0-NEW13: 自动化测试登录入口(仅 dev_mode=true 时显示) -->
|
||||||
|
<!-- 生产构建通过 import.meta.env.DEV 守卫剔除,无 dev 探测请求则不会触发 403 弹窗 -->
|
||||||
|
<div v-if="devModeEnabled" class="dev-login">
|
||||||
|
<el-divider>
|
||||||
|
<span class="dev-login-divider-text">测试账号登录</span>
|
||||||
|
</el-divider>
|
||||||
|
<div class="dev-login-row">
|
||||||
|
<el-select v-model="selectedDevUser" size="small" placeholder="选择测试角色" style="width: 180px">
|
||||||
|
<el-option
|
||||||
|
v-for="user in DEV_USERS"
|
||||||
|
:key="user.userid"
|
||||||
|
:label="user.label"
|
||||||
|
:value="user.userid"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
:loading="devLoginLoading"
|
||||||
|
@click="devLogin"
|
||||||
|
>
|
||||||
|
免企微登录
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -60,6 +86,7 @@ import { useRouter } from 'vue-router'
|
|||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { Loading, CircleCheckFilled } from '@element-plus/icons-vue'
|
import { Loading, CircleCheckFilled } from '@element-plus/icons-vue'
|
||||||
import { getQrcode, getScanStatus, handleOAuthCallback, type QrcodeResponse, type ScanStatusResponse } from '@/api/auth'
|
import { getQrcode, getScanStatus, handleOAuthCallback, type QrcodeResponse, type ScanStatusResponse } from '@/api/auth'
|
||||||
|
import apiClient from '@/api'
|
||||||
import { useAgentStore } from '@/stores/agent'
|
import { useAgentStore } from '@/stores/agent'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -96,6 +123,72 @@ let pollTimer: ReturnType<typeof setInterval> | null = null
|
|||||||
/** 当前扫码登录票据 */
|
/** 当前扫码登录票据 */
|
||||||
let currentTicket = ''
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
|||||||
// --------------------------------------------------------------------------
|
// --------------------------------------------------------------------------
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
// P0-NEW13: 仅在 dev 模式下探测 /dev/health(生产构建被 Vite 剔除,不会发请求)
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
checkDevMode()
|
||||||
|
}
|
||||||
// 检查 URL 中的 code 参数(OAuth2 回调)
|
// 检查 URL 中的 code 参数(OAuth2 回调)
|
||||||
const urlParams = new URLSearchParams(window.location.search)
|
const urlParams = new URLSearchParams(window.location.search)
|
||||||
const code = urlParams.get('code')
|
const code = urlParams.get('code')
|
||||||
|
|||||||
Reference in New Issue
Block a user