微信公众号支付开发手记(node)

微信支付

前言

总结一下最近业务开发中对微信公众号支付的开发过程,微信支付的开发前提是已经具有可上线微信公众号开发的基础上进行的,若是你的开发阶段目前停留在起步,建议参考这篇文章开始php

好了,来聊一聊微信支付。不管是今天的分享,仍是网上其余的分享,开头老是在吐槽微信的文档。我也不例外,刚开始老是以为文档写的不够具体,写的模棱两可。后来发现一个是本身太浮躁,不能沉下心去分析文档的细节,另外一方面是习惯性先去网上找相关的教程,而后发现教程传递过来的第一感觉就是——微信开发是个大坑。css

网上的经验分享仍是颇有帮助的,可是首先要清楚地明白微信支付的整个流程以及本身目前的进展,这样才能有目的的去找到本身须要的东西。html

微信支付开发和微信其余功能开发有一个共同点,就是须要耐心。而这彷佛也是微信团队的初衷,经过零散的文档,不清晰的说明来过滤掉一批缺少耐心的程序员(帮微信团队圆个场)。因此开始接触微信开发以前,会有人告诉你这里会有不少坑,会怎么样怎么样,其实彻底不用担忧,由于这里的坑并非在考验技术。前端

关于微信支付的调试过程,因为微信支付对安全要求较高,不能作内网穿透本机测试,因此须要先将项目部署到线上,开发调试不是很方便。可能会有一些本地支付沙箱之类的工具,我没有去研究,但愿有作过此类工做的小伙伴留个言提醒一下。并且微信支付是不可以经过微信开发者工具测试的,只能在真机上跑。若是在开发者工具上遇到报错,不妨在手机上跑一下。node

微信支付文档分析

无论什么开发,都要从官方文档开始。关于公众号支付的部分,须要关注文档的两个部分,一个是“公众号支付”一个是“API列表”,其余部分不是重点。git

公众号支付程序员

  • 场景介绍
  • 案例介绍
  • 开发步骤
  • 业务流程
  • 获取微信版本号
  • 微信内H5调起支付
  • 收获地址共享
  • 支持常见问题

这一级的目录,重点是业务流程微信内H5调起支付,由于这两块内容搞明白了,整个支付流程就清晰了。github

首先须要了解一下公众号支付的具体场景,也就是须要阅读第一部分“场景介绍”,了解一下这个场景是否符合具体业务。算法

若是这就是你要的,那就来到重点了,也就是“业务流程”。数据库

看到业务流程的流程图,估计有计算机专业背景的朋友会很熟悉,也很容易理解。不理解也不要紧,我这里准备了一副含有分析过程的流程图,结合实际业务,来帮助理解。

微信支付流程图分析

先来看黄色的甬道,这两块也就是实际的前台页面和后台服务,咱们的阅读顺序是自上而下,流程图中的文字是微信官方提供的,右边的说明文字是我根据业务写的,看哪部分均可以。我把整个流程用颜色分为了三大块帮助理解。

从红色部分开始,红色部分主要工做是后台生成预付款单,而后经过回调信息将内容发送到前台。

接下来是蓝色部分,这一部分包含两块,一个是付款前,一个是付款后。

首先前台拿到红色部分由后台发来的信息,而后再微信内H5页面调起支付,此时页面的付款都是由微信来控制。

付款结束后,会发送两个回调,一个发送给后台服务,也就是图片蓝色部分中的绿色区块,告诉后台具体哪一个订单如今的完成状态。另外一个发送给客户前端,通知前端交易状态。

从这部份内容了解到,须要先了解统一下单API,以后是微信H5内调起支付,而后处理支付结果通知

开始开发

准备

微信服务配置

开始开发前,须要对现有项目设置支付目录和设置受权域名,具体能够参考这里另外须要注意的是,也是微信文档里没有提到的地方。须要在微信支付平台设置API密钥。须要提醒的是,微信支付平台的配置须要超级管理员帐号登陆才能够进行配置操做。

微信支付API密钥

收集信息

上一步配置完成后,须要收集一些信息为后续开发作准备。

  • token 微信公众号后台取得
  • appid 微信公众号后台取得
  • appsecret 微信公众号后台取得
  • encodingAESKey 微信公众号后台取得
  • mch_id 商户号(微信支付平台取得)
  • notifyUrl 微信支付回调地址(服务端后台接口:POST)
  • partnerKey 微信支付API密钥(微信支付平台取得)

准备好以上信息后,就能够开始着手写代码了。

开工

首先,须要准备一个前台界面,模拟用户访问商品页面点击购买。

image

后端部分

后台这边,node开发微信支付有不少现成的封装库可使用,这里使用wechat-pay

首先在项目开始处初始化wechat-pay

下单
const Payment = require('wechat-pay').Payment;
const initConfig = {
  partnerKey: config.wechat.partnerKey,
  appId: config.wechat.appid,
  mchId: config.wechat.mch_id,
  notifyUrl: config.wechat.notifyUrl,
};
const payment = new Payment(initConfig);
复制代码

而后编写前台界面用户点击购买后的接口业务代码:

async genAdvanceOrder(ctx) {
    try {
      // 1. 经过前台发来的商品ID查询商品
      const { user } = ctx.state;
      const { product_id, comment, school_id } = ctx.request.body;
      const product = await prodDao.findOneProduct(product_id);

      if (product.length <= 0) {
        return ctx.body = new Error(C.ERROR_CODE.QUERY_EMPTY, '没有找到商品');
      }
      
      
      // 2. 经过查询结果填写预付款单
      // 获取client ip地址
      const clientIp = getClientIp(ctx.req);
      const order = {
        body: product[0].course_title,        // 商品描述
        attach: product[0].comment,           // 商品附加数据
        out_trade_no: UUID.v1().replace(/\-/g, ''),   // 商户系统内部订单号,本身生成32位随机串 unique
        total_fee: product[0].price,          // 费用(单位:分) 
        spbill_create_ip: clientIp,           // 客户端IP
        openid: user.openid,                  // 用户openid
        trade_type: 'JSAPI'
      };

      // 3. 根据预付款单回调结果往数据库插入数据(判断错误码,修改订单状态)
      // 向微信请求生成预付款单
      let payargs = await payment.getBrandWCPayRequestParams(order);
      await orderDao.add({
        user_id: user.id,
        school_id: school_id, // TODO: 增长并分配学校ID,业务逻辑须要变更
        scene: product[0].scene,
        product_id: product_id,
        price: product[0].price,
        product_price: product[0].price,
        status: C.PAY_STATUS.NO_PAY,
        wx_open_id: user.openid,
        wx_out_trade_no: order.out_trade_no,
        wx_prepay_id: payargs.package.split('=')[1], // 取prepay_id
        comment: comment
      });

      // 4. 发送预付款单内容
      ctx.body = new Success(payargs);

    } catch (e) {
      ctx.body = new Error('', '未知错误', e)
    }
  }
  
function getClientIp(req) {
    const ip = req.headers['x-forwarded-for'] ||
    req.connection.remoteAddress ||
    req.socket.remoteAddress ||
    req.connection.socket.remoteAddress;
    return ip.replace(/:|\wf/g, '');
}
复制代码

这里须要注意的是填写预付款单这里的操做。

out_trade_no是要本身生成32位随机字符串,至关因而保存在本身数据库中的订单惟一值,之后在微信回执付款信息时也会用到这个字段。

total_fee的单位是分,在开发过程当中,也应当使用分做为数据库价钱的单位,这样能够有效避免浮点数精读损失问题。

spbill_create_ip是用户端下单设备的ip,是必填项,微信那边处于安全要求每份订单都必需要填写。这个也很好获取(上面代码中贴出来了),只不过要作一些处理,由于直接经过koa ctx.ip获取的地址可能会被Nginx或者其余服务器配置服务转发成127.0.0.1。这就不是咱们须要的真实的客户端ip。最后要处理字符串前缀,通常直接拿到ip的格式是:fff:54.00.00.1

支付通知

这里就是以前的流程图中,蓝色区块中的绿色部分。能够对比流程图理解支付流程。

微信开发中,大量来自微信发送的通知都是xml格式,因此为了方便使用,须要先增长如下中间件来帮助开发。

const bodyParser = require('koa-bodyparser');
const xmlParser = require('koa-xml-body');

app.use(xmlParser({
  key: 'body'
}));
// app.use(bodyParser());
app.use(bodyParser({
  enableTypes: ['json', 'form', 'text'],
  extendTypes: {
    text: ['text/xml', 'application/xml']
  }
}));
复制代码

以后是通知部分的代码:

async wxPayNotify(ctx) {
    // TODO: 安全验证 签名验证 并校验返回的订单金额是否与商户侧的订单金额一致
    /* // 微信发送通知的内容 { appid: [ '**********' ], attach: [ '附加内容' ], bank_type: [ 'CFT' ], cash_fee: [ '1' ], fee_type: [ 'CNY' ], is_subscribe: [ 'Y' ], mch_id: [ '1498496372' ], nonce_str: [ '4KewHbQvsQPaGsaeoICLbKD1ySFDlPdL' ], openid: [ 'oZZUx0X2LSM1j652P6r2R*******' ], out_trade_no: [ '8ff9fdd0e33411e8a07c833c43c4e4e7' ], result_code: [ 'SUCCESS' ], return_code: [ 'SUCCESS' ], sign: [ '6A14538FE1651CECDFCDFE375383B9AA' ], time_end: [ '20181108165920' ], total_fee: [ '1' ], trade_type: [ 'JSAPI' ], transaction_id: [ '4200000235201811***********' ] } */
    try {

      // 1. 经过回调信息查询订单
      const content = ctx.request.body['xml'];
      const order = await orderDao.selectOne({ wx_out_trade_no: content['out_trade_no'][0] });

      if (!order) {
      // TODO: 处理查询不到订单的通知
      }

      // 2. 安全验证,对比签名和订单金额
      if (checkWeChatPaySign(content) && order.price === parseInt(content['total_fee'][0])) {
        await orderDao.update({
          wx_notify_backup: JSON.stringify(content),
          status: C.PAY_STATUS.PAID,
          wx_transaction_id: content['transaction_id'][0]
        }, { wx_out_trade_no: content['out_trade_no'][0] })
      } else {
      // TODO: 处理验证不经过的通知
      }

      // 3. 回调通知微信
      ctx.body = '<xml>' +
        '<return_code><![CDATA[SUCCESS]]></return_code>' +
        '<return_msg><![CDATA[OK]]></return_msg>' +
        '</xml>'
    } catch (e) {
    // TODO: 收款回调出错通知
    }
  }
  
function checkWeChatPaySign(obj) {
  // 1.字典排序数据集合
  let arr = [];
  for (let [k, v] of Object.entries(obj)) {
    let string = '';
    // 排除 sign 字段
    if (k === 'sign') continue;
    string += k + '=' + v[0];
    arr.push(string);
  }
  // 按字典排序
  arr.sort();
  // 2.拼接上key获得stringSignTemp字符串
  arr.push('key=' + config.wechat.partnerKey);
  const stringSignTemp = arr.join('&');
  const md5String = md5(stringSignTemp).toUpperCase();
  // 3.比较md5String 与 sign字段
  return md5String === obj.sign[0];
}
复制代码

这里比较很差理解的是验证签名,而微信文档也没有给出样例代码,因此比较混乱。并且拼签名拼串验证又容易出错,多一个空格少一个字符都不同。这里就得结合官方给出的签名算法效验工具耐着性子调试了。

前端部分

而后就回到咱们的前端部分。

因为微信H5支付是基于腾讯浏览器的,因此只有在手机微信中或者开发者工具中打开的网址,才能调用到WeixinJSBridge

我这边前端是拿Angular写的,不过代码不复杂,着重理解业务流程。

import { Component, OnInit } from '@angular/core';
import {UserService} from '../../../services/user.service';
import {OrderService} from '../../../services/order.service';
import {ActivatedRoute} from '@angular/router';

@Component({
  selector: 'app-wx-pay-test',
  templateUrl: './wx-pay-test.component.html',
  styleUrls: ['./wx-pay-test.component.scss']
})
export class WxPayTestComponent implements OnInit {
  wxBridge;
  logs = [];
  productId;

  constructor(
    private orderService: OrderService, // API
    private activateRouter: ActivatedRoute
  ) { }

  // 页面初始化就会执行的钩子函数, React 应该使用ComponentDidMount
  ngOnInit() {
    // 这里是为了获取WeixinJSBridge
    if (typeof window['WeixinJSBridge'] === 'undefined') {
      if (document.addEventListener ) {
        document.addEventListener('WeixinJSBridgeReady', this.onBridgeReady, false);
      } else if (document['attachEvent']) {
        document['attachEvent']('WeixinJSBridgeReady', this.onBridgeReady);
        document['attachEvent']('onWeixinJSBridgeReady', this.onBridgeReady);
      }
    } else {
      this.onBridgeReady();
    }
    // 获取url中的参数
    this.activateRouter.params.subscribe( params => {
      if (params.id) {
        this.productId = params.id;
      }
    });
  }

  onBridgeReady = () => {
    this.wxBridge = window['WeixinJSBridge'];
  }
  // 点击购买触发该函数
  genPreOrder(ev) {
    const that = this;
    // 一、向后台发送请求 对应后台 genAdvanceOrder
    this.orderService.genAdvanceOrder({
      product_id: this.productId
    }).then(data => {
      // 二、拿到服务端回执数据,调用invoke,请求微信支付
      that.wxBridge.invoke('getBrandWCPayRequest', data, function(res) {
        // 三、处理微信支付回执结果
        if (res.err_msg === 'get_brand_wcpay_request:ok') {
          // TODO: 显示支付成功页面
          alert('支付成功');
          // 这里能够跳转到订单完成页面向用户展现
        } else {
          // TODO: 显示支付失败页面
          alert('支付失败,请重试');
        }
      });
    }).catch(err => {
      console.log(err);
    });
  }
}
复制代码

前端的代码仍是相对容易理解的,前提是理解文章开头部分的流程图。知道每一步是处理什么问题,须要干什么。

在调用微信支付后,返回的结果代码中我只处理了支付成功的部分,可是回调还会有支付失败、超时等等,就不一一列举。

结语

文章到这里,大概也就讲清楚了微信公众号支付的整个环节。可是在流程图中最后一部分灰色区块的业务没有讲,由于以为前两部分是最主要的,后面能够自行理解流程图,根据具体业务开发。

折腾微信支付这块内容大概也有几天了,总结一下整个开发流程,分享一下,但愿可以帮助你们理解整个支付业务。

要说难吗其实也不难,主要就是考察耐心吧。

相关文章
相关标签/搜索