Files
lucky_shop/common/js/http.js

336 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}
});
}
}