298 lines
No EOL
7.5 KiB
JavaScript
298 lines
No EOL
7.5 KiB
JavaScript
/**
|
||
* 云服务相关工具函数
|
||
*/
|
||
|
||
// 导入统一配置
|
||
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
|
||
}; |