微信支付 JSAPI 接入是 PHP 開發高頻需求。本文提供一個開箱即用的 WxpayService 類,涵蓋統一下單、前端喚起參數生成、異步回調驗證、XML/簽名工具、HTTP 請求封裝等核心功能,代碼附詳細註釋,適合直接集成到項目中。
createJsBizPackage():統一下單 → 返回前端 wx.chooseWXPay 所需完整參數包notify():回調驗簽、業務邏輯預留、自動應答微信curlGet/Post、createNonceStr、arrayToXml、getSign(MD5 + ASCII 排序)mchid = $mchid; // 商戶號
$this->appid = $appid; // 公眾號 AppID
$this->key = $key; // API 密鑰(商戶平台設置)
}
/**
* 統一下單並生成 JSAPI 喚起參數
* @param string $openid 用戶 OpenID
* @param float $totalFee 金額(元)
* @param string $outTradeNo 商戶訂單號
* @param string $orderName 商品描述
* @param string $notifyUrl 異步回調地址(無問號)
* @param string $timestamp 時間戳
* @return array 含 appId, timeStamp, nonceStr, package, signType, paySign
*/
public function createJsBizPackage($openid, $totalFee, $outTradeNo, $orderName, $notifyUrl, $timestamp) {
$config = [
'mch_id' => $this->mchid,
'appid' => $this->appid,
'key' => $this->key,
];
$unified = [
'appid' => $config['appid'],
'attach' => '支付', // 附加數據
'body' => $orderName,
'mch_id' => $config['mch_id'],
'nonce_str' => self::createNonceStr(),
'notify_url' => $notifyUrl,
'openid' => $openid,
'out_trade_no' => $outTradeNo,
'spbill_create_ip' => '127.0.0.1', // 建議改為真實客戶端 IP
'total_fee' => intval($totalFee * 100), // 元轉分
'trade_type' => 'JSAPI',
];
$unified['sign'] = self::getSign($unified, $config['key']);
$responseXml = self::curlPost('https://api.mch.weixin.qq.com/pay/unifiedorder', self::arrayToXml($unified));
$unifiedOrder = simplexml_load_string($responseXml, 'SimpleXMLElement', LIBXML_NOCDATA);
if ($unifiedOrder === false) die('parse xml error');
if ($unifiedOrder->return_code != 'SUCCESS') die($unifiedOrder->return_msg);
if ($unifiedOrder->result_code != 'SUCCESS') die($unifiedOrder->err_code);
$arr = [
"appId" => $config['appid'],
"timeStamp" => $timestamp,
"nonceStr" => self::createNonceStr(),
"package" => "prepay_id=" . $unifiedOrder->prepay_id,
"signType" => 'MD5',
];
$arr['paySign'] = self::getSign($arr, $config['key']);
return $arr;
}
/**
* 異步回調入口
* @return SimpleXMLElement|false 解析後的通知對象
*/
public function notify() {
$config = [
'mch_id' => $this->mchid,
'appid' => $this->appid,
'key' => $this->key,
];
$postStr = $GLOBALS["HTTP_RAW_POST_DATA"]; // PHP 7+ 建議用 file_get_contents('php://input')
$postObj = simplexml_load_string($postStr, 'SimpleXMLElement', LIBXML_NOCDATA);
if ($postObj === false) die('parse xml error');
if ($postObj->return_code != 'SUCCESS') die($postObj->return_msg);
if ($postObj->result_code != 'SUCCESS') die($postObj->err_code);
$arr = (array)$postObj;
unset($arr['sign']);
if (self::getSign($arr, $config['key']) == $postObj->sign) {
// TODO: 這裡寫你的業務邏輯(如更新訂單狀態)
// $out_trade_no = $postObj->out_trade_no;
// $total_fee = $postObj->total_fee;
echo ' ';
return $postObj;
}
return false;
}
/* ============ 工具方法 ============ */
public static function curlGet($url, $options = []) {
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
if (!empty($options)) curl_setopt_array($ch, $options);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
$data = curl_exec($ch);
curl_close($ch);
return $data;
}
public static function curlPost($url, $postData, $options = []) {
if (is_array($postData)) $postData = http_build_query($postData);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
if (!empty($options)) curl_setopt_array($ch, $options);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
$data = curl_exec($ch);
curl_close($ch);
return $data;
}
public static function createNonceStr($length = 16) {
$chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
$str = '';
for ($i = 0; $i < $length; $i++) {
$str .= substr($chars, mt_rand(0, strlen($chars) - 1), 1);
}
return $str;
}
public static function arrayToXml($arr) {
$xml = "";
foreach ($arr as $key => $val) {
if (is_numeric($val)) {
$xml .= "<$key>$val$key>";
} else {
$xml .= "<$key>$key>";
}
}
$xml .= " ";
return $xml;
}
/**
* 簽名算法:ASCII 排序 → key=value 拼接 → 末尾加 &key=商戶密鑰 → MD5 → 大寫
*/
public static function getSign($params, $key) {
ksort($params, SORT_STRING);
$str = self::formatQueryParaMap($params, false);
return strtoupper(md5($str . "&key=" . $key));
}
protected static function formatQueryParaMap($paraMap, $urlEncode = false) {
$buff = "";
ksort($paraMap);
foreach ($paraMap as $k => $v) {
if (null != $v && "null" != $v) {
if ($urlEncode) $v = urlencode($v);
$buff .= $k . "=" . $v . "&";
}
}
return $buff ? substr($buff, 0, -1) : '';
}
}
// 後端返回的 payPackage 直接傳給 wx.chooseWXPay
wx.chooseWXPay({
timestamp: payPackage.timeStamp,
nonceStr: payPackage.nonceStr,
package: payPackage.package,
signType: payPackage.signType,
paySign: payPackage.paySign,
success: function(res) { alert('支付成功'); },
fail: function(res) { alert('支付失敗:' + res.errMsg); }
});
notify_url 必須是外網可訪問的 HTTPS 域名,且目錄需在商戶平台配置的授權目錄下。total_fee 為分,前端顯示為元,切記 ×100 / ÷100。CURLOPT_SSL_VERIFYPEER 並配置 CA 證書,避免中間人攻擊。$GLOBALS['HTTP_RAW_POST_DATA'] 已棄用,推薦 file_get_contents('php://input')。這個類已在多個項目中穩定運行,歡迎 Fork 並根據業務擴展(如退款、查訂單、紅包等接口)。