|
@@ -0,0 +1,256 @@
|
|
|
+import { RedisClientType } from '@redis/client';
|
|
|
+import Crypto, { CipherKey } from 'crypto';
|
|
|
+
|
|
|
+const XML2JS = require(`xml2js`);
|
|
|
+
|
|
|
+
|
|
|
+const randomStr = (length?:number,nonum?:boolean)=>{
|
|
|
+ let arr = '1234567890abcdefghijklmnopqrstuvwxyz';
|
|
|
+ if(nonum) arr = 'abcdefghijklmnopqrstuvwxyz';
|
|
|
+ let str = '';
|
|
|
+ length = length?length:3;
|
|
|
+ for(let i = 0;i<length;i++){
|
|
|
+ str += arr.substr(Math.floor(Math.random()*26),1);
|
|
|
+ }
|
|
|
+ return str;
|
|
|
+}
|
|
|
+
|
|
|
+const WechatMchDomain = `https://api.mch.weixin.qq.com`;
|
|
|
+
|
|
|
+class WechatMchSDK{
|
|
|
+ private _redisClient;
|
|
|
+ private _mchId;
|
|
|
+ private _appId;
|
|
|
+ private _privateKeyPem;
|
|
|
+ private _privateCertPem;
|
|
|
+ private _privateKeySerial;
|
|
|
+ private _privateAPIV3Key;
|
|
|
+ private _privateAPIKey;
|
|
|
+ private _wechatMchApiURL;
|
|
|
+ constructor(redisInstance:RedisClientType,config:any){
|
|
|
+ this._redisClient = redisInstance;
|
|
|
+ this._mchId = config.mchId;
|
|
|
+ this._appId = config.appId;
|
|
|
+ this._privateKeyPem = null;
|
|
|
+ this._privateCertPem = null;
|
|
|
+ this._privateKeySerial = config.privateKeySerial;
|
|
|
+ this._privateAPIV3Key = config.privateAPIV3Key;
|
|
|
+ this._privateAPIKey = config.privateAPIKey;
|
|
|
+ if(config.privateKeyFilePath){
|
|
|
+ this._privateKeyPem = FILE.readFileSync(config.privateKeyFilePath,'utf8');
|
|
|
+ this._privateCertPem = FILE.readFileSync(config.privateCertFilePath,'utf8');
|
|
|
+ if(!this._privateKeyPem){
|
|
|
+ throw new Error('[Wechat-Mch-SDK] Config private key pem file read error');
|
|
|
+ }
|
|
|
+ if(!this._privateCertPem){
|
|
|
+ throw new Error('[Wechat-Mch-SDK] Config private key pem file read error');
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if(config.privateKeyPEM){
|
|
|
+ this._privateKeyPem = config.privateKeyPEM;
|
|
|
+ }
|
|
|
+
|
|
|
+ if(!this._privateKeyPem){
|
|
|
+ throw new Error('[Wechat-Mch-SDK] Private Key read error');
|
|
|
+ }
|
|
|
+
|
|
|
+ this._wechatMchApiURL = {
|
|
|
+ orderTrade_JsAPI:`${WechatMchDomain}/v3/pay/transactions/jsapi`,
|
|
|
+ businessPay_To_Openid:`${WechatMchDomain}/mmpaymkttransfers/promotion/transfers`,
|
|
|
+ getPlatformCertificates:`${WechatMchDomain}/v3/certificates`,
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ getPlatformCertificates(){
|
|
|
+ return new Promise(async (resolve,reject)=>{
|
|
|
+ const certificate = await this._redisClient.get(`WECHAT_MCH_${this._mchId}_PLATFORM_CERT`);
|
|
|
+ if(certificate) return resolve(certificate);
|
|
|
+ const result = await this._getNewPlatformCertificates();
|
|
|
+ const cert = result.cert;
|
|
|
+
|
|
|
+ if(cert){
|
|
|
+ await this._redisClient.set(`WECHAT_MCH_${this._mchId}_PLATFORM_CERT`,cert.certificate);
|
|
|
+ await this._redisClient.expire(`WECHAT_MCH_${this._mchId}_PLATFORM_CERT`,7200);
|
|
|
+ resolve(cert.certificate);
|
|
|
+ }else{
|
|
|
+ resolve(null);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ async _getNewPlatformCertificates(){
|
|
|
+ const signedObj = this.makeSignRequestData(this._wechatMchApiURL.getPlatformCertificates,'GET','');
|
|
|
+ const getPlatformCertsResult = await this._requestMchV3Api(this._wechatMchApiURL.getPlatformCertificates,'GET',signedObj.timestamp,signedObj.nonce_str,'',signedObj.signature) as AnyKeyString;
|
|
|
+ if(getPlatformCertsResult.statusCode==200){
|
|
|
+ const cert = getPlatformCertsResult.data.data[0];
|
|
|
+ const certContent = this._decryptByV3Key(cert.encrypt_certificate.ciphertext,cert.encrypt_certificate.nonce,cert.encrypt_certificate.associated_data);
|
|
|
+ const certSerial = cert.serial_no;
|
|
|
+ return({cert:{serial:certSerial,certificate:certContent}});
|
|
|
+ }else{
|
|
|
+ LOGGER.error('Get new platform certificates failed');
|
|
|
+ LOGGER.error(getPlatformCertsResult.toString());
|
|
|
+ return({cert:null});
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async orderTrade_JSAPI(openid:string,trade_no:string,amount:number,desc:string,expire_unix_timestamp:number,notify_url:string){
|
|
|
+ const postData = {
|
|
|
+ appid:this._appId,
|
|
|
+ mchid:this._mchId,
|
|
|
+ description:desc,
|
|
|
+ out_trade_no:trade_no,
|
|
|
+ time_expire:Moment.unix(expire_unix_timestamp).format(),
|
|
|
+ notify_url:notify_url,
|
|
|
+ amount:{
|
|
|
+ total:amount,
|
|
|
+ currency:'CNY'
|
|
|
+ },
|
|
|
+ payer:{
|
|
|
+ openid:openid
|
|
|
+ }
|
|
|
+ };
|
|
|
+ const originBody = JSON.stringify(postData);
|
|
|
+ const signedObj = this.makeSignRequestData(this._wechatMchApiURL.orderTrade_JsAPI,'POST',postData);
|
|
|
+ const result = await this._requestMchV3Api(this._wechatMchApiURL.orderTrade_JsAPI,'POST',signedObj.timestamp,signedObj.nonce_str,originBody,signedObj.signature) as AnyKeyString;
|
|
|
+ if(result.statusCode==200){
|
|
|
+ return result.data.prepay_id;
|
|
|
+ }else{
|
|
|
+ LOGGER.error('Wechat Mch orderTrade error:');
|
|
|
+ LOGGER.error(JSON.stringify(result));
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ businessPay_To_Openid(orderid:string,openid:string,amount:number,desc:string){
|
|
|
+ return new Promise(resolve=>{
|
|
|
+ const postDataArr:any[] = [];
|
|
|
+ const postData:AnyKeyString = {
|
|
|
+ mch_appid: this._appId,
|
|
|
+ mchid: this._mchId,
|
|
|
+ nonce_str: randomStr(32).toUpperCase(),
|
|
|
+ partner_trade_no: orderid,
|
|
|
+ openid,
|
|
|
+ check_name: 'NO_CHECK',
|
|
|
+ amount,
|
|
|
+ desc
|
|
|
+ }
|
|
|
+ Object.keys(postData).forEach(key=>{
|
|
|
+ postDataArr.push(`${key}=${postData[key]}`);
|
|
|
+ });
|
|
|
+ postDataArr.sort();
|
|
|
+ let postStr = postDataArr.join('&');
|
|
|
+ postStr += `&key=${this._privateAPIKey}`;
|
|
|
+ const md5 = Crypto.createHash('md5')
|
|
|
+ postData.sign = md5.update(postStr).digest('hex').toUpperCase();
|
|
|
+
|
|
|
+ let postXml = '<xml>';
|
|
|
+ Object.keys(postData).forEach(key=>{
|
|
|
+ postXml += `<${key}>${postData[key]}</${key}>`;
|
|
|
+ });
|
|
|
+ postXml += '</xml>'
|
|
|
+
|
|
|
+ REQUEST({url:this._wechatMchApiURL.businessPay_To_Openid,method:'POST',body:postXml,encoding:'utf-8',cert:this._privateCertPem,key:this._privateKeyPem},(err:any,response:any,body:any)=>{
|
|
|
+ const x2jparser = XML2JS.parseString;
|
|
|
+ x2jparser(body, function (err:any, result:any) {
|
|
|
+ if(result.xml && result.xml.result_code[0]!='SUCCESS'){
|
|
|
+ return resolve({err:result.xml.err_code[0],data:result.xml.err_code_des[0]})
|
|
|
+ }
|
|
|
+ resolve({err:null,data:'SUCCESS'});
|
|
|
+ });
|
|
|
+ })
|
|
|
+ });
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ makeSignPaymentData(prepayId:string){
|
|
|
+ const randStr = randomStr(32).toUpperCase(),timestamp = Moment().unix(),packageStr = `prepay_id=${prepayId}`;
|
|
|
+ let preStr = ''
|
|
|
+ preStr += `${this._appId}\n`;
|
|
|
+ preStr += `${timestamp}\n`;
|
|
|
+ preStr += `${randStr}\n`;
|
|
|
+ preStr += `${packageStr}\n`;
|
|
|
+
|
|
|
+ const signedStr = this._signWithRSASHA256(preStr);
|
|
|
+
|
|
|
+ return {timestamp,nonce_str:randStr,signature:signedStr,package:packageStr,prepay_id:prepayId};
|
|
|
+ }
|
|
|
+
|
|
|
+ makeSignRequestData(requestUrl:string,method:string,content:AnyKeyString|string){
|
|
|
+ const randStr = randomStr(32).toUpperCase(),timestamp = Moment().unix();
|
|
|
+ const url = new URL(requestUrl);
|
|
|
+ const postData = (typeof content === 'object')?JSON.stringify(content):content;
|
|
|
+ let preStr = ''
|
|
|
+ preStr += `${method.toUpperCase()}\n`;
|
|
|
+ preStr += `${url.pathname}${url.search}\n`;
|
|
|
+ preStr += `${timestamp}\n`;
|
|
|
+ preStr += `${randStr}\n`;
|
|
|
+ preStr += `${postData}\n`;
|
|
|
+
|
|
|
+ const signedStr = this._signWithRSASHA256(preStr);
|
|
|
+
|
|
|
+ return {origin:preStr,timestamp,nonce_str:randStr,signature:signedStr};
|
|
|
+ }
|
|
|
+
|
|
|
+ callbackDecrypt(ciphertext:string,nonce:string,associatedData:string){
|
|
|
+ const result = this._decryptByV3Key(ciphertext,nonce,associatedData);
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ _signWithRSASHA256(data:string){
|
|
|
+ const signObj = Crypto.createSign('RSA-SHA256')
|
|
|
+ signObj.update(data);
|
|
|
+ const signedStr = signObj.sign(this._privateKeyPem,'base64');
|
|
|
+ return signedStr;
|
|
|
+ }
|
|
|
+ _requestMchV3Api(url:string,method:string,timestamp:number,nonce_str:string,originBody:string|AnyKeyString,signature:string){
|
|
|
+ return new Promise((resolve,reject)=>{
|
|
|
+ const Authorization = `WECHATPAY2-SHA256-RSA2048 mchid="${this._mchId}",nonce_str="${nonce_str}",signature="${signature}",timestamp="${timestamp}",serial_no="${this._privateKeySerial}"`;
|
|
|
+ REQUEST({url:url,method,encoding:'utf-8',jsonReviver: true,body:originBody,headers: {
|
|
|
+ 'Content-Type' : 'application/json',
|
|
|
+ 'Accept' : 'application/json',
|
|
|
+ 'User-Agent' : 'WECHAT_MCH_SDK/v1',
|
|
|
+ 'Authorization' : Authorization,
|
|
|
+ }},(err:any,response:any,body:any)=>{
|
|
|
+
|
|
|
+ if(typeof body ==='string'){
|
|
|
+ try{
|
|
|
+ body = JSON.parse(body);
|
|
|
+ }catch(e){}
|
|
|
+ }
|
|
|
+ resolve({statusCode:response.statusCode,data:body});
|
|
|
+ });
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ _decryptByV3Key(ciphertext:string,nonce:string,associatedData:string){
|
|
|
+ const ciphertextBuffer = Buffer.from(ciphertext, 'base64')
|
|
|
+ const authTag = ciphertextBuffer.slice(ciphertextBuffer.length - 16)
|
|
|
+ const data = ciphertextBuffer.slice(0, ciphertextBuffer.length - 16)
|
|
|
+ const decipherIv = Crypto.createDecipheriv('aes-256-gcm', this._privateAPIV3Key, nonce)
|
|
|
+ decipherIv.setAuthTag(Buffer.from(authTag))
|
|
|
+ decipherIv.setAAD(Buffer.from(associatedData))
|
|
|
+ let decryptStr = decipherIv.update(data, undefined, 'utf8')
|
|
|
+ decipherIv.final();
|
|
|
+ if(typeof decryptStr == 'string'){
|
|
|
+ try{
|
|
|
+ decryptStr = JSON.parse(decryptStr);
|
|
|
+ }catch(e){}
|
|
|
+ }
|
|
|
+ return decryptStr;
|
|
|
+ }
|
|
|
+ async _decryptByPlatfromKey(ciphertext:string,nonce:string,associatedData:string){
|
|
|
+ const platformKey = await this.getPlatformCertificates() as CipherKey;
|
|
|
+ const ciphertextBuffer = Buffer.from(ciphertext, 'base64');
|
|
|
+ const authTag = ciphertextBuffer.slice(ciphertextBuffer.length - 16)
|
|
|
+ const data = ciphertextBuffer.slice(0, ciphertextBuffer.length - 16)
|
|
|
+ const decipherIv = Crypto.createDecipheriv('aes-256-gcm', platformKey, nonce)
|
|
|
+ decipherIv.setAuthTag(Buffer.from(authTag))
|
|
|
+ decipherIv.setAAD(Buffer.from(associatedData))
|
|
|
+ const decryptStr = decipherIv.update(data, undefined, 'utf8')
|
|
|
+ decipherIv.final();
|
|
|
+ return decryptStr;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+module.exports = WechatMchSDK;
|