Files
zgty-mas-m/sheep/request/index.js
2025-09-30 00:08:23 +08:00

494 lines
15 KiB
JavaScript
Raw Permalink 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.
/**
* Shopro-request
* @description api模块管理loading配置请求拦截错误处理
*/
import Request from 'luch-request';
import { apiPath, baseUrl, tenantId } from '@/sheep/config';
import $store from '@/sheep/store';
import $platform from '@/sheep/platform';
import AuthUtil from '@/sheep/api/system/auth';
import { getTerminal } from '@/sheep/helper/const';
const options = {
// 显示操作成功消息 默认不显示
showSuccess: false,
// 成功提醒 默认使用后端返回值
successMsg: '',
// 显示失败消息 默认显示
showError: true,
// 失败提醒 默认使用后端返回信息
errorMsg: '',
// 显示请求时loading模态框 默认显示
showLoading: true,
// loading提醒文字
loadingMsg: '加载中',
// 需要授权才能请求 默认放开
auth: false,
// 是否传递 token
isToken: true,
};
const COMPANY_DEPT_RETRY_HEADER = '__companyDeptRetried';
const VISIT_COMPANY_STORAGE_KEY = 'visit-company-info';
const VISIT_DEPT_STORAGE_KEY = 'visit-dept-info';
// Loading全局实例
let LoadingInstance = {
target: null,
count: 0,
};
/**
* 关闭loading
*/
function closeLoading() {
if (LoadingInstance.count > 0) LoadingInstance.count--;
if (LoadingInstance.count === 0) uni.hideLoading();
}
/**
* @description 请求基础配置 可直接使用访问自定义请求
*/
const http = new Request({
baseURL: baseUrl + apiPath,
timeout: 8000,
method: 'GET',
header: {
Accept: 'text/json',
'Content-Type': 'application/json;charset=UTF-8',
platform: $platform.name,
},
// #ifdef APP-PLUS
sslVerify: false,
// #endif
// #ifdef H5
// 跨域请求时是否携带凭证cookies仅H5支持HBuilderX 2.6.15+
withCredentials: false,
// #endif
custom: options,
});
/**
* @description 请求拦截器
*/
http.interceptors.request.use(
(config) => {
// 自定义处理【auth 授权】:必须登录的接口,则跳转到登录页
if (config.custom.auth && !$store('user').isLogin) {
uni.navigateTo({
url: '/pages/login/index'
});
return Promise.reject();
}
// 自定义处理【loading 加载中】:如果需要显示 loading则显示 loading
if (config.custom.showLoading) {
LoadingInstance.count++;
LoadingInstance.count === 1 &&
uni.showLoading({
title: config.custom.loadingMsg,
mask: true,
fail: () => {
uni.hideLoading();
},
});
}
// 增加 token 令牌、terminal 终端、tenant 租户的请求头
// 检查是否在白名单中白名单中的接口不需要token
let isToken = config.custom.isToken;
whiteList.some((v) => {
if (config.url && config.url.indexOf(v) > -1) {
return (isToken = false);
}
});
const token = isToken ? getAccessToken() : undefined;
if (token) {
config.header['Authorization'] = token;
}
config.header['terminal'] = getTerminal();
config.header['Accept'] = '*/*';
config.header['tenant-id'] = getTenantId();
const visitCompanyId = getVisitCompanyId();
if (visitCompanyId !== undefined && visitCompanyId !== null && visitCompanyId !== '') {
config.header['visit-company-id'] = visitCompanyId;
const visitCompanyName = getVisitCompanyName();
if (visitCompanyName !== undefined && visitCompanyName !== null) {
config.header['visit-company-name'] = encodeURIComponent(visitCompanyName || '');
}
}
const visitDeptId = getVisitDeptId();
if (visitDeptId !== undefined && visitDeptId !== null && visitDeptId !== '') {
config.header['visit-dept-id'] = visitDeptId;
const visitDeptName = getVisitDeptName();
if (visitDeptName !== undefined && visitDeptName !== null) {
config.header['visit-dept-name'] = encodeURIComponent(visitDeptName || '');
}
}
return config;
},
(error) => {
return Promise.reject(error);
},
);
/**
* @description 响应拦截器
*/
http.interceptors.response.use(
(response) => {
// 约定:如果是 /auth/ 下的 URL 地址,并且返回了 accessToken 说明是登录相关的接口,则自动设置登陆令牌
if (response.config.url.indexOf('/system/auth/') >= 0 && response.data?.data?.accessToken) {
$store('user').setToken(response.data.data.accessToken, response.data.data.refreshToken);
}
// 自定处理【loading 加载中】:如果需要显示 loading则关闭 loading
response.config.custom.showLoading && closeLoading();
// 统一处理【公司/部门二次选择】:参考 PC 端逻辑,自动补全或提示选择后重试
if (response.data.code === 400 && Array.isArray(response.data.data)) {
const companyDeptList = response.data.data;
const config = response.config;
config.header = config.header || {};
if (companyDeptList.length === 1) {
const item = companyDeptList[0];
if (!config.header[COMPANY_DEPT_RETRY_HEADER]) {
applyCompanyDeptSelection(item, config);
return request(config);
}
uni.showToast({
title: '公司/部门信息缺失,且自动补全失败,请联系管理员',
icon: 'none',
mask: true,
});
return Promise.resolve(response.data);
} else if (companyDeptList.length > 1) {
const groupedList = normalizeCompanyDeptList(companyDeptList);
const companyDeptDialogStore = $store('company-dept');
return new Promise((resolve) => {
companyDeptDialogStore.open({
companyList: groupedList,
onConfirm: ({ companyId, deptId }) => {
const selectedCompany = groupedList.find((company) => company.companyId === companyId);
const selectedDept = selectedCompany?.depts.find((dept) => dept.deptId === deptId);
applyCompanyDeptSelection(
{
companyId,
companyName: selectedCompany?.companyName || '',
deptId,
deptName: selectedDept?.deptName || '',
},
config,
);
resolve(request(config));
},
onCancel: () => {
uni.showToast({
title: '已取消公司/部门选择',
icon: 'none',
mask: true,
});
resolve(response.data);
},
});
});
}
}
// 自定义处理【error 错误提示】:如果需要显示错误提示,则显示错误提示
if (response.data.code !== 0 && response.data.repCode !== '0000') {
// 特殊:如果 401 错误码,则跳转到登录页 or 刷新令牌
if (response.data.code === 401) {
return refreshToken(response.config);
}
// 特殊:处理分销用户绑定失败的提示
if ((response.data.code + '').includes('1011007')) {
console.error(`分销用户绑定失败,原因:${response.data.msg}`);
} else if (response.config.custom.showError) {
// 错误提示
uni.showToast({
title: response.data.msg || '服务器开小差啦,请稍后再试~',
icon: 'none',
mask: true,
});
}
// 即使是错误响应,也应该返回完整的响应数据,让调用方自行处理
return Promise.resolve(response.data);
}
// 自定义处理【showSuccess 成功提示】:如果需要显示成功提示,则显示成功提示
if (
response.config.custom.showSuccess &&
response.config.custom.successMsg !== '' &&
response.data.code === 0
) {
uni.showToast({
title: response.config.custom.successMsg,
icon: 'none',
});
}
// 返回结果:包括 code + data + msg
return Promise.resolve(response.data);
},
(error) => {
const userStore = $store('user');
const isLogin = userStore.isLogin;
let errorMessage = '网络请求出错';
if (error !== undefined) {
switch (error.statusCode) {
case 400:
errorMessage = '请求错误';
break;
case 401:
errorMessage = isLogin ? '您的登陆已过期' : '请先登录';
// 正常情况下,后端不会返回 401 错误,所以这里不处理 handleAuthorized
break;
case 403:
errorMessage = '拒绝访问';
break;
case 404:
errorMessage = '请求出错';
break;
case 408:
errorMessage = '请求超时';
break;
case 429:
errorMessage = '请求频繁, 请稍后再访问';
break;
case 500:
errorMessage = '服务器开小差啦,请稍后再试~';
break;
case 501:
errorMessage = '服务未实现';
break;
case 502:
errorMessage = '网络错误';
break;
case 503:
errorMessage = '服务不可用';
break;
case 504:
errorMessage = '网络超时';
break;
case 505:
errorMessage = 'HTTP 版本不受支持';
break;
}
if (error.errMsg.includes('timeout')) errorMessage = '请求超时';
// #ifdef H5
if (error.errMsg.includes('Network'))
errorMessage = window.navigator.onLine ? '服务器异常' : '请检查您的网络连接';
// #endif
}
if (error && error.config) {
if (error.config.custom.showError) {
uni.showToast({
title: error.data?.msg || errorMessage,
icon: 'none',
mask: true,
});
}
error.config.custom.showLoading && closeLoading();
}
return false;
},
);
// 请求白名单,无须 token 的接口
const whiteList = ['/login', '/refresh-token', '/captcha/get', '/system/captcha'];
// Axios 无感知刷新令牌,参考 https://www.dashingdog.cn/article/11 与 https://segmentfault.com/a/1190000020210980 实现
let requestList = []; // 请求队列
let isRefreshToken = false; // 是否正在刷新中
const refreshToken = async (config) => {
// 如果当前已经是 refresh-token 的 URL 地址,并且还是 401 错误,说明是刷新令牌失败了,直接返回 Promise.reject(error)
if (config.url.indexOf('/system/auth/refresh-token') >= 0) {
return Promise.reject('error');
}
// 如果未认证,并且未进行刷新令牌,说明可能是访问令牌过期了
if (!isRefreshToken) {
isRefreshToken = true;
// 1. 如果获取不到刷新令牌,则只能执行登出操作
const refreshToken = getRefreshToken();
if (!refreshToken) {
return handleAuthorized();
}
// 2. 进行刷新访问令牌
try {
const refreshTokenResult = await AuthUtil.refreshToken(refreshToken);
if (refreshTokenResult.code !== 0) {
// 如果刷新不成功,直接抛出 e 触发 2.2 的逻辑
// noinspection ExceptionCaughtLocallyJS
throw new Error('刷新令牌失败');
}
// 2.1 刷新成功,则回放队列的请求 + 当前请求
config.header.Authorization = 'Bearer ' + getAccessToken();
requestList.forEach((cb) => {
cb();
});
requestList = [];
return request(config);
} catch (e) {
// 为什么需要 catch 异常呢?刷新失败时,请求因为 Promise.reject 触发异常。
// 2.2 刷新失败,只回放队列的请求
requestList.forEach((cb) => {
cb();
});
// 提示是否要登出。即不回放当前请求!不然会形成递归
return handleAuthorized();
} finally {
requestList = [];
isRefreshToken = false;
}
} else {
// 添加到队列,等待刷新获取到新的令牌
return new Promise((resolve) => {
requestList.push(() => {
config.header.Authorization = 'Bearer ' + getAccessToken(); // 让每个请求携带自定义token 请根据实际情况自行修改
resolve(request(config));
});
});
}
};
/**
* 处理 401 未登录的错误
*/
const handleAuthorized = () => {
const userStore = $store('user');
userStore.logout(true);
uni.navigateTo({
url: '/pages/login/index'
});
// 登录超时
return Promise.reject({
code: 401,
msg: userStore.isLogin ? '您的登陆已过期' : '请先登录',
});
};
/** 获得访问令牌 */
export const getAccessToken = () => {
return uni.getStorageSync('token');
};
/** 获得刷新令牌 */
export const getRefreshToken = () => {
return uni.getStorageSync('refresh-token');
};
/** 获得租户编号 */
export const getTenantId = () => {
return uni.getStorageSync('tenant-id') || tenantId;
};
const getStorageObject = (key) => {
const value = uni.getStorageSync(key);
if (!value) {
return {};
}
if (typeof value === 'string') {
try {
return JSON.parse(value);
} catch (error) {
console.warn(`解析本地存储 ${key} 失败:`, error);
return {};
}
}
return value;
};
const setStorageObject = (key, value) => {
if (value === undefined || value === null) {
uni.removeStorageSync(key);
return;
}
uni.setStorageSync(key, value);
};
export const getVisitCompanyId = () => {
const info = getStorageObject(VISIT_COMPANY_STORAGE_KEY);
return info?.id ?? info?.companyId ?? null;
};
export const getVisitCompanyName = () => {
const info = getStorageObject(VISIT_COMPANY_STORAGE_KEY);
return info?.name ?? info?.companyName ?? '';
};
export const getVisitDeptId = () => {
const info = getStorageObject(VISIT_DEPT_STORAGE_KEY);
return info?.id ?? info?.deptId ?? null;
};
export const getVisitDeptName = () => {
const info = getStorageObject(VISIT_DEPT_STORAGE_KEY);
return info?.name ?? info?.deptName ?? '';
};
export const setVisitCompany = (companyId, companyName) => {
if (companyId === undefined || companyId === null || companyId === '') {
uni.removeStorageSync(VISIT_COMPANY_STORAGE_KEY);
return;
}
setStorageObject(VISIT_COMPANY_STORAGE_KEY, {
id: companyId,
name: companyName || '',
});
};
export const setVisitDept = (deptId, deptName) => {
if (deptId === undefined || deptId === null || deptId === '') {
uni.removeStorageSync(VISIT_DEPT_STORAGE_KEY);
return;
}
setStorageObject(VISIT_DEPT_STORAGE_KEY, {
id: deptId,
name: deptName || '',
});
};
const applyCompanyDeptSelection = (item, config) => {
setVisitCompany(item.companyId, item.companyName);
setVisitDept(item.deptId, item.deptName);
config.header['visit-company-id'] = item.companyId;
config.header['visit-company-name'] = encodeURIComponent(item.companyName || '');
config.header['visit-dept-id'] = item.deptId;
config.header['visit-dept-name'] = encodeURIComponent(item.deptName || '');
config.header[COMPANY_DEPT_RETRY_HEADER] = '1';
};
const normalizeCompanyDeptList = (list = []) => {
const companyMap = new Map();
list.forEach((item) => {
if (!companyMap.has(item.companyId)) {
companyMap.set(item.companyId, {
companyId: item.companyId,
companyName: item.companyName,
depts: [],
});
}
const company = companyMap.get(item.companyId);
if (!company.depts.some((dept) => dept.deptId === item.deptId)) {
company.depts.push({
deptId: item.deptId,
deptName: item.deptName,
});
}
});
return Array.from(companyMap.values());
};
const request = (config) => {
return http.middleware(config);
};
export default request;