import { getApiConfig } from '../config/api'; export class ApiRequestError extends Error { constructor( message: string, readonly statusCode = 0, readonly reason = '', ) { super(message); this.name = 'ApiRequestError'; } } export interface RequestOptions { method?: 'GET' | 'POST' | 'PUT' | 'DELETE'; data?: Record; header?: Record; timeout?: number; upload?: { filePath: string; uploadId: string }; } /** 底层只发请求,不触发登录;登录接口与鉴权包装共用,避免循环依赖。 */ export function requestApi( path: string, options: RequestOptions = {}, config = getApiConfig(), ): Promise { if (!path.startsWith('/') || path.startsWith('//')) throw new Error('接口路径不正确'); return new Promise((resolve, reject) => { const success = (res: { statusCode: number; data: unknown }) => { const body = res.data as { code?: number; data?: T; reason?: string; } | null; if (res.statusCode < 200 || res.statusCode >= 300) { reject( new ApiRequestError( res.statusCode === 404 ? '内容暂不可用' : '接口请求失败,请稍后重试', res.statusCode, typeof body?.reason === 'string' && /^[A-Z_]{1,64}$/.test(body.reason) ? body.reason : '', ), ); } else if ( !body || body.code !== 0 || !Object.prototype.hasOwnProperty.call(body, 'data') ) { reject( new ApiRequestError( '接口返回异常,请稍后重试', res.statusCode, ), ); } else resolve(body.data as T); }; const fail = () => reject(new ApiRequestError('连接失败,请确认后端已启动及网络配置')); if (options.upload) { // wx.uploadFile 由运行时生成 multipart boundary,不能自行设置 JSON Content-Type。 wx.uploadFile({ url: `${config.baseUrl}${path}`, filePath: options.upload.filePath, name: 'file', formData: { uploadId: options.upload.uploadId }, header: options.header, timeout: 60000, success(res) { let data: unknown; try { data = JSON.parse(res.data); } catch { reject(new ApiRequestError('上传响应格式异常')); return; } success({ statusCode: res.statusCode, data }); }, fail, }); } else wx.request({ url: `${config.baseUrl}${path}`, method: options.method || 'GET', data: options.data, header: { 'Content-Type': 'application/json', ...options.header, }, timeout: options.timeout ?? config.timeout, success, fail, }); }); }