cashier/utils/request.ts

113 lines
3.5 KiB
TypeScript

import { APP_CONFIG } from '@/config';
import { ensureLogin } from '@/utils/auth';
import { HttpMethod, httpRequest, toFormUrlEncoded } from '@/utils/http';
import { authStorage } from '@/utils/storage';
export interface RequestOptions {
method?: HttpMethod;
data?: unknown;
header?: Record<string, string>;
skipAuth?: boolean;
skipAutoLogin?: boolean;
}
export class RequestError extends Error {
constructor(
message: string,
public readonly statusCode?: number,
public readonly data?: unknown
) {
super(message);
this.name = 'RequestError';
}
}
const objectToQueryString = (data: object = {}) => {
return Object.entries(data)
.filter(([, value]) => value !== undefined && value !== null)
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`)
.join('&');
};
const buildUrl = (endpoint: string) => {
if (/^https?:\/\//.test(endpoint)) return endpoint;
return `${APP_CONFIG.API_BASE_URL}/${endpoint.replace(/^\//, '')}`;
};
const isAuthExpired = (statusCode: number, body: any) => {
const businessCode = Number(body?.code);
return statusCode === 401 || statusCode === 402 || businessCode === 401 || businessCode === 402;
};
const requestOnce = async <T>(url: string, options: RequestOptions, retried: boolean): Promise<T> => {
const headers: Record<string, string> = {
'content-type': 'application/json',
...options.header
};
if (!options.skipAuth) {
const token = authStorage.getToken();
if (token) headers.Authorization = `Bearer ${token}`;
}
const response = await httpRequest<T>({
url,
method: options.method || 'GET',
data: options.data,
header: headers
});
if (isAuthExpired(response.statusCode, response.data)) {
if (!options.skipAutoLogin && !options.skipAuth && !retried) {
authStorage.clearToken();
await ensureLogin();
return requestOnce<T>(url, options, true);
}
throw new RequestError('登录状态已失效', response.statusCode, response.data);
}
if (response.statusCode < 200 || response.statusCode >= 300) {
throw new RequestError(`HTTP 请求失败:${response.statusCode}`, response.statusCode, response.data);
}
if (Number((response.data as any)?.code) === 500) {
throw new RequestError((response.data as any)?.msg || '服务器处理失败', response.statusCode, response.data);
}
return response.data;
};
export const request = <T = any>(endpoint: string, options: RequestOptions = {}) => {
return requestOnce<T>(buildUrl(endpoint), options, false);
};
export const get = <T = any>(endpoint: string, params?: object, options: RequestOptions = {}) => {
const queryString = params ? `?${objectToQueryString(params)}` : '';
return request<T>(`${endpoint}${queryString}`, { ...options, method: 'GET' });
};
export const post = <T = any>(endpoint: string, data?: unknown, options: RequestOptions = {}) => {
return request<T>(endpoint, { ...options, method: 'POST', data });
};
export const postForm = <T = any>(endpoint: string, data?: Record<string, unknown>, options: RequestOptions = {}) => {
return request<T>(endpoint, {
...options,
method: 'POST',
data: toFormUrlEncoded(data),
header: {
...options.header,
'content-type': 'application/x-www-form-urlencoded'
}
});
};
export const put = <T = any>(endpoint: string, data?: unknown, options: RequestOptions = {}) => {
return request<T>(endpoint, { ...options, method: 'PUT', data });
};
export const del = <T = any>(endpoint: string, data?: unknown, options: RequestOptions = {}) => {
return request<T>(endpoint, { ...options, method: 'DELETE', data });
};