"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 = 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 = 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 = 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; } } exports.WechatMpInstance = WechatMpInstance;