从零玩转系列之微信支付实战PC端支付微信退款接口搭建 | 技术创作特训营第一期
一、前言
从零玩转系列之微信支付实战PC端支付微信退款接口搭建 | 技术创作特训营第一期
继前文章取消订单接口和查询订单接口此篇为申请退款流程,此篇文章过长我将分几个阶段的文章发布(项目源码都有,小程序和PC端)
在此之前已经更新了微信支付开篇、微信支付安全、微信实战基础框架搭建、本次更新为微信支付实战PC端接口搭建,实战篇分为几个章节因为代码量确实有点多哈.
本次项目使用技术栈
后端: SpringBoot3.1.x、Mysql8.0、MybatisPlus
前端: Vue3、Vite、ElementPlus
小程序: Uniapp、Uview
一、订单退款
什么是订单退款?
订单退款是指在购买商品或服务后,由于某种原因,消费者选择取消订单或者商家无法提供所承诺的商品或服务,从而触发一项退款过程。退款通常是指商家将之前从消费者支付的金额返还给消费者的操作。这可以是因为商品瑕疵、服务不满意、订单错误或其他原因,消费者和商家之间达成的一种解决方案,旨在确保消费者的权益得到保护。
场景
申请退款的场景有很多,以下是一些常见的情况:
- 商品瑕疵: 消费者收到的商品存在损坏、缺陷或不符合描述,可以申请退款以获得补偿或返还款项。
- 不满意的服务: 消费者购买的服务未达到预期,例如餐厅用餐体验差、旅行服务不如预期等。
- 订单错误: 消费者可能因误操作或信息错误而下错订单,需要取消并申请退款。
- 发货延迟: 如果商家延迟了订单的发货,消费者可能会申请退款。
- 重复支付: 消费者可能因系统问题或网络故障而重复支付了一笔订单,需要申请退款。
- 无法提供服务: 商家无法按约定提供服务,例如演出取消、酒店无法入住等情况。
- 不适用: 购买的商品或服务并不适用于消费者,例如购买了不适合自己的尺寸、不适合的软件等。
- 退货: 消费者购买了实物商品后,决定退货并获得退款。
以上是GPT3.5专业的回答
思考
思考我们程序当中需要的步骤初步形成
- 我们支付完毕后拿到该订单的订单号
- 根据订单号查询我们数据库里的支付日志表状态必须是支付成功
- 因为我们目前是没有登录功能的是不知道哪些订单是谁的所以我们需要根据某个唯一的编号来判断这个订单是这个客户的
- ‼️ 微信申请退款成功后会发起一个退款回调的请求这个后面会讲!!!!!!
我们都知道支付成功后微信会提示我们支付成功并且会生成一个交易记录我们点击进去可以看到交易订单号
好我们去查看一下文档会不会给我们这个编号
很明显我们是会拿到的~ 那么就到时候存一下呗在用户提供咱们判断一下呗 不就完事了呗~
微信说明
当交易发生之后一年内,由于买家或者卖家的原因需要退款时,卖家可以通过退款接口将支付金额退还给买家,微信支付将在收到退款请求并且验证成功之后,将支付款按原路退还至买家账号上。
⚠️注意
1、交易时间超过一年的订单无法提交退款
2、微信支付退款支持单笔交易分多次退款(不超50次),多次退款需要提交原支付订单的商户订单号和设置不同的退款单号。申请退款总金额不能超过订单金额。 一笔退款失败后重新提交,请不要更换退款单号,请使用原商户退款单号
3、错误或无效请求频率限制:6qps,即每秒钟异常或错误的退款申请请求不超过6次
4、每个支付订单的部分退款次数不能超过50次
5、如果同一个用户有多笔退款,建议分不同批次进行退款,避免并发退款导致退款失败
6、申请退款接口的返回仅代表业务的受理情况,具体退款是否成功,需要通过退款查询接口获取结果
7、一个月之前的订单申请退款频率限制为:5000/min
8、同一笔订单多次退款的请求需相隔1分钟
交互
退款状态转变如下:
请求参数必填
参数名 | 变量 | 类型长度限制 | 必填 | 描述 |
---|---|---|---|---|
微信支付订单号 | transaction_id | string1, 32 | 二选一 | body原支付交易对应的微信订单号 示例值:1217752501201407033233368018 |
商户订单号 | out_trade_no | string6, 32 | 二选一 | body原支付交易对应的商户订单号 示例值:1217752501201407033233368018 |
商户退款单号 | out_refund_no | string1, 64 | 是 | body商户系统内部的退款单号,商户系统内部唯一,只能是数字、大小写字母_-|*@ ,同一退款单号多次请求只退一笔。 示例值:1217752501201407033233368018 |
退款原因 | reason | string1, 64 | 否 | 不一定要填写 |
金额信息 | amount | object | 是 | body订单金额信息 |
退款金额 | refund | int | 是 | 退款金额,单位为分,只能为整数,不能超过原订单支付金额。示例值:888 |
原订单金额 | total | int | 是 | 原支付交易的订单总金额,单位为分,只能为整数。示例值:888 |
二、步入正题
修改 WechatNativeController 编写申请退款接口
/**
* 申请退款
* @param orderNo 订单号
* @param refundsNo 退款单号(交易订单号)
* @param reason 退款原因
*/
@PostMapping("/refunds/{orderNo}/{refundsNo}/{reason}")
public R refunds(@PathVariable String orderNo, @PathVariable String refundsNo, @PathVariable String reason) {
log.info("申请退款");
wxPayService.refund(orderNo, reason, refundsNo);
return R.ok();
}
修改 wxPayService 创建 refund 方法
/**
* 申请退款
*
* @param orderNo 订单号
* @param reason 退款原因
* @param refundsNo 退款单号
*/
@SneakyThrows
public void refund(String orderNo, String reason, String refundsNo) {
// ............
}
前面思考提到的需要查询该订单是否存在和状态要支付成功的
log.info("校验开始");
PaymentInfo paymentInfo = paymentInfoService.lambdaQuery().eq(PaymentInfo::getOrderNo, orderNo)
.eq(PaymentInfo::getTradeState, WxTradeState.SUCCESS.getType()).one();
if (null == paymentInfo) {
throw new RuntimeException("未查询到该订单,请稍后再试!");
}
在校验客户输入的交易订单号是否正确我这里就判断后四位咯
// 判断是否是本人的订单
String transactionNo = paymentInfo.getTransactionId().substring(paymentInfo.getTransactionId().length() - 4);
if (!transactionNo.equals(refundsNo)) {
throw new RuntimeException("这笔可能不是你的订单哦.请核实支付成功后微信通知当中的交易订单号后四位!");
}
搞定后我们还需要思考一下,支付这么重要的退款环节我们是不是得要记录一下? 退款啊我直接裂开没赚到钱~
那么我们就查看一下之前文章提到的退款记录表,有同学可能直接懵逼直达车前往第三章从零玩转系列之微信支付实战基础框架搭建当中的创建三层结构所提到过
CREATE TABLE `t_refund_info` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '款单id',
`order_no` varchar(50) DEFAULT NULL COMMENT '商户订单编号',
`refund_no` varchar(50) DEFAULT NULL COMMENT '商户退款单编号',
`refund_id` varchar(50) DEFAULT NULL COMMENT '支付系统退款单号',
`total_fee` int(11) DEFAULT NULL COMMENT '原订单金额(分)',
`refund` int(11) DEFAULT NULL COMMENT '退款金额(分)',
`reason` varchar(50) DEFAULT NULL COMMENT '退款原因',
`refund_status` varchar(10) DEFAULT NULL COMMENT '退款状态',
`content_return` text COMMENT '申请退款返回参数',
`content_notify` text COMMENT '退款结果通知参数',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=30 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
搞定后我们就处理创建退款单记录并且保存到数据库当中
修改 refundInfoService 创建 createRefundByOrderNo 方法
log.info("创建退款单记录");
//根据订单编号创建退款单
RefundInfo refundsInfo = refundInfoService.createRefundByOrderNo(orderNo, reason);
log.info("调用退款API");
处理创建退款单逻辑
// 根据订单号生成退款订单
RefundInfo refundInfo = new RefundInfo();
refundInfo.setOrderNo(orderNo);//订单编号
refundInfo.setRefundNo(OrderNoUtils.getRefundNo());//退款单编号
refundInfo.setTotalFee();//原订单金额(分)
refundInfo.setRefund();//退款金额(分)
refundInfo.setReason(reason);//退款原因
创建实体把参数注入进去剩下的我们还缺少订单金额 我们传递了个参数是订单编号 那么我们想想看这个在哪里搞?
调用订单服务获取金额信息💰
注入订单服务IOC
/**
* 订单信息服务
*/
private final OrderInfoService orderInfoService;
发起查询订单信息接口
// 根据订单号获取订单信息
OrderInfo orderInfo = orderInfoService.lambdaQuery().eq(OrderInfo::getOrderNo, orderNo).one();
最终插入数据库
/**
* 创建退款订单根据订单号
*
* @param orderNo 订单号
* @param reason 退款原因
* @return {@link RefundInfo}
*/
public RefundInfo createRefundByOrderNo(String orderNo, String reason) {
// 根据订单号获取订单信息
OrderInfo orderInfo = orderInfoService.lambdaQuery().eq(OrderInfo::getOrderNo, orderNo).one();
// 根据订单号生成退款订单
RefundInfo refundInfo = new RefundInfo();
refundInfo.setOrderNo(orderNo);//订单编号
refundInfo.setRefundNo(OrderNoUtils.getRefundNo());//退款单编号
refundInfo.setTotalFee(orderInfo.getTotalFee());//原订单金额(分)
refundInfo.setRefund(orderInfo.getTotalFee());//退款金额(分)
refundInfo.setReason(reason);//退款原因
//保存退款订单
baseMapper.insert(refundInfo);
return refundInfo;
}
调用退款API
请求URL:https://api.mch.weixin.qq.com/v3/refund/domestic/refunds
请求方式:POST
请求参数:
参数名 | 变量 | 类型长度限制 | 必填 | 描述 |
---|---|---|---|---|
微信支付订单号 | transaction_id | string1, 32 | 二选一 | body原支付交易对应的微信订单号 示例值:1217752501201407033233368018 |
商户订单号 | out_trade_no | string6, 32 | 二选一 | body原支付交易对应的商户订单号 示例值:1217752501201407033233368018 |
商户退款单号 | out_refund_no | string1, 64 | 是 | body商户系统内部的退款单号,商户系统内部唯一,只能是数字、大小写字母_-|*@ ,同一退款单号多次请求只退一笔。 示例值:1217752501201407033233368018 |
退款原因 | reason | string1, 64 | 否 | 不一定要填写 |
金额信息 | amount | object | 是 | body订单金额信息 |
退款金额 | refund | int | 是 | 退款金额,单位为分,只能为整数,不能超过原订单支付金额。示例值:888 |
原订单金额 | total | int | 是 | 原支付交易的订单总金额,单位为分,只能为整数。示例值:888 |
发起API前言
- 组装调用API
- 组装请求参数
- 解析返回的响应数据
- 更新订单状态和更新退款单将本次的返回json保存
- 根据前面提到的退款完成后微信会发起一个退款回调信息的处理
WxApiType 请求API 枚举 基础项目搭建的内容不要忘记咯
// 调用统一下单API
String url = wxPayConfig.getDomain().concat(WxApiType.DOMESTIC_REFUNDS.getType());
HttpPost httpPost = new HttpPost(url);
// 请求body参数
Map<String, Object> paramsMap = new HashMap<>();
paramsMap.put("out_trade_no", orderNo);//订单编号
paramsMap.put("out_refund_no", refundsInfo.getRefundNo());//退款单编号
paramsMap.put("reason", reason);//退款原因
paramsMap.put("notify_url", wxPayConfig.getNotifyDomain().concat(WxNotifyType.REFUND_NOTIFY.getType()));//退款通知地址
Map<String, Object> amountMap = new HashMap<>();
amountMap.put("refund", refundsInfo.getRefund());// 退款金额
amountMap.put("total", refundsInfo.getTotalFee());// 原订单金额
amountMap.put("currency", "CNY");// 退款币种
paramsMap.put("amount", amountMap);
//将参数转换成json字符串
String jsonParams = JSONUtil.toJsonStr(paramsMap);
log.info("请求参数 ===> {}", jsonParams);
StringEntity entity = new StringEntity(jsonParams, "utf-8");
entity.setContentType("application/json");//设置请求报文格式
httpPost.setEntity(entity);//将请求报文放入请求对象
httpPost.setHeader("Accept", "application/json");//设置响应报文格式
组装完毕执行请求
// 完成签名并执行请求,并完成验签
try (CloseableHttpResponse response = wxPayClient.execute(httpPost)) {
//解析响应结果
JSONObject resultMap = buildBodyParams(response, JSONObject.class);
log.info("退款返回结果 ===> {}", resultMap);
// 更新订单状态
orderInfoService.lambdaUpdate().eq(OrderInfo::getOrderNo, orderNo).set(OrderInfo::getOrderStatus, OrderStatus.REFUND_PROCESSING.getType()).update();
// 更新退款单
refundInfoService.updateRefund(resultMap);
}
修改 refundInfoService
创建 updateRefund
方法保存本次退款响应的部分重要信息
退款返回参数:
更新逻辑
- 根据我们创建的退款单的退款单号为条件
- 填充数据库对应字段参数
- 判断当前是否
为退款返回的响应而
不是退款回调
的响应参数 - 执行更新语句
首先说一下 序号三的问题
可以看到申请退款接口返回的响应状态字段是
status
可以看到
退款回调
接口返回的响应状态字段是refund_status
由此可以看出来我们需要做不同的处理因为到时候要调用我们这个更新退款单的方法代码逻辑都是一样的直接共用即可
完整的处理逻辑
/**
* 更新退款单
*
* @param resultMap 退款响应结果
*/
public void updateRefund(JSONObject resultMap) {
// 根据退款单编号修改退款单
QueryWrapper<RefundInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("refund_no", resultMap.getStr("out_refund_no"));
// 设置要修改的字段
RefundInfo refundInfo = new RefundInfo();
refundInfo.setRefundId(resultMap.getStr("refund_id"));//微信支付退款单号
// 查询退款和申请退款中的返回参数
if (resultMap.get("status") != null) {
refundInfo.setRefundStatus(resultMap.getStr("status"));//退款状态
refundInfo.setContentReturn(resultMap.toString());//将全部响应结果存入数据库的content字段
}
// 退款回调中的回调参数
if (resultMap.get("refund_status") != null) {
refundInfo.setRefundStatus(resultMap.getStr("refund_status"));//退款状态
refundInfo.setContentNotify(resultMap.toString());//将全部响应结果存入数据库的content字段
}
//更新退款单
baseMapper.update(refundInfo, queryWrapper);
}
三、测试
内网穿透
开启花生壳或者其他产品 用于接收支付成功回调更新订单
执行下单
生成二维码
复制codeUrl
参数打开草料 https://cli.im/url
进行扫码支付!
支付成功后去微信支付通知里面查看我们的交易订单号
调用申请退款接口
RestFul风格接口讲解
/api/wx-pay/native/refunds/{orderNo}/{refundsNo}/{reason}
orderNo 订单号
refundsNo 交易订单号 目前我们程序当中只需要后四位即可
reason 退款理由(可不填写)
退款成功
还没完~ 前面我们提到了退款的回调此时退款已经到达了用户的账户当中我们自己的数据库对应的订单数据还没更新呢
四、退款结果通知API
退款状态改变后,微信会把相关退款结果发送给商户。
注意:
对后台通知交互时,如果微信收到应答不是成功或超时,微信认为通知失败,微信会通过一定的策略定期重新发起通知,尽可能提高通知的成功率,但微信不保证通知最终能成功
• 同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。 推荐的做法是,当商户系统收到通知进行处理时,先检查对应业务数据的状态,并判断该通知是否已经处理。如果未处理,则再进行处理;如果已处理,则直接返回结果成功。在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱。
• 如果在所有通知频率后没有收到微信侧回调。商户应调用查询订单接口确认订单状态。
特别提醒:商户系统对于开启结果通知的内容一定要做签名验证,并校验通知的信息是否与商户侧的信息一致,防止数据泄露导致出现“假通知”,造成资金损失。
接口说明
请求方式:POST
请求URL:该链接是通过申请退款接口指定的notify_url,必须为https协议。如果链接无法访问,商户将无法接收到微信通知。 通知url必须为直接可访问的url,不能携带参数。示例:“https://pay.weixin.qq.com/wxpay/pay.action”
以上是官方文档的原话简单来说就是和支付成功的回调一样就行啦
修改 WechatNativeController
新建refundsNotify
回调方法
⚠️ 和支付通知一样的逻辑只是处理的对象不同直接上代码就懂得明明白白
同学们动手打打代码
修改wxPayService
创建 processRefund
方法 退款通知 处理退款单数据
此处逻辑和支付成功回调一样如果忘记了快去补习 第五章从零玩转系列之微信支付实战PC端支付微信回调接口搭建
- 解密返回的参数为明文
- 获取订单号用于查询或更新信息
- 启用锁防止重复提交
- 更新订单状态和退款单
- 释放锁
同学们手动打打代码吧~
五、测试退款回调
步骤和上面的测试一样哦
- 打开内网穿透工具
- 创建订单
- 草料生成二维码支付成功
- 调用申请退款接口
- 观察控制台的打印是否退款回调成功
⚠️ 如果3秒后退款回调没有到达那么请检查你设置的退款回调地址是否正确
退款回调完成
最后
本期结束咱们下次再见👋~ ,关注我不迷路,如果本篇文章对你有所帮助,或者你有什么疑问,欢迎在评论区留言,我一般看到都会回复的。大家点赞支持一下哟~ 💗
【选题思路】
"技术源于生活" 为什么写微信支付这种项目的文章呢? 因为我看到市面上的文章都不全面不细节不小白话更加没有配套Demo!!! 从而我的从零玩转微信支付诞生啦~ 搭配PC端、Uniapp端的不同实现.
【写作提纲】
I. 前言
A. 通过前言表达我每次的文章内容是什么东西
II. 订单退款接口实现
A. 集结生活之中的场景来解析他的逻辑
III. 步入正题
A. 集合前面的理论知识点编写出功能代码
IV. 测试
A. 通过测试用例一步步排查编写是否存在BUG