Files
websopy-taro/src/utils/request.ts
赵忠林 85a1558e84 feat(passport): 迁移并修复小程序支付页面访问 websopy API 问题
- 迁移 src/passport/pay 相关代码至当前项目,无需替换文件
- 新增环境变量 WEBSOPY_API_BASE_URL,提供 websopy API 基础地址
- 在 config/app.ts 导出 WebsopyBaseUrl,供代码引用
- 配置 env.d.ts 类型定义,消除 TypeScript 隐式 any 错误
- request 请求方法支持自定义 baseUrl 以调用不同接口地址
- passport/pay 页面调用订阅接口统一传入 WebsopyBaseUrl
- 修正 tenantId 类型为 number,调整登录用户类型断言
- 确认编译成功,dist 目录生成对应文件及正确请求地址
- 说明 type-check 依赖库问题不影响正常构建和运行
2026-07-16 01:34:27 +08:00

286 lines
8.7 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
baseUrl?: string
}
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, url: buildUrl(options.url, options.baseUrl) })
return await retryRequest<T>(config)
} catch (error) {
const requestError = error as RequestError
handleError(requestError, options)
throw requestError
}
}
const buildUrl = (url: string, customBaseUrl?: string): string => {
if (url.startsWith('http://') || url.startsWith('https://')) {
return url
}
return `${customBaseUrl || 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, config?.baseUrl) + (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, config?.baseUrl), 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, config?.baseUrl), 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, config?.baseUrl), 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, config?.baseUrl), method: 'DELETE', data, returnRaw: true, ...config })
}
export function getData<T>(url: string, params?: any, config?: Partial<RequestConfig>): Promise<T> {
const fullUrl = buildUrl(url, config?.baseUrl) + (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, config?.baseUrl), 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, config?.baseUrl), 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, config?.baseUrl), method: 'DELETE', data, returnRaw: false, ...config })
}
export default {
request,
get,
post,
put,
patch,
del,
getData,
postData,
putData,
delData,
ErrorType,
RequestError,
}