otp/utils/cloud.js
“xHuPo” 2b8870a40e init
2025-06-09 13:35:15 +08:00

298 lines
No EOL
7.5 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.

/**
* 云服务相关工具函数
*/
// 导入统一配置
const config = require('./config');
/**
* 检查JWT token是否需要刷新
* @returns {boolean}
*/
const shouldRefreshToken = () => {
try {
const token = wx.getStorageSync(config.JWT_CONFIG.storage.access);
if (!token) return true;
// 解析JWT token不验证签名
const [, payload] = token.split('.');
const { exp } = JSON.parse(atob(payload));
const expirationTime = exp * 1000; // 转换为毫秒
// 如果token将在5分钟内过期则刷新
return Date.now() + config.JWT_CONFIG.refreshThreshold > expirationTime;
} catch (error) {
console.error('Token解析失败:', error);
return true;
}
};
/**
* 刷新JWT token
* @returns {Promise<void>}
*/
const refreshToken = async () => {
try {
const refreshToken = wx.getStorageSync(config.JWT_CONFIG.storage.refresh);
if (!refreshToken) {
throw new Error('No refresh token available');
}
const response = await wx.request({
url: `${config.API_BASE_URL}${config.API_ENDPOINTS.AUTH.REFRESH}`,
method: 'POST',
header: {
'Authorization': `Bearer ${refreshToken}`
}
});
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);
}
} else {
throw new Error('Token refresh failed');
}
} catch (error) {
console.error('Token刷新失败:', error);
// 清除所有token强制用户重新登录
wx.removeStorageSync(config.JWT_CONFIG.storage.access);
wx.removeStorageSync(config.JWT_CONFIG.storage.refresh);
throw new Error('认证已过期,请重新登录');
}
};
/**
* 发送HTTP请求的通用函数
* @param {string} url - API URL
* @param {Object} options - 请求选项
* @returns {Promise<any>} 响应数据
*/
const request = async (url, options = {}) => {
// 检查并刷新token
if (shouldRefreshToken()) {
await refreshToken();
}
const accessToken = wx.getStorageSync(config.JWT_CONFIG.storage.access);
const defaultOptions = {
method: 'GET',
header: {
'Content-Type': 'application/json',
'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) => {
wx.request({
...requestOptions,
success: resolve,
fail: reject
});
});
// 处理401错误token无效
if (response.statusCode === 401) {
// 尝试刷新token并重试请求
await refreshToken();
return request(url, options); // 递归调用使用新token重试
}
if (response.statusCode >= 200 && response.statusCode < 300) {
return response.data;
}
throw new Error(response.data?.message || '请求失败');
} catch (error) {
console.error('API请求失败:', {
endpoint: url,
method: requestOptions.method,
status: error.statusCode || 'N/A',
error: error.message
});
throw error;
}
};
/**
* 上传令牌数据到云端
* @param {Array} tokens - 令牌数据数组
* @returns {Promise<string>} 云端数据ID
*/
const uploadTokens = async (tokens) => {
if (!tokens || tokens.length === 0) {
throw new Error('没有可上传的数据');
}
try {
const response = await request(config.API_ENDPOINTS.OTP.SAVE, {
method: 'POST',
data: {
tokens,
timestamp: Date.now()
}
});
if (response.success) {
return response.data.id;
}
throw new Error(response.message || '上传失败');
} catch (error) {
console.error('上传令牌数据失败:', error);
throw error;
}
};
/**
* 从云端获取最新的令牌数据
* @returns {Promise<Object>} 云端数据对象
*/
const fetchLatestTokens = async () => {
try {
const response = await request(config.API_ENDPOINTS.OTP.RECOVER, {
method: 'POST',
data: {
timestamp: Date.now()
}
});
if (response.success && response.data?.tokens) {
return {
tokens: response.data.tokens,
timestamp: response.data.timestamp,
num: response.data.tokens.length
};
}
throw new Error(response.message || '未找到云端数据');
} catch (error) {
console.error('获取云端数据失败:', error);
throw error;
}
};
/**
* 初始化云服务
* 检查认证状态并尝试恢复会话
* @returns {Promise<boolean>} 初始化是否成功
*/
const initCloud = async () => {
try {
// 检查是否有有效的访问令牌
const accessToken = wx.getStorageSync(config.JWT_CONFIG.storage.access);
if (!accessToken) {
console.log('未找到访问令牌,需要登录');
return false;
}
// 验证令牌有效性
if (shouldRefreshToken()) {
await refreshToken();
}
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;
};
module.exports = {
uploadTokens,
fetchLatestTokens,
compareTokens,
mergeTokens,
initCloud,
shouldRefreshToken,
refreshToken
};