后端: - config.ts 使用 requireAdmin 中间件,统一响应格式 - feedback.ts 使用 requireAdmin 中间件,移除重复权限检查 - feedback.ts 添加 affectedRows 检查(反馈不存在时返回 404) - 全局错误处理中间件兜底未捕获异常 前端: - request.ts 添加 15 秒请求超时设置
111 lines
3.8 KiB
TypeScript
111 lines
3.8 KiB
TypeScript
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)
|
||
}
|
||
})
|
||
})
|
||
}
|