微信支付|SpringBoot集成微信小程序创建订单&支付&退款(apiV3+SDK保姆级教程)

注:本文代码亲测可用!可复制直接使用!减少工作量!

微信小程序支付官网接口文档:https://pay.weixin.qq.com/doc/v3/merchant/4012791911

本文使用【微信支付 APIv3 Java SDK(wechatpay-java) 】实现。
官方微信支付 APIv3 Java SDK https://github.com/wechatpay-apiv3/wechatpay-java

前置条件

  • Java 1.8+。
  • 成为微信支付商户
  • 商户 API 证书:指由商户申请的,包含证书序列号、商户的商户号、公司名称、公钥信息的证书。
  • 商户 API 私钥:商户申请商户API证书时,会生成商户私钥,并保存在本地证书文件夹的文件 apiclient_key.pem 中。
  • APIv3 密钥:为了保证安全性,微信支付在回调通知和平台证书下载接口中,对关键信息进行了 AES-256-GCM 加密。APIv3 密钥是加密时使用的对称密钥。

商户平台截图

在这里插入图片描述

业务流程图

在这里插入图片描述
重点步骤说明:

步骤4: 用户下单发起支付,商户可通过JSAPI下单创建支付订单。

步骤9: 商户小程序内使用小程序调起支付API(wx.requestPayment)发起微信支付,详见小程序API文档 (opens new window)。

步骤16: 用户支付成功后,商户可接收到微信支付支付结果通知支付通知API。

步骤21: 商户在没有接收到微信支付结果通知的情况下需要主动调用查询订单API查询支付结果。

具体实现步骤如下

1.前端弹框发起支付,调用【预支付订单/统一下单(/wxMiniappPay/createOrder)】接口取得预支付参数。
2.前端将上一步获得的参数,在小程序中调用支付API(wx.requestPayment)发起微信支付,用户输入支付密码后即可成功支付。
3.用户成功支付后,微信将通过【支付回调(/wxMiniappPay/payNotify)】接口推送支付成功的信息给后端,后端根据业务进行操作即可。
4.前端在用户输入密码支付后,调用【根据商户订单号查询订单(/wxMiniappPay/queryOrderByOutTradeNo)】接口,查看支付信息,按照自己的业务进行信息展示。
5.后端通过定时任务,调用【关闭订单(/wxMiniappPay/closeOrder)】接口,对未成功支付的订单进行关闭。
6.如果需要退款操作,前端调用【退款申请(/wxMiniappPay/refund)】接口,进行退款操作。
7.成功退款后,微信会通过【微信小程序退款回调】接口,推送退款状态信息,后端同步退款状态。
8.后端通过定时任务,调用【查询单笔退款(通过商户退款单号)(/wxMiniappPay/queryByOutRefundNo)】接口,同步退款状态。

上述场景中,后端根据业务需要采用数据锁进行并发控制。这里不做赘述!

微信小程序支付实现

pom.xml加入以下依赖

		<!--微信支付-->
		<dependency>
			<groupId>com.github.wechatpay-apiv3</groupId>
			<artifactId>wechatpay-java</artifactId>
			<version>0.2.15</version>
		</dependency>

application.yml配置

# 微信商户
wx:
  miniapp:
    appid: wxe*******f6 # 微信小程序appid
    secret: 3c80b11*********1aed7 # 微信小程序密钥
    merchantId: 16******99 # 商户号
    privateKeyPath: /wechat_pay/private_keys/apiclient_key.pem # 商户API私钥路径(测试环境)
    merchantSerialNumber: 73CF06******EE7EAB3 # 商户API证书序列号
    apiV3Key: aB3dE8********WxYzZ6 # 商户APIV3密钥
    payNotifyUrl: 域名/wxMiniappPay/payNotify # 支付通知地址(测试环境)
    refundNotifyUrl: 域名/wxMiniappPay/refundNotify # 退款通知地址(测试环境)

微信小程序支付配置WxPayConfig

package com.github.config.wx;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * <p>
 * 微信小程序支付配置
 * </p>
 *
 * @author songfayuan
 * @date 2024/9/30 15:59
 */
@Data
@Component
@ConfigurationProperties(prefix = "wx.miniapp")
public class WxPayConfig {
    /**
     * 微信小程序的 AppID
     */
    private String appid;

    /**
     * 微信小程序的密钥
     */
    private String secret;

    /**
     * 商户号
     */
    private String merchantId;

    /**
     * 商户API私钥路径
     */
    private String privateKeyPath;

    /**
     * 商户证书序列号
     */
    private String merchantSerialNumber;

    /**
     * 商户APIV3密钥
     */
    private String apiV3Key;

    /**
     * 支付通知地址
     */
    private String payNotifyUrl;

    /**
     * 退款通知地址
     */
    private String refundNotifyUrl;
}

微信支付证书自动更新配置WxPayAutoCertificateConfig

package com.github.config.wx;


import com.wechat.pay.java.core.Config;
import com.wechat.pay.java.core.RSAAutoCertificateConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.annotation.Resource;

/**
 * <p>
 * 微信支付证书自动更新配置
 * 一个商户号只能初始化一个配置,否则会因为重复的下载任务报错
 * </p>
 *
 * @author songfayuan
 * @date 2024/9/30 15:57
 */
@Slf4j
@Configuration
public class WxPayAutoCertificateConfig  {
    @Resource
    private WxPayConfig wxPayConfig;

    /**
     * 初始化商户配置
     * @return
     */
    @Bean
    public Config rsaAutoCertificateConfig() {
        // 这里把Config作为配置Bean是为了避免多次创建资源,一般项目运行的时候这些东西都确定了
        // 具体的参数改为申请的数据,可以通过读配置文件的形式获取
        Config config = new RSAAutoCertificateConfig.Builder()
                .merchantId(wxPayConfig.getMerchantId())
                .privateKeyFromPath(wxPayConfig.getPrivateKeyPath())
                .merchantSerialNumber(wxPayConfig.getMerchantSerialNumber())
                .apiV3Key(wxPayConfig.getApiV3Key())
                .build();

//        Config config = new RSAAutoCertificateConfig.Builder()
//                .merchantId(wxPayConfig.getMerchantId())
//                .privateKeyFromPath(wxPayConfig.getPrivateKeyPath())
//                .merchantSerialNumber(wxPayConfig.getMerchantSerialNumber())
//                .apiV3Key(wxPayConfig.getApiV3Key())
//                .build();
        log.info("初始化微信支付商户配置完成...");
        return config;
    }
}

微信小程序支付 前端控制器Controller

package com.github.modules.miniapp.controller;

import com.github.common.utils.BaseController;
import com.github.common.utils.Response;
import com.github.modules.miniapp.entity.CreateOrderReq;
import com.github.modules.miniapp.entity.QueryOrderReq;
import com.github.modules.miniapp.entity.RefundOrderReq;
import com.github.modules.miniapp.service.WxMiniappPayService;
import com.wechat.pay.java.service.payments.jsapi.model.PrepayWithRequestPaymentResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

/**
 * <p>
 * 微信小程序支付 前端控制器
 * </p>
 *
 * @author songfayuan
 * @date 2024/9/14 14:22
 */
@Slf4j
@RestController
@RequestMapping("/wxMiniappPay")
public class WxMiniappPayController extends BaseController {
    @Autowired
    private WxMiniappPayService wxMiniappPayService;

    /**
     * 预支付订单/统一下单
     * @param req
     * @return
     */
    @RequestMapping("/createOrder")
    public Response<PrepayWithRequestPaymentResponse> createOrder(@Validated @RequestBody CreateOrderReq req) {
        log.info("------预支付订单/统一下单------");
        //微信小程序登录用户openid,用户标识 说明:用户在商户appid下的唯一标识。
        String openid = getOpenid();
        req.setWxOpenId(openid);
        return this.wxMiniappPayService.createOrder(req);
    }

    /**
     * 支付回调
     *
     * @param request
     * @return
     * @throws IOException
     */
    @PostMapping("/payNotify")
    public String payNotify(HttpServletRequest request) throws IOException {
        log.info("------收到微信支付回调通知------");
        return this.wxMiniappPayService.payNotify(request);
    }

    /**
     * 根据支付订单号查询订单
     * <pre>
     *     主动查询订单结果
     *     支付订单号:业务侧的订单号
     * </pre>
     *
     * @param req
     * @return
     */
    @PostMapping("/queryOrder")
    public Response queryOrder(@Validated @RequestBody QueryOrderReq req) {
        log.info("------根据支付订单号查询订单------");
        return this.wxMiniappPayService.queryOrder(req);
    }

    /**
     * 根据商户订单号查询订单
     * <pre>
     *     主动查询订单结果
     *     支付订单号:商户订单号
     * </pre>
     *
     * @param req
     * @return
     */
    @PostMapping("/queryOrderByOutTradeNo")
    public Response queryOrderByOutTradeNo(@Validated @RequestBody QueryOrderReq req) {
        log.info("------根据商户订单号查询订单------");
        return this.wxMiniappPayService.queryOrderByOutTradeNo(req);
    }

    /**
     * 关闭订单
     * @param req
     * @return
     */

    @PostMapping("/closeOrder")
    public Response closeOrder(@Validated @RequestBody QueryOrderReq req) {
        log.info("------微信小程序支付关闭订单------");
        return this.wxMiniappPayService.closeOrder(req);
    }

    /**
     * 退款申请
     * @param req
     * @return
     */
    @PostMapping("/refund")
    public Response refund(@Validated @RequestBody RefundOrderReq req) {
        log.info("------微信支付退款------");
        return this.wxMiniappPayService.refund(req);
    }

    /**
     * 查询单笔退款(通过商户退款单号)
     * @param outRefundNo 商户退款单号
     * @return
     */
    @GetMapping("/queryByOutRefundNo")
    public Response queryByOutRefundNo(String outRefundNo) {
        log.info("------微信支付查询单笔退款------");
        return this.wxMiniappPayService.queryByOutRefundNo(outRefundNo);
    }

    /**
     * 微信小程序退款回调
     * @param request
     * @return
     * @throws Exception
     */
    @PostMapping("/refundNotify")
    public String refundNotify(HttpServletRequest request) throws Exception {
        log.info("------微信支付微信小程序退款回调------");
        return this.wxMiniappPayService.refundNotify(request);
    }



}

微信小程序支付服务类Service

package com.github.modules.miniapp.service;

import com.github.common.utils.Response;
import com.github.modules.miniapp.entity.CreateOrderReq;
import com.github.modules.miniapp.entity.QueryOrderReq;
import com.github.modules.miniapp.entity.RefundOrderReq;
import com.wechat.pay.java.service.payments.jsapi.model.PrepayWithRequestPaymentResponse;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

/**
 * <p>
 * 微信小程序支付服务类
 * </p>
 *
 * @author songfayuan
 * @date 2024/9/14 14:22
 */
public interface WxMiniappPayService {

    /**
     * 预支付订单/统一下单
     * @param req
     * @return
     */
    Response<PrepayWithRequestPaymentResponse> createOrder(CreateOrderReq req);

    /**
     * 支付回调
     * @param request
     * @return
     * @throws IOException
     */
    String payNotify(HttpServletRequest request) throws IOException;

    /**
     * 根据支付订单号查询订单
     * @param req
     * @return
     */
    Response queryOrder(QueryOrderReq req);

    /**
     * 根据商户订单号查询订单
     * @param req
     * @return
     */
    Response queryOrderByOutTradeNo(QueryOrderReq req);

    /**
     * 关闭订单
     * @param req
     * @return
     */
    Response closeOrder(QueryOrderReq req);

    /**
     * 退款
     * @param req
     * @return
     */
    Response refund(RefundOrderReq req);

    /**
     * 查询单笔退款(通过商户退款单号)
     * @param outRefundNo 商户退款单号
     * @return
     */
    Response queryByOutRefundNo(String outRefundNo);

    /**
     * 微信小程序退款回调
     * @param request
     * @return
     */
    String refundNotify(HttpServletRequest request);
}

微信小程序支付服务实现ServiceImpl

package com.github.modules.miniapp.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.github.common.utils.HttpServletUtils;
import com.github.common.utils.Response;
import com.github.config.wx.WxPayConfig;
import com.github.modules.miniapp.entity.CreateOrderReq;
import com.github.modules.miniapp.entity.PayOrderInfo;
import com.github.modules.miniapp.entity.QueryOrderReq;
import com.github.modules.miniapp.entity.RefundOrderReq;
import com.github.modules.miniapp.service.WxMiniappPayService;
import com.wechat.pay.java.core.Config;
import com.wechat.pay.java.core.exception.HttpException;
import com.wechat.pay.java.core.exception.MalformedMessageException;
import com.wechat.pay.java.core.exception.ServiceException;
import com.wechat.pay.java.core.exception.ValidationException;
import com.wechat.pay.java.core.notification.NotificationConfig;
import com.wechat.pay.java.core.notification.NotificationParser;
import com.wechat.pay.java.core.notification.RequestParam;
import com.wechat.pay.java.service.payments.jsapi.JsapiServiceExtension;
import com.wechat.pay.java.service.payments.jsapi.model.Amount;
import com.wechat.pay.java.service.payments.jsapi.model.*;
import com.wechat.pay.java.service.payments.model.Transaction;
import com.wechat.pay.java.service.refund.RefundService;
import com.wechat.pay.java.service.refund.model.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;

/**
 * <p>
 * 微信小程序支付服务实现
 * </p>
 *
 * @author songfayuan
 * @date 2024/9/14 14:23
 */
@Slf4j
@Service
public class WxMiniappPayServiceImpl implements WxMiniappPayService {

    @Autowired
    private WxPayConfig wxPayConfig;
    @Autowired
    @Qualifier("rsaAutoCertificateConfig")
    private Config config;
    @Autowired
    @Qualifier("rsaAutoCertificateConfig")
    private Config notificationConfig;

    /**
     * 预支付订单/统一下单
     *
     * @param req
     * @return
     */
    @Override
    public Response createOrder(CreateOrderReq req) {
        // 创建本地订单
        // 这里做本地业务相关的处理,包括生成一个订单号传递给微信,等于通过这个值来形成两边的数据对应。后续微信那边会返回他们的订单编号,也建议存在自己这边的数据库里。
        PayOrderInfo order = new PayOrderInfo();
        order.setOutTradeNo("test_out_trade_no_" + System.currentTimeMillis());
        order.setDescription("测试订单");
        order.setAmount(new BigDecimal("0.01"));

        // 请求微信支付相关配置
        JsapiServiceExtension service = new JsapiServiceExtension.Builder()
                .config(config)
                .signType("RSA")
                .build();
        PrepayWithRequestPaymentResponse response = new PrepayWithRequestPaymentResponse();
        try {
            PrepayRequest request = new PrepayRequest();
            request.setAppid(wxPayConfig.getAppid());
            request.setMchid(wxPayConfig.getMerchantId());
            request.setDescription(order.getDescription());
            request.setOutTradeNo(order.getOutTradeNo());
            request.setNotifyUrl(wxPayConfig.getPayNotifyUrl());
            Amount amount = new Amount();
            // 微信支付的单位是分,这里都需要乘以100
            amount.setTotal(order.getAmount().multiply(new BigDecimal("100")).intValue());
            request.setAmount(amount);
            Payer payer = new Payer();
            payer.setOpenid(req.getWxOpenId());
            request.setPayer(payer);
            log.info("请求预支付下单,请求参数:{}", JSONObject.toJSONString(request));
            // 调用预下单接口
            response = service.prepayWithRequestPayment(request);
            log.info("订单【{}】发起预支付成功,返回信息:{}", order.getOutTradeNo(), JSONObject.toJSONString(response));
        } catch (HttpException e) {
            // 发送HTTP请求失败
            log.error("微信下单发送HTTP请求失败,错误信息:", e);
            return Response.error("下单失败");
        } catch (ServiceException e) {
            // 服务返回状态小于200或大于等于300,例如500
            log.error("微信下单服务状态错误,错误信息:", e);
            return Response.error("下单失败");
        } catch (MalformedMessageException e) {
            // 服务返回成功,返回体类型不合法,或者解析返回体失败
            log.error("服务返回成功,返回体类型不合法,或者解析返回体失败,错误信息:", e);
            return Response.error("下单失败");
        } catch (ValidationException e) {
            // 验证签名失败
            log.error("微信下单验证签名失败,错误信息:", e);
            return Response.error("下单失败");
        } catch (Exception e) {
            log.error("微信下单失败,错误信息:", e);
            return Response.error("下单失败");
        }

        // TODO 更新订单状态
        // 这里就可以更新订单状态为待支付之类的
        return Response.success(response);
    }

    /**
     * 支付回调
     * <pre>
     * 注意:
     * 对后台通知交互时,如果微信收到应答不是成功或超时,微信认为通知失败,微信会通过一定的策略定期重新发起通知,尽可能提高通知的成功率,但微信不保证通知最终能成功
     *
     * 同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。 推荐的做法是,当商户系统收到通知进行处理时,先检查对应业务数据的状态,并判断该通知是否已经处理。如果未处理,则再进行处理;如果已处理,则直接返回结果成功。在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱。
     * 如果在所有通知频率后没有收到微信侧回调。商户应调用查询订单接口确认订单状态。
     * </pre>
     *
     * @param request
     * @return
     * @throws IOException
     */
    @Override
    public String payNotify(HttpServletRequest request) throws IOException {
        // 请求头Wechatpay-Signature
        String signature = request.getHeader("Wechatpay-Signature");
        // 请求头Wechatpay-nonce
        String nonce = request.getHeader("Wechatpay-Nonce");
        // 请求头Wechatpay-Timestamp
        String timestamp = request.getHeader("Wechatpay-Timestamp");
        // 微信支付证书序列号
        String serial = request.getHeader("Wechatpay-Serial");
        // 签名方式
        String signType = request.getHeader("Wechatpay-Signature-Type");
        // 构造 RequestParam
        RequestParam requestParam = new RequestParam.Builder()
                .serialNumber(serial)
                .nonce(nonce)
                .signature(signature)
                .timestamp(timestamp)
                .signType(signType)
                .body(HttpServletUtils.getRequestBody(request))
                .build();
        log.info("微信支付回调body信息:{}", requestParam.getBody());
        // 初始化 NotificationParser
        NotificationParser parser = new NotificationParser((NotificationConfig) notificationConfig);
        // 以支付通知回调为例,验签、解密并转换成 Transaction
        log.info("验签参数:{}", JSONObject.toJSONString(requestParam));
        Map<String, String> returnMap = new HashMap<>(2);
        returnMap.put("code", "FAIL");
        returnMap.put("message", "失败");
        Transaction transaction = null;
        try {
            transaction = parser.parse(requestParam, Transaction.class);
        } catch (MalformedMessageException e) {
            log.error("验签失败,解析微信支付应答或回调报文异常,返回信息:", e);
            return JSONObject.toJSONString(returnMap);
        } catch (ValidationException e) {
            log.error("验签失败,验证签名失败,返回信息:", e);
            return JSONObject.toJSONString(returnMap);
        } catch (Exception e) {
            log.error("验签失败,返回信息:", e);
            return JSONObject.toJSONString(returnMap);
        }
        log.info("验签成功!-支付回调结果:{}", transaction.toString());
        // 判断订单状态
        if (Transaction.TradeStateEnum.SUCCESS != transaction.getTradeState()) {
            log.info("内部订单号【{}】,微信支付订单号【{}】支付未成功", transaction.getOutTradeNo(), transaction.getTransactionId());
            return JSONObject.toJSONString(returnMap);
        }

        // TODO 修改订单信息

        returnMap.put("code", "SUCCESS");
        returnMap.put("message", "成功");
        return JSONObject.toJSONString(returnMap);
    }

    /**
     * 根据支付订单号查询订单
     * <pre>
     * 需要调用查询接口的情况:
     * 当商户后台、网络、服务器等出现异常,商户系统最终未接收到支付通知。
     * 调用支付接口后,返回系统错误或未知交易状态情况。
     * 调用付款码支付API,返回USERPAYING的状态。
     * 调用关单或撤销接口API之前,需确认支付状态。
     * </pre>
     *
     * @param req
     * @return
     */
    @Override
    public Response queryOrder(QueryOrderReq req) {
        QueryOrderByIdRequest queryRequest = new QueryOrderByIdRequest();
        queryRequest.setMchid(wxPayConfig.getMerchantId());
        queryRequest.setTransactionId(req.getTransactionId());
        try {
            JsapiServiceExtension service =
                    new JsapiServiceExtension.Builder()
                            .config(config)
                            .signType("RSA")
                            .build();
            Transaction result = service.queryOrderById(queryRequest);
            if (Transaction.TradeStateEnum.SUCCESS != result.getTradeState()) {
                log.info("内部订单号【{}】,微信支付订单号【{}】支付未成功", result.getOutTradeNo(), result.getTransactionId());
                return Response.error(result.getTradeStateDesc());
            }
            log.info("根据支付订单号查询订单:内部订单号【{}】,微信支付订单号【{}】支付成功", result.getOutTradeNo(), result.getTransactionId());
            log.info("根据支付订单号查询订单:订单数据data = {}", JSONObject.toJSONString(result));

            // TODO 修改订单信息
            return Response.success(result.getTradeStateDesc());
        }  catch (ServiceException e) {
            log.error("根据支付订单号查询订单:订单查询失败,发送HTTP请求成功,返回异常,返回码:{},返回信息:", e.getErrorCode(), e);
            return Response.error("订单查询失败");
        } catch (MalformedMessageException e) {
            log.error("根据支付订单号查询订单:订单查询失败,解析微信支付应答或回调报文异常,返回信息:", e);
            return Response.error("订单查询失败");
        } catch (ValidationException e) {
            log.error("根据支付订单号查询订单:订单查询失败,验证签名失败,返回信息:", e);
            return Response.error("验证签名失败");
        } catch (HttpException e) {
            log.error("根据支付订单号查询订单:订单查询失败,发送HTTP请求失败:", e);
            return Response.error("订单查询失败");
        } catch (Exception e) {
            log.error("根据支付订单号查询订单:订单查询失败,异常:", e);
            return Response.error("订单查询失败");
        }
    }

    /**
     * 根据商户订单号查询订单
     * <pre>
     * 需要调用查询接口的情况:
     * 当商户后台、网络、服务器等出现异常,商户系统最终未接收到支付通知。
     * 调用支付接口后,返回系统错误或未知交易状态情况。
     * 调用付款码支付API,返回USERPAYING的状态。
     * 调用关单或撤销接口API之前,需确认支付状态。
     * </pre>
     *
     * @param req
     * @return
     */
    @Override
    public Response queryOrderByOutTradeNo(QueryOrderReq req) {
        QueryOrderByOutTradeNoRequest queryRequest = new QueryOrderByOutTradeNoRequest();
        queryRequest.setMchid(wxPayConfig.getMerchantId());
        queryRequest.setOutTradeNo(req.getOutTradeNo());
        try {
            JsapiServiceExtension service =
                    new JsapiServiceExtension.Builder()
                            .config(config)
                            .signType("RSA")
                            .build();
            Transaction result = service.queryOrderByOutTradeNo(queryRequest);
            // trade_state【交易状态】交易状态,枚举值:*SUCCESS:支付成功*REFUND:转入退款*NOTPAY:未支付*CLOSED:已关闭*REVOKED:已撤销(仅付款码支付会返回)*USERPAYING:用户支付中(仅付款码支付会返回)*PAYERROR:支付失败(仅付款码支付会返回)
            if (Transaction.TradeStateEnum.SUCCESS != result.getTradeState()) {
                log.info("内部订单号【{}】,微信支付订单号【{}】支付未成功", result.getOutTradeNo(), result.getTransactionId());
                return Response.error(result.getTradeStateDesc());
            }
            log.info("根据商户订单号查询订单:内部订单号【{}】,微信支付订单号【{}】支付成功", result.getOutTradeNo(), result.getTransactionId());
            log.info("根据商户订单号查询订单:订单数据data = {}", JSONObject.toJSONString(result));

            // 支付订单号
            String transactionId = result.getTransactionId();

            // TODO 修改订单信息
            return Response.success(result.getTradeStateDesc());
        } catch (ServiceException e) {
            log.error("根据商户订单号查询订单:订单查询失败,发送HTTP请求成功,返回异常,返回码:{},返回信息:", e.getErrorCode(), e);
            return Response.error("订单查询失败");
        } catch (MalformedMessageException e) {
            log.error("根据商户订单号查询订单:订单查询失败,解析微信支付应答或回调报文异常,返回信息:", e);
            return Response.error("订单查询失败");
        } catch (ValidationException e) {
            log.error("根据商户订单号查询订单:订单查询失败,验证签名失败,返回信息:", e);
            return Response.error("验证签名失败");
        } catch (HttpException e) {
            log.error("根据商户订单号查询订单:订单查询失败,发送HTTP请求失败:", e);
            return Response.error("订单查询失败");
        } catch (Exception e) {
            log.error("根据商户订单号查询订单:订单查询失败,异常:", e);
            return Response.error("订单查询失败");
        }
    }

    /**
     * 关闭订单
     * <pre>
     * 以下情况需要调用关单接口:
     * 商户订单支付失败需要生成新单号重新发起支付,要对原订单号调用关单,避免重复支付;
     * 系统下单后,用户支付超时,系统退出不再受理,避免用户继续,请调用关单接口。
     * </pre>
     *
     * @param req
     * @return
     */
    @Override
    public Response closeOrder(QueryOrderReq req) {
        // 初始化服务
        JsapiServiceExtension service =
                new JsapiServiceExtension.Builder()
                        .config(config)
                        .signType("RSA")
                        .build();
        CloseOrderRequest request = new CloseOrderRequest();
        // 调用request.setXxx(val)设置所需参数,具体参数可见Request定义
        request.setMchid(wxPayConfig.getMerchantId());
        request.setOutTradeNo(req.getOutTradeNo());
        // 调用接口
        try {
            service.closeOrder(request);
        } catch (HttpException e) {
            log.error("关闭订单申请失败,发送HTTP请求失败:", e);
            return Response.error("关闭订单申请失败");
        } catch (MalformedMessageException e) {
            log.error("关闭订单申请失败,解析微信支付应答或回调报文异常,返回信息:", e);
            return Response.error("关闭订单申请失败");
        } catch (ValidationException e) {
            log.error("关闭订单申请失败,验证签名失败,返回信息:", e);
            return Response.error("验证签名失败");
        } catch (ServiceException e) {
            log.error("关闭订单申请失败,发送HTTP请求成功,返回异常,返回码:{},返回信息:", e.getErrorCode(), e);
            return Response.error("关闭订单失败:" + e.getErrorMessage());
        } catch (Exception e) {
            log.error("关闭订单申请失败,异常:", e);
            return Response.error("关闭订单申请失败");
        }
        return Response.success("关闭订单申请成功");
    }

    /**
     * 退款
     * <pre>
     * 交易时间超过一年的订单无法提交退款(按支付成功时间+365天计算)
     * 微信支付退款支持单笔交易分多次退款,多次退款需要提交原支付订单的商户订单号和设置不同的退款单号。申请退款总金额不能超过订单金额。 一笔退款失败后重新提交,请不要更换退款单号,请使用原商户退款单号
     * 请求频率限制:150qps,即每秒钟正常的申请退款请求次数不超过150次
     * 每个支付订单的部分退款次数不能超过50次
     * 如果同一个用户有多笔退款,建议分不同批次进行退款,避免并发退款导致退款失败
     * 申请退款接口的返回仅代表业务的受理情况,具体退款是否成功,需要通过退款查询接口获取结果
     * 错误或无效请求频率限制:6qps,即每秒钟异常或错误的退款申请请求不超过6次
     * 一个月之前的订单申请退款频率限制为:5000/min
     * 同一笔订单多次退款的请求需相隔1分钟
     * </pre>
     *
     * @param req
     * @return
     */
    @Override
    public Response refund(RefundOrderReq req) {
        // 初始化服务
        RefundService service = new RefundService.Builder().config(config).build();
        CreateRequest request = new CreateRequest();
        // 调用request.setXxx(val)设置所需参数,具体参数可见Request定义
        request.setOutTradeNo(req.getOutTradeNo());
        request.setOutRefundNo("REFUND_" + req.getOutTradeNo());
        AmountReq amount = new AmountReq();
        // 订单总金额,单位为分,只能为整数,详见支付金额
        amount.setTotal(decimalToLong(req.getTotalAmount()));
        // 退款金额,单位为分,只能为整数,不能超过支付总额
        amount.setRefund(decimalToLong(req.getRefundAmount()));
        amount.setCurrency("CNY");

        request.setAmount(amount);
        request.setNotifyUrl(wxPayConfig.getRefundNotifyUrl());
        // 调用接口
        Refund refund = null;
        try {
            refund = service.create(request);
        } catch (HttpException e) {
            log.error("退款申请失败,发送HTTP请求失败:", e);
            return Response.error("退款失败");
        } catch (MalformedMessageException e) {
            log.error("退款申请失败,解析微信支付应答或回调报文异常,返回信息:", e);
            return Response.error("退款失败");
        } catch (ValidationException e) {
            log.error("退款申请失败,验证签名失败,返回信息:", e);
            return Response.error("验证签名失败");
        } catch (ServiceException e) {
            log.error("退款申请失败,发送HTTP请求成功,返回异常,返回码:{},返回信息:", e.getErrorCode(), e);
            return Response.error("退款失败:" + e.getErrorMessage());
        } catch (Exception e) {
            log.error("退款申请失败,异常:", e);
            return Response.error("退款失败");
        }
        if (Status.SUCCESS.equals(refund.getStatus())) {
            log.info("退款成功!-订单号:{}", req.getOutTradeNo());
            return Response.success("退款成功");
        } else if (Status.CLOSED.equals(refund.getStatus())) {
            log.info("退款关闭!-订单号:{}", req.getOutTradeNo());
            return Response.error("退款关闭");
        } else if (Status.PROCESSING.equals(refund.getStatus())) {
            log.info("退款处理中!-订单号:{}", req.getOutTradeNo());
            return Response.error("退款处理中");
        } else if (Status.ABNORMAL.equals(refund.getStatus())) {
            log.info("退款异常!-订单号:{}", req.getOutTradeNo());
            return Response.error("退款异常");
        }
        return Response.error("退款失败");
    }

    /**
     * 查询单笔退款(通过商户退款单号)
     * <pre>
     * 提交退款申请后,通过调用该接口查询退款状态。退款有一定延时,建议查询退款状态在提交退款申请后1分钟发起,一般来说零钱支付的退款5分钟内到账,银行卡支付的退款1-3个工作日到账。
     * </pre>
     *
     * @param outRefundNo 商户退款单号
     * @return
     */
    @Override
    public Response queryByOutRefundNo(String outRefundNo) {
        // 初始化服务
        RefundService service = new RefundService.Builder().config(config).build();
        QueryByOutRefundNoRequest request = new QueryByOutRefundNoRequest();
        // 调用request.setXxx(val)设置所需参数,具体参数可见Request定义
        request.setOutRefundNo(outRefundNo.trim());
        // 调用接口
        Refund refund = null;
        try {
            refund = service.queryByOutRefundNo(request);
            log.info("退款查询结果:{}", JSONObject.toJSONString(refund));
            //【退款状态】退款到银行发现用户的卡作废或者冻结了,导致原路退款银行卡失败,可前往商户平台(pay.weixin.qq.com)-交易中心,手动处理此笔退款。可选取值:SUCCESS:退款成功CLOSED:退款关闭PROCESSING:退款处理中ABNORMAL:退款异常【退款状态】退款到银行发现用户的卡作废或者冻结了,导致原路退款银行卡失败,可前往商户平台(pay.weixin.qq.com)-交易中心,手动处理此笔退款。可选取值:SUCCESS:退款成功CLOSED:退款关闭PROCESSING:退款处理中ABNORMAL:退款异常
            if (Status.SUCCESS.equals(refund.getStatus())) {
                log.info("退款成功!-订单号:{}", outRefundNo);
                return Response.success("退款成功");
            } else if (Status.CLOSED.equals(refund.getStatus())) {
                log.info("退款关闭!-订单号:{}", outRefundNo);
                return Response.error("退款关闭");
            } else if (Status.PROCESSING.equals(refund.getStatus())) {
                log.info("退款处理中!-订单号:{}", outRefundNo);
                return Response.success("退款处理中");
            } else if (Status.ABNORMAL.equals(refund.getStatus())) {
                log.info("退款异常!-订单号:{}", outRefundNo);
                return Response.error("退款异常");
            }
        } catch (HttpException e) {
            log.error("退款查询失败,发送HTTP请求失败:", e);
            return Response.error("退款查询失败");
        } catch (MalformedMessageException e) {
            log.error("退款查询失败,解析微信支付应答或回调报文异常,返回信息:", e);
            return Response.error("退款查询失败");
        } catch (ValidationException e) {
            log.error("退款查询失败,验证签名失败,返回信息:", e);
            return Response.error("退款查询失败");
        } catch (ServiceException e) {
            log.error("退款查询失败,发送HTTP请求成功,返回异常,返回码:{},返回信息:", e.getErrorCode(), e);
            return Response.error("退款查询失败");
        } catch (Exception e) {
            log.error("退款查询失败,异常:", e);
            return Response.error("退款查询失败");
        }

        return Response.success(refund);
    }

    /**
     * 微信小程序退款回调
     * <pre>
     * 注意:
     * 对后台通知交互时,如果微信收到应答不是成功或超时,微信认为通知失败,微信会通过一定的策略定期重新发起通知,尽可能提高通知的成功率,但微信不保证通知最终能成功
     *
     * 同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。 推荐的做法是,当商户系统收到通知进行处理时,先检查对应业务数据的状态,并判断该通知是否已经处理。如果未处理,则再进行处理;如果已处理,则直接返回结果成功。在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱。
     * 如果在所有通知频率后没有收到微信侧回调。商户应调用查询订单接口确认订单状态。
     * </pre>
     *
     * @param request
     * @return
     */
    @Override
    public String refundNotify(HttpServletRequest request) {
        Map<String, String> returnMap = new HashMap<>(2);
        returnMap.put("code", "FAIL");
        returnMap.put("message", "失败");
        try {
            // 请求头Wechatpay-Signature
            String signature = request.getHeader("Wechatpay-Signature");
            // 请求头Wechatpay-nonce
            String nonce = request.getHeader("Wechatpay-Nonce");
            // 请求头Wechatpay-Timestamp
            String timestamp = request.getHeader("Wechatpay-Timestamp");
            // 微信支付证书序列号
            String serial = request.getHeader("Wechatpay-Serial");
            // 签名方式
            String signType = request.getHeader("Wechatpay-Signature-Type");
            // 构造解析器和请求参数
            NotificationParser parser = new NotificationParser((NotificationConfig) notificationConfig);
            RequestParam requestParam = new RequestParam.Builder()
                    .serialNumber(serial)
                    .nonce(nonce)
                    .signature(signature)
                    .timestamp(timestamp)
                    .signType(signType)
                    .body(HttpServletUtils.getRequestBody(request))
                    .build();
            log.info("微信小程序支付退款回调验签参数: {}", JSON.toJSONString(requestParam));
            // 解析通知
            RefundNotification notification = null;
            try {
                notification = parser.parse(requestParam, RefundNotification.class);
            } catch (MalformedMessageException e) {
                log.error("微信小程序支付退款回调:回调通知参数不正确、解析通知数据失败:", e);
                returnMap.put("message", "回调通知参数不正确");
                return JSONObject.toJSONString(returnMap);
            } catch (ValidationException e) {
                log.error("微信小程序支付退款回调:签名验证失败 ", e);
                returnMap.put("message", "签名验证失败");
                return JSONObject.toJSONString(returnMap);
            } catch (Exception e) {
                log.error("微信小程序支付退款回调:未知异常 ", e);
                returnMap.put("message", "未知异常");
                return JSONObject.toJSONString(returnMap);
            }
            log.info("微信小程序支付退款回调解析成功: {}", JSON.toJSONString(notification));
            // 根据退款状态处理
            Status refundStatus = notification.getRefundStatus();
            switch (refundStatus) {
                case SUCCESS:
                    // TODO 退款成功逻辑

                    returnMap.put("code", "SUCCESS");
                    returnMap.put("message", "退款成功");
                    return JSONObject.toJSONString(returnMap);
                case PROCESSING:
                    log.warn("退款处理中: {}", notification);
                    returnMap.put("message", "退款处理中,请稍后查询");
                    return JSONObject.toJSONString(returnMap);
                case ABNORMAL:
                    log.error("退款异常: {}", notification);
                    returnMap.put("message", "退款异常,请联系客服");
                    return JSONObject.toJSONString(returnMap);
                case CLOSED:
                    log.warn("退款已关闭: {}", notification);
                    returnMap.put("message", "退款已关闭,操作失败");
                    return JSONObject.toJSONString(returnMap);
                default:
                    log.error("未知退款状态: {}", refundStatus);
                    returnMap.put("message", "未知退款状态");
                    return JSONObject.toJSONString(returnMap);
            }
        } catch (Exception e) {
            log.error("退款回调处理异常", e);
            returnMap.put("message", "退款处理异常");
            return JSONObject.toJSONString(returnMap);
        }
    }


    /**
     * 金额转换
     *
     * @param money
     * @return
     */
    private static long decimalToLong(BigDecimal money) {
        return money.multiply(BigDecimal.valueOf(100)).longValue();
    }

}

预支付订单/统一下单请求参数CreateOrderReq

package com.github.modules.miniapp.entity;

import lombok.Data;

/**
 * <p>
 * 预支付订单/统一下单请求参数
 * </p>
 *
 * @author songfayuan
 * @date 2024/9/30 17:37
 */
@Data
public class CreateOrderReq {
    /**
     * 微信用户openid(前端不用传参)
     */
    private String wxOpenId;
}

支付订单信息PayOrderInfo

package com.github.modules.miniapp.entity;

import lombok.Data;

import java.math.BigDecimal;

/**
 * <p>
 * 支付订单信息
 * </p>
 *
 * @author songfayuan
 * @date 2024/9/30 17:46
 */
@Data
public class PayOrderInfo {
    /**
     * 订单标题
     */
    private String description;
    /**
     * 商户订单号
     * 只能是数字、大小写字母_-*且在同一个商户号下唯一。
     */
    private String outTradeNo;
    /**
     * 支付金额,单位:元
     */
    private BigDecimal amount;
}

工具HttpServletUtils

特别注意:如果使用本工具,从request中采集不到body数据,请查看是否有其他过滤器,request被使用过了。

package com.github.common.utils;


import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;

/**
 * <p>
 * HttpServletRequest 获取请求体
 * </p>
 *
 * @author songfayuan
 * @date 2024/9/30 19:11
 */
public class HttpServletUtils {

    /**
     * 获取请求体
     *
     * @param request
     * @return
     * @throws IOException
     */
    public static String getRequestBody(HttpServletRequest request) throws IOException {
        ServletInputStream stream = null;
        BufferedReader reader = null;
        StringBuffer sb = new StringBuffer();
        try {
            stream = request.getInputStream();
            // 获取响应
            reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8));
            String line;
            while ((line = reader.readLine()) != null) {
                sb.append(line);
            }
        } catch (IOException e) {
            throw new IOException("读取返回支付接口数据流出现异常!");
        } finally {
            reader.close();
        }
        return sb.toString();
    }

}

订单查询请求QueryOrderReq

package com.github.modules.miniapp.entity;

import lombok.Data;

/**
 * <p>
 * 订单查询请求
 * </p>
 *
 * @author songfayuan
 * @date 2024/9/30 19:19
 */
@Data
public class QueryOrderReq {
    /**
     * 订单号:业务侧的订单号
     */
    private String transactionId;
    /**
     * 商户订单号
     */
    private String outTradeNo;
}

退款订单请求参数RefundOrderReq

package com.github.modules.miniapp.entity;

import lombok.Data;

import java.math.BigDecimal;

/**
 * <p>
 * 退款订单请求参数
 * </p>
 *
 * @author songfayuan
 * @date 2024/9/30 19:19
 */
@Data
public class RefundOrderReq {
    /**
     * 订单号:业务侧的订单号
     */
    private String transactionId;
    /**
     * 商户订单号
     */
    private String outTradeNo;

    /**
     * 原订单金额 说明:原支付交易的订单总金额,这里单位为元。
     */
    private BigDecimal totalAmount;

    /**
     * 退款金额 说明:退款金额,这里单位为元,不能超过原订单支付金额。
     */
    private BigDecimal refundAmount;
}

返回参数封装类Response

/**
 * Copyright (c) 2022-2030 杭租通 All rights reserved.
 * <p>
 * 1414798079@qq.com
 * <p>
 * 版权所有,侵权必究!
 */

package com.github.common.utils;

import lombok.Data;

/**
 * 返回参数封装类
 *
 * @param <T>
 * @author songfayuan
 */

@Data
public class Response<T> {
    /**
     * 状态码
     */
    protected int code;

    /**
     * 提示信息
     */
    protected String msg;

    /**
     * 数据
     */
    protected T data;

    /**
     * 成功时状态码
     */
    private static final int SUCCESS_CODE = 200;

    /**
     * 成功时提示信息
     */
    private static final String SUCCESS_MSG = "success";

    /**
     * 异常、错误信息状态码
     */
    private static final int ERROR_CODE = 500;

    /**
     * 异常、错误提示信息
     */
    private static final String ERROR_MSG = "服务器内部异常,请联系技术人员!";

    /**
     * 返回成功消息
     *
     * @param <T>
     * @return
     */
    public static <T> Response<T> success() {
        Response resp = new Response();
        resp.code = (SUCCESS_CODE);
        resp.msg = (SUCCESS_MSG);
        return resp;
    }

    /**
     * 返回成功消息
     *
     * @param <T>
     * @param msg
     * @return
     */
    public static <T> Response<T> successResponse(String msg) {
        Response resp = new Response();
        resp.code = SUCCESS_CODE;
        resp.msg = msg;
        return resp;
    }

    /**
     * 返回错误消息
     *
     * @param <T>
     * @return
     */
    public static <T> Response<T> error() {
        Response resp = new Response();
        resp.code = (ERROR_CODE);
        resp.msg = (ERROR_MSG);
        return resp;
    }

    /**
     * 返回错误消息
     *
     * @param <T>
     * @param msg
     * @return
     */
    public static <T> Response<T> errorResponse(String msg) {
        Response resp = new Response();
        resp.code = ERROR_CODE;
        resp.msg = msg;
        return resp;
    }

    /**
     * 自定义状态码、提示信息
     *
     * @param <T>
     * @param code 状态码
     * @param msg  提示信息
     * @return
     */
    public static <T> Response<T> response(int code, String msg) {
        Response resp = new Response();
        resp.code = (code);
        resp.msg = (msg);
        return resp;
    }

    /**
     * 自定义状态码、提示信息、业务数据
     *
     * @param <T>
     * @param code 状态码
     * @param msg  提示信息
     * @param data 业务数据
     * @return
     */
    public static <T> Response<T> response(int code, String msg, T data) {
        Response<T> resp = new Response<>();
        resp.code = (code);
        resp.msg = (msg);
        resp.data = data;
        return resp;
    }

    /**
     * 返回成功数据
     *
     * @param <T>
     * @param data 业务数据
     * @return
     */
    public static <T> Response<T> success(T data) {
        Response<T> resp = new Response<>();
        resp.code = (SUCCESS_CODE);
        resp.msg = (SUCCESS_MSG);
        resp.data = data;
        return resp;
    }

    /**
     * 返回错误消息
     *
     * @param <T>
     * @param data 业务数据
     * @return
     */
    public static <T> Response<T> error(T data) {
        Response<T> resp = new Response<>();
        resp.code = (ERROR_CODE);
        resp.msg = (ERROR_MSG);
        resp.data = data;
        return resp;
    }
}

开放接口验证

微信支付通知回调和微信小程序退款通知回调两个接口,得去除Token验证,让微信支付平台能直接调用,根据你所使用的框架放开限制即可。

//微信支付回调
filterMap.put("/wxMiniappPay/payNotify", "anon");
//微信小程序退款回调
filterMap.put("/wxMiniappPay/refundNotify", "anon");

【客户端】小程序调起支付API

客户端微信小程序,先做小程序登录,登录之后调起/wxMiniappPay/createOrder接口获得相关参数,后调用wx.requestPayment实现客户端调起微信支付。

wx.requestPayment(
{
	"timeStamp": "1414561699",
	"nonceStr": "5K8264ILTKCH16CQ2502SI8ZNMTM67VS",
	"package": "prepay_id=wx201410272009395522657a690389285100",
	"signType": "RSA",
	"paySign": "oR9d8PuhnIc+YZ8cBHFCwfgpaK9gd7vaRvkYD7rthRAZ\/X+QBhcCYL21N7cHCTUxbQ+EAt6Uy+lwSN22f5YZvI45MLko8Pfso0jm46v5hqcVwrk6uddkGuT+Cdvu4WBqDzaDjnNa5UK3GfE1Wfl2gHxIIY5lLdUgWFts17D4WuolLLkiFZV+JSHMvH7eaLdT9N5GBovBwu5yYKUR7skR8Fu+LozcSqQixnlEZUfyE55feLOQTUYzLmR9pNtPbPsu6WVhbNHMS3Ss2+AehHvz+n64GDmXxbX++IOBvm2olHu3PsOUGRwhudhVf7UcGcunXt8cqNjKNqZLhLw4jq\/xDg==",
	"success":function(res){},
	"fail":function(res){},
	"complete":function(res){}
})

踩坑

微信支付成功后,异步通知 NotificationParser.parse 解密数据异常
https://blog.csdn.net/u011019141/article/details/144520658

微信支付使用v3 api初始化证书 Invalid AES key length?
https://blog.csdn.net/u011019141/article/details/144526728

微信小程序支付回调通知报错java.lang.IllegalArgumentException: Last unit does not have enough valid bits
https://blog.csdn.net/u011019141/article/details/144530982

为什么HttpServletRequest 的输入流(InputStream)只能读取一次?有什么更好的替代方案?
https://blog.csdn.net/u011019141/article/details/144535951

评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

宋发元

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值