UNPKG

wx-gzh

Version:
309 lines (242 loc) 7.77 kB
/* * @Description: * @Author: chtao * @Github: https://github.com/LadyYang * @Email: 1763615252@qq.com * @Date: 2020-07-26 20:45:01 * @LastEditTime: 2020-08-03 14:00:42 * @LastEditors: chtao * @FilePath: \wx-gzh\index.ts */ import crypto from 'crypto'; import { parseStringPromise } from 'xml2js'; import { responseText, responsePay } from './lib/response'; import { createMenu } from './lib/menu'; import { getQRCode } from './lib/code'; import Observe from './lib/Observe'; import { createH5PayOrder, createJSAPIPayOrder } from './lib/pay'; import { get } from './utils'; import getSignature from './lib/sdk'; let instance: WeChat | null = null; export default class WeChat extends Observe { public appID: string; public appsecret: string; private _accessToken!: string; private authToken: string; private path: string; /** 商户账号参数 */ public payOptions?: { /** 微信支付商户号 */ mch_id: string; /** 通知地址 */ notify_url: string; client_ip: string; /** API密钥 */ key: string; }; /** 微信网页开发时需要的 ticket */ public ticket?: string; constructor( options: { appID: string; token: string; appsecret: string; menuData: object; routePath: string; payOptions?: { /** 微信支付商户号 */ mch_id: string; /** 通知地址 */ notify_url: string; client_ip: string; /** API密钥 */ key: string; }; }, web: boolean = false ) { super(); this.authToken = options.token; this.appID = options.appID; this.appsecret = options.appsecret; this.path = options.routePath; this.payOptions = options.payOptions; (async () => { try { // 获取 token await this.getWechatAccessToken(); await createMenu.call(this, options.menuData); web && (await this.getWeChatTicket()); } catch (e) { this.emit('error', e); } })(); if (instance) { return instance; } instance = this; } get accessToken() { return this._accessToken; } set accessToken(val: string) { this._accessToken = val; this.emit('ready', val); } /** * 获取 access_token */ private async getWechatAccessToken() { const result: any = await get( `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${this.appID}&secret=${this.appsecret}` ); if (result.errmsg) { throw Error(JSON.stringify(result)); } this.accessToken = result.access_token; setTimeout( this.getWechatAccessToken.bind(this), (result.expires_in - 1200) * 1000 ); } private async getWeChatTicket() { const result: any = await get( `https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=${this.accessToken}&type=jsapi` ); this.ticket = result.ticket; // if (result.errcode !== 0) { // throw new Error(result); // } setTimeout( this.getWeChatTicket.bind(this), (result.expires_in - 1200) * 1000 ); } private async dealGZHEvent(ctx: any) { const { xml } = ctx.request.body; for (let key in xml) { xml[key] = xml[key][0]; } let contents: any[]; switch (xml.MsgType) { case 'text': contents = await this.emit('text', xml); return responseText(xml, contents.join(''), ctx); } switch (xml.Event) { case 'SCAN': contents = await this.emit('scan', xml); return responseText(xml, contents.join(''), ctx); case 'subscribe': contents = await this.emit('subscribe', xml); return responseText(xml, contents.join(''), ctx); case 'unsubscribe': contents = await this.emit('unsubscribe', xml); return responseText(xml, contents.join(''), ctx); case 'CLICK': contents = await this.emit('click', xml); return responseText(xml, contents.join(''), ctx); default: contents = await this.emit('unknow', xml); return responseText(xml, contents.join(''), ctx); } } async _useMiddleware(ctx: any, next: any) { try { if (ctx.href.includes(this.path) && /^(POST)$/i.test(ctx.method)) { return new Promise(resolve => { let result = ''; ctx.req.on('data', (chunk: any) => (result += chunk)); ctx.req.on('end', async () => { try { const res = await parseStringPromise(result); if (!(ctx.request.body && typeof ctx.request.body === 'object')) { ctx.request.body = {}; } ctx.request.body.xml = res.xml; await this.dealGZHEvent(ctx); resolve(); } catch (e) { this.emit('error', e); } }); ctx.req.on('aborted', (e: any) => this.emit('error', e)); ctx.req.on('error', (e: any) => this.emit('error', e)); }); } // 验证 if (ctx.href.includes(this.path) && /^(GET)$/i.test(ctx.method)) { return await this.auth(ctx); } if (ctx.href.includes(this.payOptions?.notify_url)) { return new Promise(resolve => { let result = ''; ctx.req.on('data', (chunk: any) => (result += chunk)); ctx.req.on('end', async () => { try { const res = await parseStringPromise(result); if (!(ctx.request.body && typeof ctx.request.body === 'object')) { ctx.request.body = {}; } ctx.request.body.xml = res.xml; await this.dealPayEvent(ctx); resolve(); } catch (e) { this.emit('error', e); } }); ctx.req.on('aborted', (e: any) => this.emit('error', e)); ctx.req.on('error', (e: any) => this.emit('error', e)); }); } await next(); } catch (e) { responseText(ctx.request.body.xml, 'error', ctx); this.emit('error', e); } } private async dealPayEvent(ctx: any) { const { xml } = ctx.request.body; for (let key in xml) { xml[key] = xml[key][0]; } if (xml.return_code === 'SUCCESS') { await this.emit('paySuccess', xml); responsePay(ctx); } if (xml.return_code === 'FAIL') { await this.emit('payFail', undefined); responsePay(ctx); } } useMiddleware = this._useMiddleware.bind(this); getQRCode = getQRCode; createH5PayOrder = createH5PayOrder; createJSAPIPayOrder = createJSAPIPayOrder; getSignature = getSignature; /** 开启 web 开发,用户获取 ticket */ public async openWeb() { await this.getWeChatTicket(); } /** 验证微信接口 */ public async auth(ctx: any) { const { signature, timestamp, nonce, echostr } = ctx.request.query; const authStr = [this.authToken, timestamp, nonce].sort().join(''); const hashStr = crypto .createHash('sha1') .update(authStr, 'utf8') .digest('hex'); if (hashStr === signature) { ctx.body = echostr; } else { ctx.body = { message: '微信回调认证失败', }; } } public async getUserInfo(openid: string) { return await get( `https://api.weixin.qq.com/cgi-bin/user/info?access_token=${this.accessToken}&openid=${openid}&lang=zh_CN` ); } }