fix: 取票失败静默处理,不再弹出连接相关报错

This commit is contained in:
文睿 张 2026-10-09 13:56:48 +08:00
parent d580efebe5
commit f6a4fc6cb6
3 changed files with 101 additions and 4 deletions

View File

@ -19,7 +19,8 @@ export const authApi = {
}) })
}, },
// 换取 WebSocket 短期一次性票据:属于组织级请求,拦截器会自动带上当前公司头。 // 换取 WebSocket 短期一次性票据:属于组织级请求,拦截器会自动带上当前公司头。
// 标记 silent:取票失败属于后台辅助失败,不应弹与实时连接相关的全局错误提示。
createWebSocketTicket() { createWebSocketTicket() {
return request.post('/auth/websocket-ticket') return request.post('/auth/websocket-ticket', undefined, { silent: true })
} }
} }

View File

@ -37,6 +37,13 @@ function isOrganizationExempt(url) {
return ORGANIZATION_EXEMPT_PATHS.some(exempt => path === exempt || path.startsWith(`${exempt}/`)) return ORGANIZATION_EXEMPT_PATHS.some(exempt => path === exempt || path.startsWith(`${exempt}/`))
} }
// 单次请求可通过 config.silent 关闭全局错误提示。取票等后台辅助请求失败时,
// 不应让用户看到与自己操作无关的报错;默认不静默(未显式置 true 即照旧提示)。
// 注意:silent 只抑制“业务/连接类”提示,不影响 401 的登录态全局处理。
function isSilent(config) {
return config?.silent === true
}
// Request interceptor - add auth token // Request interceptor - add auth token
request.interceptors.request.use( request.interceptors.request.use(
(config) => { (config) => {
@ -75,7 +82,9 @@ request.interceptors.response.use(
if (res.code === 0) { if (res.code === 0) {
return res return res
} }
ElMessage.error(hasChinese(res.message) ? res.message : '请求失败') if (!isSilent(response.config)) {
ElMessage.error(hasChinese(res.message) ? res.message : '请求失败')
}
return Promise.reject(new Error(res.message)) return Promise.reject(new Error(res.message))
}, },
(error) => { (error) => {
@ -88,7 +97,10 @@ request.interceptors.response.use(
if (context && !isCurrentOrganizationContext(context)) { if (context && !isCurrentOrganizationContext(context)) {
return Promise.reject(new OrganizationContextSupersededError()) return Promise.reject(new OrganizationContextSupersededError())
} }
const silent = isSilent(error.config)
if (error.response?.status === 401) { if (error.response?.status === 401) {
// 登录态失效的全局处理保持原样:清令牌并跳登录页。这与请求是否为 silent 无关,
// silent 只抑制“业务/连接类”错误提示。
localStorage.removeItem('wmms_token') localStorage.removeItem('wmms_token')
localStorage.removeItem('wmms_user') localStorage.removeItem('wmms_user')
// 登录页自己会把失败原因写在表单里,再弹一句"登录已过期"只会让人困惑 // 登录页自己会把失败原因写在表单里,再弹一句"登录已过期"只会让人困惑
@ -112,9 +124,13 @@ request.interceptors.response.use(
msg = fields.map(f => labels[f] || f).join('、') + '不能为空' msg = fields.map(f => labels[f] || f).join('、') + '不能为空'
} }
} }
ElMessage.error(hasChinese(msg) ? msg : '请检查必填字段') if (!silent) {
ElMessage.error(hasChinese(msg) ? msg : '请检查必填字段')
}
} else { } else {
ElMessage.error(responseErrorText(error)) if (!silent) {
ElMessage.error(responseErrorText(error))
}
} }
return Promise.reject(error) return Promise.reject(error)
} }

View File

@ -0,0 +1,80 @@
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
import { ElMessage } from 'element-plus'
import request from './index'
import { commitOrganizationId, resetOrganizationContext } from '../utils/organizationContext'
// 断言 WebSocket 取票这条后台辅助请求失败时静默:不弹全局错误提示,
// 同时不破坏拦截器的组织上下文逻辑(仍注入 X-Organization-ID)。
describe('请求拦截器的 silent 静默开关', () => {
const originalAdapter = request.defaults.adapter
beforeEach(() => {
resetOrganizationContext()
commitOrganizationId(2)
})
afterEach(() => {
request.defaults.adapter = originalAdapter
vi.restoreAllMocks()
})
// 适配器直接 reject,模拟 HTTP 层失败,走响应拦截器的错误分支。
function rejectingAdapter(status, data) {
return (config) => {
const error = new Error(`Request failed with status code ${status}`)
error.config = config
error.response = { status, statusText: '', headers: {}, data }
error.isAxiosError = true
return Promise.reject(error)
}
}
// 适配器解析出响应体,模拟业务层失败(res.code !== 0)。
function resolvingAdapter(data, status = 200) {
return async (config) => ({ data, status, statusText: 'OK', headers: {}, config })
}
test('取票 403 失败静默:不弹全局错误提示', async () => {
const errorSpy = vi.spyOn(ElMessage, 'error').mockImplementation(() => {})
request.defaults.adapter = rejectingAdapter(403, { message: '当前公司不可访问' })
await expect(
request.post('/auth/websocket-ticket', undefined, { silent: true })
).rejects.toBeDefined()
expect(errorSpy).not.toHaveBeenCalled()
})
test('取票业务失败(res.code !== 0)同样静默', async () => {
const errorSpy = vi.spyOn(ElMessage, 'error').mockImplementation(() => {})
request.defaults.adapter = resolvingAdapter({ code: 403, message: '当前公司不可访问' })
await expect(
request.post('/auth/websocket-ticket', undefined, { silent: true })
).rejects.toThrow()
expect(errorSpy).not.toHaveBeenCalled()
})
test('未标记 silent 的同类失败仍会弹全局错误提示(未被全局静默)', async () => {
const errorSpy = vi.spyOn(ElMessage, 'error').mockImplementation(() => {})
request.defaults.adapter = rejectingAdapter(500, { message: '服务器错误' })
await expect(request.post('/materials')).rejects.toBeDefined()
expect(errorSpy).toHaveBeenCalled()
})
test('取票请求仍按组织级请求注入 X-Organization-ID(白名单语义未变)', async () => {
let captured
request.defaults.adapter = async (config) => {
captured = config
return { data: { code: 0, data: { ticket: 't' } }, status: 200, statusText: 'OK', headers: {}, config }
}
await request.post('/auth/websocket-ticket', undefined, { silent: true })
expect(captured.headers['X-Organization-ID']).toBe('2')
expect(captured.silent).toBe(true)
})
})