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

用户退款

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

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字符串转换成Map
    HashMap 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)
    @Override
    public void refund(String orderNo, String reason) throws Exception {
    
    

        log.info("创建退款单记录");
        //根据订单编号创建退款单
        RefundInfo refundsInfo = refundsInfoService.createRefundByOrderNo(orderNo, reason);

        log.info("调用退款API");

        //调用统一下单API
        String 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);

    //将明文转换成map
    HashMap 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);

    //用于构造HttpClient
    WechatPayHttpClientBuilder 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);
}

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

    @Resource
    private CloseableHttpClient wxPayNoSignClient; //无需应答签名,这个地方要用这个对象

    /**
     * 下载账单
     * @param billDate
     * @param type
     * @return
     * @throws Exception
     */
    @Override
    public 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

猜你喜欢

转载自blog.csdn.net/weixin_51351637/article/details/134298568
今日推荐