簡單實現微信小程式支付+php後端(回撥、查詢訂單、訂單資訊入庫)

TANKING發表於2023-01-02

本文講解如何開發微信小程式支付,包含小程式發起支付,後端統一下單,查詢訂單,訂單回撥,訂單入庫等操作。

流程

微信小程式獲取訂單引數->向後端發起同意下單請求->獲取訂單引數->小程式呼叫Api進行發起支付->支付完成->傳送回撥->支付結果入庫->查詢訂單支付狀態。

後端程式碼

getOpenid.php

需要配置的引數有 $appid、$secret、orderPrice,其中 $appid、$secret、orderPrice 是你小程式的兩個引數,orderPrice 是訂單金額,以元為單位。

<?php

// 頁面編碼
header("content-type:application/json");

// 獲得小程式傳過來的CODE
$code = trim($_GET['code']);

// 小程式appid
$appid = "這裡填寫你的";

// 小程式appscret
$secret = "這裡填寫你的";

// 授權登入api介面
$api = "https://api.weixin.qq.com/sns/jscode2session?appid=$appid&secret=$secret&js_code=$code&grant_type=authorization_code";

// 發起請求
$result = file_get_contents($api);

// 獲取openid
$arr_result = json_decode($result, true);
$openid = $arr_result["openid"];

// 返回資訊
$result = array(
    'code' => 200,
    'msg' => '獲取openid成功',
    'openid' => $openid,
    'orderPrice' => 0.01 // 單位:元
);

// 輸出JSON
echo json_encode($result,JSON_UNESCAPED_UNICODE);

?>

creatOrder.php

需要配置的引數都在程式碼中有說明。
$appid、$mchid、$xlid、$data['notify_url']、$set_body、$price

<?php

// 頁面編碼
header('Content-type:text/html; Charset=utf-8');
ini_set('date.timezone','Asia/Shanghai');

// 統一下單
function wechartAddOrder($name,$ordernumber,$money,$openid,$timeStamp,$noncestr){
    $url = "https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi";
    $urlarr = parse_url($url);

    // 需要配置以下引數
    // 需要配置以下引數
    // 需要配置以下引數
    // 需要配置以下引數
    // 需要配置以下引數
    $appid = '填寫你的小程式appid'; // appID
    $mchid = '填寫你的微信支付商戶號'; // 商戶ID
    $xlid = '填寫你的秘鑰序列號'; // 秘鑰序列號 可在這個網址中查詢 https://myssl.com/cert_decode.html

    $data = array();
    $time = $timeStamp;
    $data['appid'] = $appid;
    $data['mchid'] = $mchid;
    $data['description'] = $name; // 商品描述
    $data['out_trade_no'] = $ordernumber; // 訂單編號

    // 非同步訂單回撥線上地址
    // 就是notify.php這個檔案的線上URL
    // 例如你的notify.php所在伺服器的位置是wwwroot/xcxpay/notify.php
    // 你的域名是https://www.qq.com
    // 那麼應該按照這個格式填寫URL:https://www.qq.com/xcxpay/notify.php
    $data['notify_url'] = "填寫notify.php這個檔案的線上URL";

    $data['amount']['total'] = intval($money * 1); // 金額(單位:分)
    $data['payer']['openid'] = $openid; // 使用者openID
    $data = json_encode($data); 
    $key = getSign($data,$urlarr['path'],$noncestr,$time); // 簽名
    $token = sprintf('mchid="%s",serial_no="%s",nonce_str="%s",timestamp="%d",signature="%s"',$mchid,$xlid,$noncestr,$time,$key); // 頭部資訊
 
    $header  = array(
        'Content-Type:'.'application/json; charset=UTF-8',
        'Accept:application/json',
        'User-Agent:*/*',
        'Authorization: WECHATPAY2-SHA256-RSA2048 '.$token
    );  
    $ret = curl_post_https($url,$data,$header);
    $ret = ltrim($ret,'{"prepay_id":"');
    $ret = rtrim($ret,'}"');
    
    // 微信支付(小程式)簽名
    $str = getWechartSign($appid,$timeStamp,$noncestr,'prepay_id='.$ret);
    
    // 需返回的一些預支付引數
    $arr = array(
        'appid' => $appid,
        'timestamp' => $timeStamp,
        'package' => 'prepay_id='.$ret,
        'paySign' => $str,
        'noncestr' => $noncestr,
        'orderNum' => $ordernumber,
        'orderPrice' => intval($money * 1) // 可用number_format轉換金額
    );
    exit(json_encode($arr));
}

// 微信支付簽名
function getSign($data=array(),$url,$randstr,$time){
    $str = "POST"."\n".$url."\n".$time."\n".$randstr."\n".$data."\n";
    $key = file_get_contents('apiclient_key.pem');// 在商戶平臺下載的秘鑰
    $str = getSha256WithRSA($str,$key);
    return $str;
}
 
// 調起支付的簽名
function getWechartSign($appid,$timeStamp,$noncestr,$prepay_id){
    $str = $appid."\n".$timeStamp."\n".$noncestr."\n".$prepay_id."\n";
    $key = file_get_contents('apiclient_key.pem');
    $str = getSha256WithRSA($str,$key);
    return $str;
}
 
function getSha256WithRSA($content, $privateKey){
    $binary_signature = "";
    $algo = "SHA256";
    openssl_sign($content, $binary_signature, $privateKey, $algo);
    $sign = base64_encode($binary_signature);
    return $sign;
}
 
/* PHP CURL HTTPS POST */
function curl_post_https($url,$data,$header){ // 模擬提交資料函式
    $curl = curl_init(); // 啟動一個CURL會話
    curl_setopt($curl, CURLOPT_URL, $url); // 要訪問的地址
    curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0); // 對認證證照來源的檢查
    curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 1); // 從證照中檢查SSL加密演算法是否存在
    curl_setopt($curl, CURLOPT_USERAGENT, $_SERVER['HTTP_USER_AGENT']); // 模擬使用者使用的瀏覽器
    curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 1); // 使用自動跳轉
    curl_setopt($curl, CURLOPT_AUTOREFERER, 1); // 自動設定Referer
    curl_setopt($curl, CURLOPT_POST, 1); // 傳送一個常規的Post請求
    curl_setopt($curl, CURLOPT_POSTFIELDS, $data); // Post提交的資料包
    curl_setopt($curl, CURLOPT_TIMEOUT, 30); // 設定超時限制防止死迴圈
    curl_setopt($curl, CURLOPT_HEADER, 0); // 顯示返回的Header區域內容
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); // 獲取的資訊以檔案流的形式返回
 
    curl_setopt($curl, CURLOPT_HTTPHEADER, $header);
    $tmpInfo = curl_exec($curl); // 執行操作
    if (curl_errno($curl)) {
        echo 'Errno'.curl_error($curl);//捕抓異常
    }
    curl_close($curl); // 關閉CURL會話
    return $tmpInfo; // 返回資料,json格式
}

// 生成noncestr
function creatnoncestr($length){
    $str = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890';
    $randStr = str_shuffle($str);
    $rands= substr($randStr,0,$length);
    return $rands;
}

// 商品名稱
$set_body = '小程式支付';

// 支付金額(單位:分)
// 還需要去getOpenid.php裡面修改這個價格才會在小程式顯示準確的價格
$price = 1;

// 商戶自定義訂單號
$out_trade_no = date('Ymd').time();

// 時間戳
$timeStamp = time();

// 支付使用者
$openid = trim($_GET['openid']);

// 隨機字串
$noncestr = creatnoncestr(8);
 
// 建立訂單
wechartAddOrder($set_body,$out_trade_no,$price,$openid,$timeStamp,$noncestr);

?>

notify.php
這個檔案是支付回撥。

<?php

// 呼叫解密類
require_once('./v3OrderCallBack.php');

// 資料庫配置
require_once('./Db.php');

// 接收支付結果回撥通知
$getCallBackData = file_get_contents('php://input');

$getData = new AesUtil;
$getReturnData = $getCallBackData;

// 將變數由json型別資料轉換為陣列
$disposeReturnData = json_decode($getReturnData, true);

// 獲取associated_data資料,附加資料
$associatedData = $disposeReturnData['resource']['associated_data'];

// 獲取nonce資料,加密使用的隨機串
$nonceStr = $disposeReturnData['resource']['nonce'];

// 獲取ciphertext資料,base64編碼後的資料密文
$ciphertext = $disposeReturnData['resource']['ciphertext'];

// 呼叫微信官方給出的方法將解密後的資料賦值給變數
$result = $getData -> decryptToString($associatedData,$nonceStr,$ciphertext);

// 解析JSON
$out_trade_no = json_decode($result)->out_trade_no; // 訂單號
$trade_state = json_decode($result)->trade_state; // 支付結果
$openid = json_decode($result)->payer->openid; // 支付使用者
$payer_total = json_decode($result)->amount->payer_total; // 支付金額

// 如果支付支付結果是已支付的
if($trade_state == 'SUCCESS'){
    
    // 將結果存入資料庫
    $conn = new mysqli($dbhost, $dbuser, $dbpwd, $dbname);
    
    // 去重
    $sql_checkNotify = "SELECT * FROM xcxpay_order WHERE order_num='$out_trade_no'";
    $result_checkNotify = $conn->query($sql_checkNotify);
    if ($result_checkNotify->num_rows > 0) {
        
        // 如果已經存在這個訂單
        // 代表伺服器多次下發回撥
        // 就不需要再次存進來了
        
        // 返回接收結果
        $ret = array(
            'code' => 200,
            'message' => '接收成功'
        );
        echo json_encode($ret);
    }else{
        
        // 否則就需要把這個訂單存進來
        $sql_notify = "INSERT INTO xcxpay_order (order_num, openid, order_total) VALUES ('$out_trade_no', '$openid', '$payer_total')";
        if ($conn->query($sql_notify) === TRUE) {
            
            // 返回接收結果
            $ret = array(
                'code' => 200,
                'message' => '接收成功'
            );
            echo json_encode($ret);
            
        }else{
            
            // 返回接收結果
            $ret = array(
                'code' => 'FAIL',
                'message' => '接收失敗'
            );
            echo json_encode($ret);
            
        }
    }
}else{
    
    // 返回接收結果
    $ret = array(
        'code' => 200,
        'message' => '接收成功'
    );
    echo json_encode($ret);
}

?>

v3OrderCallBack.php
這個檔案是支付回撥的驗籤,需要配置的引數是 ApiV3Key

image.png

<?php

class AesUtil
{
    public $aesKey = '填寫你的ApiV3Key,在商戶平臺設定並獲取';
    const KEY_LENGTH_BYTE = 32;
    const AUTH_TAG_LENGTH_BYTE = 16;
    public
    function __construct()
    {
        $aesKey = '填寫你的ApiV3Key,在商戶平臺設定並獲取';
        if (strlen($aesKey) != self::KEY_LENGTH_BYTE) {
            throw new InvalidArgumentException('無效的ApiV3Key,長度應為32個位元組');
        }
        $this->aesKey = $aesKey;
    }

    public function decryptToString($associatedData, $nonceStr, $ciphertext)
    {
        $ciphertext = \base64_decode($ciphertext);
        if (strlen($ciphertext) <= self::AUTH_TAG_LENGTH_BYTE) {
            return false;
        }
        if (function_exists('\sodium_crypto_aead_aes256gcm_is_available') && \sodium_crypto_aead_aes256gcm_is_available()) {
            return \sodium_crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $this->aesKey);
        }
        if (function_exists('\Sodium\crypto_aead_aes256gcm_is_available') && \Sodium\crypto_aead_aes256gcm_is_available()) {
            return \Sodium\crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $this->aesKey);
        }
        if (PHP_VERSION_ID >= 70100 && in_array('aes-256-gcm', \openssl_get_cipher_methods())) {
            $ctext = substr($ciphertext, 0, -self::AUTH_TAG_LENGTH_BYTE);
            $authTag = substr($ciphertext, -self::AUTH_TAG_LENGTH_BYTE);

            return \openssl_decrypt($ctext, 'aes-256-gcm', $this->aesKey, \OPENSSL_RAW_DATA, $nonceStr,
                $authTag, $associatedData);
        }
        throw new \RuntimeException('AEAD_AES_256_GCM需要PHP 7.1以上或者安裝libsodium-php');
    }
}

Db.php
資料庫配置。

<?php

/**
 * 資料庫配置檔案
 * BY TANKING
 * 2023-1-2
 **/
 
$dbhost = "資料庫地址";
$dbuser = "資料庫賬號";
$dbpwd = "資料庫密碼";
$dbname = "資料庫名稱";

?>

資料庫表結構如下:

image.png

可透過以下SQL語句執行建表。

image.png

SQL語句

CREATE TABLE `xcxpay_order` (
  `id` int(10) NOT NULL COMMENT '自增ID',
  `order_num` varchar(32) DEFAULT NULL COMMENT '訂單號',
  `openid` varchar(32) DEFAULT NULL COMMENT 'openid',
  `order_total` varchar(10) DEFAULT NULL COMMENT '訂單金額(分)',
  `order_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '訂單時間'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

證照檔案
還需要前往商戶平臺下載證照檔案。

image.png

獲取方式:

image.png

小程式程式碼

pay.wxml

<view class="orderInfo">你需要支付{{orderPrice}}元</view>
<view class="paybtn" bindtap="Reqpay">立即支付</view>
<view class="paySuccess" wx:if="{{paySuccess == true}}">支付成功</view>

pay.js
需要配置域名和後端目錄名。

Page({
      data: {
            paySuccess:false
      },

      // 進入頁面載入
      onLoad(e) {
            var that = this;

            // 獲取使用者
            wx.login({
                  success: function(res) {
                        if (res.code) {
                              wx.request({
                                    url:  "https://你的域名/目錄/getOpenid.php?code="+res.code,
                                    header: {"content-type": "application/json"},
                                    success: function(res) {

                                          console.log(res.data)
                                          // 獲取到openid和訂單金額
                                          that.setData({
                                                openid:res.data.openid,
                                                orderPrice:res.data.orderPrice
                                          })
                                    }
                              });
                        }
                  }
            })
      },

      // 發起支付
      Reqpay: function(){
            var that = this;
            wx.request({
                  url: "https://你的域名/目錄/creatOrder.php?openid="+that.data.openid,
                  header: {'content-type': 'application/json'},
                  success (res) {

                        console.log(res.data)
                        // 請求支付引數
                        var timestamp_ = res.data.timestamp;
                        var noncestr_ = res.data.noncestr;
                        var package_ = res.data.package;
                        var paySign_ = res.data.paySign;

                        // 訂單引數
                        var orderNum = res.data.orderNum; // 訂單號
                        
                        wx.requestPayment({
                              timeStamp: timestamp_.toString(),
                              nonceStr: noncestr_,
                              package: package_,
                              signType: 'MD5',
                              paySign: paySign_,
                              success (res) {
                                    console.log(res)
                                    if(res.errMsg == 'requestPayment:ok'){

                                          // 支付成功
                                          console.log('支付成功')
                                          that.setData({
                                                paySuccess:true
                                          })

                                          // 請勿使用requestPayment:ok的結果作為判斷支付成功的依據
                                          // 如需確定訂單支付的真實結果請往下繼續新增你的查詢訂單支付結果的邏輯
                                    }
                              }
                        })
                  }
            })
      }
})

pay.wxss

.orderInfo{
      width: 88%;
      height: 100px;
      background: #eee;
      border-radius: 20px;
      margin: 30px auto 0;
      line-height: 100px;
      text-align: center;
      font-size: 25px;
}
.paybtn{
      width: 88%;
      height: 55px;
      line-height: 55px;
      background: #f7c58a;
      border-radius: 10px;
      margin: 10px auto 0;
      text-align: center;
      font-size: 18px;
}
.paySuccess{
      text-align: center;
      margin-top: 30px;
      font-size: 16px;
      color: #f7c58a;
      font-weight: bold;
}

pay.json

{
      "usingComponents": {},
      "navigationBarTextStyle": "black",
      "backgroundColor": "#eee",
      "navigationBarBackgroundColor": "#f7c58a",
      "navigationBarTitleText": "微信小程式支付demo"
}

圖例

image.png

作者

TANKING
有問題請聯絡Wechat:sansure2016

相關文章