wx-gzh
Version:
309 lines (242 loc) • 7.77 kB
text/typescript
/*
* @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`
);
}
}