微信文档和错误调试的信息非常丰富,节省了不少力气,唯一就是这个过程非常复杂。
直接贴代码。有兴趣的可以做参考。
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" : "错误的单号或金额"
});
}
}