流程总结
- 用户点击按钮:用户点击“立即购买”按钮,触发
initiatePayment
函数。 - 获取OpenID:从页面获取OpenID,如果为空,则提示错误。
- 发送创建订单请求:将OpenID发送到后端,创建微信支付订单。
- 处理响应:根据后端返回的数据配置微信JS-SDK。
- 调用微信支付接口:调用
wx.chooseWXPay
接口,发起微信支付。 - 支付结果处理:根据支付结果显示成功或失败的提示。
第一步:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Callback</title>
<script src="https://res.wx.qq.com/open/js/jweixin-1.4.0.js"></script>
<script>
function initiatePayment() {
const openid = document.getElementById('openid').textContent;
if (!openid) {
alert('OpenID 不能为空');
return;
}
fetch('/createOrder', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ openid: openid })
}).then(response => response.json())
.then(data => {
if (data.success) {
wx.config({
debug: false, // 开启调试模式
appId: data.appId, // 必填,公众号的唯一标识
timestamp: data.timeStamp, // 必填,生成签名的时间戳
nonceStr: data.nonceStr, // 必填,生成签名的随机串
signature: data.paySign, // 必填,签名
jsApiList: ['chooseWXPay'] // 必填,需要使用的JS接口列表
});
wx.ready(function() {
wx.chooseWXPay({
timestamp: data.timeStamp, // 支付签名时间戳
nonceStr: data.nonceStr, // 支付签名随机串
package: data.package, // 统一下单接口返回的prepay_id参数值
signType: 'MD5', // 签名方式
paySign: data.paySign, // 支付签名
success: function (res) {
alert('支付成功');
},
fail: function (res) {
alert('支付失败');
}
});
});
} else {
alert('创建订单失败');
}
}).catch(error => {
console.error('Error:', error);
});
}
</script>
</head>
<body>
<h1 th:text="${message}">Callback</h1>
<p>Authorization Code: <span id="code" th:text="${code}">No Code</span></p>
<p>Your OpenID is: <span id="openid" th:text="${openid}">OpenID</span></p>
<p>Your Nickname is: <span id="nickname" th:text="${nickname}">Nickname</span></p>
<p>Your Profile Picture: <img th:src="${headimgurl}" alt="Profile Picture"></p>
<button onclick="initiatePayment()">立即购买</button>
</body>
</html>
第二步:
package com.example.demo.controller;
import org.jdom2.JDOMException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import com.example.demo.util.PayUtil;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.logging.Logger;
@RestController
public class WeChatPayController {
private static final Logger logger = Logger.getLogger(WeChatPayController.class.getName());
@Value("${wechat.appId}")
private String appId;
@Value("${wechat.mchId}")
private String mchId;
@Value("${wechat.apiKey}")
private String apiKey;
@Value("${wechat.notifyUrl}")
private String notifyUrl;
@PostMapping("/createOrder")
public Map<String, Object> createOrder(@RequestBody Map<String, String> requestBody, HttpServletRequest request) throws JDOMException, IOException {
String openid = requestBody.get("openid");
SortedMap<String, String> params = new TreeMap<>();
params.put("appid", appId);
params.put("mch_id", mchId);
params.put("nonce_str", PayUtil.generateNonceStr());
params.put("body", "Test Product");
params.put("out_trade_no", PayUtil.generateOutTradeNo());
params.put("total_fee", "1"); // 单位为分
params.put("spbill_create_ip", request.getRemoteAddr());
params.put("notify_url", notifyUrl);
params.put("trade_type", "JSAPI");
params.put("openid", openid);
// 打印所有参数
for (Map.Entry<String, String> entry : params.entrySet()) {
logger.info(entry.getKey() + ": " + entry.getValue());
}
String sign = PayUtil.createSign(params, apiKey);
logger.info("Generated Sign: " + sign);
params.put("sign", sign);
String requestXML = PayUtil.mapToXml(params);
logger.info("Request XML: " + requestXML);
RestTemplate restTemplate = new RestTemplate();
String responseXML = restTemplate.postForObject("https://api.mch.weixin.qq.com/pay/unifiedorder", requestXML, String.class);
logger.info("Response XML: " + responseXML);
Map<String, String> resultMap = PayUtil.xmlToMap(responseXML);
if ("SUCCESS".equals(resultMap.get("return_code")) && "SUCCESS".equals(resultMap.get("result_code"))) {
SortedMap<String, String> payMap = new TreeMap<>();
payMap.put("appId", appId);
payMap.put("timeStamp", String.valueOf(System.currentTimeMillis() / 1000));
payMap.put("nonceStr", PayUtil.generateNonceStr());
payMap.put("package", "prepay_id=" + resultMap.get("prepay_id"));
payMap.put("signType", "MD5");
payMap.put("paySign", PayUtil.createSign(payMap, apiKey));
Map<String, Object> data = new HashMap<>();
data.put("success", true);
data.putAll(payMap);
return data;
} else {
logger.severe("Error creating order: " + resultMap.get("return_msg"));
Map<String, Object> data = new HashMap<>();
data.put("success", false);
data.put("message", resultMap.get("return_msg"));
return data;
}
}
}
第三步:
这段代码是一个用于处理微信支付结果通知的控制器,定义在 WeChatNotifyController
中。微信支付成功后,微信服务器会通过POST请求将支付结果发送到商户服务器。商户服务器需要实现一个接口来接收和处理这个通知。以下是详细解释:
功能概述
WeChatNotifyController
负责接收和处理微信支付的结果通知。具体步骤如下:
- 接收通知数据:从
HttpServletRequest
中读取微信服务器发送的XML格式通知数据。 - 解析通知数据:将XML格式的通知数据解析为一个
Map
。 - 验证签名:使用通知数据中的签名信息和商户的API密钥进行签名验证,确保通知数据的真实性和完整性。
- 处理业务逻辑:如果签名验证成功且支付结果为成功,执行相应的业务逻辑(如更新订单状态、发送通知等)。
- 返回响应:根据处理结果返回XML格式的响应数据,告知微信服务器通知是否成功处理。
package com.example.demo.controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.Map;
import java.util.SortedMap;
import com.example.demo.util.PayUtil;
@RestController
public class WeChatNotifyController {
@PostMapping("/notify")
public String weChatPayNotify(HttpServletRequest request) {
try {
BufferedReader br = new BufferedReader(new InputStreamReader(request.getInputStream()));
String line;
StringBuilder sb = new StringBuilder();
while ((line = br.readLine()) != null) {
sb.append(line);
}
String notifyData = sb.toString();
Map<String, String> resultMap = PayUtil.xmlToMap(notifyData);
// 验证签名
if (PayUtil.createSign((SortedMap<String, String>) resultMap, "your_api_key").equals(resultMap.get("sign"))) {
// 签名验证成功,处理业务逻辑
// 比如更新订单状态、发送通知等
if ("SUCCESS".equals(resultMap.get("result_code"))) {
// 支付成功
return "<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>";
}
}
} catch (Exception e) {
e.printStackTrace();
}
return "<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[FAIL]]></return_msg></xml>";
}
}
第四步:工具类
package com.example.demo.util;
import java.security.MessageDigest;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.UUID;
import org.jdom2.Element;
import org.jdom2.input.SAXBuilder;
import org.jdom2.JDOMException;
import org.xml.sax.InputSource;
import java.io.StringReader;
import java.io.IOException;
import java.util.List;
public class PayUtil {
public static String generateNonceStr() {
return UUID.randomUUID().toString().replaceAll("-", "").substring(0, 32);
}
public static String generateOutTradeNo() {
return UUID.randomUUID().toString().replaceAll("-", "");
}
public static String createSign(SortedMap<String, String> params, String apiKey) {
StringBuilder sb = new StringBuilder();
Set<Map.Entry<String, String>> es = params.entrySet();
for (Map.Entry<String, String> entry : es) {
String k = entry.getKey();
String v = entry.getValue();
if (null != v && !"".equals(v) && !"sign".equals(k) && !"key".equals(k)) {
sb.append(k).append("=").append(v).append("&");
}
}
sb.append("key=").append(apiKey);
return MD5(sb.toString()).toUpperCase();
}
public static String MD5(String str) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(str.getBytes("UTF-8"));
byte[] md5Bytes = md.digest();
StringBuilder hexValue = new StringBuilder();
for (byte md5Byte : md5Bytes) {
int val = ((int) md5Byte) & 0xff;
if (val < 16) hexValue.append("0");
hexValue.append(Integer.toHexString(val));
}
return hexValue.toString();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static String mapToXml(SortedMap<String, String> map) {
StringBuilder xml = new StringBuilder("<xml>");
for (Map.Entry<String, String> entry : map.entrySet()) {
xml.append("<").append(entry.getKey()).append(">");
xml.append(entry.getValue());
xml.append("</").append(entry.getKey()).append(">");
}
xml.append("</xml>");
return xml.toString();
}
public static Map<String, String> xmlToMap(String xmlStr) throws JDOMException, IOException {
SAXBuilder builder = new SAXBuilder();
InputSource source = new InputSource(new StringReader(xmlStr));
org.jdom2.Document doc = builder.build(source);
Element root = doc.getRootElement();
List<Element> list = root.getChildren();
Map<String, String> map = new TreeMap<>();
for (Element e : list) {
map.put(e.getName(), e.getText());
}
return map;
}
public static String createJsSdkSign(SortedMap<String, String> params) {
StringBuilder sb = new StringBuilder();
Set<Map.Entry<String, String>> es = params.entrySet();
for (Map.Entry<String, String> entry : es) {
String k = entry.getKey();
String v = entry.getValue();
sb.append(k).append("=").append(v).append("&");
}
sb.deleteCharAt(sb.length() - 1);
return SHA1(sb.toString());
}
public static String SHA1(String str) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-1");
digest.update(str.getBytes("UTF-8"));
byte[] messageDigest = digest.digest();
StringBuilder hexStr = new StringBuilder();
for (byte b : messageDigest) {
String shaHex = Integer.toHexString(b & 0xff);
if (shaHex.length() < 2) {
hexStr.append(0);
}
hexStr.append(shaHex);
}
return hexStr.toString();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}