概要とよくある落とし穴
WeChat PayのAPIドキュメントや公式SDKには、記述の不備やそのままでは動作しないサンプルコードが含まれていることが多く、実装時に直面する障壁になりやすい。
街中の店舗で見かける固定のQRコードは「企業微信(エンタープライズWeChat)」の送金機能を利用しているケースが大半である。一方、購入金額に応じて都度QRコードが生成され、ユーザーがスキャンして直接決済を行う方式こそが「ネイティブ払い(モード2)」に該当する。モード1の固定QRコード方式はデータ構造エラーが頻発し成功しにくいため、モード2の実装が推奨される。
決済認証ディレクトリと設定
WeChat Payの管理画面では、決済認証ディレクトリ(例:https://example.com/payment/)を設定する必要がある。この設定は非常に厳格であり、決済を実行するページの「1つ上の階層」を正確に指定しなければならない。パスが1階層でもずれると認証エラーとなる。
SDKの設定ファイル(例:WxPay.Config.php)には、以下の必須情報を設定する。API暗号化キー(KEY)はマーチャントプラットフォームで設定した32文字の文字列である。
class WeChatPaymentConfig {
const WECHAT_APP_ID = ''; // バインド済みのアプリID
const MERCHANT_ID = ''; // マーチャントID
const API_SECRET_KEY = ''; // API暗号化キー(32文字)
const APP_SECRET = ''; // JSAPI決済用シークレット(ネイティブ払いでは未使用)
}
注文入力フォーム (order_entry.php)
ユーザーが金額と備考を入力し、QRコード生成ページへPOSTリクエストを送信するフォーム。JavaScriptでフロントエンドの入力チェックを行う。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>注文情報入力</title>
</head>
<body>
<form id="paymentForm" action="generate_qr.php" method="post">
<input type="number" id="amount" name="amount" step="0.01" required placeholder="金額を入力">
<input type="text" id="remark" name="remark" placeholder="備考">
<button type="submit">QRコード生成</button>
</form>
<script>
document.getElementById('paymentForm').addEventListener('submit', function(e) {
const amt = document.getElementById('amount').value;
if (!amt || isNaN(amt) || parseFloat(amt) <= 0) {
e.preventDefault();
alert('有効な金額を入力してください');
}
});
</script>
</body>
</html>
QRコード生成とポーリング (generate_qr.php)
フォームから送信されたデータを元にDBに注文レコードを作成し、WeChat APIを通じて決済用QRコードのURLを取得する。フロントエンドでは、5秒ごとに決済ステータスを確認するポーリング処理を実行する。なお、非同期通知(コールバックURL)が正常に受信されないトラブルが報告されているため、このポーリングによる能動的なステータス確認が重要となる。
<?php
ini_set('date.timezone', 'Asia/Tokyo');
require_once "../lib/WxPay.Api.php";
require_once "WxPay.NativePay.php";
include '../../admin/db.php';
$amountYuan = (float)$_POST['amount'];
$totalFen = (int)($amountYuan * 100); // WeChat APIは分単位で指定
$memo = htmlspecialchars($_POST['remark'] ?? '', ENT_QUOTES);
$orderId = date("YmdHis") . mt_rand(100, 999);
// DBに pending ステータスで注文を保存
$stmt = $db->prepare("INSERT INTO transaction_logs (order_id, total_amount, status, memo, created_at) VALUES (:oid, :amt, 'PENDING', :memo, NOW())");
$stmt->execute([':oid' => $orderId, ':amt' => $amountYuan, ':memo' => $memo]);
// WeChat APIに注文を作成
$nativePaySvc = new NativePay();
$orderInput = new WxPayUnifiedOrder();
$orderInput->SetBody('商品決済');
$orderInput->SetAttach('決済アタッチメント');
$orderInput->SetOut_trade_no($orderId);
$orderInput->SetTotal_fee($totalFen);
$orderInput->SetTime_start(date("YmdHis"));
$orderInput->SetTime_expire(date("YmdHis", time() + 600));
$orderInput->SetNotify_url("https://example.com/payment/notify_handler.php");
$orderInput->SetTrade_type("NATIVE");
$orderInput->SetProduct_id("PROD_001");
$apiResult = $nativePaySvc->GetPayUrl($orderInput);
$qrUrl = $apiResult["code_url"];
?>
<!DOCTYPE html>
<html lang="ja">
<head><meta charset="UTF-8"><title>決済QRコード</title></head>
<body>
<div style="text-align: center; margin-top: 50px;">
<!-- 外部QRコード生成APIを利用 -->
<img src="https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=<?php echo urlencode($qrUrl);?>" alt="QR Code" style="width:300px;height:300px;"/>
<input type="hidden" id="orderId" value="<?php echo $orderId;?>" />
</div>
<script>
setInterval(() => {
const oid = document.getElementById('orderId').value;
fetch('check_status.php', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: 'order_id=' + oid
})
.then(response => response.json())
.then(data => {
if (data.trade_state === 'SUCCESS') {
alert('決済が完了しました');
window.location.href = 'order_entry.php';
}
});
}, 5000);
</script>
</body>
</html>
決済ステータス確認 (check_status.php)
ポーリングリクエストを受け付け、WeChatの注文照会APIを呼び出して現在の決済状況を返すエンドポイント。
<?php
ini_set('date.timezone', 'Asia/Tokyo');
require_once "../lib/WxPay.Api.php";
if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['order_id'])) {
$targetOrderId = $_POST['order_id'];
$queryInput = new WxPayOrderQuery();
$queryInput->SetOut_trade_no($targetOrderId);
$queryResult = WxPayApi::orderQuery($queryInput);
echo json_encode($queryResult);
exit;
}
バックエンドでの決済照合
上記の実装に加え、バックエンド側でWeChatのダウンロード対账单(決済明細ダウンロード)APIを利用して日次で決済データを取得し、自社のDBトランザクションと突合する処理を実装することで、全額の回収漏れを防ぐことが可能である。