富友支付的ThinkJS实现(H5)

富友是一个比较大的支付接口提供商,可惜这个支付商主要是提供POS机的收费场景,面向游戏并不是特别好用,如果不是需要H5支付,这个支付接口根本不是好的选择的对象。如果是针对APP,还是直接接微信和支付宝更为合适。

这个支付所提供的接口文档就是个笑话,错误百出,自相矛盾,给出来的例子完全不能代表真实的支付场景。而且回调地址无法自己控制,也就是说一个账号只能有一个回调地址,没办法通过参数传递,这一点就严重的限制了使用场景。开发中的坑特别多,痛苦指数爆表。

总之,准备使用这个支付平台的朋友们慎重慎重再慎重。

const fs      = require('fs');
const Base    = require('./base.js');
const crypto  = require('crypto');
const axios   = require('axios');
const { stringify } = require('querystring');
const { jar } = require('request');

const app_pub_key = fs.readFileSync('./xxxxx', 'ascii');
const fup_pub_key = fs.readFileSync('./xxxxx', 'ascii');

// 准备公钥的格式
let key = ['-----BEGIN PUBLIC KEY-----\n'];
let i = 0;
while( i < app_pub_key.length )
{
  key.push( app_pub_key.substring(i, i + 64) + '\n' );
    i += 64;
}
key.push('-----END PUBLIC KEY-----');

const app_public_key = key.join('');


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

const fup_public_key = key.join('');


const app_private_key = fs.readFileSync('xxxxxx', 'ascii');


module.exports = class extends Base {


  pad(num, n) 
  {
      var len = num.toString().length;
      while(len < n) {
        num = "0" + num;
        len ++;
      }
      return num;
  }


  sign(param, key) 
  {
    let map = [];
    let keys = Object.keys(param);  // 获取所有的key
    keys.sort();                      // 排序

    for (let key of keys) 
    {
      //剔除空值及签名字段
      if( key != 'sign' &&
          key != 'reservedSecretPlatId' &&
          key != 'reservedOrderNo' &&
          key != 'reservedDsc' &&
          param[key]) 
      {
        map.push(`${key}=${param[key]}`);
      }
    }

    let content = map.join('&');

    let sign;
    if( param["reservedSecretPlatId"].toUpperCase() === 'TPAY' ) {
        sign = crypto.createSign( "RSA-SHA256" );
    }
    else
    if( param["reservedSecretPlatId"].toUpperCase() === 'WPOS' ||
        param["reservedSecretPlatId"].toUpperCase() === '' ) {
        sign = crypto.createSign( "RSA-MD5" );
    }

    if( think.isEmpty(sign) ) {
        return false;
    }

    sign.update(content);
    let retval = sign.sign( key, 'base64' );
    return retval;
  }  
  
  verify(param, key) 
  {
    let map = [];
    let keys = Object.keys(param);  // 获取所有的key
    keys.sort();                      // 排序

    for (let key of keys) 
    {
      // 剔除不参与验签字段
      if( key != 'resultMsg' &&
          key != 'sign' &&
          key != 'signType' ) 
      {
          let value = param[key];
          if( key == 'data' && value != '' )
          {
              // 转换成需要验签的格式
              let a = "{openLink=" + value.openLink + "}";
              value = a;
          }
          map.push(`${key}=${value}`);
      }
    }

    let content = map.join('&');

    let verify;
    if( param["signType"].toUpperCase() === 'RSA' ) {
      verify = crypto.createVerify( "RSA-SHA256" );
    }
    else
    if( param["signType"].toUpperCase() === 'RSAMD5' || 
        param["signType"] == '' ) {
      verify = crypto.createVerify( "RSA-MD5" );
    }

    if( think.isEmpty(verify) ) {
      return false;
    }

    verify.update(content);

    let sign = param["sign"];
    let retval = verify.verify( key, sign, 'base64' );
    return retval;
  }    

  generate(length = 32) 
  {
    const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
    let nonce = '', amount = chars.length;
    while( length -- ) {
      let offset = Math.random() * amount | 0;
      nonce += chars[offset];
    }
    return nonce;
  }


  md5(value) {

    let md5 = crypto.createHash('md5');
    return md5.update(value).digest().toString("hex");
  }  
  
  
  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 = 'fpay';
    }

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

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

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

    console.log( item_name );

    // 填充商品数据
    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('order', 'mysql_nova').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" );
    }

    //  调用fpay,获取调用请求
    let data = {

        'insCd'                 : think.config('fpay').mchntcd,   // 和商户号一致
        'mchntCd'               : think.config('fpay').mchntcd,
        'termId'                : think.config('fpay').termid,
        'amt'                   : actual_price,
        'randomStr'             : this.generate( 32 ),
        'appId'                 : think.config('fpay').appid,
        'reservedSecretPlatId'  : 'tpay',
        'reservedOrderNo'       : 'T' + think.config('fpay').termid + this.pad( order_id, 10 ),
        'reservedDsc'           : item_name,
        'sign'                  : ''
    }

    data.sign = this.sign( data, app_private_key );
    if( think.isEmpty(data.sign) )
    {
      return this.fail( 500, "sign failed" );
    }

    let instance = axios.create( {
      baseURL : '',
      timeout : 5000,
      headers : {
        'Content-type'  : 'application/json',
        'Accept'        : 'application/json',
      }
    });

    let retval;
    await instance.post( 'https://tpayapi-cloud.fuioupay.com/mp/generatescheme', JSON.stringify(data) )
    .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);
      }
    });

    // data字段必须参与验签。
    if( think.isEmpty(retval.data.data) ) {
      retval.data.data = '';
    }
    
    // 验签
    if( this.verify(retval.data, fup_public_key) == false ) {
      return this.fail( 500, "verify failed" );
    }

    // 如果不是成功,则返回失败的理由
    if( retval.data.resultCode != '000000' ) {
      return this.fail( parseInt(retval.data.resultCode), retval.data.resultMsg );
    }

    // 返回的orderString,直接给客户端请求。
    return this.json( 
      {
        order_id        : order_id,
        sale_off        : sale_off,
        original_price  : original_price,
        actual_price    : actual_price,
        detail          : detail,
        result          : retval.data.data, 
      }
    );
  }
  
  
  async notifyAction() {

    // body need think pay-load 1.4.0
    let info = this.ctx.request.body;
    if( think.isEmpty(info) ) {
        return this.fail( 500, 'fail');
    }

    let value = info.post.req;
    let check = value.substring( 0, value.indexOf(',"key_sign"') ) + think.config("fpay").secret;
    let param = JSON.parse(info.post.req);

    // rawBody need think pay-load 1.4.0
    // let value = this.ctx.request.rawBody;
    // let param = info.post;

    // 检查参数是否合法
    if( this.md5(check) != param['key_sign'] )
    {
      return this.json( '0' );
    }

    // 支付是否成功,不成功就不用再通知了。
    if( param['pay_status'] != '1' ) 
    {
      return this.json( '1' );
    }

    let order_id   = param['out_trade_no'];        // 自己的订单号
    let payment_sn = param['channel_trade_no'];    // fpay关联渠道订单号
    let money      = param['total_fee'];           // 总价
    
    // 去掉T和终端号
    order_id = parseInt( order_id.substring( think.config("fpay").termid.length + 1 ) );

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

    if( retval != 0 ) {
        return this.json( 1 );
    }
    
    // 错误的单号或则金额
    return this.json( 0 );
  }  


}

你可能感兴趣的:(node.js)