349 lines
11 KiB
JavaScript
349 lines
11 KiB
JavaScript
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<string>} 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);
|
||
});
|
||
}
|