富友是一个比较大的支付接口提供商,可惜这个支付商主要是提供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 );
}
}