WeChat Pay ネイティブ払い(モード2)の実装と決済ステータス同期

概要とよくある落とし穴

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トランザクションと突合する処理を実装することで、全額の回収漏れを防ぐことが可能である。

タグ: WeChatPay PHP ネイティブ決済 QRコード決済

7月2日 17:25 投稿