本文系統整理基於 Yii2 框架的微信公眾號後台開發全流程,涵蓋伺服器接入驗證、OAuth2.0 用戶授權獲取資料、JSAPI 支付下單與回調處理、JS-SDK 簽名配置,提供可直接落地的程式碼架構與關鍵細節。
return [
'wechat' => [
'token' => 'your_token',
'appid' => 'your_appid',
'appsecret' => 'your_appsecret',
'mchid' => 'your_mchid',
'key' => 'your_pay_key',
'notifyUrl' => 'https://yourdomain.com/wechat/notify',
],
];
'urlManager' => [
'enablePrettyUrl' => true,
'enableStrictParsing' => true,
'showScriptName' => false,
'rules' => [
['class' => 'yii\rest\UrlRule', 'controller' => 'wechat', 'extraPatterns' => ['GET valid' => 'valid']],
],
],
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
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;
}
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];
}
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(' ');
}
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>" : "<$k>$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) { /* ... */ }
}
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;
}
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 覆蓋。相關專題:《Yii 框架入門及常用技巧總結》、《PHP 優秀開發框架總結》。