@neuit/wemp
Version:
用于管理微信服务号的Node.js接口库
317 lines (280 loc) • 8.92 kB
JavaScript
//#region 导入依赖
const request = require("request");
const crypto = require("crypto");
const config = require('../config');
const path = require('path');
const fs = require('fs');
//#endregion
/**
* 企业微信API基础类
* 处理access_token的获取和缓存等基础功能
*/
class Base {
constructor() {
this.appid = config.getConfig('appid');
this.secret = config.getConfig('secret');
this.cache_mode = config.getConfig('cache_mode') || 'file';
if (this.cache_mode === 'file') {
// 设置token存储路径
this.tokenDir = path.resolve(process.cwd(), '.neuit');
this.tokenFile = path.join(this.tokenDir, `${this.secret}_access_token.json`);
this.stableTokenFile = path.join(this.tokenDir, `${this.secret}_stable_access_token.json`);
}
}
//#region 公开方法
verifySignature(signature, timestamp, nonce) {
// 获取token
const token = config.getConfig('mp_token');
// 1. 将token、timestamp、nonce三个参数进行字典序排序
const arr = [token, timestamp, nonce].sort();
// 2. 将三个参数字符串拼接成一个字符串进行sha1加密
const str = arr.join('');
const sha1 = crypto.createHash('sha1');
const hash = sha1.update(str).digest('hex');
// 3. 开发者获得加密后的字符串可与signature对比,标识该请求来源于微信
return hash === signature;
}
async getJssdkConfig(url) {
const ticket = await this.getJsapiTicket();
console.log(`ticket:${ticket}`);
const timestamp = Date.now().toString().slice(0, 10); // 当前时间戳
const nonceStr = Math.random().toString(36).substring(2, 15); // 随机字符串
const string1 = `jsapi_ticket=${ticket}&noncestr=${nonceStr}×tamp=${timestamp}&url=${url}`;
console.log(`string1:${string1}`);
const signature = crypto.createHash('sha1').update(string1).digest('hex'); // 生成SHA1签名
return {
appId: this.appid,
timestamp,
nonceStr,
signature
}
}
async getJsapiTicket() {
if (this.cache_mode === 'file') {
try {
const path = require('path');
const ticketFile = path.join(this.tokenDir, `${this.secret}_jsapi_ticket.json`);
// Try reading from file first
if (fs.existsSync(ticketFile)) {
const ticketData = JSON.parse(fs.readFileSync(ticketFile));
if (ticketData.expires_time > Date.now()) {
return ticketData.ticket;
}
}
} catch (error) {
console.error('读取jsapi_ticket出错:', error);
}
}
// If file doesn't exist, expired, or error occurred, fetch new ticket
const access_token = await this.getAccessToken();
const res = await this.curl('https://api.weixin.qq.com/cgi-bin/ticket/getticket', 'GET', {
access_token,
type: 'jsapi'
});
if (res.errcode) {
throw new Error(`获取jsapi_ticket失败:${res.errmsg}`);
}
if (this.cache_mode === 'file') {
// Save new ticket to file
const ticketData = {
ticket: res.ticket,
expires_time: Date.now() + (res.expires_in - 200) * 1000
};
fs.writeFileSync(
path.join(this.tokenDir, `${this.secret}_jsapi_ticket.json`),
JSON.stringify(ticketData)
);
}
return res.ticket;
}
/**
* 获取access_token
* @param {string} _url - 请求的URL
* @returns {Promise<string>} access_token
*/
async getAccessToken(_url, type = '') {
// 如果是获取token的接口,直接返回空字符串,避免循环调用
if (_url === 'https://api.weixin.qq.com/cgi-bin/token' || _url === 'https://api.weixin.qq.com/sns/oauth2/access_token' || _url === 'https://api.weixin.qq.com/cgi-bin/stable_token') {
return '';
}
if (this.cache_mode === 'file') {
try {
const token = await this.getTokenFromFile(type === 'stable' ? this.stableTokenFile : this.tokenFile);
if (token) {
return token;
}
} catch (error) {
console.error('读取access_token出错:', error);
}
// 如果到这里,说明文件不存在、token已过期或发生错误,需要重新获取
if (type === 'stable') {
const newToken = await this.fetchStableAccessToken();
return newToken.access_token;
} else {
const newToken = await this.fetchAccessToken();
return newToken.access_token;
}
}
return '';
}
/**
* 发送HTTP请求
* @param {string} _url - 请求地址
* @param {string} _method - 请求方法
* @param {Object} _params - 请求参数
* @returns {Promise<Object>} 响应结果
*/
async curl(_url, _method, _params, type = '') {
const data = {
..._params
};
const access_token = data.access_token || await this.getAccessToken(_url, type);
console.log(`access_token:${access_token}`);
const params = this.removeControlProperties(data);
// console.log(`请求参数:${JSON.stringify(params)}`)
return new Promise((resolve, reject) => {
const options = {
url: _url,
method: _method,
json: true,
headers: {
'Content-Type': 'application/json',
'Accept': '*/*' // 接受所有类型的响应
}
};
const url = _url + '?access_token=' + access_token + '&debug=1';
if (_method.toUpperCase() === 'GET') {
// GET请求将参数添加到URL查询字符串
const queryString = new URLSearchParams(params).toString();
options.url = `${url}${url.includes('?') ? '&' : '?'}${queryString}`;
} else {
// POST等其他方法将数据放在body中
options.url = url;
options.body = params;
}
request(options, (error, response, body) => {
if (error) {
reject(error);
} else {
resolve(body);
}
});
});
}
//#endregion
//#region 内部方法
setSecret(_token_type) {
if (_token_type === 'agent') {
this.secret = config.getConfig('secret');
} else if (_token_type === 'concat') {
this.secret = config.getConfig('concat_secret');
}
if (this.cache_mode === 'file') {
const path = require('path');
// 设置token存储路径
this.tokenDir = path.resolve(process.cwd(), '.neuit');
this.tokenFile = path.join(this.tokenDir, `${this.secret}_access_token.json`);
}
}
/**
* 从企业微信服务器获取新的access_token
* @returns {Promise<Object>} 包含access_token的响应对象
* @private
*/
async fetchAccessToken() {
const res = await this.curl('https://api.weixin.qq.com/cgi-bin/token', 'GET', {
appid: this.appid,
secret: this.secret,
grant_type: 'client_credential'
});
if (res.errcode) {
throw new Error(`获取access_token失败:${res.errmsg}`);
}
if (this.cache_mode === 'file' && res.access_token) {
await this.saveTokenToFile(res);
}
return res;
}
/**
* 从企业微信服务器获取稳定版access_token
* @param {boolean} [forceRefresh=false] - 是否强制刷新token
* @returns {Promise<Object>} 包含access_token的响应对象
* @private
*/
async fetchStableAccessToken(forceRefresh = false) {
const res = await this.curl('https://api.weixin.qq.com/cgi-bin/stable_token', 'POST', {
grant_type: 'client_credential',
appid: this.appid,
secret: this.secret,
force_refresh: forceRefresh
});
if (res.errcode) {
throw new Error(`获取稳定版access_token失败:${res.errmsg}`);
}
if (this.cache_mode === 'file' && res.access_token) {
await this.saveTokenToFile(res);
}
return res;
}
/**
* 将token信息保存到文件中
* @param {Object} tokenData - 包含access_token的数据对象
* @private
*/
async saveTokenToFile(tokenData, tokenFile = this.tokenFile) {
const fs = require('fs');
const data = {
...tokenData,
expires_at: Date.now() + 7000 * 1000 // 设置过期时间为当前时间后2小时
};
// 确保存储目录存在
if (!fs.existsSync(this.tokenDir)) {
fs.mkdirSync(this.tokenDir, {
recursive: true
});
}
// 写入token文件
fs.writeFileSync(tokenFile, JSON.stringify(data, null, 2), 'utf8');
}
/**
* 从文件中读取token信息
* @returns {Promise<string|null>} 有效的access_token或null
* @private
*/
async getTokenFromFile(tokenFile = this.tokenFile) {
const fs = require('fs');
if (fs.existsSync(tokenFile)) {
try {
const tokenData = JSON.parse(fs.readFileSync(tokenFile, 'utf8'));
// console.log(`缓存参数:${JSON.stringify(tokenData)}`);
if (Date.now() < tokenData.expires_at) {
return tokenData.access_token;
} else {
return null;
}
} catch (error) {
console.error('读取access_token出错:', error);
return null;
}
}
return null;
}
/**
* 处理请求参数,移除空值属性
* @param {Object} obj - 原始参数对象
* @returns {Object} 处理后的参数对象
* @private
*/
removeControlProperties(obj) {
const result = {};
for (const key in obj) {
// 过滤掉值为空字符串、null或undefined的属性
if (obj[key] !== null && obj[key] !== undefined && obj[key] !== '') {
result[key] = obj[key];
}
}
return result;
}
//#endregion
}
module.exports = Base;