sdk_mch_redis.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. import { RedisClientType } from '@redis/client';
  2. import Crypto, { CipherKey } from 'crypto';
  3. // eslint-disable-next-line @typescript-eslint/no-var-requires
  4. const XML2JS = require(`xml2js`);
  5. // eslint-disable-next-line @typescript-eslint/no-var-requires
  6. // const random = require('string-random')
  7. const randomStr = (length?:number,nonum?:boolean)=>{
  8. let arr = '1234567890abcdefghijklmnopqrstuvwxyz';
  9. if(nonum) arr = 'abcdefghijklmnopqrstuvwxyz';
  10. let str = '';
  11. length = length?length:3;
  12. for(let i = 0;i<length;i++){
  13. str += arr.substr(Math.floor(Math.random()*26),1);
  14. }
  15. return str;
  16. }
  17. const WechatMchDomain = `https://api.mch.weixin.qq.com`;
  18. class WechatMchSDK{
  19. private _redisClient;
  20. private _mchId;
  21. private _appId;
  22. private _privateKeyPem;
  23. private _privateCertPem;
  24. private _privateKeySerial;
  25. private _privateAPIV3Key;
  26. private _privateAPIKey;
  27. private _wechatMchApiURL;
  28. constructor(redisInstance:RedisClientType,config:any){
  29. this._redisClient = redisInstance;
  30. this._mchId = config.mchId;
  31. this._appId = config.appId;
  32. this._privateKeyPem = null;
  33. this._privateCertPem = null;
  34. this._privateKeySerial = config.privateKeySerial;
  35. this._privateAPIV3Key = config.privateAPIV3Key;
  36. this._privateAPIKey = config.privateAPIKey;
  37. if(config.privateKeyFilePath){
  38. this._privateKeyPem = FILE.readFileSync(config.privateKeyFilePath,'utf8');
  39. this._privateCertPem = FILE.readFileSync(config.privateCertFilePath,'utf8');
  40. if(!this._privateKeyPem){
  41. throw new Error('[Wechat-Mch-SDK] Config private key pem file read error');
  42. }
  43. if(!this._privateCertPem){
  44. throw new Error('[Wechat-Mch-SDK] Config private key pem file read error');
  45. }
  46. }
  47. if(config.privateKeyPEM){
  48. this._privateKeyPem = config.privateKeyPEM;
  49. }
  50. if(!this._privateKeyPem){
  51. throw new Error('[Wechat-Mch-SDK] Private Key read error');
  52. }
  53. this._wechatMchApiURL = {
  54. orderTrade_JsAPI:`${WechatMchDomain}/v3/pay/transactions/jsapi`,
  55. businessPay_To_Openid:`${WechatMchDomain}/mmpaymkttransfers/promotion/transfers`,
  56. getPlatformCertificates:`${WechatMchDomain}/v3/certificates`,
  57. }
  58. }
  59. getPlatformCertificates(){
  60. return new Promise(async (resolve,reject)=>{
  61. const certificate = await this._redisClient.get(`WECHAT_MCH_${this._mchId}_PLATFORM_CERT`);
  62. if(certificate) return resolve(certificate);
  63. const result = await this._getNewPlatformCertificates();
  64. const cert = result.cert;
  65. if(cert){
  66. await this._redisClient.set(`WECHAT_MCH_${this._mchId}_PLATFORM_CERT`,cert.certificate);
  67. await this._redisClient.expire(`WECHAT_MCH_${this._mchId}_PLATFORM_CERT`,7200);
  68. resolve(cert.certificate);
  69. }else{
  70. resolve(null);
  71. }
  72. });
  73. }
  74. async _getNewPlatformCertificates(){
  75. const signedObj = this.makeSignRequestData(this._wechatMchApiURL.getPlatformCertificates,'GET','');
  76. const getPlatformCertsResult = await this._requestMchV3Api(this._wechatMchApiURL.getPlatformCertificates,'GET',signedObj.timestamp,signedObj.nonce_str,'',signedObj.signature) as AnyKeyString;
  77. if(getPlatformCertsResult.statusCode==200){
  78. const cert = getPlatformCertsResult.data.data[0];
  79. const certContent = this._decryptByV3Key(cert.encrypt_certificate.ciphertext,cert.encrypt_certificate.nonce,cert.encrypt_certificate.associated_data);
  80. const certSerial = cert.serial_no;
  81. return({cert:{serial:certSerial,certificate:certContent}});
  82. }else{
  83. LOGGER.error('Get new platform certificates failed');
  84. LOGGER.error(getPlatformCertsResult.toString());
  85. return({cert:null});
  86. }
  87. }
  88. async orderTrade_JSAPI(openid:string,trade_no:string,amount:number,desc:string,expire_unix_timestamp:number,notify_url:string){
  89. const postData = {
  90. appid:this._appId,
  91. mchid:this._mchId,
  92. description:desc,
  93. out_trade_no:trade_no,
  94. time_expire:Moment.unix(expire_unix_timestamp).format(),
  95. notify_url:notify_url,
  96. amount:{
  97. total:amount,
  98. currency:'CNY'
  99. },
  100. payer:{
  101. openid:openid
  102. }
  103. };
  104. const originBody = JSON.stringify(postData);
  105. const signedObj = this.makeSignRequestData(this._wechatMchApiURL.orderTrade_JsAPI,'POST',postData);
  106. const result = await this._requestMchV3Api(this._wechatMchApiURL.orderTrade_JsAPI,'POST',signedObj.timestamp,signedObj.nonce_str,originBody,signedObj.signature) as AnyKeyString;
  107. if(result.statusCode==200){
  108. return result.data.prepay_id;
  109. }else{
  110. LOGGER.error('Wechat Mch orderTrade error:');
  111. LOGGER.error(JSON.stringify(result));
  112. return null;
  113. }
  114. }
  115. businessPay_To_Openid(orderid:string,openid:string,amount:number,desc:string){
  116. return new Promise(resolve=>{
  117. const postDataArr:any[] = [];
  118. const postData:AnyKeyString = {
  119. mch_appid: this._appId,
  120. mchid: this._mchId,
  121. nonce_str: randomStr(32).toUpperCase(),
  122. partner_trade_no: orderid,
  123. openid,
  124. check_name: 'NO_CHECK',
  125. amount,
  126. desc
  127. }
  128. Object.keys(postData).forEach(key=>{
  129. postDataArr.push(`${key}=${postData[key]}`);
  130. });
  131. postDataArr.sort();
  132. let postStr = postDataArr.join('&');
  133. postStr += `&key=${this._privateAPIKey}`;
  134. const md5 = Crypto.createHash('md5')
  135. postData.sign = md5.update(postStr).digest('hex').toUpperCase();
  136. let postXml = '<xml>';
  137. Object.keys(postData).forEach(key=>{
  138. postXml += `<${key}>${postData[key]}</${key}>`;
  139. });
  140. postXml += '</xml>'
  141. 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)=>{
  142. const x2jparser = XML2JS.parseString;
  143. x2jparser(body, function (err:any, result:any) {
  144. if(result.xml && result.xml.result_code[0]!='SUCCESS'){
  145. return resolve({err:result.xml.err_code[0],data:result.xml.err_code_des[0]})
  146. }
  147. resolve({err:null,data:'SUCCESS'});
  148. });
  149. })
  150. });
  151. }
  152. makeSignPaymentData(prepayId:string){
  153. const randStr = randomStr(32).toUpperCase(),timeStamp = Moment().unix(),packageStr = `prepay_id=${prepayId}`;
  154. let preStr = ''
  155. preStr += `${this._appId}\n`;
  156. preStr += `${timeStamp}\n`;
  157. preStr += `${randStr}\n`;
  158. preStr += `${packageStr}\n`;
  159. // console.log(preStr)
  160. const signedStr = this._signWithRSASHA256(preStr);
  161. // console.log(signedStr)
  162. return {timeStamp:`${timeStamp}`,nonceStr:randStr,paySign:signedStr,package:packageStr,signType:'RSA'};
  163. }
  164. makeSignRequestData(requestUrl:string,method:string,content:AnyKeyString|string){
  165. const randStr = randomStr(32).toUpperCase(),timestamp = Moment().unix();
  166. const url = new URL(requestUrl);
  167. const postData = (typeof content === 'object')?JSON.stringify(content):content;
  168. let preStr = ''
  169. preStr += `${method.toUpperCase()}\n`;
  170. preStr += `${url.pathname}${url.search}\n`;
  171. preStr += `${timestamp}\n`;
  172. preStr += `${randStr}\n`;
  173. preStr += `${postData}\n`;
  174. // console.log(preStr)
  175. const signedStr = this._signWithRSASHA256(preStr);
  176. // console.log(signedStr)
  177. return {origin:preStr,timestamp,nonce_str:randStr,signature:signedStr};
  178. }
  179. callbackDecrypt(ciphertext:string,nonce:string,associatedData:string){
  180. const result = this._decryptByV3Key(ciphertext,nonce,associatedData);
  181. return result;
  182. }
  183. _signWithRSASHA256(data:string){
  184. const signObj = Crypto.createSign('RSA-SHA256')
  185. signObj.update(data);
  186. const signedStr = signObj.sign(this._privateKeyPem,'base64');
  187. return signedStr;
  188. }
  189. _requestMchV3Api(url:string,method:string,timestamp:number,nonce_str:string,originBody:string|AnyKeyString,signature:string){
  190. return new Promise((resolve,reject)=>{
  191. const Authorization = `WECHATPAY2-SHA256-RSA2048 mchid="${this._mchId}",nonce_str="${nonce_str}",signature="${signature}",timestamp="${timestamp}",serial_no="${this._privateKeySerial}"`;
  192. REQUEST({url:url,method,encoding:'utf-8',jsonReviver: true,body:originBody,headers: {
  193. 'Content-Type' : 'application/json',
  194. 'Accept' : 'application/json',
  195. 'User-Agent' : 'WECHAT_MCH_SDK/v1',
  196. 'Authorization' : Authorization,
  197. }},(err:any,response:any,body:any)=>{
  198. // console.log(response.headers,body)
  199. if(typeof body ==='string'){
  200. try{
  201. body = JSON.parse(body);
  202. }catch(e){}
  203. }
  204. resolve({statusCode:response.statusCode,data:body});
  205. });
  206. });
  207. }
  208. _decryptByV3Key(ciphertext:string,nonce:string,associatedData:string){
  209. const ciphertextBuffer = Buffer.from(ciphertext, 'base64')
  210. const authTag = ciphertextBuffer.slice(ciphertextBuffer.length - 16)
  211. const data = ciphertextBuffer.slice(0, ciphertextBuffer.length - 16)
  212. const decipherIv = Crypto.createDecipheriv('aes-256-gcm', this._privateAPIV3Key, nonce)
  213. decipherIv.setAuthTag(Buffer.from(authTag))
  214. decipherIv.setAAD(Buffer.from(associatedData))
  215. let decryptStr = decipherIv.update(data, undefined, 'utf8')
  216. decipherIv.final();
  217. if(typeof decryptStr == 'string'){
  218. try{
  219. decryptStr = JSON.parse(decryptStr);
  220. }catch(e){}
  221. }
  222. return decryptStr;
  223. }
  224. async _decryptByPlatfromKey(ciphertext:string,nonce:string,associatedData:string){
  225. const platformKey = await this.getPlatformCertificates() as CipherKey;
  226. const ciphertextBuffer = Buffer.from(ciphertext, 'base64');
  227. const authTag = ciphertextBuffer.slice(ciphertextBuffer.length - 16)
  228. const data = ciphertextBuffer.slice(0, ciphertextBuffer.length - 16)
  229. const decipherIv = Crypto.createDecipheriv('aes-256-gcm', platformKey, nonce)
  230. decipherIv.setAuthTag(Buffer.from(authTag))
  231. decipherIv.setAAD(Buffer.from(associatedData))
  232. const decryptStr = decipherIv.update(data, undefined, 'utf8')
  233. decipherIv.final();
  234. return decryptStr;
  235. }
  236. }
  237. module.exports = WechatMchSDK;