class WxRequest { constructor(options = {}) { this.baseUrl = options.baseUrl || ''; this.header = options.header || {}; this.interceptors = { request: (config) => config, response: (response) => response }; } /** * Set request interceptor * @param {Function} callback */ setRequestInterceptor(callback) { this.interceptors.request = callback; } /** * Set response interceptor * @param {Function} callback */ setResponseInterceptor(callback) { this.interceptors.response = callback; } /** * Core request method * @param {Object} options */ request(options = {}) { // Merge headers const header = { ...this.header, ...options.header }; // Prepare config let config = { url: (options.url.startsWith('http') ? options.url : this.baseUrl + options.url), method: options.method || 'GET', data: options.data || {}, header: header, timeout: options.timeout || 60000, skipToken: options.skipToken || false }; return new Promise(async (resolve, reject) => { // Apply request interceptor (Async) try { config = await Promise.resolve(this.interceptors.request(config)); } catch (ignore) { // If interceptor fails/rejects, likely means we shouldn't proceed // But for now let's just log or reject console.warn('Interceptor warning', ignore); } wx.request({ ...config, success: (res) => { // Apply response interceptor (pass the whole wx response) const processedResponse = this.interceptors.response(res); const { statusCode, data } = processedResponse; // Auto-refresh token if 401 (One-time retry) if ((statusCode === 401 || data.code === 401) && !options.skipToken && !options._retry) { const app = getApp(); if (app && app.forceRefreshLogin) { console.log('401 detected, refreshing token...'); app.forceRefreshLogin().then(() => { // Retry Original Request this.request({ ...options, _retry: true }) .then(resolve) .catch(reject); }).catch(err => { console.error('Token refresh failed', err); reject(data); }); return; } } // 1. Check HTTP Status Code if (statusCode >= 200 && statusCode < 300) { // 2. Check Business Logic Code (Assuming 200 is success based on common Go patterns, // matching the user's struct: Code, Data, Msg) // If data.code exists, we check it. If not (maybe not JSON), we pass through or handle. const businessCode = data.code; // Strict check: if code is 200, success. if (businessCode === 200) { resolve(data.data); // Return the inner Data object } else { // Business logic error this.handleError({ errMsg: data.msg || 'Error', code: businessCode }); reject(data); } } else { // Handle non-200 HTTP errors this.handleError({ ...res, errMsg: `HTTP Error: ${statusCode}` }); reject(res); } }, fail: (err) => { this.handleError({ ...err, errMsg: 'Network Error' }); reject(err); } }); }); } /** * Handle global errors (Toast) * @param {Object} error */ handleError(error) { const message = error.errMsg || error.msg || '请求失败,请稍后重试'; wx.showToast({ title: message, icon: 'none', duration: 3000 }); console.error('API Error:', error); } /** * Upload file * @param {string} filePath Local file path * @param {string} name Form field name (default: file) * @param {Object} formData Additional form data */ upload(filePath, name = 'file', formData = {}) { // Prepare config let config = { url: this.baseUrl + '/oss/upload', header: { ...this.header }, // Copy default headers filePath: filePath, name: name, formData: formData }; // Apply request interceptor (Async) return new Promise(async (resolve, reject) => { try { config = await Promise.resolve(this.interceptors.request(config)); } catch (ignore) { console.warn('Interceptor warning', ignore); } wx.uploadFile({ url: config.url, filePath: config.filePath, name: config.name, formData: config.formData, header: config.header, success: (res) => { // wx.uploadFile returns data as String, need to parse let data; try { data = JSON.parse(res.data); } catch (e) { data = { code: 500, msg: 'Response parse error', data: res.data }; } // Reconstruct a response object for interceptor const responseObj = { ...res, data: data }; // Apply response interceptor const processedResponse = this.interceptors.response(responseObj); const { statusCode, data: finalData } = processedResponse; // Status Code check (uploadFile also returns statusCode) if (statusCode >= 200 && statusCode < 300) { const businessCode = finalData.code; if (businessCode === 200) { resolve(finalData.data); } else { this.handleError({ errMsg: finalData.msg || 'Upload Error', code: businessCode }); reject(finalData); } } else { this.handleError({ ...res, errMsg: `HTTP Error: ${statusCode}` }); reject(res); } }, fail: (err) => { this.handleError({ ...err, errMsg: 'Upload Network Error' }); reject(err); } }); }); } /** * Upload file to a specific URL path * @param {string} urlPath API path (e.g. '/classify/plant') * @param {string} filePath Local file path * @param {string} name Form field name (default: file) * @param {Object} formData Additional form data */ uploadToUrl(urlPath, filePath, name = 'file', formData = {}) { let config = { url: this.baseUrl + urlPath, header: { ...this.header }, filePath: filePath, name: name, formData: formData }; return new Promise(async (resolve, reject) => { try { config = await Promise.resolve(this.interceptors.request(config)); } catch (ignore) { console.warn('Interceptor warning', ignore); } wx.uploadFile({ url: config.url, filePath: config.filePath, name: config.name, formData: config.formData, header: config.header, success: (res) => { let data; try { data = JSON.parse(res.data); } catch (e) { data = { code: 500, msg: 'Response parse error', data: res.data }; } const responseObj = { ...res, data: data }; const processedResponse = this.interceptors.response(responseObj); const { statusCode, data: finalData } = processedResponse; if (statusCode >= 200 && statusCode < 300) { const businessCode = finalData.code; if (businessCode === 200) { resolve(finalData.data); } else { this.handleError({ errMsg: finalData.msg || 'Upload Error', code: businessCode }); reject(finalData); } } else { this.handleError({ ...res, errMsg: `HTTP Error: ${statusCode}` }); reject(res); } }, fail: (err) => { this.handleError({ ...err, errMsg: 'Upload Network Error' }); reject(err); } }); }); } get(url, data = {}, header = {}) { return this.request({ url, method: 'GET', data, header }); } post(url, data = {}, header = {}) { return this.request({ url, method: 'POST', data, header }); } } // Initialize with default instance const request = new WxRequest({ baseUrl: 'http://192.168.0.184:8889', //baseUrl: 'https://go.sundynix.cn/api', header: { 'Content-Type': 'application/json' } }); // Example: Setup default interceptors request.setRequestInterceptor(async (config) => { // Skip checking token for login API itself if (config.url.includes('/auth/miniLogin') || config.skipToken) { return config; } let token = wx.getStorageSync('token'); // If no token, attempt to wait for login if (!token) { const app = getApp(); if (app && app.ensureLogin) { try { token = await app.ensureLogin(); } catch (e) { // Login failed console.warn('Auto-login failed in interceptor', e); } } } // Inject token if available if (token) { config.header['Authorization'] = `Bearer ${token}`; } return config; }); request.setResponseInterceptor((response) => { // Handle global token expiration (e.g., HTTP 401 or Business Code 401) if (response.statusCode === 401 || response.data?.code === 401) { wx.removeStorageSync('token'); // Redirect to login if needed, or just warn // wx.reLaunch({ url: '/pages/login/index' }); } return response; }); export default request; export { WxRequest };