Files
xiaocai/client/src/utils/request.ts
wangxiaogang 860c599b7c fix: 系统检查修复 — 安全和稳定性
后端:
- config.ts 使用 requireAdmin 中间件,统一响应格式
- feedback.ts 使用 requireAdmin 中间件,移除重复权限检查
- feedback.ts 添加 affectedRows 检查(反馈不存在时返回 404)
- 全局错误处理中间件兜底未捕获异常

前端:
- request.ts 添加 15 秒请求超时设置
2026-06-08 14:48:39 +08:00

111 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { API_BASE } from '@/config'
import type { LoginResult } from '@/api/auth'
import { trackApiError } from './tracker'
interface RequestOptions {
url: string
method?: 'GET' | 'POST' | 'PUT' | 'DELETE'
data?: any
}
let isRedirectingToLogin = false
/** 重登录期间挂起的请求,登录成功后统一重试 */
let pendingRetries: Array<{ resolve: (v: any) => void; reject: (e: any) => void; options: RequestOptions }> = []
/** 保存登录结果到本地存储 */
export function saveLoginResult(data: LoginResult) {
uni.setStorageSync('xc:token', data.token)
uni.setStorageSync('xc:userId', data.userId)
uni.setStorageSync('xc:nickname', data.nickname)
uni.setStorageSync('xc:avatar_url', data.avatar_url)
if (data.role) uni.setStorageSync('xc:role', data.role)
}
/** 执行重登录 */
async function reLogin(): Promise<void> {
// H5 环境demo 登录
if (typeof window !== 'undefined') {
const data = await request<LoginResult>({ url: '/auth/demo-login', method: 'POST' })
saveLoginResult(data)
return
}
// 小程序环境wx.login
const loginRes = await new Promise<UniApp.LoginRes>((resolve, reject) => {
uni.login({ provider: 'weixin', success: resolve, fail: reject })
})
if (!loginRes.code) throw new Error('wx.login failed')
const data = await request<LoginResult>({ url: '/auth/login', method: 'POST', data: { code: loginRes.code } })
saveLoginResult(data)
}
export function request<T = any>(options: RequestOptions): Promise<T> {
return new Promise((resolve, reject) => {
const token = uni.getStorageSync('xc:token')
uni.request({
url: API_BASE + options.url,
method: options.method || 'GET',
data: options.data,
timeout: 15000, // 15秒超时
header: {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {})
},
success: (res: any) => {
const { statusCode, data } = res
if (statusCode === 401) {
uni.removeStorageSync('xc:token')
uni.removeStorageSync('xc:userId')
uni.removeStorageSync('xc:nickname')
uni.removeStorageSync('xc:avatar_url')
uni.removeStorageSync('xc:role')
uni.removeStorageSync('xc:currentGroupId')
// 加入重试队列
pendingRetries.push({ resolve, reject, options })
if (!isRedirectingToLogin) {
isRedirectingToLogin = true
reLogin()
.then(() => {
// 处理快照时已入队的 + reLogin 期间新入队的请求
const retries = [...pendingRetries]
pendingRetries = []
retries.forEach(r => request(r.options).then(r.resolve).catch(r.reject))
})
.catch(() => {
uni.showToast({ title: '登录失败,请刷新页面', icon: 'none' })
const retries = [...pendingRetries]
pendingRetries = []
retries.forEach(r => r.reject({ code: 40100, message: '未登录' }))
})
.finally(() => { isRedirectingToLogin = false })
}
return
}
if (data.code === 0) {
resolve(data.data)
} else {
// 埋点API 业务错误
trackApiError(options.url, statusCode, data.message || '请求失败')
// "用户不存在" 不弹 toast首次登录竞态
if (data.code !== 40400 || data.message !== '用户不存在') {
uni.showToast({ title: data.message || '请求失败', icon: 'none' })
}
reject(data)
}
},
fail: (err: any) => {
// 埋点:网络错误
trackApiError(options.url, 0, '网络错误')
uni.showToast({ title: '网络错误', icon: 'none' })
reject(err)
}
})
})
}