微信支付

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;

 

posted @ 2025-10-14 14:55  一江春水向东刘小姐  阅读(8)  评论(0)    收藏  举报