当前位置: 首页 > news >正文

1.5 微信Native支付 - 申请退款、查询退款、退款通知、账单

文章目录

  • 用户退款
  • 一、 申请退款
    • 1.1 数据库表
    • 1.2 Controller
    • 1.3 Service
      • 1.3.1 创建退款单记录
      • 1.3.2 更新订单状态与更新退款单
      • 1.3.3 调用退款API
      • 1.3.4 效果图
  • 二、查询退款API
  • 三、退款结果通知
    • 3.1 Controller
    • 3.2 Service处理退款单
  • 四、账单
    • 4.1 申请账单API
    • 4.2 下载账单API

用户退款

其实用户退款的流程和用户支付的流程是差不多的,所以可以参考支付的代码进行编写

1.3 微信Native支付 -下单、定时查单、取消订单、签名-CSDN博客

1.4 内网穿透与通知、查询用户订单-CSDN博客

一、 申请退款

官方退款请求

image-20231107110133398

1.1 数据库表

image-20231107134421565

image-20231107134319712

@Data
@TableName("t_refund_info")
public class RefundInfo extends BaseEntity{private String orderNo;//商品订单编号private String refundNo;//退款单编号private String refundId;//支付系统退款单号private Integer totalFee;//原订单金额(分)private Integer refund;//退款金额(分)private String reason;//退款原因private String refundStatus;//退款单状态private String contentReturn;//申请退款返回参数private String contentNotify;//退款结果通知参数
}

1.2 Controller

@ApiOperation("申请退款")
@PostMapping("/refunds/{orderNo}/{reason}")
public R refunds(@PathVariable String orderNo,@PathVariable String reason){log.info("申请亏款");wxPayService.refund(orderNo,reason);return R.ok();
}

1.3 Service

1.3.1 创建退款单记录

log.info("创建退款单记录");
//根据订单编号创建退款单
RefundInfo refundsInfo = refundsInfoService.createRefundByOrderNo(orderNo, reason);
/*** 根据订单号创建退款订单* @param orderNo* @return*/
@Override
public RefundInfo createRefundByOrderNo(String orderNo, String reason) {//根据订单号获取订单信息OrderInfo orderInfo = orderInfoService.getOrderByOrderNo(orderNo);//根据订单号生成退款订单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;
}

1.3.2 更新订单状态与更新退款单

//更新订单状态
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_PROCESSING);//更新退款单
refundsInfoService.updateRefund(bodyAsString);
/*** 记录退款记录* @param content*/
@Override
public void updateRefund(String content) {//将json字符串转换成MapHashMap resultMap = JSONObject.parseObject(content, HashMap.class);//根据退款单编号修改退款单QueryWrapper<RefundInfo> queryWrapper = new QueryWrapper<>();queryWrapper.eq("refund_no", resultMap.get("out_refund_no"));//设置要修改的字段RefundInfo refundInfo = new RefundInfo();refundInfo.setRefundId( resultMap.get("refund_id").toString());//微信支付退款单号//查询退款和申请退款中的返回参数if(resultMap.get("status") != null){refundInfo.setRefundStatus( resultMap.get("status").toString());//退款状态refundInfo.setContentReturn(content);//将全部响应结果存入数据库的content字段}//退款回调中的回调参数if(resultMap.get("refund_status") != null){refundInfo.setRefundStatus(resultMap.get("refund_status").toString());//退款状态refundInfo.setContentNotify(content);//将全部响应结果存入数据库的content字段}//更新退款单baseMapper.update(refundInfo, queryWrapper);
}
/*** 根据订单号更新订单状态*/
@Override
public void updateStatusByOrderNo(String outTradeNo, OrderStatus orderStatus) {log.info("更新订单状态 ===> {}", orderStatus.getType());QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();queryWrapper.eq("order_no", outTradeNo);OrderInfo orderInfo = new OrderInfo();orderInfo.setOrderStatus(orderStatus.getType());baseMapper.update(orderInfo, queryWrapper);
}

1.3.3 调用退款API

我们这个地方退款就是退全款,如果想退部分金额也可以,多次退款也可以

 /*** 用户退款* @param orderNo 商户订单号* @param reason  退款原因*/@Transactional(rollbackFor = Exception.class)@Overridepublic void refund(String orderNo, String reason) throws Exception {log.info("创建退款单记录");//根据订单编号创建退款单RefundInfo refundsInfo = refundsInfoService.createRefundByOrderNo(orderNo, reason);log.info("调用退款API");//调用统一下单APIString url = wxPayConfig.getDomain().concat(WxApiType.DOMESTIC_REFUNDS.getType());HttpPost httpPost = new HttpPost(url);// 请求body参数Map 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 amountMap = new HashMap();amountMap.put("refund", refundsInfo.getRefund());//退款金额amountMap.put("total", refundsInfo.getTotalFee());//原订单金额amountMap.put("currency", "CNY");//退款币种paramsMap.put("amount", amountMap);//将参数转换成json字符串
//        String jsonParams = gson.toJson(paramsMap);String jsonParams = JSONObject.toJSONString(paramsMap);log.info("请求参数 ===> {}" + jsonParams);StringEntity entity = new StringEntity(jsonParams,"utf-8");entity.setContentType("application/json");//设置请求报文格式httpPost.setEntity(entity);//将请求报文放入请求对象httpPost.setHeader("Accept", "application/json");//设置响应报文格式//完成签名并执行请求,并完成验签CloseableHttpResponse response = httpClient.execute(httpPost);try {//解析响应结果String bodyAsString = EntityUtils.toString(response.getEntity());int statusCode = response.getStatusLine().getStatusCode();if (statusCode == 200) {log.info("成功, 退款返回结果 = " + bodyAsString);} else if (statusCode == 204) {log.info("成功");} else {throw new RuntimeException("退款异常, 响应码 = " + statusCode+ ", 退款返回结果 = " + bodyAsString);}//更新订单状态orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_PROCESSING);//更新退款单refundsInfoService.updateRefund(bodyAsString);} finally {response.close();}}

1.3.4 效果图

image-20231107142535438

二、查询退款API

查询退款API和查询订单API几乎是一个样子的

我们规定,当商户平台的某个退款订单在五分钟之内没有收到退款通知,我们商户平台就会主动调用查询退款API,查查是怎么个事

image-20231107142950482

注意商户退款单号是一个路径参数

image-20231107143017622

/*** 查询退款接口调用* @param refundNo* @return*/
@Override
public String queryRefund(String refundNo) throws Exception {log.info("查询退款接口调用 ===> {}", refundNo);String url =  String.format(WxApiType.DOMESTIC_REFUNDS_QUERY.getType(), refundNo);url = wxPayConfig.getDomain().concat(url);//创建远程Get 请求对象HttpGet httpGet = new HttpGet(url);httpGet.setHeader("Accept", "application/json");//完成签名并执行请求CloseableHttpResponse response = httpClient.execute(httpGet);try {String bodyAsString = EntityUtils.toString(response.getEntity());int statusCode = response.getStatusLine().getStatusCode();if (statusCode == 200) {log.info("成功, 查询退款返回结果 = " + bodyAsString);} else if (statusCode == 204) {log.info("成功");} else {throw new RuntimeException("查询退款异常, 响应码 = " + statusCode+ ", 查询退款返回结果 = " + bodyAsString);}return bodyAsString;} finally {response.close();}
}

自己写个接口测试一下

/*** 查询退款* @param refundNo* @return* @throws Exception*/
@ApiOperation("查询退款:测试用")
@GetMapping("/query-refund/{refundNo}")
public R queryRefund(@PathVariable String refundNo) throws Exception {log.info("查询退款");String result = wxPayService.queryRefund(refundNo);return R.ok().setMessage("查询成功").data("result", result);
}

image-20231107145609857

三、退款结果通知

与之前写的很类似,只不过一个是支付通知,一个是退款通知1.4 内网穿透与通知、查询用户订单-CSDN博客

3.1 Controller

/*** 退款结果通知* 退款状态改变后,微信会把相关退款结果发送给商户。*/
@ApiOperation("退款结果通知")
@PostMapping("/refunds/notify")
public String refundsNotify(HttpServletRequest request, HttpServletResponse response){log.info("退款通知执行");Map<String, String> map = new HashMap<>();//应答对象try {//处理通知参数String body = HttpUtils.readData(request);HashMap<String, Object> bodyMap = JSONObject.parseObject(body, HashMap.class);String requestId = (String)bodyMap.get("id");log.info("支付通知的id ===> {}", requestId);//签名的验证WechatPay2ValidatorForRequest wechatPay2ValidatorForRequest= new WechatPay2ValidatorForRequest(verifier, requestId, body);if(!wechatPay2ValidatorForRequest.validate(request)){log.error("通知验签失败");//失败应答response.setStatus(500);map.put("code", "ERROR");map.put("message", "通知验签失败");return JSONObject.toJSONString(map);}log.info("通知验签成功");//处理退款单wxPayService.processRefund(bodyMap);//成功应答response.setStatus(200);map.put("code", "SUCCESS");map.put("message", "成功");return JSONObject.toJSONString(map);} catch (Exception e) {e.printStackTrace();//失败应答response.setStatus(500);map.put("code", "ERROR");map.put("message", "失败");return JSONObject.toJSONString(map);}
}

3.2 Service处理退款单

/*** 处理退款单*/
@Transactional(rollbackFor = Exception.class)
@Override
public void processRefund(Map<String, Object> bodyMap) throws Exception {log.info("退款单");JSONObject jsonObject = JSONObject.parseObject(JSONObject.toJSONString(bodyMap));//解密报文String plainText = decryptFromResource(jsonObject);//将明文转换成mapHashMap plainTextMap = JSONObject.parseObject(plainText,HashMap.class);String orderNo = (String)plainTextMap.get("out_trade_no");if(lock.tryLock()){try {String orderStatus = orderInfoService.getOrderStatus(orderNo);if (!OrderStatus.REFUND_PROCESSING.getType().equals(orderStatus)) {return;}//更新订单状态orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_SUCCESS);//更新退款单refundsInfoService.updateRefund(plainText);} finally {//要主动释放锁lock.unlock();}}
}

四、账单

账单的作用就是方便商户进行对账

https://pay.weixin.qq.com/docs/merchant/apis/native-payment/download-bill.htmlimage-20231107151923117

现申请账单的url,虽然后将url当做下载交易/资金账单的参数向微信支付平台发起请求下载账单

4.1 申请账单API

下图所示是交易账单,交易账单更多的是针对微信用户

image-20231107152122643

下图所示是资金账单,更多的是侧重资金流水

image-20231107152230894

我们为了方便,下载交易账单和资金账单都写在一块,请求的时候添加一个type区分是申请交易账单还是下载资金账单

@ApiOperation("获取账单url:测试用")
@GetMapping("/querybill/{billDate}/{type}")
public R queryTradeBill(@PathVariable String billDate,@PathVariable String type) throws Exception {log.info("获取账单url");String downloadUrl = wxPayService.queryBill(billDate, type);return R.ok().setMessage("获取账单url成功").data("downloadUrl", downloadUrl);
}

Service层处理

/*** 申请账单* @param billDate* @param type* @return* @throws Exception*/
@Override
public String queryBill(String billDate, String type) throws Exception {log.warn("申请账单接口调用 {}", billDate);String url = "";if("tradebill".equals(type)){url =  WxApiType.TRADE_BILLS.getType();}else if("fundflowbill".equals(type)){url =  WxApiType.FUND_FLOW_BILLS.getType();}else{throw new RuntimeException("不支持的账单类型");}url = wxPayConfig.getDomain().concat(url).concat("?bill_date=").concat(billDate);//创建远程Get 请求对象HttpGet httpGet = new HttpGet(url);httpGet.addHeader("Accept", "application/json");//使用wxPayClient发送请求得到响应CloseableHttpResponse response = httpClient.execute(httpGet);try {String bodyAsString = EntityUtils.toString(response.getEntity());int statusCode = response.getStatusLine().getStatusCode();if (statusCode == 200) {log.info("成功, 申请账单返回结果 = " + bodyAsString);} else if (statusCode == 204) {log.info("成功");} else {throw new RuntimeException("申请账单异常, 响应码 = " + statusCode+ ", 申请账单返回结果 = " + bodyAsString);}//获取账单下载地址Map<String, String> resultMap = JSONObject.parseObject(bodyAsString, HashMap.class);return resultMap.get("download_url");} finally {response.close();}
}

image-20231107153550393

我们复制上面的"downloadUrl": "https://api.mch.weixin.qq.com/v3/billdownload/file?token=U62KXq-sD-MreORg6ZzSRIjztZAdN-LNcSlwOKIkhnizBe2jMYDnhWkB7xXfC_Gk"url是没有办法下载的,只能通过下载账单API进行下载

4.2 下载账单API

首先记得在配置类中增加一个wxPayNoSignClient,这个对象不需要验签,因为我们下载账单API需要跳过验签的流程

image-20231107170744599

/*** 获取HttpClient,无需进行应答签名验证,跳过验签的流程*/
@Bean(name = "wxPayNoSignClient")
public CloseableHttpClient getWxPayNoSignClient(){//获取商户私钥PrivateKey privateKey = getPrivateKey(privateKeyPath);//用于构造HttpClientWechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()//设置商户信息.withMerchant(mchId, mchSerialNo, privateKey)//无需进行签名验证、通过withValidator((response) -> true)实现.withValidator((response) -> true);// 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新CloseableHttpClient httpClient = builder.build();log.info("== getWxPayNoSignClient END ==");return httpClient;
}

下载账单的接口

@GetMapping("/downloadbill/{billDate}/{type}")
public R downloadBill(@PathVariable String billDate,@PathVariable String type) throws Exception {log.info("下载账单");String result = wxPayService.downloadBill(billDate, type);return R.ok().data("result", result);
}

下载账单的具体实现,一定要记得跳过验签

    @Resourceprivate CloseableHttpClient wxPayNoSignClient; //无需应答签名,这个地方要用这个对象/*** 下载账单* @param billDate* @param type* @return* @throws Exception*/@Overridepublic String downloadBill(String billDate, String type) throws Exception {log.warn("下载账单接口调用 {}, {}", billDate, type);//获取账单url地址String downloadUrl = this.queryBill(billDate, type);//创建远程Get 请求对象HttpGet httpGet = new HttpGet(downloadUrl);httpGet.addHeader("Accept", "application/json");//使用wxPayClient发送请求得到响应CloseableHttpResponse response = wxPayNoSignClient.execute(httpGet);try {String bodyAsString = EntityUtils.toString(response.getEntity());int statusCode = response.getStatusLine().getStatusCode();if (statusCode == 200) {log.info("成功, 下载账单返回结果 = " + bodyAsString);} else if (statusCode == 204) {log.info("成功");} else {throw new RuntimeException("下载账单异常, 响应码 = " + statusCode+ ", 下载账单返回结果 = " + bodyAsString);}return bodyAsString;} finally {response.close();}}

在Swagger中进行测试

这个地方是前端将数据转换成文件的,在后端将数据转成文件然后传输给前端也可以

image-20231107171412741

image-20231107172829536

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • vue3+setup 解决:this.$refs引用子组件报错 is not a function
  • 【数据结构】单链表
  • 【hcie-cloud】【2】华为云Stack解决方案介绍、缩略语整理 【下】
  • SpringCloudAlibaba系列之Nacos配置管理
  • 视频转码教程:轻松制作GIF动态图,一键高效剪辑操作
  • Luatos Air700 改变BL0942串口波特率
  • 深度学习理论知识入门【EM算法、VAE算法、GAN算法】和【RBM算法、MCMC算法、HMC算法】
  • 合并两个有序链表OJ
  • 浮点数保留指定位数的小数,小数位自动去掉多余的0
  • Mysql高阶语句
  • 【软件逆向】如何逆向Unity3D+il2cpp开发的安卓app【IDA Pro+il2CppDumper+DnSpy+AndroidKiller】
  • 大数据毕业设计选题推荐-市天气预警实时监控平台-Hadoop-Spark-Hive
  • NetworkManager 图形化配置 bond
  • 用 Wireshark 在 Firefox 或 Google Chrome 上使用 SSLKEYLOGFILE 环境变量解密 SSL 流量
  • C语言面试
  • [译] 怎样写一个基础的编译器
  • 「译」Node.js Streams 基础
  • 【跃迁之路】【733天】程序员高效学习方法论探索系列(实验阶段490-2019.2.23)...
  • ES6 ...操作符
  • Mocha测试初探
  • Netty 4.1 源代码学习:线程模型
  • php ci框架整合银盛支付
  • Python 基础起步 (十) 什么叫函数?
  • React组件设计模式(一)
  • sublime配置文件
  • UEditor初始化失败(实例已存在,但视图未渲染出来,单页化)
  • web标准化(下)
  • 分布式事物理论与实践
  • 浅析微信支付:申请退款、退款回调接口、查询退款
  • 数据库写操作弃用“SELECT ... FOR UPDATE”解决方案
  • d²y/dx²; 偏导数问题 请问f1 f2是什么意思
  • 积累各种好的链接
  • ​ 全球云科技基础设施:亚马逊云科技的海外服务器网络如何演进
  • ​经​纬​恒​润​二​面​​三​七​互​娱​一​面​​元​象​二​面​
  • $(selector).each()和$.each()的区别
  • ( )的作用是将计算机中的信息传送给用户,计算机应用基础 吉大15春学期《计算机应用基础》在线作业二及答案...
  • (2024,RWKV-5/6,RNN,矩阵值注意力状态,数据依赖线性插值,LoRA,多语言分词器)Eagle 和 Finch
  • (delphi11最新学习资料) Object Pascal 学习笔记---第13章第1节 (全局数据、栈和堆)
  • (超详细)2-YOLOV5改进-添加SimAM注意力机制
  • (创新)基于VMD-CNN-BiLSTM的电力负荷预测—代码+数据
  • (二)Kafka离线安装 - Zookeeper下载及安装
  • (附源码)spring boot校园拼车微信小程序 毕业设计 091617
  • (回溯) LeetCode 46. 全排列
  • (五)activiti-modeler 编辑器初步优化
  • (转)h264中avc和flv数据的解析
  • (自用)仿写程序
  • .net core IResultFilter 的 OnResultExecuted和OnResultExecuting的区别
  • .NET Core6.0 MVC+layui+SqlSugar 简单增删改查
  • .NET程序集编辑器/调试器 dnSpy 使用介绍
  • .NET的微型Web框架 Nancy
  • .Net下C#针对Excel开发控件汇总(ClosedXML,EPPlus,NPOI)
  • .NET中两种OCR方式对比
  • .skip() 和 .only() 的使用
  • @JSONField或@JsonProperty注解使用
  • @ohos.systemParameterEnhance系统参数接口调用:控制设备硬件(执行shell命令方式)