/** * 云服务相关工具函数 */ // 导入统一配置 const config = require('./config'); const auth = require('./auth'); /** * 检查是否有有效的认证令牌 * @returns {boolean} 是否有有效的访问令牌和刷新令牌 */ const hasValidTokens = () => { const accessToken = wx.getStorageSync(config.JWT_CONFIG.storage.access); const refreshToken = wx.getStorageSync(config.JWT_CONFIG.storage.refresh); // 检查token是否存在且未过期 if (!accessToken || !refreshToken) { return false; } // 使用shouldRefreshToken验证token有效性 const { needsRefresh } = shouldRefreshToken(); return !needsRefresh; }; /** * 检查JWT token是否需要刷新 * @returns {Object} 包含needsRefresh和hasRefreshToken两个布尔值 */ const shouldRefreshToken = () => { try { const accessToken = wx.getStorageSync(config.JWT_CONFIG.storage.access); const refreshToken = wx.getStorageSync(config.JWT_CONFIG.storage.refresh); // 如果没有访问令牌或刷新令牌,则不需要刷新 if (!accessToken) { return { needsRefresh: false, hasRefreshToken: !!refreshToken }; } // 使用auth模块解析JWT token const decoded = auth.parseToken(accessToken); // 定义expirationTime变量 let expirationTime; if (decoded && decoded.exp) { expirationTime = decoded.exp * 1000; // 转换为毫秒 } else { console.error('解析JWT token失败: 无法获取过期时间'); // 如果解析失败,假设token将在1小时后过期 expirationTime = Date.now() + 3600 * 1000; } // 如果token将在5分钟内过期,则需要刷新 const needsRefresh = Date.now() + config.JWT_CONFIG.refreshThreshold > expirationTime; return { needsRefresh, hasRefreshToken: !!refreshToken }; } catch (error) { console.error('Token解析失败:', error); const refreshToken = wx.getStorageSync(config.JWT_CONFIG.storage.refresh); return { needsRefresh: false, hasRefreshToken: !!refreshToken }; } }; /** * 刷新JWT token * @returns {Promise} 刷新是否成功 */ const refreshToken = async () => { try { const refreshToken = wx.getStorageSync(config.JWT_CONFIG.storage.refresh); if (!refreshToken) { console.warn('没有可用的刷新令牌,无法刷新访问令牌'); return false; } // 使用Promise包装wx.request以便使用await const response = await new Promise((resolve, reject) => { wx.request({ url: `${config.API_BASE_URL}${config.API_ENDPOINTS.AUTH.REFRESH}`, method: 'POST', header: { 'Content-Type': 'application/json', 'Authorization': `${config.JWT_CONFIG.tokenPrefix}${refreshToken}` }, success: (res) => { // 确保response对象包含完整的信息 resolve({ ...res, requestOptions: { url: `${config.API_BASE_URL}${config.API_ENDPOINTS.AUTH.REFRESH}`, method: 'POST', header: { 'Content-Type': 'application/json', 'Authorization': `${config.JWT_CONFIG.tokenPrefix}${refreshToken}` } } }); }, fail: (error) => { // 增强错误信息 const enhancedError = new Error(error.errMsg || '刷新令牌请求失败'); enhancedError.originalError = error; enhancedError.requestOptions = { url: `${config.API_BASE_URL}${config.API_ENDPOINTS.AUTH.REFRESH}`, method: 'POST' }; reject(enhancedError); } }); }); if (response.statusCode === 200 && response.data.access_token) { wx.setStorageSync(config.JWT_CONFIG.storage.access, response.data.access_token); if (response.data.refresh_token) { wx.setStorageSync(config.JWT_CONFIG.storage.refresh, response.data.refresh_token); } console.log('Token刷新成功'); return true; } else { console.error(`Token刷新失败: ${response.statusCode}`); // 清除所有token,强制用户重新登录 wx.removeStorageSync(config.JWT_CONFIG.storage.access); wx.removeStorageSync(config.JWT_CONFIG.storage.refresh); return false; } } catch (error) { console.error('Token刷新失败:', error); // 清除所有token,强制用户重新登录 wx.removeStorageSync(config.JWT_CONFIG.storage.access); wx.removeStorageSync(config.JWT_CONFIG.storage.refresh); return false; } }; /** * 发送HTTP请求的通用函数 * @param {string} url - API URL * @param {Object} options - 请求选项 * @returns {Promise} 响应数据 */ const request = async (url, options = {}) => { // 对于登录请求,跳过token刷新检查 const isLoginRequest = url === config.API_ENDPOINTS.AUTH.LOGIN; if (!isLoginRequest) { // 检查是否需要刷新token const tokenStatus = shouldRefreshToken(); if (tokenStatus.needsRefresh && tokenStatus.hasRefreshToken) { const refreshSuccess = await refreshToken(); if (!refreshSuccess) { console.warn('Token刷新失败,继续使用当前token'); } } } const accessToken = wx.getStorageSync(config.JWT_CONFIG.storage.access); const defaultOptions = { method: 'GET', header: { 'Content-Type': 'application/json' } }; // 只有在有访问令牌且不是登录请求时才添加Authorization头 if (accessToken && !isLoginRequest) { defaultOptions.header['Authorization'] = `Bearer ${accessToken}`; } const requestOptions = { ...defaultOptions, ...options, url: `${config.API_BASE_URL}${url}` }; // 合并headers requestOptions.header = { ...defaultOptions.header, ...options.header }; try { const response = await new Promise((resolve, reject) => { const requestTask = wx.request({ ...requestOptions, success: (res) => { // 对于404错误,静默处理,不要让wx.request打印错误日志 if (res.statusCode === 404 && url === config.API_ENDPOINTS.OTP.RECOVER) { // 静默处理404错误 res.silent404 = true; } // 确保response对象包含完整的信息 resolve({ ...res, requestOptions: { url: requestOptions.url, method: requestOptions.method, header: requestOptions.header } }); }, fail: (error) => { // 增强错误信息 const enhancedError = new Error(error.errMsg || '网络请求失败'); enhancedError.originalError = error; enhancedError.requestOptions = { url: requestOptions.url, method: requestOptions.method, header: requestOptions.header }; reject(enhancedError); } }); }); // 处理401错误(token无效) if (response.statusCode === 401 && !isLoginRequest) { // 只有在有刷新令牌的情况下才尝试刷新token const tokenStatus = shouldRefreshToken(); if (tokenStatus.hasRefreshToken) { const refreshSuccess = await refreshToken(); if (refreshSuccess) { return request(url, options); // 递归调用,使用新token重试 } } // 如果没有刷新令牌或刷新失败,抛出认证错误 throw new Error('认证失败,请重新登录'); } if (response.statusCode >= 200 && response.statusCode < 300) { return response.data; } // 增强错误信息 const error = new Error(response.data?.message || '请求失败'); error.response = response; error.statusCode = response.statusCode; throw error; } catch (error) { // 只有非404错误才打印错误日志 if (error.statusCode !== 404) { console.error('API请求失败:', { endpoint: url, method: requestOptions.method, status: error.statusCode || 'N/A', error: error.message, requestOptions: error.requestOptions || requestOptions, originalError: error.originalError || error }); } // 重新抛出增强的错误 const enhancedError = new Error(error.message || '网络请求失败'); enhancedError.originalError = error; enhancedError.requestOptions = error.requestOptions || requestOptions; throw enhancedError; } }; /** * 上传令牌数据到云端 * @param {Array} tokens - 令牌数据数组 * @returns {Promise} 云端数据ID */ const uploadTokens = async (tokens) => { if (!tokens || tokens.length === 0) { throw new Error('没有可上传的数据'); } try { // 从access_token中获取用户ID const accessToken = wx.getStorageSync(config.JWT_CONFIG.storage.access); if (!accessToken) { throw new Error('未登录'); } // 使用auth模块解析JWT token获取用户ID const decoded = auth.parseToken(accessToken); if (!decoded) { throw new Error('无法解析访问令牌'); } const userId = decoded.sub || decoded.user_id || decoded.userId; // 尝试不同的可能字段名 if (!userId) { throw new Error('用户未登录'); } // 处理令牌数据,确保符合后端要求 const processedTokens = tokens.map(token => { // 创建一个新对象,只包含后端需要的字段 const processedToken = { id: token.id, issuer: token.issuer, account: token.account, secret: token.secret, type: token.type, period: token.period, digits: token.digits, algorithm: token.algorithm }; // 只有HOTP类型的令牌才设置counter字段,且必须大于等于0 if (token.type && token.type.toUpperCase() === 'HOTP') { processedToken.counter = token.counter || 0; } // TOTP类型的令牌不设置counter字段 return processedToken; }); const response = await request(config.API_ENDPOINTS.OTP.SAVE, { method: 'POST', data: { tokens: processedTokens, userId } }); if (response.success) { return response.data.id; } throw new Error(response.message || '上传失败'); } catch (error) { console.error('上传令牌数据失败:', error); throw error; } }; /** * 从云端获取最新的令牌数据 * @returns {Promise} 云端数据对象 */ const fetchLatestTokens = async () => { try { // 从access_token中获取用户ID const accessToken = wx.getStorageSync(config.JWT_CONFIG.storage.access); if (!accessToken) { throw new Error('未登录'); } // 使用auth模块解析JWT token获取用户ID const decoded = auth.parseToken(accessToken); if (!decoded) { throw new Error('无法解析访问令牌'); } const userId = decoded.sub || decoded.user_id || decoded.userId; // 尝试不同的可能字段名 if (!userId) { throw new Error('无法获取用户ID'); } const response = await request(config.API_ENDPOINTS.OTP.RECOVER, { method: 'POST', data: { userId } }); if (response.success) { // 当云端无数据时,返回空数组 if (!response.data?.tokens) { return { tokens: [], timestamp: new Date().toISOString(), num: 0 }; } // 处理从后端获取的令牌数据,确保符合前端要求 const processedTokens = response.data.tokens.map(token => { // 创建一个新对象,包含前端需要的字段 const processedToken = { ...token, createTime: token.createTime || new Date().toISOString(), lastUpdate: token.lastUpdate || new Date().toISOString(), code: token.code || '' }; // 确保HOTP类型的令牌有counter字段 if (token.type && token.type.toUpperCase() === 'HOTP') { processedToken.counter = token.counter || 0; } // TOTP类型的令牌不需要counter字段 return processedToken; }); return { tokens: processedTokens, timestamp: response.data.timestamp, num: processedTokens.length }; } // 当云端无数据时,后端应返回404状态码 if (response.statusCode === 404) { return { tokens: [], timestamp: new Date().toISOString(), num: 0 }; } throw new Error(response.message || '获取云端数据失败'); } catch (error) { // 当状态码为404时,说明云端无数据,返回空数组 if (error.statusCode === 404) { return { tokens: [], timestamp: new Date().toISOString(), num: 0 }; } throw error; } }; /** * 初始化云服务 * 检查认证状态并尝试恢复会话 * @returns {Promise} 初始化是否成功 */ const initCloud = async () => { try { // 检查是否有有效的令牌 if (!hasValidTokens()) { console.log('未找到有效令牌,需要登录'); return false; } // 验证令牌有效性 const tokenStatus = shouldRefreshToken(); if (tokenStatus.needsRefresh) { const refreshSuccess = await refreshToken(); if (!refreshSuccess) { console.log('令牌刷新失败,需要重新登录'); return false; } } return true; } catch (error) { console.warn('云服务初始化失败:', error); // 清除所有认证信息 wx.removeStorageSync(config.JWT_CONFIG.storage.access); wx.removeStorageSync(config.JWT_CONFIG.storage.refresh); return false; } }; /** * 比较本地和云端数据 * @param {Array} localTokens - 本地令牌数组 * @param {Array} cloudTokens - 云端令牌数组 * @returns {Object} 比较结果 */ const compareTokens = (localTokens, cloudTokens) => { const result = { added: [], removed: [], modified: [], unchanged: [] }; // 创建查找映射 const localMap = new Map(localTokens.map(t => [t.id, t])); const cloudMap = new Map(cloudTokens.map(t => [t.id, t])); // 查找添加和修改的令牌 cloudTokens.forEach(cloudToken => { const localToken = localMap.get(cloudToken.id); if (!localToken) { result.added.push(cloudToken); } else if (JSON.stringify(localToken) !== JSON.stringify(cloudToken)) { result.modified.push(cloudToken); } else { result.unchanged.push(cloudToken); } }); // 查找删除的令牌 localTokens.forEach(localToken => { if (!cloudMap.has(localToken.id)) { result.removed.push(localToken); } }); return result; }; /** * 合并本地和云端数据 * @param {Array} localTokens - 本地令牌数组 * @param {Array} cloudTokens - 云端令牌数组 * @param {Object} options - 合并选项 * @param {boolean} [options.preferCloud=true] - 冲突时是否优先使用云端数据 * @returns {Array} 合并后的令牌数组 */ const mergeTokens = (localTokens, cloudTokens, options = { preferCloud: true }) => { const comparison = compareTokens(localTokens, cloudTokens); const result = [...comparison.unchanged]; // 添加新令牌 result.push(...comparison.added); // 处理修改的令牌 comparison.modified.forEach(cloudToken => { if (options.preferCloud) { result.push(cloudToken); } else { const localToken = localTokens.find(t => t.id === cloudToken.id); result.push(localToken); } }); // 如果不优先使用云端数据,保留本地删除的令牌 if (!options.preferCloud) { result.push(...comparison.removed); } return result; }; /** * 清空云端所有令牌数据 * @returns {Promise} 是否成功 */ const clearAllTokens = async () => { try { // 从access_token中获取用户ID const accessToken = wx.getStorageSync(config.JWT_CONFIG.storage.access); if (!accessToken) { throw new Error('未登录'); } // 使用auth模块解析JWT token获取用户ID const decoded = auth.parseToken(accessToken); if (!decoded) { throw new Error('无法解析访问令牌'); } const userId = decoded.sub || decoded.user_id || decoded.userId; if (!userId) { throw new Error('无法获取用户ID'); } const response = await request(config.API_ENDPOINTS.OTP.CLEAR_ALL, { method: 'POST', data: { userId } }); if (response.success) { return true; } throw new Error(response.message || '清空失败'); } catch (error) { console.error('清空云端数据失败:', error); throw error; } }; module.exports = { uploadTokens, fetchLatestTokens, compareTokens, mergeTokens, initCloud, shouldRefreshToken, refreshToken, request, hasValidTokens, clearAllTokens };