// 移动端统一请求工具 - 与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', }), // 修改密码 resetPassword: (data) => request({ url: '/system/user/profile/updatePwd', method: 'PUT', header: { isEncrypt: 'true', // 修复:使用字符串 'true' 而不是布尔值 true repeatSubmit: false, }, data: { oldPassword: data.oldPassword, newPassword: data.newPassword, }, }), // 更新用户头像 // updateUserAvatar: (data) => // request({ // url: '/system/user/profile/updateAvatar', // method: 'PUT', // data: { // avatar: data.avatar, // }, // }), }; // 便捷请求方法封装 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.devServerUrl}${REQUEST_CONFIG.baseURL}${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.devServerUrl}${REQUEST_CONFIG.baseURL}${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.devServerUrl}${REQUEST_CONFIG.baseURL}${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); };