评论

微信支付-退款 V3版本

微信退款V3版本,使用的是微信提供的wechatpay-apache-httpclient这个sdk,踩了不少坑,特此记录。

微信支付-退款 V3版本

微信官方文档:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_9.shtml

环境说明:

spring版本:4.1.7.RELEASE
jdk版本:1.8
maven版本:3.8.5

可能需要用的依赖,如果实际运行提示缺少什么包中的的什么方法,就去maven仓库中下载,我也是一点一点摸着过来的

	<dependency>
             <groupId>com.github.wechatpay-apiv3</groupId>
             <artifactId>wechatpay-apache-httpclient</artifactId>
             <version>0.4.7</version>
	</dependency>
	<dependency>
	     <groupId>cn.hutool</groupId>
	     <artifactId>hutool-all</artifactId>
	     <version>5.8.3</version>
	</dependency>
	<dependency>
	<groupId>com.fasterxml.jackson.core</groupId>
	     <artifactId>jackson-core</artifactId>
	     <version>2.13.3</version>
	</dependency>
	<dependency>
	     <groupId>com.fasterxml.jackson.core</groupId>
	     <artifactId>jackson-databind</artifactId>
	     <version>2.13.3</version>
	</dependency>
	<dependency>
	     <groupId>com.fasterxml.jackson.core</groupId>
             <artifactId>jackson-annotations</artifactId>
	     <version>2.13.3</version>
	</dependency>

参数说明

微信支付商户号:merchantId
商户API证书序列号:merchantSerialNumber
商户API私钥地址:merchantPrivateKeyPath
商户apiV3秘钥:apiV3Key

这四个参数是必须的,需要设置为全局变量,会在退款方法跟回调方法中做双向验签时使用。具体如何获得四个参数参看微信支付文档即可

证书管理器: certificatesManager,该参数需要设置为全局变量,在退款方法中会下载证书并储存在这个对象中,然后在回调方法中会使用这个对象得到验证器去做验证
 CertificatesManager certificatesManager = CertificatesManager.getInstance();

我写的demo使用的是微信官方提供的sdk : wechatpay-apache-httpclient

https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay6_0.shtml
wechatpay-apache-httpclient,适用于使用Apache HttpClient处理HTTP的Java开发者。

过程

1,按文档中请求参数要求,获得需要的参数信息

(我只填写了必填项以及回调参数,其他参数请按实际需求填写)

    @RequestMapping("/XXXXXXXXXX")
    @ResponseBody		
		public JSONObject acquireWXParams(Long reservationId) {
        return acquireWXParams(reservationId, refundNotify);
    }
    ---------------------华丽的分界线-------------------------
     public JSONObject acquireWXParams(Long reservationId, String refundNotify) {
        //储存微信退款所需要的参数
        Map<String, Object> wxParams = new HashMap<>();
        //获取微信支付订单号
        if(reservationId == null) {
            return resultJSON(false, null);
        }
        //根据reservation获取预定信息实例
        //此处为查询数据库操作,我需要根据预订信息表中的数据得到相应的参数,具体参数获得请按照实际项目需要填写
        ReservationInformation reservation = reservationInformationDao.get(reservationId);
        if(reservation == null) {
            return resultJSON(false, null);
        }
        //获取微信支付订单号
        String transaction_id = reservation.getOrderMchid();
        if(transaction_id == null || transaction_id.isEmpty()) {
            return resultJSON(false, null);
        }
        //生成商户退款单号
        String refundNumber = "RM_TK"+System.currentTimeMillis();
        //获得金额信息(包括退款金额,原订单金额,退款币种)
        Map<String, Object> amount = new HashMap<>();
        //退款金额
        BigDecimal bigDecimal = reservation.getPrice();
        if(bigDecimal == null) {
            return resultJSON(false, null);
        }
        //微信需要的该参数单位是分,所以乘以100
        long refund = bigDecimal.longValue() * 100;
        //原订单金额
        //我们不做一笔订单多次退款,只做一次退全款,所以退款金额跟原订单金额相同
        long total = refund;

        //金额信息设置-----开始
        amount.put("refund", refund);
        amount.put("total", total);
        amount.put("currency", "CNY");
        //金额信息设置-----结束

        //返回参数设置-----开始
        //微信支付订单号
        wxParams.put("transaction_id", transaction_id);
        //商户退款单号
        wxParams.put("out_refund_no", refundNumber);
        //金额信息
        wxParams.put("amount", amount);
        //回调路径
        wxParams.put("notify_url", refundNotify);
        //返回参数设置-----结束
        return resultJSON(true, wxParams);
    }
     ---------------------华丽的分界线-------------------------
     public JSONObject resultJSON(boolean isRight, Object data) {
        JSONObject json = new JSONObject();
        json.put("result", isRight);
        if(data != null) {
            json.put("data", data);
        }
        return json;
    }

2,接下来拿着上面得到的退款接口所需要的全部参数,向微信退款接口发起post请求

    @RequestMapping("/XXXXXXXXXX")
    public void WXRefundApi(String wxparams) {//wxparams即为微信退款接口所需要的数据了,就是将上边json格式的数据转换为String格式(我当时脑子可能不太好使,得到参数后向js响应,然后再用js再带着参数再请求这个接口)
      //merchantPrivateKeyPath = "/apiclient_key.pem"
      //我是将私钥商户API私钥放在了resource中,所以我可以读到该文件的内容,大家的私钥文件根据项目实际情况放置即可,只要能够读到文件内容即可
      //也可以将私钥中的内容复制出来,存在一个字符串中,直接读这个字符串也行,wechatpay-apache-httpclient这个sdk的 README.md中也有写到:
      //# 示例:私钥为String字符串
      /**PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(
        new ByteArrayInputStream(privateKey.getBytes("utf-8")));*/
        try (InputStream is = new ClassPathResource(merchantPrivateKeyPath).getInputStream()) {
            PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(is);
            certificatesManager = CertificatesManager.getInstance();
            // 向证书管理器增加需要自动更新平台证书的商户信息
            certificatesManager.putMerchant(merchantId, new WechatPay2Credentials(merchantId, new PrivateKeySigner(merchantSerialNumber, merchantPrivateKey)), apiV3Key.getBytes(StandardCharsets.UTF_8));
            // 从证书管理器中获取verifier
            Verifier verifier = certificatesManager.getVerifier(merchantId);
            WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
                    .withMerchant(merchantId, merchantSerialNumber, merchantPrivateKey)
                    .withValidator(new WechatPay2Validator(verifier));
            // 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签
            try (CloseableHttpClient httpClient = builder.build()) {
                HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/v3/refund/domestic/refunds");
                httpPost.addHeader("Accept", "application/json");
                httpPost.addHeader("Content-type","application/json; charset=utf-8");
                httpPost.setEntity(new StringEntity(wxparams));
                CloseableHttpResponse response = httpClient.execute(httpPost);
								//这就是请求微信退款接口得到的响应数据
                String bodyAsString = EntityUtils.toString(response.getEntity());
                System.out.println(bodyAsString);
              	//接下来就可以按照实际项目逻辑做一些处理
              	//。。。。。。。。。。。。。
              	//。。。。。。。。。。。。。
            }
        } catch (IOException | GeneralSecurityException | HttpCodeException | NotFoundException e) {
            throw new RuntimeException(e);
        }
    }

3,对微信端做出正确的响应

当我们向微信发起退款请求完成后,微信会调用我们之前填写的回调地址,向我们响应退款结果,如果我们没能正确给微信响应,他会反复请求我们的回调地址,下边是微信的通知规则

通知规则

商户退款完成后,微信会把相关退款结果和用户信息发送给清算机构,清算机构需要接收处理后返回应答成功,然后继续给异步通知到下游从业机构。

对后台通知交互时,如果微信收到应答不是成功或超时,微信认为通知失败,微信会通过一定的策略定期重新发起通知,尽可能提高通知的成功率,但微信不保证通知最终能成功。(通知频率为15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h - 总计 24h4m)

    @RequestMapping("/XXXXXXXXX")
    @ResponseBody
    public void refundNotify(HttpServletRequest request, HttpServletResponse response) {
        LOGGER.info("进入回调接口");
        //这里设置应答状态码200以及成功的信息仅是为了对微信端做出正确的响应,有关业务逻辑问题在后边进行处理
        response.setHeader("Content-Type", "application/json");
        response.setHeader("Accept", "application/json");
        response.setStatus(200);
        response.getOutputStream().write(mapResult("SUCCESS", "成功"));
        response.flushBuffer();
        try{
            //获得NotificationRequest对象
            NotificationRequest notificationRequest = getNotificationRequest(request);
            //获得微信支付通知报文解密结果
            Notification notification = getParseResult(notificationRequest);
            //验证通过,确认是微信官方发送的的请求
            //接下来要实现业务逻辑
            handle(notification);
            
        } catch (ValidationException | NotFoundException | IOException | ParseException e) {
            LOGGER.error(e.getMessage());
        }
        LOGGER.info("回调结束");
    }

-----------------------华丽的分割线-----------------------
  public byte[] mapResult(String code, String message) {
        Map<String, String> map = new HashMap<>();
        map.put("code", code);
        map.put("message", message);
        return JSONUtil.toJsonStr(map).getBytes(StandardCharsets.UTF_8);
    }
-----------------------华丽的分割线-----------------------
  public NotificationRequest getNotificationRequest(HttpServletRequest request) throws IOException {
        //从请求头获取验签字段
        String Timestamp = request.getHeader("Wechatpay-Timestamp");
        String Nonce = request.getHeader("Wechatpay-Nonce");
        String Signature = request.getHeader("Wechatpay-Signature");
        String Serial = request.getHeader("Wechatpay-Serial");
        //读取请求体的信息
        InputStream inputStream = request.getInputStream();
        StringBuffer sb = new StringBuffer();
        BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
        String s;
        while((s = br.readLine()) != null) {
            sb.append(s);
        }
        String requestBody = String.valueOf(sb);

        return new NotificationRequest.Builder().withSerialNumber(Serial)
                .withNonce(Nonce)
                .withTimestamp(Timestamp)
                .withSignature(Signature)
                .withBody(requestBody)
                .build();
    }
-----------------------华丽的分割线-----------------------
  public Notification getParseResult(NotificationRequest notificationRequest) throws NotFoundException, ValidationException, ParseException {
        //验签和解析请求体
        // 从证书管理器中获取verifier
        Verifier verifier = certificatesManager.getVerifier(merchantId);
        NotificationHandler handler = new NotificationHandler(verifier, apiV3Key.getBytes(StandardCharsets.UTF_8));
        //在解析微信支付通知请求结果时会自动验签,不通过会抛出异常
        return handler.parse(notificationRequest);
    }
-----------------------华丽的分割线-----------------------
  public void handle(Notification notification) {
        //从notification中获取解密报文
        String decryptData = notification.getDecryptData();
        /**
        解密后的明文如下
        {
          "mchid": "1900000100",
          "transaction_id": "1008450740201411110005820873",
          "out_trade_no": "20150806125346",
          "refund_id": "50200207182018070300011301001",
          "out_refund_no": "7752501201407033233368018",
          "refund_status": "SUCCESS",
          "success_time": "2018-06-08T10:34:56+08:00",
          "user_received_account": "招商银行信用卡0403",
          "amount" : {
              "total": 999,
              "refund": 999,
              "payer_total": 999,
              "payer_refund": 999
          }
      }
      */
      /**
  	已经得到了明文,接下来就可以按照项目逻辑进行处理
      */
      //...................
      //...................
    }
最后一次编辑于  2022-06-20  
点赞 1
收藏
评论
登录 后发表内容