import Config from './config.js' import Util from './util.js' import store from '@/store/index.js' import Platform from './platform.js' const { appType, appTypeName } = Platform.getPlatformInfo(); let _Lang = null; function getLang() { if (!_Lang) { _Lang = require('./lang.js').default; } return _Lang; } const DEFAULT_TIMEOUT = 15000; const DEFAULT_MAX_RETRIES = 2; const RETRY_DELAY_BASE = 2000; function _shouldRetry(errMsg) { if (!errMsg) return false; if (errMsg === 'request:fail url not in domain list') return false; if (errMsg === 'request:fail abort') return false; const retryableErrors = [ 'request:fail timeout', 'request:fail connect', 'request:fail network' ]; return retryableErrors.some(err => errMsg.startsWith(err)); } function _classifyError(errMsg, statusCode) { if (!errMsg && !statusCode) return 'unknown'; if (errMsg === 'request:fail abort') return 'aborted'; if (errMsg && errMsg.indexOf('timeout') !== -1) return 'timeout'; if (errMsg && (errMsg.indexOf('connect') !== -1 || errMsg.indexOf('network') !== -1)) return 'networkError'; if (statusCode && statusCode >= 500) return 'serverError'; if (errMsg && errMsg.indexOf('url not in domain list') !== -1) return 'domainNotInList'; return 'unknown'; } function _sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } function _makeRequest(url, method, data, header, dataType, responseType, timeout) { const reqId = 'req_' + Date.now() + '_' + Math.floor(Math.random() * 10000); const reqStart = Date.now(); console.log('[HTTP][REQUEST]', reqId, method, url, JSON.stringify({ data: data, header: header, dataType: dataType, responseType: responseType, timeout: timeout })); return new Promise((resolve, reject) => { uni.request({ url: url, method: method, data: data, header: header, dataType: dataType, responseType: responseType, timeout: timeout, success: (res) => { console.log('[HTTP][RESPONSE]', reqId, method, url, 'statusCode=', res.statusCode, 'cost=', (Date.now() - reqStart) + 'ms', JSON.stringify(res.data)); if (res.statusCode && [200, 500].indexOf(res.statusCode) == -1) { reject({ errMsg: `request:fail statusCode=${res.statusCode}`, statusCode: res.statusCode }); return; } if (res.data.code == -3 && store.state.siteState > 0) { store.commit('setSiteState', -3); Util.redirectTo('/pages_tool/storeclose/storeclose', {}, 'reLaunch'); return; } if (res.data.refreshtoken) { store.commit('setToken', res.data.refreshtoken); } if (res.data.code == -10009 || res.data.code == -10010) { store.commit('setToken', ''); store.commit('setMemberInfo', ''); } resolve(res.data); }, fail: (res) => { console.error('[HTTP][FAIL]', reqId, method, url, 'cost=', (Date.now() - reqStart) + 'ms', JSON.stringify(res)); reject(res); } }); }); } async function _requestWithRetry(params, url, method, data, header, dataType, responseType) { const timeout = params.timeout || DEFAULT_TIMEOUT; const maxRetries = params.noRetry ? 0 : (params.maxRetries || DEFAULT_MAX_RETRIES); let lastError = null; let lastStatusCode = null; for (let attempt = 0; attempt <= maxRetries; attempt++) { try { if (attempt > 0) { console.log('[HTTP][RETRY]', method, url, 'attempt=', attempt, 'maxRetries=', maxRetries); } const result = await _makeRequest(url, method, data, header, dataType, responseType, timeout); return result; } catch (err) { lastError = err; lastStatusCode = err && err.statusCode; console.warn('[HTTP][RETRY-FAIL]', method, url, 'attempt=', attempt, 'errMsg=', err && err.errMsg, 'statusCode=', lastStatusCode); if (attempt >= maxRetries || !_shouldRetry(err.errMsg)) { break; } const delay = RETRY_DELAY_BASE * Math.pow(2, attempt); console.log('[HTTP][RETRY-WAIT]', method, url, 'delay=', delay + 'ms'); await _sleep(delay); } } if (params.silentError) { throw lastError; } const errorType = _classifyError(lastError && lastError.errMsg, lastStatusCode); const lang = getLang(); if (errorType === 'domainNotInList') { uni.showToast({ title: lang.lang('http.domainNotInList', { url: Config.baseUrl }), 'icon': 'none', duration: 10000 }); } else { const i18nKey = 'http.' + errorType; uni.showToast({ title: lang.lang(i18nKey), 'icon': 'none', duration: 10000 }); } throw lastError; } export default { sendRequest(params) { if (!Config.baseUrl) { uni.showToast({ title: getLang().lang('http.baseUrlNotConfigured'), 'icon': 'none', duration: 10000 }); return; } const _isRemoteOnly = getLang().isRemoteOnly && getLang().isRemoteOnly(); const _bootDone = getLang().isBootCompleted && getLang().isBootCompleted(); if (_isRemoteOnly && !_bootDone) { let bootPromise = null; try { const app = getApp(); if (app && app.$langReady) bootPromise = app.$langReady; } catch (e) {} if (!bootPromise) bootPromise = getLang().boot(); if (params.async === false) { return bootPromise.then(() => this._sendRequestNow(params)); } else { bootPromise.then(() => this._sendRequestNow(params)); return; } } return this._sendRequestNow(params); }, _sendRequestNow(params) { // ✅ 关键修复 1:支持调用方指定 params.method(GET/POST/PUT/DELETE); // 没指定时按老规则:有 data → POST,没 data → GET var method; if (params.method && typeof params.method === 'string') { method = String(params.method).toUpperCase(); if (['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'].indexOf(method) === -1) { method = 'GET'; } } else { method = params.data != undefined ? 'POST' : 'GET'; } var url = (Config.baseUrl + params.url).replace(/(? { typeof params.complete == 'function' && params.complete(res); return res; }) .catch(err => { typeof params.complete == 'function' && params.complete(); throw err; }); } else { _requestWithRetry(params, url, method, data, header, dataType, responseType) .then(res => { typeof params.success == 'function' && params.success(res); typeof params.complete == 'function' && params.complete(res); }) .catch(err => { typeof params.fail == 'function' && params.fail(err); typeof params.complete == 'function' && params.complete(); }); } }, uploadBase64(params) { console.log('[HTTP][REQUEST] uploadBase64', Config.baseUrl + '/api/upload/headimgBase64'); uni.request({ url: Config.baseUrl + '/api/upload/headimgBase64', method: 'POST', // ✅ 修复:application/json,后端能直接 json_decode header: { 'content-type': 'application/json' }, data: { app_type: appType, app_type_name: appTypeName, images: params.base64 }, dataType: 'json', responseType: 'text', success: res => { console.log('[HTTP][RESPONSE] uploadBase64', 'statusCode=', res.statusCode, JSON.stringify(res.data)); typeof params.success == 'function' && params.success(res.data); }, fail: (res) => { console.error('[HTTP][FAIL] uploadBase64', JSON.stringify(res)); typeof params.fail == 'function' && params.fail(res); } }); }, pullImage(params) { console.log('[HTTP][REQUEST] pullImage', Config.baseUrl + '/api/upload/headimgPull', 'path=', params.path); uni.request({ url: Config.baseUrl + '/api/upload/headimgPull', method: 'POST', // ✅ 修复:application/json header: { 'content-type': 'application/json' }, data: { app_type: appType, app_type_name: appTypeName, path: params.path }, dataType: 'json', responseType: 'text', success: res => { console.log('[HTTP][RESPONSE] pullImage', 'statusCode=', res.statusCode, JSON.stringify(res.data)); typeof params.success == 'function' && params.success(res.data); }, fail: (res) => { console.error('[HTTP][FAIL] pullImage', JSON.stringify(res)); typeof params.fail == 'function' && params.fail(res); } }); }, upload(params) { console.log('[HTTP][REQUEST] upload', Config.baseUrl + params.url, 'name=', params.name || 'file'); // ✅ 关键修复:uni.uploadFile **绝对不能手动设置 Content-Type**! // 因为 uni.uploadFile 会自动设置成 multipart/form-data; boundary=----xxxxxx // 手动设置会覆盖掉 boundary,后端无法解析分段上传,导致文件/表单字段解析为空 var uploadHeader = {}; // 只合并调用方传的自定义 header,不默认加 content-type if (params.header && typeof params.header === 'object') { Object.assign(uploadHeader, params.header); // 兜底:如果调用方误传了 content-type,就删掉,让 uni 自动设置 multipart/form-data delete uploadHeader['content-type']; delete uploadHeader['Content-Type']; } uni.uploadFile({ url: Config.baseUrl + params.url, filePath: params.filePath, name: params.name || 'file', fileType: params.fileType || 'image', formData: { app_type: appType, app_type_name: appTypeName, token: store.state.token || '', uniacid: uni.getStorageSync('uniacid') || Config.uniacid }, header: uploadHeader, success: (res) => { console.log('[HTTP][RESPONSE] upload', Config.baseUrl + params.url, 'statusCode=', res.statusCode, res.data); typeof params.success == 'function' && params.success(JSON.parse(res.data)); }, fail: (res) => { console.error('[HTTP][FAIL] upload', Config.baseUrl + params.url, JSON.stringify(res)); typeof params.fail == 'function' && params.fail(res); } }); } }