微信支付(教学版):下单 → 扫码 → 回调 → 关单

微信支付(教学版):下单 → 扫码 → 回调 → 关单

这份文档不是“概览”,而是“照着做”的教程:你按步骤走一遍,就能知道微信支付这条链路每一步在哪里、怎么调用、关键代码长什么样。

你现在项目里的“微信支付下单”实际用的是 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.java
  • music-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.javacreateAppPayOrder(...)

(精简版,保留关键字段/调用)

// 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>'

返回字段重点:

  • paymentStatus0 待支付 / 1 已支付 / 2 支付失败 / 3 已退款
  • orderStatus0 待处理 / 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 报错)