微信APP支付ApiV3的thinkjs/nodejs服务端的实现

5 篇文章 0 订阅
4 篇文章 0 订阅

微信文档和错误调试的信息非常丰富,节省了不少力气,唯一就是这个过程非常复杂。

直接贴代码。有兴趣的可以做参考。

const Base   = require('./base.js');
const fs     = require('fs');
const crypto = require('crypto');
const axios  = require('axios');
const x509   = require('@peculiar/x509');


// 加载私钥文件
const private_key = fs.readFileSync('./path/pri-key.txt', 'ascii');

// 加载上一次保留的证书(数组,json)
let wxp_pub_keys    = JSON.parse( fs.readFileSync('./path/pub_key.txt', 'ascii') );
let wxp_last_update = 0;


module.exports = class extends Base {

  pad(num, n) 
  {
      var len = num.toString().length;
      while(len < n) {
        num = "0" + num;
        len ++;
      }
      return num;
  }
  
  
  generate(length = 32) 
  {
    const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
    let nonce = '', amount = chars.length;
    while( length -- ) {
      let offset = Math.random() * amount | 0;
      nonce += chars[offset];
    }
    return nonce;
  }



  async call(url, path, method, body) 
  {
    // 此处调用使用商户证书的key值。
    let wxpay   = think.config( 'weixin' ).serial_no;
    let nonce   = this.generate();
    let time    = Math.floor( Date.now() / 1000 );
    let sign    = this.sign ( `${method}\n${path}\n${time}\n${nonce}\n${body}\n`, private_key );
    let token   = this.token( think.config('weixin').mch_id, nonce, time, wxpay, sign );

    let instance = axios.create( {
      baseURL : '',
      timeout : 5000,
      headers : {
        'Content-type'  : 'application/json',
        'Accept'        : 'application/json',
        'Authorization' : 'WECHATPAY2-SHA256-RSA2048 ' + token,
        'User-Agent'    : ''
      }
    });


    let retval;
    if( method == 'POST' )
    {
      await instance.post( url, body )
      .then(function (response) 
      {
        retval = response;
      })
      .catch(function (error) 
      {
        if (error.response) {
          // 请求已发出,但服务器响应的状态码不在 2xx 范围内
          console.log(error.response.data.message);
        } else {
          // Something happened in setting up the request that triggered an Error
          console.log('Error', error.message);
        }
      });
    }
    else 
    if( method == 'GET' )
    {
      await instance.get( url )
      .then(function (response) 
      {
        retval = response;
      })
      .catch(function (error) 
      {
        if (error.response) {
          // 请求已发出,但服务器响应的状态码不在 2xx 范围内
          console.log(error.response.data.message);
        } else {
          // Something happened in setting up the request that triggered an Error
          console.log('Error', error.message);
        }
      });
    }
    return retval;
  }



  // 通用解密方法
  decode(value) 
  {
    // 商户需先在【商户平台】->【API安全】的页面设置apiV3密钥,
    // 请求才能通过微信支付的签名校验。密钥的长度为32个字节。密钥由用户自行设置

    let { ciphertext, associated_data, nonce } = value;
    let apiv3_buffer = Buffer.from( think.config('weixin').secret, 'utf8' );   // apiV3 key
    let nonce_buffer = Buffer.from( nonce, 'utf8' );                           // nonce,解密向量
    let assoc_buffer = Buffer.from( associated_data, 'utf8' );                 // 附加内容
    let texts_buffer = Buffer.from( ciphertext, 'base64' );                    // 密文

    // 计算减去16位长度
    let AUTH_KEY_LENGTH = 16;
    let texts_length = texts_buffer.length - AUTH_KEY_LENGTH;
    
    // 拆分密文
    let cipher = texts_buffer.slice( 0, texts_length );
    
    // 拆分auth tag
    let au_tag = texts_buffer.slice( texts_length, texts_buffer.length );

    const decipher = crypto.createDecipheriv(
      'aes-256-gcm', apiv3_buffer, nonce_buffer
    );

    decipher.setAuthTag( au_tag );
    decipher.setAAD( Buffer.from(assoc_buffer) );

    let output = Buffer.concat([
      decipher.update(cipher),
      decipher.final(),
    ]);

    return output.toString();
  }



  async get_public_key(key = '') 
  {
    // 如果超过了6小时,则尝试更新一次
    let time = Math.floor( Date.now() / 1000 );
    if( time > wxp_last_update + 21600 ) 
    {
      let retval = await this.call( 
        'https://api.mch.weixin.qq.com/v3/certificates',
        '/v3/certificates', 
        'GET', '');

      if( think.isEmpty(retval) == false && think.isEmpty(retval.data) == false ) 
      {
        let certificates = typeof retval.data == 'string' ? 
        JSON.parse( retval.data ).data : 
        retval.data.data;

        // 遍历所有的证书
        for( let cert of certificates )
        {
          let output = this.decode( cert.encrypt_certificate );
          cert.decrypt_certificate = output;

          let start = cert.decrypt_certificate.indexOf( '-\n' );
          let close = cert.decrypt_certificate.indexOf( '\n-' );

          let value = cert.decrypt_certificate.substring( start + 2, close );
          let x509d  = new x509.X509Certificate( Buffer.from(value, 'base64') );
          let p_key = Buffer.from( x509d.publicKey.rawData ).toString( 'base64' );

          let key = ['-----BEGIN PUBLIC KEY-----\n'];
          let i = 0;
          while( i < p_key.length )
          {
            key.push( p_key.substring(i, i + 64) + '\n' );
            i += 64;
          }
          key.push('-----END PUBLIC KEY-----');
          
          cert.public_key = key.join( '' );
        }

        wxp_pub_keys    = certificates;
        wxp_last_update = time;

        // 写入文件
        let json = JSON.stringify( certificates );
        fs.writeFileSync( './wxpay/pub_cert.txt', json );        
      }
    }
    
    let result = {
      serial_no  : '',
      public_key : ''
    };

    // 如果key有效,则继续查找
    if( think.isEmpty(key) == false ) 
    {
      for( let node of wxp_pub_keys )
      {
        if( node.serial_no != key ) {
          continue;
        }

        result.serial_no  = node.serial_no;
        result.public_key = node.public_key;
        break;
      }
    } 
    return result;
  }


  sign(value, key)
  {
    let sign = crypto.createSign( "RSA-SHA256" );
    if( think.isEmpty(sign) ) {
      return '';
    }

    sign.update( value );

    let retval = sign.sign( key, 'base64' );
    return retval;
  }


  verify(value, key, sign)
  {
    let verify = crypto.createVerify( "RSA-SHA256" );
    if( think.isEmpty(verify) ) {
      return '';
    }

    verify.update( value );

    let retval = verify.verify( key, sign, 'base64' );
    return retval;
  }
  
  

  token(mchid, nonce, timestamp, serial, signature) 
  {
    return `mchid="${mchid}",nonce_str="${nonce}",timestamp="${timestamp}",serial_no="${serial}",signature="${signature}"`;
  }



  async submit_orderAction() {

    let account         = this.get('account');      // 
    let item_name       = this.get('item_name');    // 商品识别码(gamme server和网站必须相同)
    let count           = this.get('count');
    let comment         = this.get('comment');
    let pkgname         = this.get('package');

    if( think.isEmpty(account)   ||
        think.isEmpty(item_name) || 
        think.isEmpty(count) )
    {
        return this.fail( 6000, "invalid argument" );
    }

    if( think.isEmpty(pkgname) ) {
        pkgname = 'wxpay';
    }

    if( think.isEmpty(comment) ) {
        comment = '';
    }

    // 查询得到商品的数据=
    let item = await this.model('', 'mysql').find_item( item_name );

    if( think.isEmpty(item) ) {
        return this.fail( 6001, "invalid item" );
    }

    // 填充商品数据
    let detail          = item.name + " * " + count;                       // 比如xxx * 3
    let sale_off        = item.sale_off;
    let original_price  = item.price * count;
    let actual_price    = original_price * sale_off;

    // 创建订单并插入到数据库中
    let order_id = await this.model('', 'mysql').save(
      account, pkgname, item_name, detail, count, original_price, actual_price, sale_off, comment );

    if( think.isEmpty(order_id) ) {
        return this.fail( 6002, "create order failed" );
    }

    let json = {
      appid             : think.config('weixin').appid,  
      mchid             : think.config('weixin').mch_id,  
      description       : detail,  
      out_trade_no      : this.pad( order_id, 20 ),
      attach            : item_name,
      notify_url        : think.config('weixin').notify_url,
      amount            : {
        total     : actual_price,
        currency  : 'CNY'
      }
    };

    let retval = await this.call( 
      'https://api.mch.weixin.qq.com/v3/pay/transactions/app',
      '/v3/pay/transactions/app',
      'POST',
      JSON.stringify(json) );

    if( think.isEmpty(retval) || think.isEmpty(retval.data) ) {
      return this.fail( 6003, "request failed." )
    }

    let appid = think.config('weixin').appid;
    let mchid = think.config('weixin').mch_id;
    let nonce = this.generate();
    let time  = Math.floor( Date.now() / 1000 );

    // 给客户端生成签名
    let result = {
      mch_id    : mchid,
      prepayid  : retval.data.prepay_id,
      nonce     : nonce,
      timestamp : time,
      sign      : this.sign( `${appid}\n${time}\n${nonce}\n${retval.data.prepay_id}\n`, private_key )
    }
    

    return this.json( 
      {
        order_id        : order_id,
        sale_off        : sale_off,
        original_price  : original_price,
        actual_price    : actual_price,
        detail          : detail,
        result          : result
      }
    );
  }


  async notifyAction() {

    let info = this.ctx.request.body;
    if( think.isEmpty(info) ) {
      return this.fail( 500, {
        "code"    : "FAIL",
        "message" : "错误的参数"
      } );
    }

    if( info.post.event_type != 'TRANSACTION.SUCCESS' ) {
      return this.fail( 500, {
        "code"    : "FAIL",
        "message" : "无效的交易结果"
      });
    }

    if( info.post.resource_type != 'encrypt-resource' ) {
      return this.fail( 500, {
        "code"    : "FAIL",
        "message" : "错误的资源类型"
      });
    }

    // AEAD_AES_256_GCM 解密
    if( info.post.resource.algorithm != 'AEAD_AES_256_GCM' ) {
      return this.fail( 500, {
        "code"    : "FAIL",
        "message" : "错误的加密算法"
      } );
    }

    // 验签
    let stamp = this.header('wechatpay-timestamp');   // 通知的时间戳
    let nonce = this.header('wechatpay-nonce');       // 通知的随机字符串
    let index = this.header('wechatpay-serial');      // 通知的微信支付公钥index
    let signs = this.header('wechatpay-signature');   // 签名

    // 通过API获取证书
    let wxpay = await this.get_public_key( index );

    // 判断是否相同
    if( index != wxpay.serial_no ) {
      return this.fail( 500, {
        "code"    : "FAIL",
        "message" : "无效的证书编号"
      });
    }

    // 原始的json数据
    let prior = JSON.stringify( info.post );

    // 验证签名
    if( this.verify(`${stamp}\n${nonce}\n${prior}\n`, wxpay.public_key, signs) == false ) 
    {
      return this.fail( 500, {
        "code"    : "FAIL",
        "message" : "参数校验错误"
      });
    }
    
    // 解码数据
    let encrypt = this.decode( info.post.resource );
    let receipt = JSON.parse ( encrypt.toString() );


    let item       = receipt.attach;
    let order_id   = parseInt( receipt.out_trade_no );
    let payment_sn = receipt.transaction_id;
    let money      = receipt.amount.total;

    // 更新订单(仅更新一个标志,反复读写不会有任何障碍)
    let retval = await this.model('', 'mysql').
      update_payment(order_id, payment_sn, item, parseInt(money) );

    if( retval == 1 ) {
        return this.success();
    }
    
    // 错误的单号或则金额
    return this.fail( 500, {
      "code"    : "FAIL",
      "message" : "错误的单号或金额"
    });
  }


}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值