This commit is contained in:
“xHuPo” 2025-06-09 13:35:15 +08:00
commit 2b8870a40e
51 changed files with 5845 additions and 0 deletions

298
utils/cloud.js Normal file
View file

@ -0,0 +1,298 @@
/**
* 云服务相关工具函数
*/
// 导入统一配置
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
};