Files
hzdyjs/mobile/src/common/request.js
T
2025-08-19 15:50:09 +08:00

891 lines
31 KiB
JavaScript

// 移动端统一请求工具 - 与mobile工程保持一致
import {
generateAesKey,
encryptBase64,
decryptBase64,
encryptWithAes,
decryptWithAes,
encrypt,
decrypt,
} from './crypto.js';
import { ENCRYPT_CONFIG, REQUEST_CONFIG } from './config.js';
import { sessionCache, userStorage } from './storage.js';
/**
* 获取token
*/
function getToken() {
try {
const token = uni.getStorageSync('access_token');
console.log(
'🔍 获取Token:',
token ? `Bearer ${token.substring(0, 20)}...` : '❌ Token为空',
);
return token ? `Bearer ${token}` : '';
} catch (e) {
console.error('❌ 获取Token失败:', e);
return '';
}
}
/**
* 处理401未授权响应
*/
function handleUnauthorized(message = '登录已失效,请重新登录') {
console.log('🔒 处理401未授权响应:', message);
// 获取当前页面路径
const pages = getCurrentPages();
const currentPage = pages[pages.length - 1];
const currentPath = '/' + currentPage.route;
// 如果是新生报名页面,不跳转到登录页,只是静默处理
if (currentPath === '/pages/enrollment/index') {
console.log('📋 新生报名页面401错误,使用兜底数据,不跳转登录页');
return;
}
// 清除登录信息
try {
uni.removeStorageSync('access_token');
uni.removeStorageSync('userInfo');
uni.removeStorageSync('isLoggedIn');
console.log('📤 已清除本地登录信息');
} catch (e) {
console.error('❌ 清除登录信息失败:', e);
}
// 显示提示信息
uni.showToast({
title: message,
icon: 'none',
duration: 2000,
});
// 延迟跳转,确保Toast显示
setTimeout(() => {
// 如果当前不是登录页,则跳转到登录页
if (currentPath !== '/pages/login/index') {
console.log('🔄 从', currentPath, '跳转到登录页');
uni.reLaunch({
url: '/pages/login/index',
fail: (error) => {
console.error('❌ 跳转登录页失败:', error);
// 如果reLaunch失败,尝试其他方式
uni.redirectTo({
url: '/pages/login/index',
});
},
});
}
}, 500);
}
/**
* 统一请求方法
*/
export const request = (options) => {
return new Promise((resolve, reject) => {
// 获取token
const token = getToken();
// 处理GET请求的params参数
let url = REQUEST_CONFIG.baseURL + options.url;
if (options.params && Object.keys(options.params).length > 0) {
// 将params对象转换为查询字符串
const queryString = Object.keys(options.params)
.filter((key) => options.params[key] !== undefined && options.params[key] !== null)
.map(
(key) =>
`${encodeURIComponent(key)}=${encodeURIComponent(options.params[key])}`,
)
.join('&');
if (queryString) {
url += (url.indexOf('?') > -1 ? '&' : '?') + queryString;
}
console.log('🔍 处理GET请求params:', {
原始params: options.params,
查询字符串: queryString,
最终URL: url,
});
}
// 处理请求配置
const config = {
url: url,
method: options.method || 'GET',
data: options.data || {},
timeout: options.timeout || REQUEST_CONFIG.timeout,
responseType: options.responseType || 'text', // 支持responseType参数
header: {
// 吴自龙 注释 get方法,后台不接受json 20250811
// 'Content-Type': 'application/json',
authorization: token,
clientid: options.clientId || REQUEST_CONFIG.clientId,
// 注意:在H5环境中,浏览器会自动设置referer,手动设置会报错
// referer: options.referer || REQUEST_CONFIG.referer,
...options.header,
},
};
// 检查特殊header配置
const isToken = options.header?.isToken === false;
const isRepeatSubmit = options.header?.repeatSubmit === false;
const isEncrypt = options.header?.isEncrypt === 'true';
// 如果不需要token,移除authorization
if (isToken) {
delete config.header.authorization;
}
// 防重复提交检查
if (!isRepeatSubmit && (config.method === 'POST' || config.method === 'PUT')) {
const requestObj = {
url: config.url,
data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data,
time: new Date().getTime(),
};
const sessionObj = sessionCache.get('sessionObj');
if (sessionObj) {
const s_url = sessionObj.url;
const s_data = sessionObj.data;
const s_time = sessionObj.time;
const interval = REQUEST_CONFIG.repeatSubmitInterval;
if (
s_data === requestObj.data &&
requestObj.time - s_time < interval &&
s_url === requestObj.url
) {
const message = '数据正在处理,请勿重复提交';
console.warn(`[${s_url}]: ` + message);
uni.showToast({
title: message,
icon: 'none',
});
return reject(new Error(message));
}
}
sessionCache.set('sessionObj', requestObj);
}
// 加密处理
if (
REQUEST_CONFIG.encryptEnabled &&
isEncrypt &&
(config.method === 'POST' || config.method === 'PUT')
) {
try {
console.log('🔐 开始请求加密处理...');
// 生成AES密钥
const aesKey = generateAesKey();
// 使用RSA加密AES密钥
const encryptedKey = encrypt(encryptBase64(aesKey));
if (!encryptedKey) {
throw new Error('RSA加密AES密钥失败');
}
// 设置加密头
config.header[ENCRYPT_CONFIG.ENCRYPT_HEADER] = encryptedKey;
// 加密请求数据
const dataToEncrypt =
typeof config.data === 'object' ? JSON.stringify(config.data) : config.data;
config.data = encryptWithAes(dataToEncrypt, aesKey);
console.log('✅ 请求数据加密完成');
} catch (error) {
console.error('❌ 请求加密失败:', error);
uni.showToast({
title: '请求加密失败',
icon: 'none',
});
return reject(error);
}
}
// FormData处理
if (config.data instanceof globalThis.FormData) {
delete config.header['Content-Type'];
}
console.log('📤 发送请求:', {
url: config.url,
method: config.method,
hasToken: !!token,
isEncrypt: isEncrypt,
});
// 发送请求
uni.request({
...config,
success: (res) => {
console.log('📥 收到响应:', {
statusCode: res.statusCode,
url: config.url,
hasEncryptHeader: !!res.header[ENCRYPT_CONFIG.ENCRYPT_HEADER],
});
// 解密处理 - 只对JSON响应进行解密
if (
REQUEST_CONFIG.encryptEnabled &&
res.header[ENCRYPT_CONFIG.ENCRYPT_HEADER] &&
config.responseType === 'text'
) {
try {
console.log('🔓 开始响应解密处理...');
// 获取加密的AES密钥
const keyStr = res.header[ENCRYPT_CONFIG.ENCRYPT_HEADER];
// RSA解密获取AES密钥
const base64Str = decrypt(keyStr);
if (!base64Str) {
throw new Error('RSA解密AES密钥失败');
}
// Base64解码获取AES密钥
const aesKey = decryptBase64(base64Str);
// AES解密响应数据
const decryptedData = decryptWithAes(res.data, aesKey);
// 解析JSON
res.data = JSON.parse(decryptedData);
console.log('✅ 响应数据解密完成');
} catch (error) {
console.error('❌ 响应解密失败:', error);
uni.showToast({
title: '响应解密失败',
icon: 'none',
});
return reject(error);
}
}
// 处理响应
if (res.statusCode === 200) {
// 对于arraybuffer类型的响应,直接返回数据
if (config.responseType === 'arraybuffer') {
resolve(res.data);
return;
}
// 检查业务状态码
if (res.data && res.data.code !== undefined) {
if (res.data.code === 200) {
resolve(res.data);
} else if (res.data.code === 401) {
// 401未授权,跳转到登录页
console.log('🔒 Token失效,自动跳转到登录页');
handleUnauthorized(res.data.msg || '登录已失效,请重新登录');
reject(res.data);
} else {
// 其他业务错误
const errorMsg = res.data.msg || '请求失败';
if (res.data.code !== 404) {
// 404错误静默处理
uni.showToast({
title: errorMsg,
icon: 'none',
});
}
reject(res.data);
}
} else {
resolve(res.data);
}
} else if (res.statusCode === 401) {
// HTTP 401未授权
console.log('🔒 HTTP 401未授权,自动跳转到登录页');
handleUnauthorized('登录已失效,请重新登录');
reject(res);
} else if (res.statusCode === 404) {
// 404错误静默处理
console.log('404错误被静默处理,不显示用户提示');
reject(res);
} else {
// 其他HTTP状态码错误
const errorMsg = `请求失败: ${res.statusCode}`;
uni.showToast({
title: errorMsg,
icon: 'none',
});
reject(res);
}
},
fail: (error) => {
console.error('❌ 网络请求失败:', error);
let message = '网络请求失败';
if (error.errMsg) {
if (error.errMsg.includes('timeout')) {
message = '请求超时,请检查网络连接';
} else if (error.errMsg.includes('fail')) {
message = '网络连接失败,请检查网络';
}
}
uni.showToast({
title: message,
icon: 'none',
});
reject(error);
},
});
});
};
// API方法封装
export const api = {
// 用户登录 - 使用统一的request方法和加密拦截器
login: (data) => {
console.log('🔐 开始登录流程...');
return request({
url: '/auth/login',
method: 'POST',
data: {
username: data.username,
password: data.password,
tenantId: data.tenantId || '000000',
clientId: data.clientId || REQUEST_CONFIG.clientId,
grantType: data.grantType || 'password',
},
header: {
isToken: false, // 登录接口不需要token
isEncrypt: 'true', // 启用加密
},
});
},
// 获取用户信息
getUserInfo: () => {
const token = getToken();
console.log(
'🎫 获取用户信息,当前token:',
token ? token.substring(0, 20) + '...' : '无token',
);
return request({
url: '/system/user/getInfo',
method: 'GET',
});
},
// 获取租户列表
getTenantList: (isToken = false) =>
request({
url: '/auth/tenant/list',
method: 'GET',
header: {
isToken: isToken,
},
}),
// 注销登录
logout: () =>
request({
url: '/auth/logout',
method: 'POST',
}),
};
// 便捷请求方法封装
export const get = (url, data, header) => {
return request({
url,
method: 'GET',
data,
header,
});
};
export const post = (url, data, header) => {
return request({
url,
method: 'POST',
data,
header,
});
};
export const put = (url, data, header) => {
return request({
url,
method: 'PUT',
data,
header,
});
};
export const del = (url, data, header) => {
return request({
url,
method: 'DELETE',
data,
header,
});
};
// 文件上传方法
export const uploadFile = (url, filePath, formData) => {
return new Promise((resolve, reject) => {
const token = getToken();
const clientid = uni.getStorageSync('clientid') || REQUEST_CONFIG.clientId;
// 构建完整URL - 使用统一配置的服务器地址
let fullUrl;
if (REQUEST_CONFIG.baseURL.startsWith('/')) {
// 开发环境,baseURL是'/api',需要使用完整的服务器URL
fullUrl = `${REQUEST_CONFIG.serverUrl}${url}`;
} else {
// 生产环境,直接拼接
fullUrl = REQUEST_CONFIG.baseURL + url;
}
console.log('🚀 上传文件URL:', fullUrl);
console.log('🗂️ 文件路径:', filePath);
console.log('📊 表单数据:', formData);
const headers = {
// 移除Content-Type设置,让uni.uploadFile自动处理
authorization: token,
clientid: clientid,
};
uni.uploadFile({
url: fullUrl,
filePath,
name: 'file',
formData,
header: headers,
success: (res) => {
console.log('✅ 上传成功响应:', res);
try {
const data = JSON.parse(res.data);
resolve(data);
} catch (error) {
console.error('❌ 解析响应数据失败:', error);
reject(new Error('解析响应数据失败'));
}
},
fail: (err) => {
console.error('❌ 上传失败:', err);
reject(new Error(err.errMsg || '上传失败'));
},
});
});
};
// 文件下载方法(支持多平台)
export const downloadFile = (url, fileName = null) => {
return new Promise((resolve, reject) => {
const token = getToken();
const clientid = uni.getStorageSync('clientid') || REQUEST_CONFIG.clientId;
console.log('REQUEST_CONFIG.baseURL:', REQUEST_CONFIG.baseURL);
// 构建完整URL - 使用统一配置的服务器地址
let fullUrl;
if (REQUEST_CONFIG.baseURL.startsWith('/')) {
// 开发环境,baseURL是'/api',需要使用完整的服务器URL
fullUrl = `${REQUEST_CONFIG.serverUrl}${url}`;
} else {
// 生产环境,直接拼接
fullUrl = REQUEST_CONFIG.baseURL + url;
}
console.log('📥 下载文件URL:', fullUrl);
const headers = {
authorization: token,
clientid: clientid,
};
// #ifdef H5
// H5端使用fetch + blob的方式实现浏览器下载
console.log('🌐 H5端下载处理');
fetch(fullUrl, {
method: 'GET',
headers: headers,
})
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.blob();
})
.then((blob) => {
// 创建下载链接
const downloadUrl = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = downloadUrl;
// 尝试从响应头获取文件名,或使用传入的文件名
let downloadFileName = fileName;
if (!downloadFileName) {
// 从URL中提取文件名
const urlParts = url.split('/');
downloadFileName = urlParts[urlParts.length - 1] || 'download';
}
link.download = downloadFileName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
// 清理blob URL
window.URL.revokeObjectURL(downloadUrl);
console.log('✅ H5下载触发成功');
resolve({ statusCode: 200, message: 'H5下载已触发' });
})
.catch((error) => {
console.error('❌ H5下载失败:', error);
reject(new Error(error.message || 'H5下载失败'));
});
// #endif
// #ifndef H5
// 非H5端使用uni.downloadFile
uni.downloadFile({
url: fullUrl,
header: headers,
success: (res) => {
console.log('✅ 下载成功:', res);
resolve(res);
},
fail: (err) => {
console.error('❌ 下载失败:', err);
reject(new Error(err.errMsg || '下载失败'));
},
});
// #endif
});
};
// 通用下载方法
export const download = (url, params, fileName) => {
// 显示加载提示
uni.showLoading({
title: '正在下载数据,请稍候',
mask: true,
});
return request({
url: url,
method: 'POST',
data: params,
header: {
'Content-Type': 'application/x-www-form-urlencoded',
},
timeout: 1200000, // 20分钟超时
responseType: 'arraybuffer',
})
.then((resp) => {
// 处理下载响应
console.log('文件下载完成:', fileName);
uni.hideLoading();
// 这里需要根据平台进行不同的文件保存处理
// 具体实现可能需要平台特定的代码
return resp;
})
.catch((error) => {
console.error('下载失败:', error);
uni.hideLoading();
uni.showToast({
title: '下载文件出现错误,请联系管理员!',
icon: 'none',
});
throw error;
});
};
// 微信环境专用文件上传方法(解决微信浏览器兼容性问题)
export const uploadFileForWeChat = (url, file, formData, options = {}) => {
return new Promise((resolve, reject) => {
const token = getToken();
const clientid = uni.getStorageSync('clientid') || REQUEST_CONFIG.clientId;
console.log('🔧 微信环境专用文件上传...');
console.log('📁 文件对象:', file);
console.log('📊 表单数据:', formData);
// 构建完整URL
let fullUrl;
if (REQUEST_CONFIG.baseURL.startsWith('/')) {
fullUrl = `${REQUEST_CONFIG.serverUrl}${url}`;
} else {
fullUrl = REQUEST_CONFIG.baseURL + url;
}
console.log('🚀 上传URL:', fullUrl);
// 检测是否为blob文件
if (file._blob || (file.url && file.url.startsWith('blob:'))) {
console.log('🎨 检测到Blob文件,使用FormData+fetch方案');
return uploadBlobFileWithFetch(fullUrl, file, formData, token, clientid)
.then(resolve)
.catch(reject);
}
// 检测文件路径格式并尝试不同的上传策略
const filePath = file.url || file.tempFilePath || file.path;
if (!filePath) {
const error = new Error('文件路径为空,无法上传');
console.error('❌', error.message);
return reject(error);
}
console.log('📂 使用文件路径上传:', filePath);
// 策略1: 先尝试标准的uni.uploadFile
console.log('🔄 策略1: 尝试标准uni.uploadFile');
uni.uploadFile({
url: fullUrl,
filePath: filePath,
name: 'file',
formData,
header: {
authorization: token,
clientid: clientid,
},
success: (res) => {
console.log('✅ 标准uni.uploadFile成功:', res);
try {
const data = JSON.parse(res.data);
resolve(data);
} catch (error) {
console.error('❌ 解析响应数据失败:', error);
reject(new Error('解析响应数据失败'));
}
},
fail: (err) => {
console.log('⚠️ 标准uni.uploadFile失败,尝试备用方案:', err);
// 策略2: 尝试使用FileReader转换后上传
if (typeof FileReader !== 'undefined') {
console.log('🔄 策略2: 尝试FileReader+fetch方案');
// 通过input[type=file]重新选择文件进行上传
createFileInputForUpload(fullUrl, formData, token, clientid, file)
.then(resolve)
.catch((fetchError) => {
console.error('❌ FileReader+fetch方案也失败:', fetchError);
reject(
new Error(
`所有上传方案失败: ${err.errMsg || err.message} / ${
fetchError.message
}`,
),
);
});
} else {
// 策略3: 提示用户使用系统相册
console.log('🔄 策略3: 引导用户使用系统相册');
reject(new Error('当前环境不支持此文件格式,请使用系统相册选择图片'));
}
},
});
});
};
// 使用FormData+fetch上传Blob文件(专门处理Canvas生成的图片)
function uploadBlobFileWithFetch(url, file, formData, token, clientid) {
return new Promise((resolve, reject) => {
try {
console.log('🎨 开始Blob文件上传流程...');
const formDataObj = new FormData();
// 添加基本表单数据
Object.keys(formData || {}).forEach((key) => {
formDataObj.append(key, formData[key]);
});
// 处理Blob文件
let blob = file._blob;
let fileName = file.name || 'upload-image.png';
let mimeType = file.type || 'image/png';
if (!blob && file.url && file.url.startsWith('blob:')) {
// 如果没有直接的blob引用,尝试从blob URL获取
console.log('🔗 从blob URL获取文件数据...');
fetch(file.url)
.then((response) => response.blob())
.then((fetchedBlob) => {
formDataObj.append('file', fetchedBlob, fileName);
return performFetchUpload(url, formDataObj, token, clientid);
})
.then(resolve)
.catch(reject);
return;
}
if (!blob) {
throw new Error('无法获取Blob数据');
}
// 直接添加blob到formData
console.log('📎 添加Blob到FormData:', {
fileName,
mimeType,
size: blob.size,
});
formDataObj.append('file', blob, fileName);
// 执行fetch上传
performFetchUpload(url, formDataObj, token, clientid).then(resolve).catch(reject);
} catch (error) {
console.error('❌ Blob文件上传准备失败:', error);
reject(error);
}
});
}
// 执行fetch上传请求
function performFetchUpload(url, formData, token, clientid) {
return fetch(url, {
method: 'POST',
headers: {
authorization: token,
clientid: clientid,
// 注意:不要设置Content-Type,让浏览器自动设置multipart/form-data边界
},
body: formData,
})
.then((response) => {
console.log('📡 Fetch上传响应状态:', response.status);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.text();
})
.then((responseText) => {
console.log('📥 Fetch上传响应数据:', responseText);
try {
const data = JSON.parse(responseText);
return data;
} catch (parseError) {
console.error('❌ 解析响应JSON失败:', parseError);
throw new Error('服务器返回数据格式错误');
}
});
}
// 创建隐藏的file input来重新选择文件(微信环境后备方案)
function createFileInputForUpload(url, formData, token, clientid, originalFile) {
return new Promise((resolve, reject) => {
try {
console.log('📝 创建隐藏文件选择器...');
// 在微信环境中提示用户重新选择文件
uni.showModal({
title: '文件上传',
content: '检测到上传兼容性问题,需要重新选择文件。点击确定后请重新选择图片。',
showCancel: true,
success: (modalRes) => {
if (modalRes.confirm) {
// 重新选择文件
uni.chooseImage({
count: 1,
sourceType: ['album', 'camera'],
success: (chooseRes) => {
console.log('🔄 重新选择文件成功:', chooseRes);
if (chooseRes.tempFilePaths && chooseRes.tempFilePaths.length > 0) {
const newFilePath = chooseRes.tempFilePaths[0];
// 使用新选择的文件进行上传
uni.uploadFile({
url: url,
filePath: newFilePath,
name: 'file',
formData,
header: {
authorization: token,
clientid: clientid,
},
success: (res) => {
console.log('✅ 重新上传成功:', res);
try {
const data = JSON.parse(res.data);
resolve(data);
} catch (error) {
reject(new Error('解析响应数据失败'));
}
},
fail: (err) => {
console.error('❌ 重新上传失败:', err);
reject(new Error(err.errMsg || '重新上传失败'));
},
});
} else {
reject(new Error('未选择任何文件'));
}
},
fail: (err) => {
console.error('❌ 重新选择文件失败:', err);
reject(new Error('重新选择文件失败'));
},
});
} else {
reject(new Error('用户取消了重新选择文件'));
}
},
});
} catch (error) {
console.error('❌ 创建文件选择器失败:', error);
reject(error);
}
});
}
// 检测是否在微信环境
export const isWeChat = () => {
if (typeof navigator === 'undefined') return false;
return /MicroMessenger/i.test(navigator.userAgent || '');
};
// 智能文件上传方法(根据环境自动选择最佳方案)
export const smartUploadFile = (url, fileOrPath, formData, options = {}) => {
console.log('🧠 智能文件上传开始...');
console.log('🌐 当前环境:', {
isWeChat: isWeChat(),
userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : 'unknown',
});
// 如果在微信环境或遇到特殊文件格式,使用增强版上传
if (
isWeChat() ||
(typeof fileOrPath === 'object' &&
(fileOrPath._blob || fileOrPath.url?.startsWith('blob:')))
) {
console.log('📱 使用微信环境专用上传方法');
return uploadFileForWeChat(url, fileOrPath, formData, options);
}
// 否则使用标准上传方法
console.log('💻 使用标准上传方法');
const filePath =
typeof fileOrPath === 'string'
? fileOrPath
: fileOrPath.url || fileOrPath.tempFilePath || fileOrPath.path;
return uploadFile(url, filePath, formData);
};