123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256 |
- import { RedisClientType } from '@redis/client';
- import Crypto, { CipherKey } from 'crypto';
- // eslint-disable-next-line @typescript-eslint/no-var-requires
- const XML2JS = require(`xml2js`);
- // eslint-disable-next-line @typescript-eslint/no-var-requires
- // const random = require('string-random')
- 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`;
- // console.log(preStr)
- const signedStr = this._signWithRSASHA256(preStr);
- // console.log(signedStr)
- return {timeStamp:`${timeStamp}`,nonceStr:randStr,paySign:signedStr,package:packageStr,signType:'RSA'};
- }
- 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`;
- // console.log(preStr)
- const signedStr = this._signWithRSASHA256(preStr);
- // console.log(signedStr)
- 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)=>{
- // console.log(response.headers,body)
- 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;
|