params = XmlUtil.xmlToMap(xmlBody);
// 3. 極早驗證簽名 (防偽造)
if (!wxPayService.verifySign(params)) {
log.warn("簽名驗證失敗: {}", params);
return buildFailXml("簽名無效");
}
// 4. 業務邏輯處理 (異步執行,快速響應微信)
CompletableFuture.runAsync(() -> processPaymentResult(params));
// 5. 立即返回成功,避免微信重試
return buildSuccessXml();
}
private void processPaymentResult(Map params) {
String outTradeNo = params.get("out_trade_no"); // 商戶訂單號
String transactionId = params.get("transaction_id"); // 微信訂單號
String resultCode = params.get("result_code"); // SUCCESS/FAIL
try {
// 核心:冪等性檢查 (Redis 分佈式鎖或數據庫唯一索引)
if (!orderService.markPaidIfNotProcessed(outTradeNo, transactionId)) {
log.info("訂單已處理,跳過: {}", outTradeNo);
return;
}
if ("SUCCESS".equals(resultCode)) {
orderService.handlePaymentSuccess(outTradeNo, transactionId, params);
} else {
orderService.handlePaymentFail(outTradeNo, params.get("err_code_des"));
}
} catch (Exception e) {
log.error("處理支付結果異常: {}", outTradeNo, e);
// 可選:發送告警、進入人工處理隊列
}
}
private String buildSuccessXml() {
return " ";
}
private String buildFailXml(String msg) {
return " ";
}
}
public class XmlUtil {
/** XML 字符串轉 Map (屬性值去除 CDATA) */
public static Map xmlToMap(String xml) {
Map map = new HashMap<>();
try {
Document doc = DocumentHelper.parseText(xml);
Element root = doc.getRootElement();
for (Element element : root.elements()) {
map.put(element.getName(), element.getTextTrim());
}
} catch (DocumentException e) {
throw new RuntimeException("XML 解析失敗", e);
}
return map;
}
/** Map 轉 XML (自動包裹 CDATA) */
public static String mapToXml(Map params) {
Document doc = DocumentHelper.createDocument();
Element root = doc.addElement("xml");
for (Map.Entry entry : params.entrySet()) {
Element el = root.addElement(entry.getKey());
el.addCDATA(entry.getValue());
}
return doc.asXML();
}
}
params) {
String receivedSign = params.remove("sign"); // 取出並移除 sign 字段
if (receivedSign == null) return false;
// 1. 參數按 ASCII 排序
String signString = params.entrySet().stream()
.filter(e -> e.getValue() != null && !e.getValue().isEmpty())
.sorted(Map.Entry.comparingByKey())
.map(e -> e.getKey() + "=" + e.getValue())
.collect(Collectors.joining("&"));
// 2. 拼接商戶密鑰
signString += "&key=" + apiKey;
// 3. MD5 加密並轉大寫
String calculatedSign = DigestUtils.md5DigestAsHex(signString.getBytes(StandardCharsets.UTF_8)).toUpperCase();
return calculatedSign.equals(receivedSign);
}
/** 統一下單等主動請求的簽名生成 */
public String generateSign(Map params) {
String signString = params.entrySet().stream()
.filter(e -> e.getValue() != null && !e.getValue().isEmpty())
.sorted(Map.Entry.comparingByKey())
.map(e -> e.getKey() + "=" + e.getValue())
.collect(Collectors.joining("&")) + "&key=" + apiKey;
return DigestUtils.md5DigestAsHex(signString.getBytes(StandardCharsets.UTF_8)).toUpperCase();
}
}
| 層級 | 實現方式 | 代碼示例 |
|---|---|---|
| 1. 數據庫唯一約束 | payment_log 表 transaction_id 唯一索引 | ALTER TABLE payment_log ADD UNIQUE KEY uk_txn_id (transaction_id); |
| 2. Redis 分佈式鎖 | 處理前加鎖,處理完釋放 | redisTemplate.opsForValue().setIfAbsent("pay:lock:" + outTradeNo, "1", 30, TimeUnit.SECONDS) |
| 3. 業務狀態機檢查 | 僅處理「待支付」狀態訂單 | if (order.getStatus() != OrderStatus.PENDING) return false; |
) conn -> conn.set(lockKey.getBytes(), "1".getBytes(),
Expiration.seconds(30), RedisStringCommands.SetOption.ifAbsent()));
if (!acquired) return false; // 正在處理中
try {
// 雙重檢查:數據庫狀態
Order order = orderMapper.selectByOutTradeNo(outTradeNo);
if (order == null || order.getStatus() != OrderStatus.PENDING) {
return false;
}
// 記錄支付流水 (唯一索引防重)
PaymentLog log = new PaymentLog();
log.setOutTradeNo(outTradeNo);
log.setTransactionId(transactionId);
log.setStatus(PaymentStatus.SUCCESS);
paymentLogMapper.insert(log); // 唯一索引衝突拋異常
return true; // 獲得處理權
} catch (DuplicateKeyException e) {
return false; // 已存在流水
} finally {
redisTemplate.delete(lockKey);
}
}
}
微信支付回調處理核心三步:讀原始 XML → 驗簽名 → 異步冪等處理。關鍵點:不能用 @RequestBody、必須驗簽、必須冪等、必須秒級響應。生產環境建議引入可靠消息隊列 (RocketMQ/Kafka) 做異步處理,配合數據庫唯一索引 + Redis 分佈式鎖 雙重冪等保障。