WeChat公式アカウント決済の実装手順

事前準備:必要なパラメータ

// 商户ID
public static final String MERCHANT_ID = "xxxxxxxx";
// APIキー
public static final String API_SECRET = "xxxxxxxx";
// コールバックURL(決済成功後の通知受信)
public static final String NOTIFY_URL = "https://yourdomain.com/payment/callback";

ステップ1:フロントエンドで決済を起動

ユーザーが「支払う」ボタンを押したときに呼ばれる関数。

function initiatePayment(amount) {
    var params = {
        fee: amount, // 支払い金額(単位:分)
        // 必要に応じて追加パラメータ
    };
    var returnUrl = 'https://yourdomain.com/wechat/pay/charge.html';
    // WeChat OAuth2.0認証を利用してcodeを取得
    var authUrl = 'https://open.weixin.qq.com/connect/oauth2/authorize?appid=' + APP_ID +
        '&redirect_uri=' + encodeURIComponent(returnUrl) +
        '&response_type=code&scope=snsapi_base&state=' + JSON.stringify(params) +
        '#wechat_redirect';
    window.location.href = authUrl;
}

ステップ2:コールバックページ(charge.html)での処理

ユーザーが認証後にリダイレクトされるページ。ここでcodeを受け取り、バックエンドへ送信してプリペイIDを取得し、JSAPIで支払いを呼び出します。

<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>決済中...</title></head>
<body>
<script src="https://res.wx.qq.com/open/js/jweixin-1.4.0.js"></script>
<script>
    var code = getUrlParam('code');
    var state = JSON.parse(getUrlParam('state'));
    state.code = code;

    // バックエンドにcodeを送信し、プリペイIDを取得
    fetch('/api/wechat/prepay', {
        method: 'POST',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify(state)
    }).then(res => res.text()).then(prepayId => {
        // WeixinJSBridgeの準備
        if (typeof WeixinJSBridge == 'undefined') {
            if (document.addEventListener) {
                document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false);
            } else if (document.attachEvent) {
                document.attachEvent('WeixinJSBridgeReady', onBridgeReady);
                document.attachEvent('onWeixinJSBridgeReady', onBridgeReady);
            }
        } else {
            onBridgeReady();
        }

        function onBridgeReady() {
            var timestamp = Math.floor(Date.now() / 1000).toString();
            var nonceStr = 'random_string_123456';
            var packageStr = 'prepay_id=' + prepayId;
            // 署名生成(サーバーサイドで生成することを推奨)
            var signStr = 'appId=' + APP_ID + '&nonceStr=' + nonceStr + '&package=' + packageStr +
                '&signType=MD5&timeStamp=' + timestamp + '&key=' + API_SECRET;
            var paySign = md5(signStr).toUpperCase();

            WeixinJSBridge.invoke(
                'getBrandWCPayRequest', {
                    "appId": APP_ID,
                    "timeStamp": timestamp,
                    "nonceStr": nonceStr,
                    "package": packageStr,
                    "signType": "MD5",
                    "paySign": paySign
                },
                function(res) {
                    if (res.err_msg == "get_brand_wcpay_request:ok") {
                        // 支払い成功
                        window.location.href = 'https://yourdomain.com/success';
                    } else {
                        alert('決済失敗');
                        window.location.href = 'https://yourdomain.com/fail';
                    }
                }
            );
        }
    });

    // URLパラメータ取得関数
    function getUrlParam(name) {
        var match = location.search.match(new RegExp('[\\?&]' + name + '=([^&#]*)'));
        return match ? decodeURIComponent(match[1]) : null;
    }
</script>
</body>
</html>

ステップ3:バックエンド(Java + Spring)

@RestController
@RequestMapping("/api/wechat")
public class WechatPaymentController {

    @Autowired
    private PaymentService paymentService;

    @PostMapping("/prepay")
    public String prepay(@RequestBody Map request) {
        String code = (String) request.get("code");   // ユーザーの認証コード
        String feeStr = (String) request.get("fee");  // 支払い金額(文字列)

        // 1. codeを使ってopenidを取得
        String openid = fetchOpenId(code);
        if (openid == null) {
            return null;
        }

        // 2. 注文レコード作成(DB)
        String orderNo = generateOrderNo();
        OrderRecord order = new OrderRecord();
        order.setOrderNo(orderNo);
        order.setAmount(Integer.parseInt(feeStr));
        order.setStatus("PENDING");
        paymentService.saveOrder(order);

        // 3. WeChat統一下単APIを呼び出し、プリペイIDを所得
        String ip = getClientIp();
        Map params = new LinkedHashMap<>();
        params.put("appid", WxConfig.APP_ID);
        params.put("attach", "商品説明");
        params.put("body", "オンライン決済");
        params.put("mch_id", WxConfig.MERCHANT_ID);
        params.put("nonce_str", generateRandomString(32));
        params.put("notify_url", WxConfig.NOTIFY_URL);
        params.put("openid", openid);
        params.put("out_trade_no", orderNo);
        params.put("spbill_create_ip", ip);
        params.put("total_fee", feeStr);
        params.put("trade_type", "JSAPI");

        // 署名生成
        String sign = generateSign(params); // MD5
        params.put("sign", sign);

        String xml = buildXml(params);
        String responseXml = HttpClientUtil.postXml("https://api.mch.weixin.qq.com/pay/unifiedorder", xml);
        Map respMap = parseXml(responseXml);
        String prepayId = respMap.get("prepay_id");
        return prepayId;
    }

    private String fetchOpenId(String code) {
        String url = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=" + WxConfig.APP_ID +
                "&secret=" + WxConfig.APP_SECRET + "&code=" + code + "&grant_type=authorization_code";
        String resp = HttpClientUtil.get(url);
        Map map = JsonUtils.toMap(resp);
        return map.get("openid");
    }

    private String getClientIp() {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        if (ip != null && ip.contains(",")) {
            ip = ip.split(",")[0];
        }
        return ip;
    }
}

ステップ4:決済成功コールバック

@PostMapping("/callback")
public String paymentCallback(HttpServletRequest request) {
    try {
        // コールバックXMLを解析
        String xml = getRequestBody(request);
        Map callbackData = XmlParseUtil.parse(xml);

        String resultCode = callbackData.get("result_code");
        String outTradeNo = callbackData.get("out_trade_no");
        String totalFee = callbackData.get("total_fee");

        if ("SUCCESS".equals(resultCode)) {
            // 注文ステータス更新
            paymentService.updateOrderStatus(outTradeNo, "PAID", totalFee);
            // その他のビジネスロジック
            return responseXml("SUCCESS");
        }
        return responseXml("FAIL");
    } catch (Exception e) {
        logger.error("コールバック処理エラー", e);
        return responseXml("FAIL");
    }
}

private String getRequestBody(HttpServletRequest request) throws IOException {
    BufferedReader reader = request.getReader();
    StringBuilder sb = new StringBuilder();
    String line;
    while ((line = reader.readLine()) != null) {
        sb.append(line);
    }
    return sb.toString();
}

private String responseXml(String code) {
    return "<xml><return_code><![CDATA[" + code + "]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>";
}

補足:XML解析ユーティリティ

import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.input.SAXBuilder;
import java.io.ByteArrayInputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class XmlParseUtil {

    public static Map parse(String xml) throws Exception {
        SAXBuilder builder = new SAXBuilder();
        builder.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
        builder.setFeature("http://xml.org/sax/features/external-general-entities", false);
        builder.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
        Document doc = builder.build(new ByteArrayInputStream(xml.getBytes("UTF-8")));
        Element root = doc.getRootElement();
        Map map = new HashMap<>();
        List<Element> children = root.getChildren();
        for (Element child : children) {
            if (child.getChildren().isEmpty()) {
                map.put(child.getName(), child.getText());
            } else {
                // 子要素がある場合は再帰的処理(または連結)
                map.put(child.getName(), getChildrenText(child));
            }
        }
        return map;
    }

    private static String getChildrenText(Element parent) {
        StringBuilder sb = new StringBuilder();
        for (Element child : parent.getChildren()) {
            sb.append("<").append(child.getName()).append(">");
            if (!child.getChildren().isEmpty()) {
                sb.append(getChildrenText(child));
            }
            sb.append(child.getText());
            sb.append("</").append(child.getName()).append(">");
        }
        return sb.toString();
    }
}

上記の流れで、ユーザーがWeChat内で安全に決済できるようになります。各パラメータは実際の環境に合わせて設定してください。

タグ: WeChat Pay JSAPI Java Spring Payment Integration

6月1日 18:08 投稿