Files
websopy-taro/src/utils/request.ts
赵忠林 9df90c1176 feat(address): 新增收货地址与售后申请功能
- 新增地址编辑页面,支持地址智能识别、地图选点、地区选择和默认地址设置
- 实现地址列表页面,支持地址查看、删除、设为默认及选择返回结算页
- 新增售后申请页面,支持退款类型选择、商品选择、原因填写、图片上传和提交审核
- 修复 passport 分包配置,移除不存在分包并补充缺失声明,避免 Taro 编译报错
- 新增地址类型定义,增强前端地址数据结构类型安全
- 优化页面交互体验,完善表单校验及错误提示逻辑
- 统一代码格式与命名规范,保持代码风格一致性
2026-07-16 01:18:42 +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,
}