const axios = require("axios"); const fs = require('fs'); const path = require('path'); /** * Token 缓存信息 * @typedef {Object} TokenCache * @property {string} token - Token 值 * @property {string} expiresAt - 过期时间 (ISO 字符串) */ /** * 外部 Token 获取服务 * 支持文件持久化和自动过期更新 * @class ExternalToken */ class ExternalToken { constructor() { /** * Token 文件存储路径 * @type {string} * @private */ this._tokenFilePath = path.join(__dirname, '.token-cache.json'); /** * Token 有效期(毫秒),默认 30 分钟 * @type {number} * @private */ this._tokenValidityMs = 30 * 60 * 1000; /** * 是否正在刷新 Token(防止并发重复请求) * @type {Promise|null} * @private */ this._refreshingPromise = null; /** * 内存中的 Token 缓存 * @type {TokenCache|null} * @private */ this._memoryCache = null; // 初始化时从文件加载 Token this._loadTokenFromFile(); } /** * 从文件加载 Token * @private */ _loadTokenFromFile() { try { if (fs.existsSync(this._tokenFilePath)) { const data = fs.readFileSync(this._tokenFilePath, 'utf-8'); const cache = JSON.parse(data); // 验证数据结构 if (cache.token && cache.expiresAt) { this._memoryCache = { token: cache.token, expiresAt: new Date(cache.expiresAt) }; const isExpired = this._isTokenExpired(); console.log(`📂 从文件加载 Token ${isExpired ? '(已过期)' : '(有效)'}`); console.log(` 过期时间: ${this._memoryCache.expiresAt.toLocaleString('zh-CN')}`); } else { console.warn('⚠️ Token 文件格式无效,将重新获取'); this._memoryCache = null; } } else { console.log('📄 Token 文件不存在,将创建新文件'); } } catch (error) { console.error('❌ 读取 Token 文件失败:', error.message); this._memoryCache = null; } } /** * 保存 Token 到文件 * @param {string} token - Token 值 * @private */ _saveTokenToFile(token) { try { const expiresAt = new Date(Date.now() + this._tokenValidityMs); const cache = { token, expiresAt: expiresAt.toISOString(), updatedAt: new Date().toISOString() }; fs.writeFileSync(this._tokenFilePath, JSON.stringify(cache, null, 2), 'utf-8'); console.log(`💾 Token 已保存到文件: ${this._tokenFilePath}`); console.log(` 过期时间: ${expiresAt.toLocaleString('zh-CN')}`); } catch (error) { console.error('❌ 保存 Token 文件失败:', error.message); } } /** * 删除 Token 文件 * @private */ _deleteTokenFile() { try { if (fs.existsSync(this._tokenFilePath)) { fs.unlinkSync(this._tokenFilePath); console.log('🗑️ Token 文件已删除'); } } catch (error) { console.error('❌ 删除 Token 文件失败:', error.message); } } /** * 检查 Token 是否过期 * @returns {boolean} * @private */ _isTokenExpired() { if (!this._memoryCache) { return true; } const now = new Date(); return now >= this._memoryCache.expiresAt; } /** * 清除 Token 缓存(内存和文件) */ clearTokenCache() { this._memoryCache = null; this._deleteTokenFile(); console.log('🗑️ Token 缓存已清除'); } /** * 设置 Token 有效期 * @param {number} minutes - 有效期(分钟) */ setTokenValidity(minutes) { this._tokenValidityMs = minutes * 60 * 1000; console.log(`⏱️ Token 有效期已设置为 ${minutes} 分钟`); } /** * 获取新的登录 Token * @returns {Promise} Token 值 * @throws {Error} 当获取失败时抛出错误 * @private */ async _fetchNewToken() { const config = { method: 'POST', url: 'https://xdwlgyl.yciccloud.com/gdpaas/login/doLogin.htm', headers: { 'host': 'xdwlgyl.yciccloud.com', 'sec-ch-ua': '"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"', 'accept': 'application/json, text/javascript, */*; q=0.01', 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8', 'x-requested-with': 'XMLHttpRequest', 'sec-ch-ua-mobile': '?0', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 'sec-ch-ua-platform': '"Windows"', 'origin': 'https://xdwlgyl.yciccloud.com', 'sec-fetch-site': 'same-origin', 'sec-fetch-mode': 'cors', 'sec-fetch-dest': 'empty', 'referer': 'https://xdwlgyl.yciccloud.com/gdpaas/login/index.htm', 'accept-encoding': 'gzip, deflate, br', 'accept-language': 'zh-CN,zh;q=0.9', 'cookie': 'SYS-A7EE-D0E8-BE614B80=1108%405b2bc1b4b8be40dfb104b1f5c84c1245;SameSite=Lax;HWWAFSESTIME=1767662772583;HWWAFSESID=7817173569731ea415;JSESSIONID=7AF7F82CBD7C1FFC6CBCAF67CFD22AC6', 'Connection': 'keep-alive' }, data: 'userId=15758339512&pwd=4E71002969FCD46813B869E931AEDF4B&randCode=&langId=' }; try { console.log('🔄 正在请求新 Token...'); const response = await axios.request(config); // 安全地提取 tokenId const tokenId = response?.data?.data?.user?.exts?.tokenId; if (!tokenId) { throw new Error('响应中未找到 tokenId'); } console.log('✅ 新 Token 获取成功:', tokenId); // 保存到内存和文件 const expiresAt = new Date(Date.now() + this._tokenValidityMs); this._memoryCache = { token: tokenId, expiresAt }; this._saveTokenToFile(tokenId); return tokenId; } catch (error) { const errorMsg = error.response?.data || error.message; console.error('❌ Token 获取失败:', errorMsg); throw new Error(`Token 获取失败: ${errorMsg}`); } } /** * 获取登录 Token(支持文件持久化和自动过期更新) * @param {boolean} forceRefresh - 是否强制刷新 Token * @returns {Promise<{success: boolean, data?: string, error?: string, timestamp: string, cached?: boolean, source?: string}>} */ async getToken(forceRefresh = false) { try { // 如果未强制刷新且 Token 未过期,直接返回缓存 if (!forceRefresh && !this._isTokenExpired()) { const source = this._memoryCache ? '文件缓存' : '内存缓存'; console.log(`♻️ 使用${source}的 Token`); return { success: true, data: this._memoryCache.token, timestamp: new Date().toISOString(), cached: true, source }; } // 如果正在刷新,等待刷新完成(防止并发请求) if (this._refreshingPromise) { console.log('⏳ Token 刷新中,等待完成...'); await this._refreshingPromise; return { success: true, data: this._memoryCache.token, timestamp: new Date().toISOString(), cached: false, source: '新获取' }; } // 开始刷新 Token this._refreshingPromise = this._fetchNewToken(); const token = await this._refreshingPromise; // 清除刷新标记 this._refreshingPromise = null; return { success: true, data: token, timestamp: new Date().toISOString(), cached: false, source: '新获取' }; } catch (error) { this._refreshingPromise = null; return { success: false, error: error.message, timestamp: new Date().toISOString() }; } } /** * 获取当前缓存的 Token(不检查过期) * @returns {string|null} */ getCachedToken() { return this._memoryCache?.token || null; } /** * 获取 Token 过期时间 * @returns {Date|null} */ getTokenExpiresAt() { return this._memoryCache?.expiresAt || null; } /** * 检查 Token 是否即将过期(剩余时间少于 5 分钟) * @returns {boolean} */ isTokenExpiringSoon() { if (!this._memoryCache) { return true; } const now = new Date(); const timeLeft = this._memoryCache.expiresAt - now; const fiveMinutes = 5 * 60 * 1000; return timeLeft < fiveMinutes; } /** * 获取 Token 剩余有效时间(秒) * @returns {number|null} 剩余秒数,如果 Token 不存在或已过期返回 null */ getTokenRemainingTime() { if (!this._memoryCache) { return null; } const now = new Date(); const timeLeft = Math.max(0, this._memoryCache.expiresAt - now); return Math.floor(timeLeft / 1000); } /** * 获取 Token 文件路径 * @returns {string} */ getTokenFilePath() { return this._tokenFilePath; } } /** * 导出单例实例 */ module.exports = new ExternalToken(); /** * 如果直接运行此文件,执行示例请求 */ if (require.main === module) { const collector = require('./loginApi.js'); collector.getToken() .then(result => { if (result.success) { console.log('✅ 数据获取成功'); console.log('📅 时间:', result.timestamp); console.log('📊 数据预览:', JSON.stringify(result.data, null, 2)); } else { console.error('❌ 数据获取失败:', result.error); } }) .catch(error => { console.error('💥 未捕获的错误:', error); }); }