微信支付 V3 接入流程与实战

目录

  1. 配置参数说明

  2. 下单流程与接口

  3. 回调与订单状态处理

  4. 退款流程

  5. 订单分账


配置参数说明

application.yml 中配置微信支付参数:处理了的参数,大概就是这样的形式数据

 
wechat:
  pay:
    merchant-id: "1723221537"           # 微信支付商户号
    merchant-serial-number: "16E1233E2F2DF3EC7C9C5763A6390E03A19A5484" # 商户证书序列号
    api-v3-key: "B2k5M8pQ1sT4127zY0wA3dC6fH9jL2n1" # APIv3密钥
    app-id: "wxb006873432368c15"        # 小程序/公众号APPID
    notify-url: "https://songjiahao.top/prod-api/cb/notify" # 支付回调地址
    private-key-path: "cert/apiclient_key.pem" # 商户私钥路径
    public-key-path: "cert/pub_key.pem"       # 微信平台公钥路径
    public-key: "PUB_KEY_ID_0117232395672025373100212005001601" # 微信平台证书序列号
    secret: "e200f68659d39144534c55c40b127312" # 小程序/公众号AppSecret

参数说明:

  • merchant-id:商户号

  • merchant-serial-number:商户证书序列号

  • api-v3-key:APIv3 密钥

  • app-id:小程序/公众号 AppID

  • notify-url:支付回调地址

  • private-key-path:商户私钥

  • public-key-path:微信平台公钥

  • public-key:微信平台证书序列号(KeyId)

  • secret:AppSecret

🔑 1. merchant-id (商户号)

  • 说明:微信支付分配给你的 商户号(MchID),固定为 10位数字

  • 获取方式:

    • 登录 微信支付商户平台 → 账号中心 → 商户信息 → 商户号。


🔑 2. merchant-serial-number (商户证书序列号)

  • 说明:商户 API 证书的序列号,用于请求头签名。

  • 获取方式:

    • 登录商户平台 → 账户中心 → API安全 → API证书 → 下载 API证书(apiclient_cert.pem、apiclient_key.pem)。

    • 在证书详情页可以看到 证书序列号,或者用 openssl 查看:

       

      openssl x509 -in apiclient_cert.pem -noout -serial

      输出的 serial=16E1233E2F2DF3EC... 就是。


🔑 3. api-v3-key (APIv3 密钥)

  • 说明:一个 32 位随机字符串,用来解密回调数据、签名。

  • 获取方式:

    • 登录商户平台 → 账户中心 → API安全 → APIv3密钥 → 设置。

    • 你自己生成并保存,微信只显示一次,一定要妥善保存。


🔑 4. app-id (小程序/公众号 AppID)

  • 说明:发起支付的应用 ID。

  • 获取方式:

    • 微信公众平台(mp.weixin.qq.com) → 开发 → 开发设置 → AppID。

    • 小程序和公众号各自有一个 AppID。


🔑 5. notify-url (支付回调地址)

  • 说明:支付成功/失败后,微信回调的接口地址。

  • 设置方式:

    • 自己在代码里配置,比如 /cb/notify

    • 必须是 公网可访问的 HTTPS 地址


🔑 6. private-key-path (商户私钥路径)

  • 说明:商户 API 证书私钥文件 apiclient_key.pem 的路径。

  • 获取方式:

    • 在商户平台下载 API证书(一个 zip 包),解压后有:

      • apiclient_cert.pem (公钥证书)

      • apiclient_key.pem (私钥)

      • apiclient_cert.p12

    • apiclient_key.pem 放到你的项目 cert/ 目录下。


🔑 7. public-key-path (微信平台公钥路径)

  • 说明:用于验签的微信支付平台公钥。

  • 获取方式:

    • 登录商户平台 → 账户中心 → API安全 → 平台证书 → 下载。

    • 微信平台会提供 微信支付平台证书(.pem 文件),公钥就在里面。


🔑 8. public-key (微信平台证书序列号 / keyId)

  • 说明:微信支付平台证书的序列号(对应你下载的微信平台证书)。

  • 获取方式:

    • 下载平台证书(微信支付 v3 提供),解压后有一个 pem 文件。

    • 查看证书序列号:

       

      openssl x509 -in wechatpay_cert.pem -noout -serial

    • 得到的值就是 public-key


🔑 9. secret (小程序/公众号 AppSecret)

  • 说明:配合 app-id 使用的 应用密钥,主要用于调用微信开放平台接口(比如获取 openid、用户信息)。

  • 获取方式:

    • 微信公众平台 → 开发 → 开发设置 → AppSecret。


下单流程与接口

下单到取消订单,涉及 5 个核心接口:

  1. 预下单接口:获取 prepay_id,返回前端支付参数

  2. 前端调起支付:调用 wx.requestPayment

  3. 回调接口:微信支付异步通知,确认订单状态

  4. 主动查询订单状态:补偿机制,确认微信端最终状态

  5. 关单接口:超时未支付时关闭订单

流程图

开始 → 前端下单 → 后端统一下单 → 微信返回 prepay_id → 前端调起支付 → 用户支付 → 微信回调 → 后端验签更新订单 → 返回成功 → 前端显示结果 → 结束


1. 后端预支付接口

 @PostMapping("/prepay")
    public R<WxPayResponse> createPrepay(@RequestBody WxPayPrepayDto dto) {

        // 1. 构建微信支付请求
        PrepayRequest request = new PrepayRequest();
        Amount amount = new Amount();
        int totalAmount = dto.getAmount().multiply(BigDecimal.valueOf(100)).intValue();
        amount.setCurrency("CNY");
        request.setMchid(wxPayBaseConfig.getMerchantId());
        amount.setTotal(totalAmount); // 金额(单位:分)
        request.setAppid(appId); // 公众号或小程序的appId
        request.setAmount(amount);
        request.setDescription(dto.getDescription());
        request.setNotifyUrl("https://www.百度.cn/prod-api/cb/notify"); // 支付结果回调地址 内网穿透测试
        request.setOutTradeNo(dto.getOutTradeNo()); // 商户订单号
        SettleInfo settleInfo = new SettleInfo();
        // 开启分账
        settleInfo.setProfitSharing(true);
        request.setSettleInfo(settleInfo);
        // 5. 设置支付者OpenID(必须)
        Payer payer = new Payer();
        payer.setOpenid(dto.getOpenid()); // 替换为实际用户的OpenID
        request.setPayer(payer);

        // 2. 调用微信API获取预支付参数
        PrepayWithRequestPaymentResponse prepayResponse = jsapiService.prepayWithRequestPayment(request);


        // 3. 返回给前端的参数(小程序支付专用格式)

        WxPayResponse response = new WxPayResponse();
        response.setTimeStamp(prepayResponse.getTimeStamp());
        response.setNonceStr(prepayResponse.getNonceStr());
        response.setPackageVal(prepayResponse.getPackageVal());
        response.setSignType("RSA");
        response.setPaySign(prepayResponse.getPaySign());

        return R.ok(response);
    }

2. 前端拉起支付

 
wxpay(outTradeNo){
  this.$u.api.getPayOrder({
    openid: this.vuex_user.openId,
    outTradeNo: outTradeNo,
    description: this.productArr[0].storeName,
    amount: this.totalPrice
  }).then((response) => {
    uni.requestPayment({
      provider:'wxpay',
      timeStamp:response.timeStamp,
      nonceStr:response.nonceStr,
      package:response.packageVal,
      signType:response.signType,
      paySign:response.paySign,
      success:(res) => this.toResult({ id: outTradeNo, status: 1 }),
      fail:(err) => this.toResult({ id: outTradeNo, status: 0 })
    })
  })
}

建议:支付成功后,前端调用一次 订单查询接口,由后端返回数据库中的订单最终状态。


回调与订单状态处理

1. 支付回调接口

 
 @PostMapping("/notify")
    @Transactional(rollbackFor = Exception.class)
    public String wxNotify(HttpServletRequest request) throws IOException {

        String signature = request.getHeader("Wechatpay-Signature");
        String serial = request.getHeader("Wechatpay-Serial");
        String timestamp = request.getHeader("Wechatpay-Timestamp");
        String nonce = request.getHeader("Wechatpay-Nonce");
        StringBuilder body = new StringBuilder();
        //读取密文
        try {
            String str = null;
            BufferedReader br = request.getReader();
            while ((str = br.readLine()) != null) {
                body.append(str);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }


        //1 构建回调通知的请求参数
        com.wechat.pay.java.core.notification.RequestParam requestParam = new RequestParam.Builder().serialNumber(serial)
                .nonce(nonce)
                .signature(signature)
                .timestamp(timestamp)
                .body(body.toString())
                .build();

        //2 回调通知的配置
        String publicKey = this.loadKeyFromClasspath(wxPayBaseConfig.getPublicKeyPath());
        NotificationConfig builder = new RSAPublicKeyNotificationConfig.Builder()
                .publicKey(publicKey)
                .publicKeyId(wxPayBaseConfig.getPublicKeyId())
                .apiV3Key(wxPayBaseConfig.getApiV3key())
                .build();


        //3  回调通知解析器
        NotificationParser notificationParser = new NotificationParser(builder);
        HashMap<String, String> map = new HashMap<>();
        Transaction transactional = null;
        try {
            transactional = notificationParser.parse(requestParam, Transaction.class);
            System.out.println("transactional ============= " + transactional);
            log.error(transactional.toString());
        } catch (Exception e) {
            System.out.println("验签失败");
            return "失败";
        }

        Integer total = transactional.getAmount().getTotal();
        //除以100是当前金额
        String successTime = transactional.getSuccessTime();
        String transactionId = transactional.getTransactionId();
        String outTradeNo = transactional.getOutTradeNo();
        String tradeStateDesc = transactional.getTradeStateDesc();
        System.out.println("微信回复订单状态===" + tradeStateDesc);
        map.put("code", "success");
        map.put("message", "支付成功");
        //校验订单号与订单金额
        List<KxStoreOrderVo> KxStoreOrderList = appOrderService.selectListVoByWrapper(
                new QueryWrapper<KxStoreOrder>()
                        .eq("order_id", outTradeNo));

        if (CollectionUtils.isEmpty(KxStoreOrderList)) {
            return WxPayNotifyResponse.fail("订单不存在 orderNo=" + outTradeNo);
        }
        KxStoreOrderVo order = KxStoreOrderList.get(0);
        // 检查这个订单是否已经处理过
        if (order.getStatus() != OrderStatusType.UNPAY.getCode()) {
            return WxPayNotifyResponse.success("订单已经处理成功!");
        }
        Integer totalFee = transactional.getAmount().getTotal();
        if (!totalFee.equals(order.getPayPrice().multiply(new BigDecimal(100)).intValue())) {
            return WxPayNotifyResponse.fail(order.getOrderId() + " : 支付金额不符合 totalFee=" + totalFee);
        }

        //改变订单状态


        //2冻结库存减 -> 增加销售量
        //根据商品订单号查询商品列表和数量

       //如果用户使用了优惠卷将优惠卷改为已使用状态

        return WxPayNotifyResponse.success("支付成功");
    }

2. 主动查询订单状态

 
public String queryOrderByOutTradeNo(String orderId) {
    QueryOrderByOutTradeNoRequest request = new QueryOrderByOutTradeNoRequest();
    request.setOutTradeNo(orderId);
    request.setMchid(wxPayBaseConfig.getMerchantId());
    Transaction transaction = jsapiService.queryOrderByOutTradeNo(request);
    return transaction.getTradeState().name();
}

3. 关闭订单

 
    @GetMapping("/closeOrder")
    public String wxNotify(String orderId) throws IOException {
        System.out.println("有人调用了我们的api");
        CloseOrderRequest request = new CloseOrderRequest();
        request.setOutTradeNo(orderId);
        request.setMchid(wxPayBaseConfig.getMerchantId());

        JsapiServiceExtension jsapiService = wxPayBaseConfig.jsapiService(wxPayBaseConfig.wechatPayConfig());

        jsapiService.closeOrder(request);
        return "关闭成功";
    }

退款流程

1. 退款

 
    @GetMapping("/refund")
    @SaCheckPermission("order:refund")
    public R<String> refund(String orderId, String total, String reFee, String transactionId) {
        if (StringUtils.isBlank(orderId) || StringUtils.isBlank(reFee)) {
            return R.fail("订单号或退款金额不能为空");
        }

        // 查询订单
        KxStoreOrder kxStoreOrder = orderMapper.selectOne(
                new LambdaQueryWrapper<KxStoreOrder>().eq(KxStoreOrder::getOrderId, orderId)
        );

        if (kxStoreOrder == null) {
            return R.fail("订单不存在");
        }

        // 订单总金额(分)
        Long totalFee = kxStoreOrder.getPayPrice().multiply(new BigDecimal(100)).longValue();

        // 退款金额(分)
        Long refundFee = new BigDecimal(reFee).multiply(new BigDecimal(100)).longValue();

        if (refundFee <= 0) {
            return R.fail("退款金额必须大于0");
        }
        if (refundFee > totalFee) {
            return R.fail("退款金额不能大于支付金额");
        }

        // 构造金额请求
        AmountReq amountReq = new AmountReq();
        amountReq.setTotal(totalFee);
        amountReq.setRefund(refundFee);
        amountReq.setCurrency("CNY");

        // 构造退款请求
        CreateRequest createRequest = new CreateRequest();
        createRequest.setAmount(amountReq);

        if (StringUtils.isNotBlank(transactionId)) {
            createRequest.setTransactionId(transactionId); // 微信支付订单号
        } else {
            createRequest.setOutTradeNo(orderId);          // 商户订单号
        }

        // 退款单号:必须唯一
        String outRefundNo = 
        try {
            RefundService refundService = wxPayBaseConfig.refundService(wxPayBaseConfig.wechatPayConfig());
            Refund refund = refundService.create(createRequest);

            //修改退款金额订单
   

            logger.info("申请退款成功,退款单号:{},状态:{}", outRefundNo, refund.getStatus());

            return R.ok("退款成功,状态:" + refund.getStatus().toString());
        } catch (Exception e) {
            logger.error("申请退款异常,订单号:{}", orderId, e);
            return R.fail("退款失败:" + e.getMessage());
        }
    }

2. 查询退款

    @GetMapping("/refundQuery")
    public String refundQuery(String orderId) {
        QueryByOutRefundNoRequest request = new
                QueryByOutRefundNoRequest();
        request.setOutRefundNo(orderId);
        //微信抛异常
        try {
            RefundService refundService = wxPayBaseConfig.refundService(wxPayBaseConfig.wechatPayConfig());
            Refund refund = refundService.queryByOutRefundNo(request);

            return refund.toString();
        } catch (Exception e) {
            return e.getMessage();
        }
    }

订单分账

  1. 添加分账对象

  2. 申请分账

  3. 分账回退

  4. 解冻剩余资金

  

5.1 添加分账对象

调用微信 addReceiver 接口,添加分账接收方。

  • 支持 商户号个人openid

  • 可设置 关系类型(如:SERVICE_PROVIDER、CUSTOM)

 
@PostMapping("/profitSharingReturn")
public Map<String, Object> profitSharingReturn(@RequestBody ProfitSharingReturnRequestDTO dto) throws Exception {
    ProfitsharingService service = new ProfitsharingService.Builder()
            .config(wxPayBaseConfig.wechatPayConfig())
            .build();

    CreateReturnOrderRequest request = new CreateReturnOrderRequest();
    request.setOrderId(dto.getOrderId());
    request.setOutOrderNo(dto.getOutOrderNo());
    request.setOutReturnNo(dto.getOutReturnNo());
    request.setAmount(dto.getReturnAmount());
    request.setDescription(dto.getDescription());
    request.setReturnMchid(dto.getReturnMchid());

    ReturnOrdersEntity result = service.createReturnOrder(request);
    return Map.of("success", true, "returnId", result.getReturnId(), "result", result.getResult());
}

5.2 申请分账

发起分账请求,指定交易号和接收方分账金额。

 
private Map<String, Object> callWechatProfitSharing(ProfitSharingRequestDTO dto) throws Exception {
    ProfitsharingService service = new ProfitsharingService.Builder()
            .config(wxPayBaseConfig.wechatPayConfig())
            .build();

    CreateOrderRequest request = new CreateOrderRequest();
    request.setAppid(wxPayBaseConfig.getAppId());
    request.setTransactionId(dto.getTransactionId());
    request.setOutOrderNo(dto.getOutOrderNo());
    request.setUnfreezeUnsplit(false);

    List<CreateOrderReceiver> receiverList = new ArrayList<>();
    for (ProfitSharingRequestDTO.ReceiverRatio rr : dto.getReceivers()) {
        CreateOrderReceiver receiver = new CreateOrderReceiver();
        receiver.setType(rr.getType());
        receiver.setAccount(rr.getAccount());
        receiver.setAmount(rr.getAmount().longValue());
        receiver.setDescription(rr.getDescription());
        receiverList.add(receiver);
    }
    request.setReceivers(receiverList);

    OrdersEntity order = service.createOrder(request);
    return Map.of("status", order.getState(), "orderId", order.getOrderId());
}

5.3 分账回退

当订单退款时,需要先将已分账的金额回退。

 
@PostMapping("/profitSharingReturn")
public Map<String, Object> profitSharingReturn(@RequestBody ProfitSharingReturnRequestDTO dto) throws Exception {
    ProfitsharingService service = new ProfitsharingService.Builder()
            .config(wxPayBaseConfig.wechatPayConfig())
            .build();

    CreateReturnOrderRequest request = new CreateReturnOrderRequest();
    request.setOrderId(dto.getOrderId());
    request.setOutOrderNo(dto.getOutOrderNo());
    request.setOutReturnNo(dto.getOutReturnNo());
    request.setAmount(dto.getReturnAmount());
    request.setDescription(dto.getDescription());
    request.setReturnMchid(dto.getReturnMchid());

    ReturnOrdersEntity result = service.createReturnOrder(request);
    return Map.of("success", true, "returnId", result.getReturnId(), "result", result.getResult());
}

5.4 解冻剩余资金

@PostMapping("/unfreeze")
public Map<String, Object> unfreeze() {
    ProfitsharingService service = new ProfitsharingService.Builder()
            .config(wxPayBaseConfig.wechatPayConfig())
            .build();

    UnfreezeOrderRequest request = new UnfreezeOrderRequest();
    request.setTransactionId("4200002411202508311514674338");
    request.setOutOrderNo("84109169010000");
    request.setDescription("解冻剩余资金");

    OrdersEntity ordersEntity = service.unfreezeOrder(request);
    return Map.of("status", ordersEntity.getState(), "orderId", ordersEntity.getOrderId());
}

更多推荐