业务示例代码

更新时间:2025.09.03

1. 目标

通过本教程的学习,你应该可以:

2. 业务处理流程

2.1 申请退款

当交易发生之后一段时间内,由于买家或者卖家的原因需要退款时,卖家可以通过退款接口将支付款退还给买家,微信支付将在收到退款请求并且验证成功之后,按照退款规则将支付款按原路退到买家账户上。

申请退款需要3类信息:

  • 商户信息

    • 子商户号

  • 原支付订单信息

    • 微信支付订单号或者商户订单号

    • 原订单金额

  • 退款信息

    • 商户退款单号:标识该服务商商户下的退款单,同一个服务商商户下不同商户退款单号表示不同的退款单

    • 退款原因

    • 退款金额

    • 退款出资信息

    • 退款回调URL

    • 退款商品信息

1package com.java.partner.refund;
2
3import com.java.demo.Create; // 使用合作伙伴申请退款接口:https://pay.weixin.qq.com/doc/v3/partner/4013080625
4import static com.java.demo.Create.*;
5import com.java.utils.WXPayUtility; // 引用微信支付工具库,参考:https://pay.weixin.qq.com/doc/v3/partner/4014985777
6import java.util.ArrayList;
7
8public class CreateRefundDemo {
9  // TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/partner/4013080340
10  // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考
11  // https://pay.weixin.qq.com/doc/v3/partner/4013080340
12  private static String mchid = "19xxxxxxxx";
13  // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013058924
14  private static String certificateSerialNo = "1DDE55AD98Exxxxxxxxxx";
15  // 商户API证书私钥文件路径,本地文件路径
16  private static String privateKeyFilePath = "/path/to/apiclient_key.pem";
17  // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013038589
18  private static String wechatPayPublicKeyId = "PUB_KEY_ID_xxxxxxxxxxxxx";
19  // 微信支付公钥文件路径,本地文件路径
20  private static String wechatPayPublicKeyFilePath = "/path/to/wxp_pub.pem";
21
22  public static void main(String[] args) {
23    Create ceateRefund = new Create(
24      mchid,
25      certificateSerialNo,
26      privateKeyFilePath,
27      wechatPayPublicKeyId,
28      wechatPayPublicKeyFilePath
29    );
30
31    CreateRequest createRefundRequest = new CreateRequest();
32    // 商户信息
33    createRefundRequest.subMchid = "1900000109";
34
35    // 原支付订单信息
36    createRefundRequest.transactionId = "4200000020202506035017900000";
37    createRefundRequest.amount = new AmountReq();
38    createRefundRequest.amount.total = 800L;
39
40    // 退款信息
41    // 商户退款单号生成方式商户视实际情况生成,需要保证服务商商户下全局唯一
42    createRefundRequest.outRefundNo = "4200000020202506035017900000_1";
43    createRefundRequest.reason = "成团退差价";
44    createRefundRequest.amount.refund = 200L;
45
46    // 退款成功,退款关闭,退款异常时需要微信支付通知商户商户,可以加上回调URL,退款到这几个状态时会调用这个URL通知商户
47    createRefundRequest.notifyUrl = "https://weixin.qq.com";
48
49    // 退款资金来源
50    // 1. 对于出行预付押金退款
51    // - 不填写表示已结退款,从出行商户基本户出资退款
52    // - 填写UNSETTLED,表示未结退款,从押金账户出资退款
53    // 2.
54    // 对于子商户为老资金流的商户(老资金流也叫旧资金流,旧资金流如何区分见:https://kf.qq.com/faq/161223N7fi2E161223EvY3Ir.html)
55    // - 不填写表示未结退款,从未结账户出资退款
56    // - 填写AVAILABLE,表示从可用余额出资退款
57    // createRefundRequest.fundsAccount = ReqFundsAccount.AVAILABLE;
58
59    // 退款商品信息
60    createRefundRequest.goodsDetail = new ArrayList<>();
61    {
62      GoodsDetail goodDetail = new GoodsDetail();
63      goodDetail.merchantGoodsId = "1217752501201407033233368018";
64      goodDetail.wechatpayGoodsId = "1001";
65      goodDetail.goodsName = "iPhone6s 16G";
66      goodDetail.unitPrice = 528800L;
67      goodDetail.refundAmount = 528800L;
68      goodDetail.refundQuantity = 1L;
69      createRefundRequest.goodsDetail.add(goodDetail);
70    }
71    ;
72
73    Refund refund;
74    try {
75      refund = ceateRefund.run(createRefundRequest);
76    } catch (WXPayUtility.ApiException e) {
77      // 申请退款处理异常处理逻辑
78      if (e.getErrorCode().equals("SYSTEM_ERROR")) {
79        // 错误:系统错误
80        // 解决方式:稍后原单重试
81        // 描述:微信支付系统失败,一定要原单重试,系统失败直接立即重试大概率还会是系统失败,建议等1分钟后再重试
82      } else if (e.getErrorCode().equals("FREQUENCY_LIMITED")) {
83        // 错误:限频报错
84        // 解决方式:稍后原单重试
85        // 描述: 退款接口频率限制,一定要原单重试,直接立即重试大概率还会是系统失败,建议等1分钟后再重试
86      } else if (e.getErrorCode().equals("MCH_NOT_EXISTS")) {
87        // 错误:子商户不存在
88        // 解决方式:检查输入的子商户号是否填写正确,修改正确之后原单重试
89        // 描述:输入子商户账户不存在,需要填写存在的账户之后重试
90      } else if (e.getErrorCode().equals("USER_ACCOUNT_ABNORMAL")) {
91        // 错误:用户账户异常
92        // 解决方式:退款受理失败,商户可以自行处理退款,也可以登录商户平台进入交易中心发起异常退款
93        // 描述:用户注销或者被管控了,这笔订单已经不能发起退款,这个错误就不需要重试了
94      } else if (e.getErrorCode().equals("NOT_ENOUGH")) {
95        // 错误:账户余额不足
96        // 解决方式:商户充值之后原单重试
97        // 描述:商户账户余额不足,导致退款失败,商户充值之后一定要原单重试
98      } else if (e.getErrorCode().equals("NO_AUTH")) {
99        // 错误:无退款权限
100        // 解决方式:
101        // 1. 检查商户是否是被处罚:登录商户平台进入账户中心-违约记录查询是否违约记录,如果有按照上面的指引解决违约记录之后原单重试
102        // 2.
103        // 检查子商户是否已授权服务商商户API退款权限:登录商户平台进入产品中心-特约商户授权产品,在特约商户列表中看看该子商户是否存在并且授权状态为“已授权”,
104        // 如果不存在或者授权状态不是“已授权”就需要发起授权邀请,等子商户审批之后重新查询一遍,授权状态为“已授权”之后再原单重试
105        // 描述:服务商商户被处罚或者服务商商户和子商户没有父子受理关系
106      } else if (e.getErrorCode().equals("SIGN_ERROR")) {
107        // 错误:签名错误
108        // 解决方式:检查服务商商户证书序列号,证书私钥文件,公钥ID,公钥文件,同时确认下签名过程是不是按照微信支付的签名方式
109        // 描述:签名报错,需要确认签名材料和签名流程是否正确
110      } else if (e.getErrorCode().equals("PARAM_ERROR")) {
111        // 错误:参数错误
112        // 解决方式:按照报错返回的message,重新输入请求参数
113        // 描述:参数的类型,长度,或者必填选项没有填写等
114      } else if (e.getErrorCode().equals("INVALID_REQUEST")) {
115        // 错误:请求非法,请求参数正确,但是不符合退款业务规则
116        // 解决方式:根据具体message查看具体哪里不符合业务规则,如果可以修改参数达到符合业务规则的修改请求符合业务规则之后再原单重试
117        // 描述:不符合业务规则的场景如:
118        // - 交易时间超过365天,不允许发起退款
119        // - 一笔订单退款次数已经超过50次了,这笔订单不允许发起新的退款
120        // - 申请退款的金额超过了这笔订单剩余可退金额,不允许申请退款
121      } else {
122        // 其他类型错误:稍等一会后原单重试
123      }
124
125    }
126  }
127}

2.2 查询退款

上面申请退款成功之后,说明微信支付成功受理了这笔退款,并不是已经退款到账,商户可以通过查询退款接口查询退款结果

退款单的状态机:

1package com.java.partner.refund;
2
3import com.java.demo.Create; // 使用合作伙伴申请退款接口:https://pay.weixin.qq.com/doc/v3/partner/4013080625
4import static com.java.demo.Create.*;
5import com.java.demo.QueryByOutRefundNo; // 使用合作伙伴查询单笔退款(按商户退款单号)接口,见:https://pay.weixin.qq.com/doc/v3/partner/4013080626
6import static com.java.demo.QueryByOutRefundNo.*;
7import com.java.demo.CreateAbnormalRefund; // 使用合作伙伴发起异常退款接口,见:https://pay.weixin.qq.com/doc/v3/partner/4013080627
8import static com.java.demo.CreateAbnormalRefund.*;
9import com.java.utils.WXPayUtility; // 引用微信支付工具库,参考:https://pay.weixin.qq.com/doc/v3/partner/4014985777
10import java.util.ArrayList;
11
12public class QueryRefundDemo {
13  // TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/partner/4013080340
14  // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考
15  // https://pay.weixin.qq.com/doc/v3/partner/4013080340
16  private static String mchid = "19xxxxxxxx";
17  // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013058924
18  private static String certificateSerialNo = "1DDE55AD98Exxxxxxxxxx";
19  // 商户API证书私钥文件路径,本地文件路径
20  private static String privateKeyFilePath = "/path/to/apiclient_key.pem";
21  // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013038589
22  private static String wechatPayPublicKeyId = "PUB_KEY_ID_xxxxxxxxxxxxx";
23  // 微信支付公钥文件路径,本地文件路径
24  private static String wechatPayPublicKeyFilePath = "/path/to/wxp_pub.pem";
25
26  public static void main(String[] args) {
27    QueryByOutRefundNo client = new QueryByOutRefundNo(
28      mchid,
29      certificateSerialNo,
30      privateKeyFilePath,
31      wechatPayPublicKeyId,
32      wechatPayPublicKeyFilePath
33    );
34
35    QueryByOutRefundNoRequest request = new QueryByOutRefundNoRequest();
36    request.outRefundNo = "4200000020202506035017900000_1";
37    request.subMchid = "1900000109";
38    try {
39      QueryByOutRefundNo.Refund refund = client.run(request);
40      switch (refund.status) {
41        case SUCCESS:
42          // 终态:退款成功(退款到账)
43          // 更新商户自己的业务单据状态为成功
44          // UpdateRefund2Succ();
45          break;
46        case CLOSED:
47          // 终态:退款关闭
48          // 更新商户自己的业务单据状态为关闭
49          // UpdateRefund2Closed();
50          // 如果还需要退款,需要换单(换outRefundNo)重新申请退款
51          Create createRefund = new Create(
52            mchid,
53            certificateSerialNo,
54            privateKeyFilePath,
55            wechatPayPublicKeyId,
56            wechatPayPublicKeyFilePath
57          );
58
59          CreateRequest createRefundRequest = new CreateRequest();
60          // 其他参数和之前申请退款参数一样,只改商户退款单号
61          createRefundRequest.outRefundNo = "4200000020202506035017900000_2";
62          createRefund.run(createRefundRequest);
63
64          break;
65        case PROCESSING:
66          // 非终态:退款处理中 - 还未到账,需要隔一段时间再来查询
67          break;
68        case ABNORMAL:
69          // 非终态:退款异常 - 用户异常、用户被管控等原因不能原路退
70          // 可以用两种处理方式:
71          // 1. 商户登录商户平台进入交易中心发起异常退款,把钱退到用户指定的银行卡或者商户的银行
72          // 2. 请求异常退款API
73          // TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/partner/4013080340
74          CreateAbnormalRefund createAbnormalRefund = new CreateAbnormalRefund(
75            mchid,
76            certificateSerialNo,
77            privateKeyFilePath,
78            wechatPayPublicKeyId,
79            wechatPayPublicKeyFilePath
80          );
81          CreateAbnormalRefundRequest abnormalRefundRequest = new CreateAbnormalRefundRequest();
82          abnormalRefundRequest.refundId = refund.refundId;
83          abnormalRefundRequest.subMchid = "1900000109";
84          abnormalRefundRequest.outRefundNo = "4200000020202506035017900000_1";
85          abnormalRefundRequest.type = AbnormalReceiveType.USER_BANK_CARD;
86          abnormalRefundRequest.bankType = "ICBC_DEBIT";
87          abnormalRefundRequest.bankAccount = createAbnormalRefund.encrypt("请填 用户的银行卡号");
88          abnormalRefundRequest.realName = createAbnormalRefund.encrypt("请填 收款用户姓名");
89          CreateAbnormalRefund.Refund abnormalRefund = createAbnormalRefund.run(abnormalRefundRequest);
90          // 发起异常退款成功之后,退款单会进入退款中,还是需要通过查单接口查询退款单的状态
91          break;
92        default:
93          // 非法状态
94          throw new IllegalArgumentException("微信支付退款单状态非法");
95      }
96    } catch (WXPayUtility.ApiException e) {
97      // 查询退款处理异常处理逻辑
98      if (e.getErrorCode().equals("RESOURCE_NOT_EXISTS")) {
99        // 错误:退款单不存在
100        // 解决方式:原参数申请退款
101        // 描述:退款受理没成功,原参数发起退款申请即可
102      } else if (e.getErrorCode().equals("SYSTEM_ERROR")) {
103        // 错我:系统错误
104        // 解决方式:稍后原单重试
105        // 描述:微信支付系统失败,直接立即重试大概率还会是系统失败,建议等1分钟后再重试
106      } else if (e.getErrorCode().equals("FREQUENCY_LIMITED")) {
107        // 错误:限频报错
108        // 处理逻辑:稍后原单重试
109        // 描述: 退款接口频率限制,直接立即重试大概率还会是系统失败,建议等1分钟后再重试
110      } else if (e.getErrorCode().equals("MCH_NOT_EXISTS"))  {
111        // 错误:子商户不存在
112        // 处理逻辑:检查输入的子商户号是否填写正确,修改正确之后原单重试
113        // 描述:输入子商户账户不存在,需要填写存在的账户之后重试
114      } else if (e.getErrorCode().equals("NO_AUTH")) {
115        // 错误:无退款权限
116        // 处理逻辑:检查服务商商户是否退款权限,服务商商户和子商户是否有父子受理关系,需要开通服务商商户的退款权限和添加子商户的父子受理权限才能发起退款
117        // 描述:服务商商户无退款权限或者服务商商户和子商户没有父子受理关系
118      } else if (e.getErrorCode().equals("SIGN_ERROR")) {
119        // 错误:签名错误
120        // 处理逻辑:检查服务商商户证书序列号,证书私钥文件,公钥ID,公钥文件,同时确认下签名过程是不是按照微信支付的签名方式
121        // 描述:签名报错,需要确认签名材料和签名流程是否正确
122      } else if (e.getErrorCode().equals("PARAM_ERROR")) {
123        // 错误:参数错误
124        // 处理逻辑:按照报错返回的message,重新输入请求参数
125        // 描述:参数的类型,长度,或者必填选项没有填写等
126      } else {
127        // 其他类型错误:稍等一会后原单重试
128      }
129    }
130  }
131}

2.3 退款结果通知

如果商户申请退款时设置了notifyUrl,或者在商户平台-交易中心-交易管理-退款管理-退款配置 中设置了通知URL,那么退款状态变更为SUCCESS、CLOSED或者ABNORMAL后会通过HTTP POST方法回调到这个URL上

如果退款申请是设置了notifyUrl,同时退款配置中也设置了通知URL,已退款申请的URL为准

1package com.java.partner.refund;
2
3import com.java.utils.WXPayUtility; // 引用微信支付工具库,参考:https://pay.weixin.qq.com/doc/v3/partner/4014985777
4import com.java.utils.WXPayUtility.Notification;
5import java.security.PublicKey;
6import okhttp3.Headers;
7import okhttp3.Response;
8import com.google.gson.annotations.SerializedName;
9
10public class RefundNotifyDemo {
11  // TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/partner/4013080340
12  // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考
13  // https://pay.weixin.qq.com/doc/v3/partner/4013080340
14  private static String mchid = "19xxxxxxxx";
15  // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013058924
16  private static String certificateSerialNo = "1DDE55AD98Exxxxxxxxxx";
17  // 商户API证书私钥文件路径,本地文件路径
18  private static String privateKeyFilePath = "/path/to/apiclient_key.pem";
19  // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013038589
20  private static String wechatPayPublicKeyId = "PUB_KEY_ID_xxxxxxxxxxxxx";
21  // 微信支付公钥文件路径,本地文件路径
22  private static String wechatPayPublicKeyFilePath = "/path/to/wxp_pub.pem";
23  // 商户APIv3密钥,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013059095
24  private static String apiv3Key = "your apiv3 key";
25
26  public static class RefundAmount {
27    @SerializedName("total")
28    public Long total;
29
30    @SerializedName("refund")
31    public Long refund;
32
33    @SerializedName("payer_total")
34    public Long payerTotal;
35
36    @SerializedName("payer_refund")
37    public Long payerRefund;
38  }
39
40  public static class Refund {
41    @SerializedName("sp_mchid")
42    public String spMchid;
43
44    @SerializedName("sub_mchid")
45    public String subMchid;
46
47    @SerializedName("out_trade_no")
48    public String outTradeNo;
49
50    @SerializedName("transaction_id")
51    public String transactionId;
52
53    @SerializedName("out_refund_no")
54    public String outRefundNo;
55
56    @SerializedName("refund_id")
57    public String refundId;
58
59    @SerializedName("refund_status")
60    public String refundStatus;
61
62    @SerializedName("success_time")
63    public String successTime;
64
65    @SerializedName("user_received_account")
66    public String userReceivedAccount;
67
68    @SerializedName("amount")
69    public RefundAmount amount;
70  }
71
72  public void refundNotify(Response httpResponse) {
73    String refundNotifyStr = WXPayUtility.extractBody(httpResponse); // 微信支付退款通知的原始字符串
74    Headers headers = httpResponse.headers(); // 微信支付退款通知的Headers
75    PublicKey wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
76
77    Notification notification = WXPayUtility.parseNotification(
78        apiv3Key,
79        wechatPayPublicKeyId, wechatPayPublicKey, headers,
80        refundNotifyStr);
81
82    // 检查这个ID是否已经处理过了,ID是微信支付系统生成的唯一ID,每个ID只需要处理一次
83    // CheckIfIdProcessed(notification.getId());
84
85    // 解析退款单
86    Refund refund = WXPayUtility.fromJson(notification.getResource().getCiphertext(), Refund.class);
87
88    // 处理退款事件
89    switch (notification.getEventType()) {
90      case "REFUND.SUCCESS":
91        // 退款成功, 理论上退款单的状态是成功态,这里多做一次校验
92        if (refund.refundStatus.equals("SUCCESS")) {
93          // 退款成功, 处理退款成功逻辑
94          // UpdateRefund2Succ();
95        } else {
96          // 退款单状态非法,报错返回
97          throw new IllegalArgumentException("退款单状态非法");
98        }
99        break;
100      case "REFUND.ABNORMAL":
101        // 退款异常, 理论上退款单的状态应该是异常态,这里多做一次校验
102        if (refund.refundStatus.equals("ABNORMAL")) {
103          // 退款异常, 处理退款异常逻辑
104          // 可以和查单发现退款单异常处理逻辑一样,可以用两种处理方式:
105          // 1. 商户登录商户平台进入交易中心发起异常退款,把钱退到用户指定的银行卡或者商户的银行
106          // 2. 请求异常退款API
107          // UpdateRefund2Abnormal();
108        } else {
109          // 退款单状态非法,报错返回
110          throw new IllegalArgumentException("退款单状态非法");
111        }
112        break;
113      case "REFUND.CLOSED":
114        // 退款关闭, 理论上退款单的状态应该是关闭态,这里多做一次校验
115        if (refund.refundStatus.equals("CLOSED")) {
116          // 退款关闭, 处理退款关闭逻辑
117          // 可以和查单发现退款单关闭处理逻辑一样(如果还需要退款,需要换单之后重新发起退款)
118          // UpdateRefund2Closed();
119        } else {
120          // 退款单状态非法,报错返回
121          throw new IllegalArgumentException("退款单状态非法");
122        }
123        break;
124      default:
125        // 非法事件类型,报错返回
126        throw new IllegalArgumentException("退款事件非法");
127    }
128
129  }
130
131}
132

 

反馈
咨询
目录
置顶