首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 綜合知識

Yii2 微信後台開發完整實戰:接入驗證、用戶授權、支付流程、JS-SDK 配置

     編輯:PHP 綜合知識

本文系統整理基於 Yii2 框架的微信公眾號後台開發全流程,涵蓋伺服器接入驗證、OAuth2.0 用戶授權獲取資料、JSAPI 支付下單與回調處理、JS-SDK 簽名配置,提供可直接落地的程式碼架構與關鍵細節。

一、微信伺服器接入驗證

1. 參數配置 (config/params.php)

return [
    'wechat' => [
        'token' => 'your_token',
        'appid' => 'your_appid',
        'appsecret' => 'your_appsecret',
        'mchid' => 'your_mchid',
        'key' => 'your_pay_key',
        'notifyUrl' => 'https://yourdomain.com/wechat/notify',
    ],
];

2. 路由規則 (config/main.php)

'urlManager' => [
    'enablePrettyUrl' => true,
    'enableStrictParsing' => true,
    'showScriptName' => false,
    'rules' => [
        ['class' => 'yii\rest\UrlRule', 'controller' => 'wechat', 'extraPatterns' => ['GET valid' => 'valid']],
    ],
],

3. 控制器實作 (api/controllers/WechatController.php)

namespace api\controllers;
use Yii;
use yii\rest\ActiveController;

class WechatController extends ActiveController {
    public $modelClass = '';

    public function actionValid() {
        $echoStr = Yii::$app->request->get('echostr');
        $signature = Yii::$app->request->get('signature');
        $timestamp = Yii::$app->request->get('timestamp');
        $nonce = Yii::$app->request->get('nonce');
        if ($this->checkSignature($signature, $timestamp, $nonce)) {
            echo $echoStr;
        }
    }

    private function checkSignature($signature, $timestamp, $nonce) {
        $token = Yii::$app->params['wechat']['token'];
        if (!$token) { echo 'TOKEN 未配置'; return false; }
        $tmpArr = [$token, $timestamp, $nonce];
        sort($tmpArr, SORT_STRING);
        return sha1(implode($tmpArr)) === $signature;
    }
}

微信後台填寫

URL: https://yourdomain.com/wechats/valid
Token: your_token

二、用戶授權與資料獲取 (OAuth2.0)

資料表設計 (wechat_user)

CREATE TABLE `wechat_user` (
  `id` INT(11) NOT NULL AUTO_INCREMENT,
  `openid` VARCHAR(255) NOT NULL,
  `nickname` VARCHAR(50) NOT NULL COMMENT '微信暱稱',
  `sex` TINYINT(4) NOT NULL COMMENT '性別',
  `headimgurl` VARCHAR(255) NOT NULL COMMENT '頭像',
  `country` VARCHAR(50) NOT NULL,
  `province` VARCHAR(50) NOT NULL,
  `city` VARCHAR(50) NOT NULL,
  `access_token` VARCHAR(255) NOT NULL,
  `refresh_token` VARCHAR(255) NOT NULL,
  `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

控制器擴充方法

// 1. 授權回調:換取 access_token、openid、拉取用戶資料、入庫
public function actionAccesstoken() {
    $code = Yii::$app->request->get('code');
    $state = Yii::$app->request->get('state');
    $appid = Yii::$app->params['wechat']['appid'];
    $appsecret = Yii::$app->params['wechat']['appsecret'];
    $url = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=$appid&secret=$appsecret&code=$code&grant_type=authorization_code";
    $result = json_decode($this->curlGet($url), true);
    if (empty($result['access_token'])) { /* 錯誤處理 */ return; }
    $userInfo = $this->getUserInfo($result['access_token'], $result['openid']);
    // 入庫/更新邏輯 (使用 WechatUser ActiveRecord)
    // ...
    return $this->redirect($state . $result['openid']);
}

// 2. 獲取用戶詳細資料
private function getUserInfo($access_token, $openid) {
    $url = "https://api.weixin.qq.com/sns/userinfo?access_token=$access_token&openid=$openid&lang=zh_CN";
    return json_decode($this->curlGet($url), true);
}

// 3. 前端查詢用戶資料介面
public function actionUserinfo() {
    $openid = Yii::$app->request->get('openid');
    if (!$openid) { return ['error'=>1, 'msg'=>'openid為空']; }
    $user = WechatUser::find()->where(['openid'=>$openid])->one();
    return $user ? ['error'=>0, 'msg'=>'成功', 'user'=>$user] : ['error'=>1, 'msg'=>'用戶不存在'];
}

// 通用 cURL GET
private function curlGet($url) {
    $ch = curl_init($url);
    curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>1, CURLOPT_TIMEOUT=>30, CURLOPT_SSL_VERIFYPEER=>false, CURLOPT_SSL_VERIFYHOST=>false]);
    $data = curl_exec($ch);
    curl_close($ch);
    return $data;
}

三、微信 JSAPI 支付完整流程

1. 下單介面 (actionPay)

public function actionPay() {
    $uid = Yii::$app->request->post('uid');
    $oid = Yii::$app->request->post('oid');
    $totalFee = Yii::$app->request->post('totalFee'); // 元
    if (!$uid || !$oid || !$totalFee) { return ['error'=>1, 'msg'=>'參數錯誤']; }
    $wxPay = new \api\sdk\WechatPay(
        Yii::$app->params['wechat']['mchid'],
        Yii::$app->params['wechat']['appid'],
        Yii::$app->params['wechat']['key']
    );
    $package = $wxPay->createJsBizPackage($uid, $totalFee, $oid, Yii::$app->params['wechat']['notifyUrl'], time());
    return ['error'=>0, 'msg'=>'下單成功', 'package'=>$package];
}

2. 異步通知處理 (actionNotify)

public function actionNotify() {
    $xml = file_get_contents('php://input');
    $obj = simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA);
    if (!$obj || $obj->return_code != 'SUCCESS' || $obj->result_code != 'SUCCESS') {
        exit('FAIL');
    }
    $wxPay = new \api\sdk\WechatPay(/*...*/);
    $arr = (array)$obj;
    unset($arr['sign']);
    if ($wxPay->getSign($arr, Yii::$app->params['wechat']['key']) != $obj->sign) {
        exit('SIGN ERROR');
    }
    // 業務邏輯:更新訂單狀態、防重複處理
    $orders = Order::find()->where(['uid'=>$obj->openid, 'oid'=>$obj->out_trade_no, 'status'=>0])->all();
    foreach ($orders as $order) { $order->status = 1; $order->save(); }
    exit('');
}

3. 核心支付 SDK (api/sdk/WechatPay.php) - 關鍵方法

namespace api\sdk;
use Yii;
class WechatPay {
    protected $mchid, $appid, $key;
    public function __construct($mchid, $appid, $key) { $this->mchid=$mchid; $this->appid=$appid; $this->key=$key; }

    public function createJsBizPackage($openid, $totalFee, $outTradeNo, $notifyUrl, $timestamp) {
        $unified = [
            'appid'=>$this->appid, 'mch_id'=>$this->mchid, 'nonce_str'=>self::createNonceStr(),
            'body'=>'支付', 'out_trade_no'=>$outTradeNo, 'total_fee'=>intval($totalFee*100),
            'spbill_create_ip'=>'127.0.0.1', 'notify_url'=>$notifyUrl, 'trade_type'=>'JSAPI', 'openid'=>$openid
        ];
        $unified['sign'] = self::getSign($unified, $this->key);
        $xml = self::arrayToXml($unified);
        $resp = simplexml_load_string(self::curlPost('https://api.mch.weixin.qq.com/pay/unifiedorder', $xml), 'SimpleXMLElement', LIBXML_NOCDATA);
        if (!$resp || $resp->return_code!='SUCCESS' || $resp->result_code!='SUCCESS') { /* 錯誤處理 */ }
        $pkg = [
            'appId'=>$this->appid, 'timeStamp'=>$timestamp, 'nonceStr'=>self::createNonceStr(),
            'package'=>'prepay_id='.$resp->prepay_id, 'signType'=>'MD5'
        ];
        $pkg['paySign'] = self::getSign($pkg, $this->key);
        return $pkg;
    }

    // 簽名、XML轉換、cURL、隨機字串等工具方法...
    public static function getSign($params, $key) {
        ksort($params, SORT_STRING);
        $str = self::formatQueryParaMap($params, false) . "&key=$key";
        return strtoupper(md5($str));
    }
    public static function arrayToXml($arr) {
        $xml = "";
        foreach ($arr as $k=>$v) {
            $xml .= is_numeric($v) ? "<$k>$v" : "<$k>";
        }
        return $xml."";
    }
    public static function curlPost($url, $data) { /* ... */ }
    public static function curlGet($url) { /* ... */ }
    public static function createNonceStr($len=16) { /* ... */ }
    protected static function formatQueryParaMap($map, $urlEncode=false) { /* ... */ }
}

四、JS-SDK 配置簽名 (getSignPackage)

WechatPay 擴充方法

public function getSignPackage($url) {
    $ticket = self::getJsApiTicket(); // 含 Redis 快取
    $timestamp = time();
    $nonceStr = self::createNonceStr();
    $string = "jsapi_ticket=$ticket&noncestr=$nonceStr×tamp=$timestamp&url=$url";
    $signature = sha1($string);
    return [
        'appId'=>$this->appid, 'nonceStr'=>$nonceStr, 'timestamp'=>$timestamp,
        'url'=>$url, 'signature'=>$signature, 'rawString'=>$string
    ];
}

public static function getJsApiTicket() {
    $redis = Yii::$app->redis;
    if ($ticket = $redis->get('wechat:jsapi_ticket')) return $ticket;
    $accessToken = self::getAccessToken();
    $res = json_decode(self::curlGet("https://api.weixin.qq.com/cgi-bin/ticket/getticket?type=jsapi&access_token=$accessToken"));
    if ($res->ticket) { $redis->set('wechat:jsapi_ticket', $res->ticket); $redis->expire('wechat:jsapi_ticket', 7000); }
    return $res->ticket;
}

public static function getAccessToken() {
    $redis = Yii::$app->redis;
    if ($token = $redis->get('wechat:access_token')) return $token;
    $res = json_decode(self::curlGet("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=".Yii::$app->params['wechat']['appid']."&secret=".Yii::$app->params['wechat']['appsecret']));
    if ($res->access_token) { $redis->set('wechat:access_token', $res->access_token); $redis->expire('wechat:access_token', 7000); }
    return $res->access_token;
}

前端介面 (actionConfig)

public function actionConfig() {
    $url = Yii::$app->request->get('url');
    if (!$url) return ['error'=>1, 'msg'=>'缺少 url 參數'];
    $wxPay = new \api\sdk\WechatPay(/*...*/);
    return ['error'=>0, 'msg'=>'成功', 'config'=>$wxPay->getSignPackage($url)];
}

五、前端調用範例

wx.config({
    debug: true,
    appId: config.appId,
    timestamp: config.timestamp,
    nonceStr: config.nonceStr,
    signature: config.signature,
    jsApiList: ['chooseWXPay', 'scanQRCode', 'onMenuShareTimeline']
});
wx.ready(() => {
    wx.chooseWXPay({ /* pay package */ });
});

總結與最佳實踐

  • 所有微信敏感配置集中於 params.php,環境區分用 params-local.php 覆蓋。
  • Access Token / JSAPI Ticket 必須集中快取(Redis),避免重複請求導致頻率限制。
  • 支付回調需冪等設計:檢查訂單狀態、防重複更新、正確回應微信。
  • 錯誤日誌完整記錄(請求參數、微信回應、堆疊追蹤),便於排查。
  • 使用 Yii2 RESTful 控制器簡化路由與動作對應。

相關專題:《Yii 框架入門及常用技巧總結》、《PHP 優秀開發框架總結》。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved