温斯顿吴 ☀️摩诃般若波罗蜜

微信公众号支付功能开发总结

2017-04-10

本文总结了项目中开发微信公众号支付功能的核心流程,主要包括:

  • 开发微信支付的各种账号、域名的配置;
  • 获取微信用户信息(open_id);
  • 下发微信jsSDK配置的接口;
  • 下发微信支付配置的接口;
  • 一个js调用微信支付的前端页面demo;
  • 处理微信支付结果回调;

1. 部署后端服务代码

登录服务器(比如阿里云ecs):

  • 拉代码
  • 新增nginx vhost配置 mobile.conf
    server {
    listen  80;
    server_name mobile.demo.com;
    // ... 详略
    
  • 绑定host:
    0.0.0.0 mobile.demo.com
    
  • reload nginx:
    nginx -s reload
    

2. 绑定域名

demo.com A记录:

mobile 106.14.**.**

3. 登录微信公众号后台,设置服务器配置

url: http://mobile.demo.com/wechat/callback  // 主要用于处理微信事件回调,比如用户关注、发消息等,本例用不到
token: ***
EncodingAESKey : 随机

4. 后台代码配置

后台项目使用Phalcon框架实现,以下为config.ini文件的配置。 其他语言、框架的配置类似,只要能够被后端代码读取就行,这里只是用来说明需要哪些配置项。

(1)配置文件

[wechat]
DEBUG = ; 为true时,可以打印一些log 
WECHAT_APPID = ; 微信公众号的APPID
WECHAT_SECRET = ; 微信公众号的SECRET
WECHAT_TOKEN = ; 微信公众号的TOKEN
WECHAT_AES_KEY = ; 微信公众号的AES_KEY
MERCHAT_ID = ; 商户平台ID
MERCHAT_KEY = ; 商户平台API密钥
CERT_PATH = ; 商户平台apiclient_cert.pem证书位置(绝对路径)
KEY_PATH = ; 商户平台apiclient_key.pem证书位置(绝对路径)
NOTIFY_URL = ; 默认的支付成功回调URL,可以在实际调用时覆盖

(2)注册DI服务

后台项目使用Phalcon框架实现,以下为\Phalcon\Mvc\Micro的DI配置,其他语言、框架的配置类似。

<?php
// ...

$di->setShared('wechat', function () use ($di) {
  $options = [
    'debug'  => $di->get('config')->wechat['DEBUG'],
    'app_id' => $di->get('config')->wechat['WECHAT_APPID'],
    'secret' => $di->get('config')->wechat['WECHAT_SECRET'],
    'token'  => $di->get('config')->wechat['WECHAT_TOKEN'],
    // 'aes_key' => null, // 可选
  
    'log' => [
      'level' => 'debug',
      'file'  => DIR_LOG . '/easywechat.log',
    ],

    'oauth' => [
      'scopes'   => ['snsapi_userinfo'],    // 公众平台网页授权获取用户信息
      'callback' => '/wechat/authcallback', // 获取用户信息后的回调页面
    ],

    'payment' => [
      'merchant_id' => $di->get('config')->wechat['MERCHAT_ID'],
      'key'         => $di->get('config')->wechat['MERCHAT_KEY'],
      'cert_path'   => $di->get('config')->wechat['CERT_PATH'],
      'key_path'    => $di->get('config')->wechat['KEY_PATH'],
      'notify_url'  => $di->get('config')->wechat['NOTIFY_URL'], 
    ],
];

  $wechatApplication = new Application($options);
  return $wechatApplication;
});

5. 开发获取jsSDK配置的接口

使用的easyWechat开源库,在phalcon DI中注册实例为wechat(读取配置,返回实例对象,详细代码略)。

<?php
// ...

public function jsSdkAction()
{
  $this->wechat->js->setUrl(urldecode($this->input['url']));

  // getBrandWCPayRequest为需要调用的wx js接口
  return $this->responseData($this->wechat->js->config(
    $apiArray = ['getBrandWCPayRequest',]
  ));
}

测试: http://mobile.demo.com/wechat/jsSdk 返回:

{
  "success": true,
  "data": "{\"debug\":false,\"beta\":false,\"appId\":\"wx2dbfcf8d32f00e22\",\"nonceStr\":\"H1Tz1QW9xi\",\"timestamp\":1491669904,\"url\":\"http:\\/\\/mobile.demo.com\\/wechat\\/jsSdk\",\"signature\":\"250eb980e3326bc502384193a224af969965cfbf\",\"jsApiList\":[\"getBrandWCPayRequest\"]}",
  "code": 1,
  "message": "成功"
}

猜测前端拿到数据后,通过wxJsBridge唤起微信支付时,微信会通过nonceStr加其他数据生成签名后与signature进行比较来判断真实性。

6. 登录微信公众号管理后台设置各种域名

  • 设置js接口安全域名 mobile.demo.com
  • 设置业务域名 mobile.demo.com // 这样微信在访问该域名时就不会弹出警告信息,影响排版
  • 授权回调页面域名 mobile.demo.com // 用户在网页授权页同意授权给公众号后,微信会将授权数据传给一个回调页面,回调页面需在此域名下,以确保安全可靠。
  • 公众号支付-支付授权目录 http://mobile.demo.com/

7. 开发获取用户open_id的接口

因为调用微信支付统一下单接口需要open_id,所以需要获取用户的open_id。大体步骤如下:

(1)开发获取用户信息的中间页路由

<?php
// ...

// wechat/authpage
public function authpageAction()
{
  /* 
    用户将实际需要访问的页面的路由作为参数来访问该路由
    比如需要访问http://mobile.demo.com/jsWxPayDemo.html,而该页面需要用到open_id,那么实际的访问地址为:
    http://mobile.demo.com/wechat/authpage?redirect=jsWxPayDemo.html
  */
  
  $page = $this->input[self::REDIRECT_PAGE];
  if (empty($page)) {
    $page = '';
  }

  // 判断open_id是否已存在,只有在不存在的情况下才需要获取
  $openId = $this->cookieData['open_id'];
  if (!empty($openId)) {
    // open_id已存在,则直接跳转相应的页面
    $this->redirectPage($page);
  }
  else {
    // open_id不存在,需要走获取open_id的流程:
    // 将当期用户需要访问的页面路由存入session中;
    $this->session->set(self::REDIRECT_PAGE, $page);

    // 重定向至微信OAuth认证页面
    return $this->wechat->oauth->redirect()->send();
  }
}

(2)处理微信获取用户信息后的回调

<?php
// ...

// wechat/authcallback
public function authcallbackAction()
{
  $openId = '';

  try {
    $oauth = $this->wechat->oauth;
    $user = $oauth->user();
    $openId = $user->getId();
  } catch (\Exception $e) {
    $this->log->error('openId获取失败!');
    $this->log->error(json_encode($e));
  }

  // 将获取到的微信用户信息存入cookie中
  CommonBusiness::instance()->updateCookies(CookieKeys::COOKIES_USER_INFO, [
    'open_id' => $openId,
  ]);

  // 从session中获取用户的实际访问路由
  $page = $this->session->get(self::REDIRECT_PAGE);

  // 重定向至用户路由
  $this->redirectPage($page);
}

8. 开发获取支付配置的接口

本例是通过微信支付充值平台点数。

<?php
// ...

// wechat/chargePayConfig
public function chargePayConfigAction()
{
  // 判断登录态 & 获取当前用户信息
  $userInfo = UserBusiness::instance()->getCurrentUser();
  if(empty($userInfo)){
    return $this->responseError('请登录!');
  }

  // 获取充值点数
  $chance = intval($this->input['chance']);
  if($chance < 1){
    return $this->responseError('充值点数非法!');
  }

  // 新建充值记录信息(充值记录类似订单)
  $chargeInfo = new ChanceChargeInfo();
  $chargeInfo->chance = $this->input['chance'];
  $chargeInfo->userId = $userInfo->id;
  $chargeInfo->paidAmount = $chance * 1;  // 1个充值点数 = 1分钱人民币
  $chargeInfo->status = ChanceChargeStatus::UNPAID; // 充值记录的状态为待付款

  $service = new ChanceChargeInfoService();
  $ret = $service->add($chargeInfo);
  if(!$ret){
    return $this->responseError('充值记录创建失败!');
  }

  // 获取本次充值记录的微信支付配置(最终通过调用微信支付统一下单接口得到)
  $ret =  WechatBusiness::instance()->getChargePayConfig($chargeInfo);
  if(false === $ret){
    return $this->responseError('充值订单创建失败!');
  }

  return $this->responseData($ret);
}

实际获取微信支付配置的代码如下:

<?php
// ...

public function getChargePayConfig(ChanceChargeInfo $info)
{
  // 设置参数
  $attributes = [
    'trade_type'   => 'JSAPI',      // 通过jsSDK拉起微信支付
    'body'         => '充值测试',  
    'detail'       => '充值支付详情', 
    'out_trade_no' => $info->id, // 重要!用于支付成功后回调时判断是哪个订单
    'total_fee'    => $info->paidAmount, // 单位:分
    'notify_url'   => $this->config->env['host'].'/callback/chargePaid', // 重要!用于告诉微信,支付成功后的回调
    'openid'       => $this->cookieData['open_id'], // 用户的open_id
  ];

  // 创建微信支付订单
  $order = new Order($attributes);

  // 获取支付配置
  $ret = $this->wechat->payment->prepare($order);

  if ($ret->return_code == 'SUCCESS' && $ret->result_code == 'SUCCESS') {
    return $this->wechat->payment->configForJSSDKPayment($ret->prepay_id);
  } else {
    return FALSE;
  }
}

9. 开发微信支付前端页面

本节参考微信官方文档写了一个最基本的微信支付前端页面:/jsWxPayDemo.html 因为微信支付需要获取微信用户信息(open_id),所以不能直接跳转相应的支付页面,而是应该先跳转获取微信用户信息的中间页,根据前文,实际的跳转地址为:http://mobile.demo.com/wechat/authpage?redirect=jsWxPayDemo.html,可以将该地址配置为微信公众号的菜单。

大体的工作流程分为两步:

  • (1)调用jsSDK配置下发接口,获取到微信jsSDK配置后,配置wx对象;
  • (2)提供一个触发支付的方式(点击按钮),在事件触发后,调用下发微信支付配置的接口,用获取到的数据拉起微信支付; 全文如下:
<html>
<head>
    <script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
    <script type="text/javascript" src="http://res.wx.qq.com/open/js/jweixin-1.2.0.js"></script>
</head>

<body>
<script>
    $(document).ready(function () {
        console.log(wx);
        var wxPayConfig = null;
        var wxJsConfig = null;

        // ====================== jsSDK Config ======================
        //var currUrl = 'http://mobile.bz.com/jsWxPayDemo.html';
        var currUrl = 'http://mobile.demo.com/jsWxPayDemo.html';

        // 先请求后端接口,获取jsSDK的配置信息
        // url为当前页面的url,在使用了react router的情况下,可能是根url,而不是前端的url
        $.post("/wechat/jsSdkConfig",
            {url: currUrl},
            function (ret, status) {
                // todo 错误处理
                wxJsConfig = JSON.parse(ret.data);

                // debug
                alert(JSON.stringify(wxJsConfig));

                // 配置wx对象,这样才能使用wxJsBridge的接口,包括支付、定位、扫码等等
                wx.config({
                    debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
                    appId: wxJsConfig.appId, // 必填,公众号的唯一标识
                    timestamp: wxJsConfig.timestamp, // 必填,生成签名的时间戳
                    nonceStr: wxJsConfig.nonceStr, // 必填,生成签名的随机串
                    signature: wxJsConfig.signature,// 必填,签名,见附录1
                    jsApiList: wxJsConfig.jsApiList // 必填,需要使用的JS接口列表,所有JS接口列表见附录2
                });
            }
        );

        // config信息验证后会执行ready方法,所有接口调用都必须在config接口获得结果之后,config是一个客户端的异步操作,所以如果需要在页面加载时就调用相关接口,则须把相关接口放在ready函数中调用来确保正确执行。对于用户触发时才调用的接口,则可以直接调用,不需要放在ready函数中。
        wx.ready(function () {
            alert('config ready!');
            // todo
        });

        // config信息验证失败会执行error函数,如签名过期导致验证失败,具体错误信息可以打开config的debug模式查看,也可以在返回的res参数中查看,对于SPA可以在这里更新签名。
        wx.error(function (res) {
            alert('config error!');
            // todo
        });


        $("#btn").click(function () {

            // ====================== wxpay Config ======================
            // 获取支付配置(后台会创建预支付订单,微信支付成功后回调处理订单状态)
            $.post("/wechat/chargePayConfig",
                {chance: 1},
                function (ret, status) {
                    // todo 错误处理
                    wxPayConfig = ret.data;

                    // debug
                    alert(JSON.stringify(wxPayConfig));

                    // 发起支付
                    wx.chooseWXPay({
                        timestamp: wxPayConfig.timestamp, // 支付签名时间戳,注意微信jssdk中的所有使用timestamp字段均为小写。但最新版的支付后台生成签名使用的timeStamp字段名需大写其中的S字符
                        nonceStr: wxPayConfig.nonceStr, // 支付签名随机串,不长于 32 位
                        package: wxPayConfig.package, // 统一支付接口返回的prepay_id参数值,提交格式如:prepay_id=***)
                        signType: wxPayConfig.signType, // 签名方式,默认为'SHA1',使用新版支付需传入'MD5'
                        paySign: wxPayConfig.paySign, // 支付签名
                        success: function (res) {
                            // 支付成功后的回调函数,比如展示支付成功
                            // todo
                            alert('支付成功!');
                        }
                    });

                }
            );

        });
    });
</script>
<br/><br/><br/>

<button id="btn">支付测试</button>

</body>
</html>

10. 处理微信支付成功的回调

<?php
// ...

// callback/chargePaid
public function chargePaidAction()
{
  // $this->log->info($this->request->get());
  $response =  ChanceBusiness::instance()->paidCallback();
  $response->send();
}

实际的处理逻辑:

<?php
// ...

public function paidCallback()
{
  $response = $this->wechat->payment->handleNotify(function ($notify, $successful) {
            
  // 获取充值订单
  $chanceChargeInfoService = new ChanceChargeInfoService();
  $chanceChargeInfo = $chanceChargeInfoService->findById($notify->out_trade_no);
    if (!$chanceChargeInfo) {
      return FALSE;  // 通知微信,充值订单不存在
    }

    // 如果已支付,或者已取消,则跳过(因为可能多次通知)
    if ($chanceChargeInfo->status != ChanceChargeStatus::UNPAID) {
      return TRUE;
    }

    // 更新用户充值信息
    $chanceInfoService = new ChanceInfoService();
    $chanceInfo = new ChanceInfo();
    $chanceInfo->userId = $chanceChargeInfo->userId;
    $chanceInfo = $chanceInfoService->findUnique($chanceInfo);
  
    if (empty($chanceInfo)) {
      // 出错
      if (FALSE === $chanceInfo) {
        return FALSE;
      }

      // 尚无记录,新建一条
      $chanceInfo = new ChanceInfo();
      $chanceInfo->userId = $chanceChargeInfo->userId;
      $chanceInfo->chance = 0;
      $chanceInfo = $chanceInfoService->add($chanceInfo);
      if (!$chanceInfo) {
        return FALSE;
      }
    }

    // 使用事务更新相关订单
    $transactionConn = TransactionManager::start($chanceChargeInfoService, $chanceInfoService);
    if ($successful) { // 支付成功
      $chanceChargeInfo->status = ChanceChargeStatus::PAID;
      $chanceChargeInfo->transactionId = $notify->transaction_id;
      $chanceChargeInfo->remark = json_encode($notify);
      $chanceInfo->chance += $chanceChargeInfo->chance;
    } else { // 支付失败
      $chanceChargeInfo->status = ChanceChargeStatus::CANCELED;
      $chanceChargeInfo->remark = '支付失败';
    }
    
    // 修改充值订单状态
    if (!$chanceChargeInfoService->update($chanceChargeInfo)) {
      TransactionManager::rollback($transactionConn);
      $this->log->error('充值记录付款回调失败: ' . json_encode($chanceChargeInfo));
      return FALSE;
    }
    
    // 修改用户当前点数
    if (!$chanceInfoService->update($chanceInfo)) {
      TransactionManager::rollback($transactionConn);
      $this->log->error('用户充值点数更新失败: ' . json_encode($chanceInfo));
      return FALSE;
    }

    TransactionManager::commit($transactionConn);

    return TRUE;
  });

  return $response;
}

$notify的内容:

{
    "appid": "wx2dbfcf8d32f00e22",
    "bank_type": "CFT",
    "cash_fee": "1",
    "fee_type": "CNY",
    "is_subscribe": "Y",
    "mch_id": "**",
    "nonce_str": "58e9dd0b356e4",
    "openid": "omSPewKRJuP0oIpJFVGdB-ntnOJo",
    "out_trade_no": "60",
    "result_code": "SUCCESS",
    "return_code": "SUCCESS",
    "sign": "7A5B833C1645D0A7BBBD1E84F7950FB0",
    "time_end": "20170409150452",
    "total_fee": "1",
    "trade_type": "JSAPI",
    "transaction_id": "4008012001201704096413270224"
}

微信公众号:时空波隐者
文章目录