136 lines
5.5 KiB
JavaScript
136 lines
5.5 KiB
JavaScript
require('../../utils/fetch');
|
||
import crypto from 'crypto';
|
||
import { Buffer } from 'buffer';
|
||
import URL from 'url';
|
||
import { OakExternalException, OakNetworkException, OakServerProxyException, } from 'oak-domain/lib/types/Exception';
|
||
import { assert } from 'oak-domain/lib/utils/assert';
|
||
export class WechatNativeInstance {
|
||
appId;
|
||
appSecret;
|
||
accessToken;
|
||
refreshAccessTokenHandler;
|
||
externalRefreshFn;
|
||
constructor(appId, appSecret, accessToken, externalRefreshFn) {
|
||
this.appId = appId;
|
||
this.appSecret = appSecret;
|
||
this.externalRefreshFn = externalRefreshFn;
|
||
if (!appSecret && !externalRefreshFn) {
|
||
assert(false, 'appSecret和externalRefreshFn必须至少支持一个');
|
||
}
|
||
if (accessToken) {
|
||
this.accessToken = accessToken;
|
||
}
|
||
else {
|
||
this.refreshAccessToken().then(() => {
|
||
console.log(`WechatMp初始化获取accessToken成功,appId: [${this.appId}]`);
|
||
}).catch(() => {
|
||
console.log(`WechatMp初始化获取accessToken失败,appId: [${this.appId}]`);
|
||
});
|
||
}
|
||
}
|
||
async getAccessToken() {
|
||
while (true) {
|
||
if (this.accessToken) {
|
||
return this.accessToken;
|
||
}
|
||
await new Promise((resolve) => setTimeout(() => resolve(0), 500));
|
||
}
|
||
}
|
||
async access(url, init, mockData) {
|
||
if (process.env.NODE_ENV === 'development' && mockData) {
|
||
return mockData;
|
||
}
|
||
let response;
|
||
try {
|
||
response = await global.fetch(url, init);
|
||
}
|
||
catch (err) {
|
||
throw new OakNetworkException(`访问wechat接口失败,「${url}」`);
|
||
}
|
||
const { headers, status } = response;
|
||
if (![200, 201].includes(status)) {
|
||
throw new OakServerProxyException(`访问wechat接口失败,「${url}」,「${status}」`);
|
||
}
|
||
const contentType = headers['Content-Type'] || headers.get('Content-Type');
|
||
if (contentType?.includes('application/json')) {
|
||
const json = await response.json();
|
||
if (typeof json.errcode === 'number' && json.errcode !== 0) {
|
||
if ([40001, 42001].includes(json.errcode)) {
|
||
return this.refreshAccessToken(url, init);
|
||
}
|
||
throw new OakExternalException('wechat', json.errcode, json.errmsg);
|
||
}
|
||
return json;
|
||
}
|
||
if (contentType?.includes('text') ||
|
||
contentType?.includes('xml') ||
|
||
contentType?.includes('html')) {
|
||
const data = await response.text();
|
||
// 某些接口返回contentType为text/plain, 里面text是json结构
|
||
const isJson = this.isJson(data);
|
||
if (isJson) {
|
||
const json = JSON.parse(data);
|
||
if (typeof json.errcode === 'number' && json.errcode !== 0) {
|
||
if ([40001, 42001].includes(json.errcode)) {
|
||
return this.refreshAccessToken(url, init);
|
||
}
|
||
throw new OakExternalException('wechat', json.errcode, json.errmsg);
|
||
}
|
||
return json;
|
||
}
|
||
return data;
|
||
}
|
||
if (contentType?.includes('application/octet-stream')) {
|
||
return await response.arrayBuffer();
|
||
}
|
||
return response;
|
||
}
|
||
async code2Session(code) {
|
||
const result = await this.access(`https://api.weixin.qq.com/sns/oauth2/access_token?appid=${this.appId}&secret=${this.appSecret}&code=${code}&grant_type=authorization_code`, undefined, { session_key: 'aaa', openid: code, unionid: code });
|
||
const { session_key, openid, unionid } = typeof result === 'string' ? JSON.parse(result) : result; // 这里微信返回的数据有时候竟然是text/plain
|
||
return {
|
||
sessionKey: session_key,
|
||
openId: openid,
|
||
unionId: unionid,
|
||
};
|
||
}
|
||
async refreshAccessToken(url, init) {
|
||
const result = this.externalRefreshFn
|
||
? await this.externalRefreshFn(this.appId)
|
||
: await this.access(`https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${this.appId}&secret=${this.appSecret}`, undefined, { access_token: 'mockToken', expires_in: 600 });
|
||
const { access_token, expires_in } = result;
|
||
this.accessToken = access_token;
|
||
// 生成下次刷新的定时器
|
||
this.refreshAccessTokenHandler = setTimeout(() => {
|
||
this.refreshAccessToken();
|
||
}, (expires_in - 10) * 1000);
|
||
if (url) {
|
||
const url2 = new URL.URL(url);
|
||
url2.searchParams.set('access_token', access_token);
|
||
return this.access(url2.toString(), init);
|
||
}
|
||
}
|
||
isJson(data) {
|
||
try {
|
||
JSON.parse(data);
|
||
return true;
|
||
}
|
||
catch (e) {
|
||
return false;
|
||
}
|
||
}
|
||
decryptData(sessionKey, encryptedData, iv, signature) {
|
||
const skBuf = Buffer.from(sessionKey, 'base64');
|
||
// const edBuf = Buffer.from(encryptedData, 'base64');
|
||
const ivBuf = Buffer.from(iv, 'base64');
|
||
const decipher = crypto.createDecipheriv('aes-128-cbc', skBuf, ivBuf);
|
||
// 设置自动 padding 为 true,删除填充补位
|
||
decipher.setAutoPadding(true);
|
||
let decoded = decipher.update(encryptedData, 'base64', 'utf8');
|
||
decoded += decipher.final('utf8');
|
||
const data = JSON.parse(decoded);
|
||
assert(data.watermark.appid === this.appId);
|
||
return data;
|
||
}
|
||
}
|