事前準備:必要なパラメータ
// 商户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内で安全に決済できるようになります。各パラメータは実際の環境に合わせて設定してください。