微信支付(教学版):下单 → 扫码 → 回调 → 关单
微信支付(教学版):下单 → 扫码 → 回调 → 关单
这份文档不是“概览”,而是“照着做”的教程:你按步骤走一遍,就能知道微信支付这条链路每一步在哪里、怎么调用、关键代码长什么样。
你现在项目里的“微信支付下单”实际用的是 Native 扫码(返回
codeUrl二维码链接),不是 App SDK 拉起支付参数那种模式。
0. 你要做的事(目标)
- 让用户在 App/前端发起“购买套餐” → 服务端创建本地订单并向微信统一下单 → 前端展示二维码 → 用户支付后微信回调 → 服务端验签/验金额/幂等更新订单并发放权益。
1. 第 0 步:把配置补齐(不贴真实密钥)
配置文件:music-app-api-standalone/src/main/resources/application.yml
wxpay:
app-id: ${WXPAY_APP_ID}
mch-id: ${WXPAY_MCH_ID}
mch-key: ${WXPAY_MCH_KEY} # V2 API key
notify-url: https://<你的域名>/app-api/pay/wechat/notify
use-sandbox-env: false关键点:
notify-url必须和微信商户平台配置一致(否则回调收不到)。- 回调接口必须放行匿名访问(否则微信回调会被 401/403)。
放行位置:music-app-api-standalone/src/main/java/com/music/api/config/SecurityConfig.java
.antMatchers("/pay/wechat/notify").permitAll();SDK 初始化位置(想看“wxpay.* 是怎么注入进 SDK 的”就从这里读):
music-app-api-standalone/src/main/java/com/music/api/config/WxPayProperties.javamusic-app-api-standalone/src/main/java/com/music/api/config/WxPayConfiguration.java
2. 第 1 步:前端/客户端先“下单”(拿到二维码 codeUrl)
2.1 调接口(给你一个可复制的 curl 模板)
接口:POST /order/package/app/pay
入口:music-app-api-standalone/src/main/java/com/music/api/controller/order/PackageController.java
请求体(DTO):music-app-api-standalone/src/main/java/com/music/api/dto/PackagePurchaseRequest.java
curl -X POST 'http://127.0.0.1:8080/order/package/app/pay' ^
-H 'Content-Type: application/json' ^
-H 'Authorization: Bearer <你的JWT>' ^
-d '{"packageId": 1}'成功响应(核心是 orderNo + codeUrl):
{
"code": 200,
"msg": "下单成功",
"data": {
"orderNo": "ORDxxxxxxxxxxxxxxxx",
"tradeType": "NATIVE",
"codeUrl": "weixin://wxpay/bizpayurl?pr=xxxx"
}
}2.2 关键代码长什么样(你主要看这几行)
下单核心实现:music-app-api-standalone/src/main/java/com/music/api/service/impl/PackageServiceImpl.java 的 createAppPayOrder(...)
(精简版,保留关键字段/调用)
// 1) 先落库本地订单(待支付)
MusicOrder order = new MusicOrder();
order.setOrderNo(generateOrderNo());
order.setUserId(userId);
order.setPackageId(packageId);
order.setOrderAmount(pkg.getPrice());
order.setPaymentMethod("1"); // 1=微信
order.setPaymentStatus("0"); // 0=待支付
order.setOrderStatus("0"); // 0=待处理
musicOrderMapper.insert(order);
// 2) 调微信统一下单(Native 扫码)
WxPayUnifiedOrderRequest wxRequest = new WxPayUnifiedOrderRequest();
wxRequest.setOutTradeNo(order.getOrderNo());
wxRequest.setTradeType(WxPayConstants.TradeType.NATIVE);
wxRequest.setTotalFee(priceYuan.multiply(new BigDecimal("100")).setScale(0).intValue()); // 元→分
wxRequest.setSpbillCreateIp(clientIp);
WxPayNativeOrderResult wxResult =
wxPayService.createOrder(WxPayConstants.TradeType.Specific.NATIVE, wxRequest);
// 3) 返回二维码链接
resp.setOrderNo(order.getOrderNo());
resp.setTradeType(WxPayConstants.TradeType.NATIVE);
resp.setCodeUrl(wxResult.getCodeUrl());3. 第 2 步:前端展示二维码 + 轮询订单状态
二维码:把 codeUrl 用前端库生成二维码即可(这是微信 Native 方案)。
轮询接口:GET /order/package/status?orderNo=...
入口:music-app-api-standalone/src/main/java/com/music/api/controller/order/PackageController.java
curl 'http://127.0.0.1:8080/order/package/status?orderNo=ORDxxx' ^
-H 'Authorization: Bearer <你的JWT>'返回字段重点:
paymentStatus:0待支付 /1已支付 /2支付失败 /3已退款orderStatus:0待处理 /1已完成 /2已取消 /3已退款
4. 第 3 步:微信回调怎么处理(验签/验金额/幂等/发权益)
回调入口:music-app-api-standalone/src/main/java/com/music/api/controller/order/WxPayNotifyController.java
4.1 回调的请求长什么样
- 微信回调是 XML(请求体就是 XML 字符串)
- 你服务端做的第一件事是:读 body →
wxPayService.parseOrderNotifyResult(xml)(解析+验签)
4.2 回调核心逻辑(精简版)
String xml = readRequestBody(request);
// 1) 解析 + 验签(V2 key)
WxPayOrderNotifyResult notify = wxPayService.parseOrderNotifyResult(xml);
// 2) 只处理 SUCCESS
if (!"SUCCESS".equalsIgnoreCase(notify.getResultCode())) {
return WxPayNotifyResponse.success("ignore");
}
// 3) 查本地订单
MusicOrder order = musicOrderMapper.selectByOrderNo(notify.getOutTradeNo());
if (order == null) {
return WxPayNotifyResponse.fail("order not found");
}
// 4) 幂等:已支付直接返回 OK(微信会重试回调)
if ("1".equals(order.getPaymentStatus())) {
return WxPayNotifyResponse.success("OK");
}
// 5) 验金额:微信 total_fee(分) vs 本地 order_amount(元)
BigDecimal notifyAmountYuan = new BigDecimal(notify.getTotalFee()).divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP);
if (order.getOrderAmount() != null && order.getOrderAmount().compareTo(notifyAmountYuan) != 0) {
return WxPayNotifyResponse.fail("amount mismatch");
}
// 6) 更新订单为已支付 + 发放权益(music_user_permission 等)
handleOrderPaid(order, notify.getTransactionId());
return WxPayNotifyResponse.success("OK");教学重点:回调必须做 验签、验金额、幂等,否则你会遇到“伪造回调/金额被篡改/重复发权益”等问题。
5. 第 4 步:用户不付钱怎么办(超时自动关单)
定时任务:music-app-api-standalone/src/main/java/com/music/api/job/OrderTimeoutScheduler.java
你可以把它理解为:每分钟扫一批“超过 N 分钟还没付的订单”,调用微信关单,然后把本地订单标记为“取消/失败”。
核心代码(精简版):
@Scheduled(fixedDelay = 60_000L)
public void scanAndCloseExpiredOrders() {
// Redis 锁:避免多实例重复处理
boolean locked = redisCache.setCacheObjectIfAbsent("app:order_timeout:lock", lockValue, 180, TimeUnit.SECONDS);
if (!locked) return;
// 查待支付订单(payment_method=1 微信)
List<MusicOrder> orders = musicOrderMapper.selectList(
new QueryWrapper<MusicOrder>()
.eq("payment_method", "1")
.eq("payment_status", "0")
.eq("order_status", "0")
.le("create_time", deadline)
.last("LIMIT " + batchSize)
);
// 逐个 closeOrder + 本地置为取消
}6. 后台“退款”怎么用(注意:不走微信退款)
入口:ruoyi-admin/src/main/java/com/ruoyi/web/controller/music/order/MusicOrderController.java
实现:ruoyi-system/src/main/java/com/ruoyi/system/service/impl/music/order/MusicOrderServiceImpl.java
教学结论:
- 当前退款接口只做 权益回退 + 订单状态更新为已退款。
- 不会调用微信退款接口(资金需要人工线下处理)。
7. 调试建议(快速定位问题)
- 看下单日志:
PackageController/PackageServiceImpl(是否落库成功、是否拿到 codeUrl) - 看回调日志:
WxPayNotifyController(是否收到 XML、是否 parse/验签失败、是否金额不一致、是否幂等) - 看关单日志:
OrderTimeoutScheduler(是否触发、是否 closeOrder 报错)