微信支付
1、安装依赖及使用教程链接
(NodeJS >= v12.9.0)
npm install wechatpay-axios-plugin https://developers.weixin.qq.com/community/develop/article/doc/000ca44ae3cff894e9fbb46ba5b413
https://gitee.com/TheNorthMemory/wechatpay-axios-plugin
2、接入微信商店,查看文件信息
商户号、商户证书序列号、商户私钥(apiclient_key.pem),商户证书(apiclient_cert.pem)、自己随机生成的32位秘钥、
3、初始化
const {Wechatpay, Formatter} = require('wechatpay-axios-plugin') const wxpay = new Wechatpay({ // 商户号 mchid: 'your_merchant_id', // 商户证书序列号 serial: 'serial_number_of_your_merchant_public_cert', // 商户API私钥 PEM格式的文本字符串或者文件buffer privateKey: '-----BEGIN PRIVATE KEY-----\n-FULL-OF-THE-FILE-CONTENT-\n-----END PRIVATE KEY-----', certs: { // CLI `wxpay crt -m {商户号} -s {商户证书序列号} -f {商户API私钥文件路径} -k {APIv3密钥(32字节)} -o {保存地址}` 生成 'serial_number': '-----BEGIN CERTIFICATE-----\n-FULL-OF-THE-FILE-CONTENT-\n-----END CERTIFICATE-----', }, // APIv2密钥(32字节) v0.4 开始支持 secret: 'your_merchant_secret_key_string', // 接口不要求证书情形,例如仅收款merchant对象参数可选 merchant: { // 商户证书 PEM格式的文本字符串或者文件buffer cert: '-----BEGIN CERTIFICATE-----\n-FULL-OF-THE-FILE-CONTENT-\n-----END CERTIFICATE-----', // 商户API私钥 PEM格式的文本字符串或者文件buffer key: '-----BEGIN PRIVATE KEY-----\n-FULL-OF-THE-FILE-CONTENT-\n-----END PRIVATE KEY-----', }, })
4、Native下单
wxpay.v3.pay.transactions.native .post({ "appid": wxAppid, "mchid": "1900006XXX", "out_trade_no": "native12177525012014070332333", "appid": "wxdace645e0bc2cXXX", "description": "Image形象店-深圳腾大-QQ公仔", "notify_url": "https://weixin.qq.com/", "amount": { "total": 1, "currency": "CNY" } }) .then(({data: {code_url}}) => console.info(code_url)) .catch(({response: {status, statusText, data}}) => console.error(status, statusText, data))
参数名 | 变量 | 类型[长度限制] | 必填 | 描述 | 示例 |
---|---|---|---|---|---|
应用ID | appid | string[1,32] | 是 | 由微信生成的应用ID,全局唯一。请求基础下单接口时请注意APPID的应用属性,例如公众号场景下,需使用应用属性为公众号的APPID。 | wxd678efh567hg6787 |
直连商户号 | mchid | string[1,32] | 是 | 直连商户的商户号,由微信支付生成并下发。 | 1230000109 |
商品描述 | description | string[1,127] | 是 | 商品描述 | Image形象店-深圳腾大-QQ公仔 |
商户订单号 | out_trade_no | string[6,32] | 是 | 商户系统内部订单号,只能是数字、大小写字母_-*且在同一个商户号下唯一 | 1217752501201407033233368018 |
交易结束时间 | time_expire | string[1,64] | 否 | 订单失效时间,遵循rfc3339标准格式,格式为yyyy-MM-DDTHH:mm:ss+TIMEZONE,yyyy-MM-DD表示年月日,T出现在字符串中,表示time元素的开头,HH:mm:ss表示时分秒,TIMEZONE表示时区(+08:00表示东八区时间,领先UTC8小时,即北京时间)。例如:2015-05-20T13:29:35+08:00表示,北京时间2015年5月20日 13点29分35秒。 | 2018-06-08T10:34:56+08:00 |
附加数据 | attach | string[1,128] | 否 | 附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用,实际情况下只有支付完成状态才会返回该字段。 | 自定义数据 |
通知地址 | notify_url | string[1,256] | 是 | 通知URL必须为直接可访问的URL,不允许携带查询串,要求必须为https地址。 | 格式:URL |
订单优惠标记 | goods_tag | string[1,32] | 否 | 订单优惠标记 | WXG |
电子发票入口开放标识 | support_fapiao | boolean | 否 | 传入true时,支付成功消息和支付详情页将出现开票入口。需要在微信支付商户平台或微信公众平台开通电子发票功能,传此字段才可生效。true:是 false:否 | true |
5、Native调起支付
6、在实例中演示
6.1 controller>wxpay.js
'use strict'; const BaseController = require('../core/base_controller'); // const moment = require('moment'); const crypto = require('crypto'); const {Rsa, Formatter} = require('wechatpay-axios-plugin') const { readFileSync } = require('fs'); const {wx_appId,wxpaySecret} = require('../extend/indexconfig'); const helper = require("../extend/helper"); // 从本地文件中加载「商户API私钥」 const merchantPrivateKeyFilePath = './app/extend/wxpay/merchant/apiclient_key.pem'; class WxPayController extends BaseController { /** * 商户下单 */ async pay(){ const ctx = this.ctx; let entity = { ...ctx.request.body }; // console.log("entity>>>>>>>>>>",entity); // 查找一下订单中有没有相同的未支付的订单 const orderinfo = await ctx.service.orderInfo.searchOrderInfo(entity); // console.log(">>>>orderinfo",orderinfo); if(orderinfo&&orderinfo.code_url){ this.success({code_url:orderinfo.code_url,order_no:orderinfo.order_no}); return; }else{ // 生成订单编号 let out_trade_no = 'owl_order_'+ helper.orderNo();//订单编号 // 创建订单 const addOrderInfo = await ctx.service.orderInfo.addOrderInfo({ user_id:entity.user_id, order_no:out_trade_no, order_status:'未支付', alarminfo_id:entity.id, total_fee:entity.amount, code_url:'', create_time:new Date(), note:entity.note }) if(!addOrderInfo){ this.error({message:'添加订单信息失败'}); // return; } entity['order_no'] = out_trade_no; let code_url = await ctx.service.wxpay.prepaid(entity); if(code_url){ // 更新订单地址 await ctx.service.orderInfo.updateOrderInfo('code_url',code_url,entity.user_id,out_trade_no); this.success({code_url,order_no:out_trade_no}); }else{ this.error({message:'请求出错'}) } } } // 对称解密 decode(params) { const AUTH_KEY_LENGTH = 16; // ciphertext = 密文,associated_data = 随机字符串, nonce = 随机字符串 const { ciphertext, associated_data, nonce } = params; // 密钥APV3 const key_bytes = Buffer.from(wxpaySecret, 'utf8'); // 位移 const nonce_bytes = Buffer.from(nonce, 'utf8'); // 填充内容 const associated_data_bytes = Buffer.from(associated_data, 'utf8'); // 密文Buffer const ciphertext_bytes = Buffer.from(ciphertext, 'base64'); // 计算减去16位长度 const cipherdata_length = ciphertext_bytes.length - AUTH_KEY_LENGTH; // upodata const cipherdata_bytes = ciphertext_bytes.slice(0, cipherdata_length); // tag const auth_tag_bytes = ciphertext_bytes.slice(cipherdata_length, ciphertext_bytes.length); const decipher = crypto.createDecipheriv( 'aes-256-gcm', key_bytes, nonce_bytes ); decipher.setAuthTag(auth_tag_bytes); decipher.setAAD(Buffer.from(associated_data_bytes)); const decryptionInfo = Buffer.concat([ decipher.update(cipherdata_bytes), decipher.final(), ]); let req_info = decryptionInfo.toString('utf-8');//BUffer数据转化成字符串 let decryption_req_info = JSON.parse(req_info);//将字符串转化成JSON数据 return decryption_req_info; } // 支付结果通知 async notify(){ console.info('支付结果通知》》》') try { const {req:{headers,body}} = this.ctx; console.info('验证是否已重复'); console.log('支付成功通知>>>',headers,body); // TODO 签名验证 // headers.wechatpay-timestamp // headers.wechatpay-nonce // 解密(获得的是响应的支付信息) let decryption_req_info = await this.decode(body.resource); console.log('解密==============>>>>:',decryption_req_info); // TODO 如何处理多线程并发控制? // 处理重复的通知 let result = await this.ctx.service.orderInfo.searchOrderStatus(decryption_req_info.out_trade_no); if(result && result.order_status != '未支付'){ return; } // 修改订单状态 await this.ctx.service.orderInfo.updateOrderStatus(decryption_req_info); // 添加支付记录 await this.ctx.service.paymentRecords.addPaymentRecordse(decryption_req_info); // 成功应答 this.ctx.status = 200; } catch (error) { // 失败应答 this.ctx.status = 500; this.ctx.body = { 'code':'FAL', 'message':'失败' }; } } // 查询订单状态 async getOrderStatus(){ const ctx = this.ctx; const { order_no } = ctx.params; let result = await this.ctx.service.orderInfo.searchOrderStatus(order_no); if(result){ if(result.order_status == '支付成功'){ this.success({'code':1,'message':result.order_status}) }else{ this.success({'code':0,'message':'支付中...'}) } }else{ this.error({'code':204,'message':'查找失败'}) } // if(result){ // if(result.order_status == '支付成功'){ // this.success({'code':1,'message':result.order_status}) // }else if(result.order_status == '订单未支付'|| result.order_status == '用户已取消'){ // this.success({'code':2,'message':result.order_status}) // }else{ // this.success({'code':0,'message':'支付中...'}) // } // }else{ // this.error({'code':204,'message':'查找失败'}) // } } // 取消订单(根据订单号) async cancelOrder(){ const {order_no} = this.ctx.request.body; let result = await this.ctx.service.wxpay.cancelOrder(order_no); if(result.status == 204){ this.success() }else{ this.error(result) } } // 查询订单 async searchOrder(offminute){ // 获取 超过5分钟未支付的订单 的编号 let resultOrderInfoNo = await this.ctx.service.orderInfo.getNoPayOrderByDuration(offminute); // console.log(resultOrderInfoNo); if(resultOrderInfoNo){ resultOrderInfoNo.map( async order => { const {dataValues: {order_no}} = order; // 通过订单编号查询微信订单状态 let result = await this.ctx.service.wxpay.searchOrder(order_no); if(result){ const {trade_state,trade_state_desc,out_trade_no} = result; if(trade_state_desc == '支付成功'){ console.info('支付成功') // 更新本地订单状态 await this.ctx.service.orderInfo.updateOrderStatus(result); // 记录支付日志 await this.ctx.service.paymentRecords.addPaymentRecordse(result) } if(trade_state_desc == '订单未支付'){ console.info('订单未支付') // 未支付,调用微信关单功能 await this.ctx.service.wxpay.closeOrder(out_trade_no); // 更新本地订单状态 await this.ctx.service.orderInfo.updateOrderStatus(result); } } }) } } // 小程序查询订单状态 async JAppletOrderStatus(){ const ctx = this.ctx; const { order_no } = ctx.params; let result = await this.ctx.service.wxpay.searchOrder(order_no); if(result){ // 支付成功 if(result.trade_state == 'SUCCESS'){ // 修改订单状态 await this.ctx.service.orderInfo.updateOrderStatus(result); // 记录支付日志 await this.ctx.service.paymentRecords.addPaymentRecordse(result) } this.success({status:result.trade_state,message:result.trade_state_desc}) }else{ this.error({message:"查询订单状态失败"}) } } // 小程序数据签名 JAppletSignature(prepay_id){ if(!prepay_id)return; const privateKey = readFileSync(merchantPrivateKeyFilePath) const params = { appId: wx_appId, timeStamp: `${Formatter.timestamp()}`, nonceStr: Formatter.nonce(), package: `prepay_id=${prepay_id}`, signType: 'RSA', } params.paySign = Rsa.sign(Formatter.joinedByLineFeed( params.appId, params.timeStamp, params.nonceStr, params.package ), privateKey) return params } // 小程序 下单 async JApplet(){ const ctx = this.ctx; let entity = { ...ctx.request.body }; // 查找一下订单中有没有相同的未支付的订单 const orderinfo = await ctx.service.orderInfo.searchOrderInfo(entity); if(orderinfo&&orderinfo.prepay_id){ let prepaySignature = this.JAppletSignature(orderinfo.prepay_id);//获取签名,小程序调起支付 this.success({order_no:orderinfo.order_no,prepaySignature}); }else{ // 生成订单编号 let out_trade_no = 'owl_order_'+ helper.orderNo();//订单编号 // 创建订单 const addOrderInfo = await ctx.service.orderInfo.addOrderInfo({ user_id:entity.user_id, order_no:out_trade_no, order_status:'未支付', alarminfo_id:entity.id, total_fee:entity.amount, code_url:'', prepay_id:'', create_time:new Date(), note:entity.note }) if(!addOrderInfo){ this.error({message:'添加订单信息失败'}); return; } entity['order_no'] = out_trade_no; let resultPrepay = await ctx.service.wxpay.prepaidJs(entity); if(resultPrepay){ let prepaySignature = this.JAppletSignature(resultPrepay.prepay_id);//获取签名,小程序调起支付 // 更新订单地址 await ctx.service.orderInfo.updateOrderInfo('prepay_id',resultPrepay.prepay_id,entity.user_id,out_trade_no); this.success({order_no:out_trade_no,prepaySignature}); }else{ this.error({message:'请求出错'}) } } } // 获取微信小程序的openid async jscode2session(){ const ctx = this.ctx; const { code } = ctx.query; if(code ==null){ this.error({message:'code为空'}); return; } let result = await ctx.service.wxpay.getOpenId(code); if(result){ this.success(result); }else{ this.error({message:'获取用户openId失败'}) } } } module.exports = WxPayController;
server>wxpay.js
'use strict'; const Service = require('egg').Service; const { Wechatpay } = require('wechatpay-axios-plugin'); const { readFileSync } = require('fs'); const moment = require('moment'); const {wx_appId,wx_appSecret,wxAppid,merchantId,merchantCertificateSerial,platformCertificateSerial,wxpaySecret} = require('../extend/indexconfig'); // //绑定的公众号 // const wxAppid = ''; // // 商户号,支持「普通商户/特约商户」或「服务商商户」 // const merchantId = ''; // // 「商户API证书」的「证书序列号」 // const merchantCertificateSerial = ''; // 从本地文件中加载「商户API私钥」 const merchantPrivateKeyFilePath = './app/extend/wxpay/merchant/apiclient_key.pem'; const merchantPrivateKeyInstance = readFileSync(merchantPrivateKeyFilePath); // // 「微信支付平台证书」的「证书序列号」,下载器下载后有提示`serial`序列号字段 // const platformCertificateSerial = ''; // 从本地文件中加载「微信支付平台证书」,用来验证微信支付请求响应体的签名 const platformCertificateFilePath = './app/extend/wxpay/tmp/wechatpay_cert.pem'; const platformCertificateInstance = readFileSync(platformCertificateFilePath); const wxpay = new Wechatpay({ mchid: merchantId, serial: merchantCertificateSerial, privateKey: merchantPrivateKeyInstance, certs: { [platformCertificateSerial]: platformCertificateInstance }, miyao:wxpaySecret }); class WxPayService extends Service { /** * 下单 * @param {JSON} entity * https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_1.shtml */ async prepaid(entity) { if(!entity.user_id){ return; } let result = await wxpay.v3.pay.transactions.native.post({ 'appid': wxAppid, 'mchid': merchantId, 'out_trade_no': entity.order_no, 'description': '预警充值', 'notify_url': 'https地址对应的是controller中的notify /wxpay/native/notify', 'amount': { 'total': entity.amount * 100 ,//单位为分 'currency': 'CNY' } }) if(result){ const {data: {code_url}} = result; return code_url; }else{ return false; } } // 微信关单 async closeOrder(order_no){ try { let result = await wxpay.v3.pay.transactions.outTradeNo[order_no].close.post({mchid: merchantId}); if(result.status == 204){ console.info('成功关闭订单'); return {status: result.status}; }else{ const {status, statusText, data} =result; console.info('关闭订单失败',status, statusText, data) return {status, statusText, data}; } } catch (error) { } // wxpay.v3.pay.transactions.outTradeNo[order_no].close.post({mchid: merchantId}) // .then(({status, statusText}) => { // console.info(status, statusText) // if(status == 204){ // console.log('成功关闭订单'); // return true; // }else{ // console.info('关闭订单失败',status, statusText) // return {status, statusText} // } // }) // .catch(({response: {status, statusText, data}}) => console.error(status, statusText, data)) } // 用户取消订单 async cancelOrder(order_no){ // 微信关单接口 let result = await this.closeOrder(order_no); // 更新订单状态 await this.ctx.service.orderInfo.updateOrderStatus({ trade_state_desc:'用户已取消', out_trade_no:order_no }); return result; } // 查询订单 async searchOrder(order_no){ let result = await wxpay.v3.pay.transactions.outTradeNo['{out-trade-no}'].get({params: {mchid:merchantId}, 'out-trade-no': order_no}); // console.log(result); if(result.status == 200){ return result.data }else{ return false } } // 小程序下单 async prepaidJs(entity){ let result = await wxpay.v3.pay.transactions.jsapi.post({ 'appid': wx_appId, 'mchid': merchantId, 'out_trade_no': entity.order_no, 'description': '预警充值', 'notify_url': 'https://www.owl-smart.com:7002/api/wxpay/native/notify', 'amount': { 'total': entity.amount * 100 ,//单位为分 'currency': 'CNY' }, 'payer': { 'openid': entity.openid } }) if(result.status == 200){ return result.data; }else{ return; } } // 获取微信小程序用户的 openId async getOpenId(code){ let url = `https://api.weixin.qq.com/sns/jscode2session?appid=${wx_appId}&secret=${wx_appSecret}&js_code=${code}&grant_type=authorization_code` let option={ method:'GET', dataType: 'json', contentType: 'application/x-www-form-urlencoded' } let res = await this.app.httpclient.request(url, option); if(res.status ==200){ return res.data; }else{ return ; } } } module.exports = WxPayService;