336 lines
11 KiB
JavaScript
336 lines
11 KiB
JavaScript
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(/(?<!:)\/+/g, '/'),
|
||
data = {
|
||
app_type: appType,
|
||
app_type_name: appTypeName
|
||
};
|
||
|
||
data.token = store.state.token || '';
|
||
data.uniacid = uni.getStorageSync('uniacid') || Config.uniacid;
|
||
|
||
var default_store_info = store.state.defaultStoreInfo;
|
||
if (default_store_info) {
|
||
data.store_id = default_store_info.store_id;
|
||
}
|
||
|
||
var store_info = store.state.globalStoreInfo;
|
||
|
||
if (store_info) data.store_id = store_info.store_id;
|
||
|
||
if (params.data != undefined) Object.assign(data, params.data);
|
||
|
||
// ✅ 关键修复 2:Content-Type 合法值,两个 MIME 类型不要乱拼接
|
||
// 规则:
|
||
// - 调用方 params.header 里指定了 content-type → 优先用调用方的(保持原有 override 能力)
|
||
// - GET/HEAD/DELETE:application/x-www-form-urlencoded(URL query 编码)
|
||
// - POST/PUT/PATCH Object data:application/json(uni.request 会自动 JSON.stringify)
|
||
// - 如果调用方传 FormData / ArrayBuffer 等二进制:不设 Content-Type,让 uni 自动处理
|
||
var autoHeader = {};
|
||
if (method === 'GET' || method === 'HEAD' || method === 'DELETE') {
|
||
autoHeader['content-type'] = 'application/x-www-form-urlencoded';
|
||
} else {
|
||
// POST/PUT/PATCH:默认用 application/json,后端能直接 json_decode(file_get_contents('php://input'))
|
||
autoHeader['content-type'] = 'application/json';
|
||
}
|
||
// 调用方 params.header 合并(调用方指定的覆盖自动设置的)
|
||
const header = Object.assign({}, autoHeader, (params.header && typeof params.header === 'object') ? params.header : {});
|
||
|
||
const dataType = params.dataType || 'json';
|
||
const responseType = params.responseType || 'text';
|
||
|
||
if (params.async === false) {
|
||
return _requestWithRetry(params, url, method, data, header, dataType, responseType)
|
||
.then(res => {
|
||
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);
|
||
}
|
||
});
|
||
}
|
||
} |