oak-external-sdk/lib/service/wechat/WechatMp.js

495 lines
19 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.WechatMpInstance = void 0;
const tslib_1 = require("tslib");
require('../../utils/fetch');
const crypto_1 = tslib_1.__importDefault(require("crypto"));
const buffer_1 = require("buffer");
const url_1 = tslib_1.__importDefault(require("url"));
const form_data_1 = tslib_1.__importDefault(require("../../utils/form-data"));
const Exception_1 = require("oak-domain/lib/types/Exception");
const assert_1 = require("oak-domain/lib/utils/assert");
class WechatMpInstance {
appId;
appSecret;
accessToken;
refreshAccessTokenHandler;
externalRefreshFn;
constructor(appId, appSecret, accessToken, externalRefreshFn) {
this.appId = appId;
this.appSecret = appSecret;
this.externalRefreshFn = externalRefreshFn;
if (!appSecret && !externalRefreshFn) {
(0, assert_1.assert)(false, 'appSecret和externalRefreshFn必须至少支持一个');
}
if (accessToken) {
this.accessToken = accessToken;
}
else {
this.refreshAccessToken();
}
}
async getAccessToken() {
while (true) {
if (this.accessToken) {
return this.accessToken;
}
await new Promise((resolve) => setTimeout(() => resolve(0), 500));
}
}
async access(url, init, fresh) {
let response;
try {
response = await global.fetch(url, init);
}
catch (err) {
throw new Exception_1.OakNetworkException(`访问wechatMp接口失败${url}`);
}
const { headers, status } = response;
if (![200, 201].includes(status)) {
throw new Exception_1.OakServerProxyException(`访问wechatMp接口失败${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 ([42001, 40001].includes(json.errcode)) {
if (fresh) {
throw new Exception_1.OakServerProxyException('刚刷新的token不可能马上过期请检查是否有并发刷新token的逻辑');
}
return this.refreshAccessToken(url, init);
}
throw new Exception_1.OakExternalException('wechatMp', 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)) {
if (fresh) {
throw new Exception_1.OakServerProxyException('刚刷新的token不可能马上过期请检查是否有并发刷新token的逻辑');
}
return this.refreshAccessToken(url, init);
}
throw new Exception_1.OakExternalException('wechatMp', 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/jscode2session?appid=${this.appId}&secret=${this.appSecret}&js_code=${code}&grant_type=authorization_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}`);
const { access_token, expires_in } = result;
this.accessToken = access_token;
if (process.env.NODE_ENV === 'development') {
console.log(`小程序获得新的accessToken。appId:[${this.appId}], token: [${access_token}]`);
}
// 生成下次刷新的定时器
this.refreshAccessTokenHandler = setTimeout(() => {
this.refreshAccessToken();
}, (expires_in - 10) * 1000);
if (url) {
const url2 = new url_1.default.URL(url);
url2.searchParams.set('access_token', access_token);
return this.access(url2.toString(), init);
}
}
decryptData(sessionKey, encryptedData, iv, signature) {
const skBuf = buffer_1.Buffer.from(sessionKey, 'base64');
// const edBuf = Buffer.from(encryptedData, 'base64');
const ivBuf = buffer_1.Buffer.from(iv, 'base64');
const decipher = crypto_1.default.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);
(0, assert_1.assert)(data.watermark.appid === this.appId);
return data;
}
async getMpUnlimitWxaCode({ scene, page, envVersion = 'release', width, autoColor, lineColor, isHyaline, }) {
const token = await this.getAccessToken();
const result = await this.access(`https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=${token}`, {
method: 'POST',
headers: {
'Content-type': 'application/json',
Accept: 'image/jpg',
},
body: JSON.stringify({
scene,
page,
env_version: envVersion,
width,
auto_color: autoColor,
line_color: lineColor,
is_hyaline: isHyaline,
}),
});
return (await result.arrayBuffer());
}
async getUserPhoneNumber(code) {
const token = await this.getAccessToken();
const result = (await this.access(`https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=${token}`, {
method: 'POST',
headers: {
'Content-type': 'application/json',
},
body: JSON.stringify({
code,
}),
}));
return result.phone_info;
}
/**
* 发送订阅消息
* @param param0
* @returns
* https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/mp-message-management/subscribe-message/sendMessage.html
*/
async sendSubscribedMessage({ templateId, page, openId, data, state, lang, }) {
const token = await this.getAccessToken();
/**
* 实测若用户未订阅会抛出errcode: 43101, errmsg: user refuse to accept the msg
*/
return this.access(`https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=${token}`, {
body: JSON.stringify({
template_id: templateId,
page,
touser: openId,
data,
miniprogram_state: state || 'formal',
lang: lang || 'zh_CN',
}),
method: 'post',
});
}
//创建临时素材
async createTemporaryMaterial(options) {
const { type, media, filetype, filename } = options;
const formData = new form_data_1.default();
formData.append('media', media, {
contentType: filetype,
filename: filename, // 微信识别需要
});
const getLength = () => {
return new Promise((resolve, reject) => {
formData.getLength((err, length) => {
if (err) {
reject(err);
}
else {
resolve(length);
}
});
});
};
const contentLength = await getLength();
const headers = formData.getHeaders();
headers['Content-Length'] = contentLength;
const myInit = {
method: 'POST',
headers,
};
Object.assign(myInit, {
body: formData,
});
const token = await this.getAccessToken();
const result = await this.access(`https://api.weixin.qq.com/cgi-bin/media/upload?access_token=${token}&type=${type}`, myInit);
return result;
}
// 获取临时素材
async getTemporaryMaterial(options) {
const { mediaId } = options;
const myInit = {
method: 'GET',
};
const token = await this.getAccessToken();
const result = await this.access(`https://api.weixin.qq.com/cgi-bin/media/get?access_token=${token}&media_id=${mediaId}`, myInit);
if (this.isJson(result)) {
return result;
}
const arrayBuffer = await result.arrayBuffer();
return arrayBuffer;
}
async sendServeMessage(options) {
const { openId, type } = options;
const myInit = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
};
switch (type) {
case 'text': {
Object.assign(myInit, {
body: JSON.stringify({
touser: openId,
msgtype: 'text',
text: {
content: options.content,
},
}),
});
break;
}
case 'image': {
Object.assign(myInit, {
body: JSON.stringify({
touser: openId,
msgtype: 'image',
image: {
media_id: options.mediaId,
},
}),
});
break;
}
case 'news': {
Object.assign(myInit, {
body: JSON.stringify({
touser: openId,
msgtype: 'link',
link: {
title: options.title,
description: options.description,
url: options.url,
thumb_url: options.picurl,
},
}),
});
break;
}
case 'mp': {
Object.assign(myInit, {
body: JSON.stringify({
touser: openId,
msgtype: 'miniprogrampage',
miniprogrampage: {
title: options.data.title,
pagepath: options.data.pagepath,
thumb_media_id: options.data.thumbnailId,
},
}),
});
break;
}
default: {
(0, assert_1.assert)(false, '当前消息类型暂不支持');
}
}
const token = await this.getAccessToken();
const result = await this.access(`https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=${token}`, myInit);
const { errcode } = result;
if (errcode === 0) {
return Object.assign({ success: true }, result);
}
return Object.assign({ success: false }, result);
}
async getAllPrivateTemplate() {
const myInit = {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
};
const token = await this.getAccessToken();
const result = (await this.access(`https://api.weixin.qq.com/wxaapi/newtmpl/gettemplate?access_token=${token}`, myInit));
return result.data;
}
isJson(data) {
try {
JSON.parse(data);
return true;
}
catch (e) {
return false;
}
}
async getURLScheme(options) {
const { jump_wxa, expiresAt, expireType = 0, expireInterval } = options;
const token = await this.getAccessToken();
const result = await this.access(`https://api.weixin.qq.com/wxa/generatescheme?access_token=${token}`, {
method: 'POST',
body: JSON.stringify({
jump_wxa: jump_wxa,
is_expire: true,
expire_type: expireType,
expire_time: expiresAt,
expire_interval: expireInterval,
}),
});
return result;
}
/**
* https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/business-capabilities/order-shipping/order-shipping.html#%E4%B8%80%E3%80%81%E5%8F%91%E8%B4%A7%E4%BF%A1%E6%81%AF%E5%BD%95%E5%85%A5%E6%8E%A5%E5%8F%A3
* 发货信息录入
*/
async uploadShippingInfo(info) {
// 检查一下数据
const { order_number_type, transaction_id, mchid, out_trade_no } = info.order_key;
(0, assert_1.assert)(order_number_type === 1 && mchid && out_trade_no || order_number_type === 2 && transaction_id);
const { delivery_mode, is_all_delivered, shipping_list } = info;
(0, assert_1.assert)(delivery_mode === 1 || typeof is_all_delivered === 'boolean');
(0, assert_1.assert)(shipping_list.length > 0 && shipping_list.length <= 10);
const token = await this.getAccessToken();
const result = await this.access(`https://api.weixin.qq.com/wxa/sec/order/upload_shipping_info?access_token=${token}`, {
method: 'POST',
body: JSON.stringify(info),
});
return result;
}
/**
* 查询订单的发货状态
* https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/business-capabilities/order-shipping/order-shipping.html#%E4%B8%89%E3%80%81%E6%9F%A5%E8%AF%A2%E8%AE%A2%E5%8D%95%E5%8F%91%E8%B4%A7%E7%8A%B6%E6%80%81
* @param info
*/
async getOrderState(info) {
const token = await this.getAccessToken();
const result = await this.access(`https://api.weixin.qq.com/wxa/sec/order/get_order?access_token=${token}`, {
method: 'POST',
body: JSON.stringify(info),
});
const { order } = result;
const { order_state, in_complaint, shipping } = order;
return {
orderState: order_state,
inComplaint: in_complaint,
shipping: shipping
};
}
/**
* 获取运力id列表get_delivery_list
* https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/industry/express/business/express_search.html#%E8%8E%B7%E5%8F%96%E8%BF%90%E5%8A%9Bid%E5%88%97%E8%A1%A8get-delivery-list
*/
async getDeliveryList() {
const token = await this.getAccessToken();
const result = await this.access(`https://api.weixin.qq.com/cgi-bin/express/delivery/open_msg/get_delivery_list?access_token=${token}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({}),
});
// const { errcode, delivery_list, count } = result;
return result;
}
/**
* 生成物流运单
* https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/express/express-by-business/addOrder.html
*/
async addExpressOrder(data) {
const token = await this.getAccessToken();
const result = await this.access(`https://api.weixin.qq.com/cgi-bin/express/business/order/add?access_token=${token}`, {
method: 'POST',
body: JSON.stringify(data),
});
const { waybill_id, waybill_data } = result;
return {
waybill_id,
waybill_data,
};
}
/**
* 取消物流运单
* https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/express/express-by-business/cancelOrder.html
* @param data
*/
async cancelExpressOrder(data) {
const token = await this.getAccessToken();
await this.access(`https://api.weixin.qq.com/cgi-bin/express/business/order/cancel?access_token=${token}`, {
method: 'POST',
body: JSON.stringify(data),
});
}
/**
* 查询运单轨迹
* https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/express/express-by-business/getPath.html
* @param data
*/
async getExpressPath(data) {
const token = await this.getAccessToken();
const result = await this.access(`https://api.weixin.qq.com/cgi-bin/express/business/path/get?access_token=${token}`, {
method: 'POST',
body: JSON.stringify(data),
});
const { path_item_num, path_item_list } = result;
return {
path_item_num,
path_item_list,
};
}
/**
* 获取运单数据
* https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/express/express-by-business/getOrder.html
* @param data
*/
async getExpressOrder(data) {
const token = await this.getAccessToken();
const result = await this.access(`https://api.weixin.qq.com/cgi-bin/express/business/order/get?access_token=${token}`, {
method: 'POST',
body: JSON.stringify(data),
});
return result;
}
/**
* 确认收货提醒
* https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/business-capabilities/order-shipping/order-shipping.html
* @param data
*/
async notifyConfirmReceive(data) {
const token = await this.getAccessToken();
await this.access(`https://api.weixin.qq.com/wxa/sec/order/notify_confirm_receive?access_token=${token}`, {
method: 'POST',
body: JSON.stringify(data),
});
}
/**
* 绑定/解绑物流账号
* https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/express/express-by-business/bindAccount.html
* @param data
*/
async bindExpressAccount(data) {
const token = await this.getAccessToken();
const result = await this.access(`https://api.weixin.qq.com/cgi-bin/express/business/account/bind?access_token=${token}`, {
method: 'POST',
body: JSON.stringify(data),
});
return result;
}
/**
* 获取所有绑定的物流账号
* https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/express/express-by-business/getAllAccount.html
*/
async getExpressAccount() {
const token = await this.getAccessToken();
const result = await this.access(`https://api.weixin.qq.com/cgi-bin/express/business/account/getall?access_token=${token}`, {
method: 'GET'
});
return result;
}
}
exports.WechatMpInstance = WechatMpInstance;