Files
xinlong-shop-taro/src/utils/request.ts
赵忠林 1fa58040f3 feat(user): 新增收货地址管理及售后申请页面
- 新增地址类型定义,增强前端地址数据结构
- 新增地址编辑页面,支持地址智能识别和定位选点功能
- 地址编辑支持省市区选择及默认地址设置
- 新增地址列表页面,支持地址展示、删除、编辑和选择功能
- 实现售后申请页面,支持选择售后类型和退款原因
- 售后申请支持商品选择、退款金额计算和凭证上传
- 新增售后详情页面,支持售后状态展示及申请取消
- 优化页面加载和用户交互体验,增加错误提示和权限处理
2026-07-01 12:11:56 +08:00

285 lines
8.4 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 Taro from '@tarojs/taro'
import { BaseUrl, TenantId } from '@/config/app'
import { clearStorageByLoginUser } from '@/utils/server'
export interface RequestConfig {
url: string
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
data?: any
header?: Record<string, string>
timeout?: number
retry?: number
showLoading?: boolean
showError?: boolean
returnRaw?: boolean
}
interface ApiResponse<T = any> {
code: number
message?: string
data?: T
}
export enum ErrorType {
NETWORK_ERROR = 'NETWORK_ERROR',
TIMEOUT_ERROR = 'TIMEOUT_ERROR',
BUSINESS_ERROR = 'BUSINESS_ERROR',
AUTH_ERROR = 'AUTH_ERROR',
UNKNOWN_ERROR = 'UNKNOWN_ERROR',
}
export class RequestError extends Error {
public type: ErrorType
public code?: number
public data?: any
constructor(message: string, type: ErrorType, code?: number, data?: any) {
super(message)
this.name = 'RequestError'
this.type = type
this.code = code
this.data = data
}
}
const DEFAULT_CONFIG: Required<Pick<RequestConfig, 'timeout' | 'retry' | 'showLoading' | 'showError' | 'returnRaw'>> = {
timeout: 10000,
retry: 2,
showLoading: false,
showError: true,
returnRaw: true,
}
const baseUrl = BaseUrl
const requestInterceptor = (config: RequestConfig): RequestConfig => {
const token = Taro.getStorageSync('access_token')
const tenantId = Taro.getStorageSync('TenantId') || TenantId
const defaultHeaders: Record<string, string> = {
'Content-Type': 'application/json',
TenantId: String(tenantId),
}
if (token) {
defaultHeaders.Authorization = token
}
config.header = { ...defaultHeaders, ...config.header }
if (config.showLoading) {
Taro.showLoading({ title: '加载中...' })
}
return config
}
// Redis 临时故障关键词 - 这类 401 是服务端问题,不应清除本地 token
const REDIS_ERROR_KEYWORDS = ['RedisSystemException', 'RedisException', 'Operation timed out', 'Redis']
const isRedisError = (message?: string): boolean => {
if (!message) return false
return REDIS_ERROR_KEYWORDS.some((keyword) => message.includes(keyword))
}
// 防抖:避免同一时间多个接口都触发跳登录页
let authErrorTimer: ReturnType<typeof setTimeout> | null = null
const handleAuthError = (message?: string) => {
// Redis 临时故障导致的 401不清除 token静默处理
if (isRedisError(message)) {
console.warn('[Auth] Redis 临时故障导致 401忽略处理:', message)
return
}
clearStorageByLoginUser()
// 防抖:多个接口同时 401 只弹一次 Toast
if (authErrorTimer) return
authErrorTimer = setTimeout(() => {
authErrorTimer = null
}, 3000)
Taro.showToast({
title: '登录已过期,请重新登录',
icon: 'none',
duration: 2000,
})
}
const responseInterceptor = <T>(response: any, config: RequestConfig): T => {
if (config.showLoading) {
Taro.hideLoading()
}
const { statusCode, data } = response
if (statusCode !== 200) {
throw new RequestError(`HTTP错误: ${statusCode}`, ErrorType.NETWORK_ERROR, statusCode, data)
}
if (data === null || data === undefined) {
throw new RequestError('响应数据为空', ErrorType.NETWORK_ERROR, statusCode, data)
}
if (typeof data === 'object' && data !== null && 'code' in data) {
const apiResponse = data as ApiResponse<T>
if (apiResponse.code === 0) {
return (config.returnRaw ? data : apiResponse.data) as T
}
if (apiResponse.code === 401 || apiResponse.code === 403) {
handleAuthError(apiResponse.message)
throw new RequestError(apiResponse.message || '认证失败', ErrorType.AUTH_ERROR, apiResponse.code, apiResponse.data)
}
throw new RequestError(apiResponse.message || '请求失败', ErrorType.BUSINESS_ERROR, apiResponse.code, apiResponse.data)
}
return data as T
}
const handleError = (error: RequestError, config: RequestConfig) => {
if (config.showLoading) {
Taro.hideLoading()
}
if (!config.showError) return
const titleMap: Record<ErrorType, string> = {
[ErrorType.NETWORK_ERROR]: '网络连接失败',
[ErrorType.TIMEOUT_ERROR]: '请求超时',
[ErrorType.BUSINESS_ERROR]: error.message || '操作失败',
[ErrorType.AUTH_ERROR]: '认证失败',
[ErrorType.UNKNOWN_ERROR]: '未知错误',
}
Taro.showToast({
title: titleMap[error.type] || '请求失败',
icon: 'none',
duration: 2000,
})
}
const executeRequest = <T>(config: RequestConfig): Promise<T> =>
new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(new RequestError('请求超时', ErrorType.TIMEOUT_ERROR))
}, config.timeout || DEFAULT_CONFIG.timeout)
Taro.request({
url: config.url,
method: config.method || 'GET',
data: config.data || {},
header: config.header || {},
success: (res) => {
clearTimeout(timer)
try {
resolve(responseInterceptor<T>(res, config))
} catch (error) {
reject(error)
}
},
fail: (err) => {
clearTimeout(timer)
reject(new RequestError(err.errMsg || '网络请求失败', ErrorType.NETWORK_ERROR, undefined, err))
},
})
})
const retryRequest = async <T>(config: RequestConfig, retryCount = 0): Promise<T> => {
try {
return await executeRequest<T>(config)
} catch (error) {
const requestError = error as RequestError
if (requestError.type === ErrorType.AUTH_ERROR || requestError.type === ErrorType.BUSINESS_ERROR) {
throw error
}
if (retryCount < (config.retry || DEFAULT_CONFIG.retry)) {
await new Promise((resolve) => setTimeout(resolve, 1000 * (retryCount + 1)))
return retryRequest<T>(config, retryCount + 1)
}
throw error
}
}
export async function request<T>(options: RequestConfig): Promise<T> {
try {
const config = requestInterceptor({ ...DEFAULT_CONFIG, ...options })
return await retryRequest<T>(config)
} catch (error) {
const requestError = error as RequestError
handleError(requestError, options)
throw requestError
}
}
const buildUrl = (url: string): string => {
if (url.startsWith('http://') || url.startsWith('https://')) {
return url
}
return `${baseUrl}${url}`
}
const buildQueryString = (params: Record<string, any>): string => {
const queryString = Object.keys(params)
.filter((key) => params[key] !== undefined && params[key] !== null)
.map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
.join('&')
return queryString ? `?${queryString}` : ''
}
export function get<T>(url: string, params?: any, config?: Partial<RequestConfig>): Promise<T> {
const fullUrl = buildUrl(url) + (params ? buildQueryString(params) : '')
return request<T>({ url: fullUrl, method: 'GET', returnRaw: true, ...config })
}
export function post<T>(url: string, data?: any, config?: Partial<RequestConfig>): Promise<T> {
return request<T>({ url: buildUrl(url), method: 'POST', data, returnRaw: true, ...config })
}
export function put<T>(url: string, data?: any, config?: Partial<RequestConfig>): Promise<T> {
return request<T>({ url: buildUrl(url), method: 'PUT', data, returnRaw: true, ...config })
}
export function patch<T>(url: string, data?: any, config?: Partial<RequestConfig>): Promise<T> {
return request<T>({ url: buildUrl(url), method: 'PATCH', data, returnRaw: true, ...config })
}
export function del<T>(url: string, data?: any, config?: Partial<RequestConfig>): Promise<T> {
return request<T>({ url: buildUrl(url), method: 'DELETE', data, returnRaw: true, ...config })
}
export function getData<T>(url: string, params?: any, config?: Partial<RequestConfig>): Promise<T> {
const fullUrl = buildUrl(url) + (params ? buildQueryString(params) : '')
return request<T>({ url: fullUrl, method: 'GET', returnRaw: false, ...config })
}
export function postData<T>(url: string, data?: any, config?: Partial<RequestConfig>): Promise<T> {
return request<T>({ url: buildUrl(url), method: 'POST', data, returnRaw: false, ...config })
}
export function putData<T>(url: string, data?: any, config?: Partial<RequestConfig>): Promise<T> {
return request<T>({ url: buildUrl(url), method: 'PUT', data, returnRaw: false, ...config })
}
export function delData<T>(url: string, data?: any, config?: Partial<RequestConfig>): Promise<T> {
return request<T>({ url: buildUrl(url), method: 'DELETE', data, returnRaw: false, ...config })
}
export default {
request,
get,
post,
put,
patch,
del,
getData,
postData,
putData,
delData,
ErrorType,
RequestError,
}