Files
danyang-match/miniapp/utils/api.ts
T

148 lines
4.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.
/**
* API 统一请求层(TypeScript)
*
* 职责:纯 HTTP 客户端,统一处理 Token / 超时 / 错误码 / Session 失效。
*/
import { API_BASE_URL } from '../env';
import type { ApiResponse } from '../types';
export const REQUEST_ERROR = {
NETWORK_ERROR: 'NETWORK_ERROR',
UNAUTHORIZED: 'UNAUTHORIZED',
FORBIDDEN: 'FORBIDDEN',
RATE_LIMITED: 'RATE_LIMITED',
BUSINESS_ERROR: 'BUSINESS_ERROR',
SERVER_ERROR: 'SERVER_ERROR',
MAINTENANCE: 'MAINTENANCE',
} as const;
export interface ApiError {
code: string;
message: string;
requestId: string;
error?: unknown;
}
export class ApiClient {
baseURL: string;
token: string | null = null;
refreshToken: string | null = null;
onSessionExpired: ((reason: any) => void) | null = null;
onMaintenance: ((data: any) => void) | null = null;
constructor() {
this.baseURL = API_BASE_URL;
}
getToken(): string | null {
if (this.token) return this.token;
try {
this.token = wx.getStorageSync('accessToken') as string || null;
this.refreshToken = wx.getStorageSync('refreshToken') as string || null;
} catch { /* ignore */ }
return this.token;
}
getRefreshToken(): string | null {
if (this.refreshToken) return this.refreshToken;
try {
this.refreshToken = wx.getStorageSync('refreshToken') as string || null;
} catch { /* ignore */ }
return this.refreshToken;
}
saveToken(token: string, refreshToken?: string): void {
this.token = token;
this.refreshToken = refreshToken || null;
wx.setStorageSync('accessToken', token);
if (refreshToken) wx.setStorageSync('refreshToken', refreshToken);
}
clearToken(): void {
this.token = null;
this.refreshToken = null;
wx.removeStorageSync('accessToken');
wx.removeStorageSync('refreshToken');
}
generateRequestId(): string {
return Date.now().toString(36) + Math.random().toString(36).substring(2, 8);
}
request<T>(options: {
url: string;
method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
data?: unknown;
headers?: Record<string, string>;
idempotencyKey?: string;
timeout?: number;
}): Promise<T> {
const { url, method = 'GET', data, headers = {}, idempotencyKey, timeout = 10000 } = options;
const token = this.getToken();
const requestId = this.generateRequestId();
return new Promise((resolve, reject) => {
wx.request({
url: this.baseURL + url,
method,
data,
timeout,
header: {
'Content-Type': 'application/json',
'X-Request-Id': requestId,
...(idempotencyKey ? { 'Idempotency-Key': idempotencyKey } : {}),
...(token ? { Authorization: `Bearer ${token}` } : {}),
...headers,
},
success: (res: any) => {
if (res.statusCode === 503) {
if (this.onMaintenance) this.onMaintenance(res.data || {});
reject({ code: REQUEST_ERROR.MAINTENANCE, message: '平台正在维护,请稍后再试', requestId });
return;
}
if (res.statusCode === 200) {
const body = (res.data as any) || {};
if (body.success) {
resolve(body.data as T);
} else {
reject({ code: body.code || REQUEST_ERROR.BUSINESS_ERROR, message: body.message, requestId });
}
} else if (res.statusCode === 401) {
this.clearToken();
if (this.onSessionExpired) this.onSessionExpired({});
reject({ code: REQUEST_ERROR.UNAUTHORIZED, message: '请重新登录', requestId });
} else if (res.statusCode === 403) {
reject({ code: REQUEST_ERROR.FORBIDDEN, message: '无权限访问', requestId });
} else if (res.statusCode === 429) {
reject({ code: REQUEST_ERROR.RATE_LIMITED, message: '请求过于频繁,请稍后重试', requestId });
} else {
reject({ code: REQUEST_ERROR.SERVER_ERROR, message: '服务暂时不可用', requestId });
}
},
fail: (err) => {
reject({ code: REQUEST_ERROR.NETWORK_ERROR, message: '网络连接失败', requestId, error: err });
},
});
});
}
get<T>(url: string, data?: unknown, headers?: Record<string, string>): Promise<T> {
return this.request<T>({ url, method: 'GET', data, headers });
}
post<T>(url: string, data?: unknown, headers?: Record<string, string>, idempotencyKey?: string): Promise<T> {
return this.request<T>({ url, method: 'POST', data, headers, idempotencyKey });
}
put<T>(url: string, data?: unknown, headers?: Record<string, string>): Promise<T> {
return this.request<T>({ url, method: 'PUT', data, headers });
}
delete<T>(url: string, data?: unknown, headers?: Record<string, string>): Promise<T> {
return this.request<T>({ url, method: 'DELETE', data, headers });
}
}
export const api = new ApiClient();
export default api;