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格式的文本字符串或者文件bufferprivateKey: '-----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格式的文本字符串或者文件buffercert: '-----BEGIN CERTIFICATE-----\n-FULL-OF-THE-FILE-CONTENT-\n-----END CERTIFICATE-----',// 商户API私钥 PEM格式的文本字符串或者文件bufferkey: '-----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;// 密钥APV3const key_bytes = Buffer.from(wxpaySecret, 'utf8');// 位移const nonce_bytes = Buffer.from(nonce, 'utf8');// 填充内容const associated_data_bytes = Buffer.from(associated_data, 'utf8');// 密文Bufferconst ciphertext_bytes = Buffer.from(ciphertext, 'base64');// 计算减去16位长度const cipherdata_length = ciphertext_bytes.length - AUTH_KEY_LENGTH;// upodataconst cipherdata_bytes = ciphertext_bytes.slice(0, cipherdata_length);// tagconst 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:'请求出错'})}}}// 获取微信小程序的openidasync 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;}}// 获取微信小程序用户的 openIdasync 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;