SpringBoot 整合微信小程序微信支付V3 jsapi (支付、退款)

03-29 2457阅读 0评论
SpringBoot 整合微信小程序微信支付V3 jsapi (支付、退款) 第1张
最近的一个微信小程序项目里有用到微信支付,网上找的资料都是特别乱,看起来特别懵,结合了好多文章的内容,终于做了出来,可能我的这个博文看起来也是特别乱,但是是可以直接C走简单改一改就可以用的。(支付成功回调,和退款回调因为昨天刚在阿里申请的域名还不让备案,目前回调还不确定有什么问题,但是支付和退款经过反复确认是没有问题的了)等域名备案成功后,回调如果有什么问题在更新改一下。

这是整体的一个微信支付+退款的项目结构。

如果大家需要的话,我可以等确认回调没有问题以后可以把我这个项目中完整的jsapi支付及退款做一个demo。或者按照图片的顺序把代码贴进来。

SpringBoot 整合微信小程序微信支付V3 jsapi (支付、退款) 第2张 

1.微信支付-准备工作 

1.获取商户号

微信商户平台 申请成为商户 => 提交资料 => 签署协议 => 获取商户号

2.获取AppID

微信公众平台 注册服务号 => 服务号认证 => 获取APPID => 绑定商户号

3.申请商户证书

登录商户平台 => 选择 账户中心 => 安全中心 => API安全 => 申请API证书 包括商户证书和商户私钥

4.获取微信的证书

获取APIv3秘钥(在微信支付回调通知和商户获取平台证书使用APIv3密钥)

登录商户平台 => 选择 账户中心 => 安全中心 => API安全 => 设置APIv3密钥

2.开干!!!!

1.引入pom.xml

        
        
            com.github.wechatpay-apiv3
            wechatpay-apache-httpclient
            0.4.2
        
        
            com.google.code.gson
            gson
            2.8.9
        
        
            org.apache.commons
            commons-lang3
            3.12.0
        
        
            com.alibaba
            fastjson
            1.2.78
        

2.配置application.yml

# 微信相关配置
vx:
  # appid
  appId: appid
  # 小程序密钥
  secret: 小程序密钥
  # 商户号
  mchId: 商户号
  # 证书序列号
  mchSerialNo: 证书序列号
  # api密钥
  apiKey: api密钥
  # 证书地址
  keyPath: D:/wxPayPem/apiclient_key.pem
  certPath: D:/wxPayPem/apiclient_cert.pem
  certP12Path: D:/wxPayPem/apiclient_cert.p12

说明:这个证书地址是在微信商户平台内生成下载的API证书文件

具体教程:什么是商户API证书?如何获取商户API证书?

SpringBoot 整合微信小程序微信支付V3 jsapi (支付、退款) 第3张SpringBoot 整合微信小程序微信支付V3 jsapi (支付、退款) 第4张

3.都配置完了之后创建对应的配置实体。

import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Credentials;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator;
import com.wechat.pay.contrib.apache.httpclient.cert.CertificatesManager;
import com.wechat.pay.contrib.apache.httpclient.exception.HttpCodeException;
import com.wechat.pay.contrib.apache.httpclient.exception.NotFoundException;
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
import lombok.Data;
import org.apache.http.impl.client.CloseableHttpClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.PrivateKey;
/**
 * 

* 配置信息实体 *

* * @author Lch * @dateTime 2024/2/26 15:31 */ @Configuration @Data public class WxPayConfig { /** * appid */ @Value("${vx.appId}") private String appId; /** * 小程序密钥 */ @Value("${vx.secret}") private String secret; /** * 商户号 */ @Value("${vx.mchId}") private String mchId; /** * 证书序列号 */ @Value("${vx.mchSerialNo}") private String mchSerialNo; /** * api密钥 */ @Value("${vx.apiKey}") private String apiKey; /** * 证书地址 */ @Value("${vx.keyPath}") private String keyPath; /** * 获取商户的私钥文件 * * @param filename 证书地址 * @return 私钥文件 */ public PrivateKey getPrivateKey(String filename) { try { return PemUtil.loadPrivateKey(new FileInputStream(filename)); } catch (FileNotFoundException e) { throw new RuntimeException("私钥文件不存在"); } } /** * 获取签名验证器 */ @Bean public Verifier getVerifier() { // 获取商户私钥 final PrivateKey privateKey = getPrivateKey(keyPath); // 私钥签名对象 PrivateKeySigner privateKeySigner = new PrivateKeySigner(mchSerialNo, privateKey); // 身份认证对象 WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, privateKeySigner); // 获取证书管理器实例 CertificatesManager certificatesManager = CertificatesManager.getInstance(); try { // 向证书管理器增加需要自动更新平台证书的商户信息 certificatesManager.putMerchant(mchId, wechatPay2Credentials, apiKey.getBytes(StandardCharsets.UTF_8)); } catch (IOException | GeneralSecurityException | HttpCodeException e) { e.printStackTrace(); } try { return certificatesManager.getVerifier(mchId); } catch (NotFoundException e) { e.printStackTrace(); throw new RuntimeException("获取签名验证器失败"); } } /** * 获取微信支付的远程请求对象 * @return Http请求对象 */ @Bean public CloseableHttpClient getWxPayClient() { // 获取签名验证器 Verifier verifier = getVerifier(); // 获取商户私钥 final PrivateKey privateKey = getPrivateKey(keyPath); WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create().withMerchant(mchId, mchSerialNo, privateKey) .withValidator(new WechatPay2Validator(verifier)); return builder.build(); } }

4.创建一个枚举类 type用到了什么就写什么,或者不创建枚举类都可以。

import lombok.AllArgsConstructor;
import lombok.Getter;
/**
 * 

* 请求地址枚举类 *

* * @author Lch * @dateTime 2024/2/26 15:41 */ @AllArgsConstructor @Getter public enum WxApiConstants { /** * jsapi下单 */ JSAPI_PAY("/v3/pay/transactions/jsapi"), /** * 申请退款 */ DOMESTIC_REFUNDS("/v3/refund/domestic/refunds"); /** * 类型 */ private final String type; public String getType() { return type; } }

5.微信支付求数据的对象

import lombok.Data;
import lombok.experimental.Accessors;
import javax.validation.constraints.NotBlank;
/**
 * 

* 预支付参数 *

* * @author Lch * @dateTime 2024/2/26 13:30 */ @Data @Accessors(chain = true) public class WxPayReqParam { /** * 总金额 */ @NotBlank(message = "总金额不能为空!") private String totalPrice; /** * 商品名称 */ @NotBlank(message = "商品名称不能为空!") private String goodsName; /** * openId */ @NotBlank(message = "openId不能为空!") private String openId; /** * 订单号 */ @NotBlank(message = "商品订单号不能为空!") private String orderNumber; }

6.将请求参数封装成Map集合+创建微信支付订单的三种方式(Native,Jsapi,App),

我使用的是Jsapi,别的支付方式可以在刚才的枚举类中“WxApiConstants”,添加别的下单路径,或者就是写死了也可以,如果你也是Jsapi支付,应该就不需要改动什么。
开发指引-JSAPI支付 | 微信支付商户平台文档中心

import com.cx.sasmc.vxpay.reqparam.WxPayReqParam;
import com.cx.sasmc.vxpay.vxapienum.WxApiConstants;
import com.google.gson.Gson;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
 * 

* 将请求参数封装成Map集合 *

* * @author Lch * @dateTime 2024/2/26 15:54 */ public class WxPayCommon { private final static Logger logger = LoggerFactory.getLogger(WxPayCommon.class); /** * 封装基础通用请求数据 * @param wxPayConfig 微信的配置文件 * @param basePayData 微信支付基础请求数据 * @return 封装后的map对象 */ public static Map getBasePayParams(WxPayConfig wxPayConfig, WxPayReqParam basePayData) { Map paramsMap = new HashMap(); paramsMap.put("appid", wxPayConfig.getAppId()); paramsMap.put("mchid", wxPayConfig.getMchId()); // 如果商品名称过长则截取 String title = basePayData.getGoodsName().length() > 62 ? basePayData.getGoodsName().substring(0, 62) : basePayData.getGoodsName(); paramsMap.put("description",title); paramsMap.put("out_trade_no", basePayData.getOrderNumber()); paramsMap.put("notify_url", "https://你自己的回调域名.cn/vxPay/payNotify"); Map amountMap = new HashMap(); amountMap.put("total", Integer.valueOf(basePayData.getTotalPrice())); paramsMap.put("amount", amountMap); return paramsMap; } /** * 获取请求对象(Post请求) * @param paramsMap 请求参数 * @return Post请求对象 */ public static HttpPost getHttpPost(String type, Map paramsMap) { // 1.设置请求地址 HttpPost httpPost = new HttpPost(type); // 2.设置请求数据 Gson gson = new Gson(); String jsonParams = gson.toJson(paramsMap); // 3.设置请求信息 StringEntity entity = new StringEntity(jsonParams, "utf-8"); entity.setContentType("application/json"); httpPost.setEntity(entity); httpPost.setHeader("Accept", "application/json"); return httpPost; } /** * 解析响应数据 * @param response 发送请求成功后,返回的数据 * @return 微信返回的参数 */ public static HashMap resolverResponse(CloseableHttpResponse response) { try { // 1.获取请求码 int statusCode = response.getStatusLine().getStatusCode(); // 2.获取返回值 String 格式 final String bodyAsString = EntityUtils.toString(response.getEntity()); Gson gson = new Gson(); if (statusCode == 200) { // 3.如果请求成功则解析成Map对象返回 HashMap resultMap = gson.fromJson(bodyAsString, HashMap.class); return resultMap; } else { if (StringUtils.isNoneBlank(bodyAsString)) { logger.error("微信支付请求失败,提示信息:{}", bodyAsString); // 4.请求码显示失败,则尝试获取提示信息 HashMap resultMap = gson.fromJson(bodyAsString, HashMap.class); throw new RuntimeException(resultMap.get("message")); } logger.error("微信支付请求失败,未查询到原因,提示信息:{}", response); // 其他异常,微信也没有返回数据,这就需要具体排查了 throw new IOException("request failed"); } } catch (Exception e) { e.printStackTrace(); throw new RuntimeException(e.getMessage()); } finally { try { response.close(); } catch (IOException e) { e.printStackTrace(); } } } /** * 创建微信支付订单-jsapi方式 * @param wxPayConfig 微信配置信息 * @param basePayData 基础请求信息,商品标题、商家订单id、订单价格 * @param openId 通过微信小程序或者公众号获取到用户的openId * @param wxPayClient 微信请求客户端() * @return 微信支付二维码地址 */ public static String wxJsApiPay(WxPayConfig wxPayConfig, WxPayReqParam basePayData, String openId, CloseableHttpClient wxPayClient) { // 1.获取请求参数的Map格式 Map paramsMap = getBasePayParams(wxPayConfig, basePayData); // 1.1 添加支付者信息 Map payerMap = new HashMap(); payerMap.put("openid",openId); paramsMap.put("payer",payerMap); // 2.获取请求对象 HttpPost httpPost = getHttpPost("https://api.mch.weixin.qq.com"+WxApiConstants.JSAPI_PAY.getType(),paramsMap); // 3.完成签名并执行请求 CloseableHttpResponse response = null; try { response = wxPayClient.execute(httpPost); } catch (IOException e) { e.printStackTrace(); throw new RuntimeException("微信支付请求失败"); } // 4.解析response对象 HashMap resultMap = resolverResponse(response); if (resultMap != null) { // native请求返回的是二维码链接,前端将链接转换成二维码即可 return resultMap.get("prepay_id"); } return null; } }

7.创建实体存储前端微信支付所需参数(WxChatPayDto)

 小程序内需要多个参数才可以唤起微信支付,就是输入密码支付的那个支付页面。

import lombok.Data;
/**
 * 

* 前端微信支付所需参数 *

* * @author Lch * @dateTime 2024/2/26 16:26 */ @Data public class WxChatPayDto { /** * 需要支付的小程序id */ private String appid; /** * 时间戳(当前的时间) */ private String timeStamp; /** * 随机字符串,不长于32位。 */ private String nonceStr; /** * 小程序下单接口返回的prepay_id参数值,提交格式如:prepay_id=*** */ private String prepayId; /** * 签名类型,默认为RSA,仅支持RSA。 */ private String signType; /** * 签名,使用字段appId、timeStamp、nonceStr、package计算得出的签名值 */ private String paySign; }

8. 微信支付

 /**
     * 微信用户调用微信支付
     */
    @Override
    public WxChatPayDto wxPay(WxPayReqParam param) {
            String prepayId = WxPayCommon.wxJsApiPay(wxPayConfig, param, param.getOpenId(), wxPayClient);
            WxChatPayDto wxChatPayDto = new WxChatPayDto();
            wxChatPayDto.setAppid(wxPayConfig.getAppId());
            wxChatPayDto.setTimeStamp(String.valueOf(System.currentTimeMillis() / 1000));
            wxChatPayDto.setNonceStr(UUID.randomUUID().toString().replaceAll("-", ""));
            wxChatPayDto.setPrepayId("prepay_id=" + prepayId);
            wxChatPayDto.setSignType("RSA");
            wxChatPayDto.setPaySign(getSign(wxChatPayDto.getNonceStr(),wxChatPayDto.getAppid(),wxChatPayDto.getPrepayId(),Long.parseLong(wxChatPayDto.getTimeStamp())));
            return wxChatPayDto;
    }

返回给小程序的wxChatPayDto就可以唤起支付页面了。

9.成功回调

import com.alibaba.fastjson.JSONObject;
import com.cx.sasmc.vxpay.config.WxPayConfig;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import com.wechat.pay.contrib.apache.httpclient.util.AesUtil;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.security.GeneralSecurityException;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
 * 

* 微信支付回调工具类 *

* * @author Lch * @dateTime 2024/2/27 8:50 */ @Component public class WxPayCallbackUtil { @Resource private Verifier verifier; @Resource private WxPayConfig wxPayConfig; /** * 获取回调数据 * @param request * @param response * @return */ public Map wxChatPayCallback(HttpServletRequest request, HttpServletResponse response) { //获取报文 String body = getRequestBody(request); //随机串 String nonceStr = request.getHeader("Wechatpay-Nonce"); //微信传递过来的签名 String signature = request.getHeader("Wechatpay-Signature"); //证书序列号(微信平台) String serialNo = request.getHeader("Wechatpay-Serial"); //时间戳 String timestamp = request.getHeader("Wechatpay-Timestamp"); //构造签名串 应答时间戳\n,应答随机串\n,应答报文主体\n String signStr = Stream.of(timestamp, nonceStr, body).collect(Collectors.joining("\n", "", "\n")); Map map = new HashMap(2); try { //验证签名是否通过 boolean result = verifiedSign(serialNo, signStr, signature); if(result){ //解密数据 String plainBody = decryptBody(body); return convertWechatPayMsgToMap(plainBody); } } catch (Exception e) { e.printStackTrace(); } return map; } /** * 转换body为map * @param plainBody * @return */ public Map convertWechatPayMsgToMap(String plainBody){ Map paramsMap = new HashMap(2); JSONObject jsonObject = JSONObject.parseObject(plainBody); //商户订单号 paramsMap.put("out_trade_no",jsonObject.getString("out_trade_no")); //交易状态 paramsMap.put("trade_state",jsonObject.getString("trade_state")); //附加数据 paramsMap.put("attach",jsonObject.getString("attach")); if (jsonObject.getJSONObject("attach") != null && !jsonObject.getJSONObject("attach").equals("")){ paramsMap.put("account_no",jsonObject.getJSONObject("attach").getString("accountNo")); } return paramsMap; } /** * 解密body的密文 * * "resource": { * "original_type": "transaction", * "algorithm": "AEAD_AES_256_GCM", * "ciphertext": "", * "associated_data": "", * "nonce": "" * } * * @param body body * @return */ public String decryptBody(String body) throws UnsupportedEncodingException, GeneralSecurityException { AesUtil aesUtil = new AesUtil(wxPayConfig.getApiKey().getBytes("utf-8")); JSONObject object = JSONObject.parseObject(body); JSONObject resource = object.getJSONObject("resource"); String ciphertext = resource.getString("ciphertext"); String associatedData = resource.getString("associated_data"); String nonce = resource.getString("nonce"); return aesUtil.decryptToString(associatedData.getBytes("utf-8"),nonce.getBytes("utf-8"),ciphertext); } /** * 验证签名 * * @param serialNo 微信平台-证书序列号 * @param signStr 自己组装的签名串 * @param signature 微信返回的签名 * @return * @throws UnsupportedEncodingException */ public boolean verifiedSign(String serialNo, String signStr, String signature) throws UnsupportedEncodingException { return verifier.verify(serialNo, signStr.getBytes("utf-8"), signature); } /** * 读取请求数据流 * * @param request * @return */ public String getRequestBody(HttpServletRequest request) { StringBuffer sb = new StringBuffer(); try (ServletInputStream inputStream = request.getInputStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); ) { String line; while ((line = reader.readLine()) != null) { sb.append(line); } } catch (IOException e) { e.printStackTrace(); } return sb.toString(); } }

成功回调方法

/**
     * 微信支付成功回调
     * @param request request
     * @param response response
     * @return map
     */
    @Override
    public Map wxOrderCallBack(HttpServletRequest request, HttpServletResponse response) {
        Map map = new HashMap(2);
        try {
            Map stringMap = wxChatPayCallback.wxChatPayCallback(request, response);
            //支付成功
            if (stringMap.get("trade_state").equals("SUCCESS")){
                // 获取咱们自己生成的订单号
                String out_trade_no = stringMap.get("out_trade_no");
                if (!stringRedisTemplate.hasKey("ORDER_NO:"+out_trade_no)){
                    QueryWrapper queryWrapper = new QueryWrapper();
                    queryWrapper.eq("orderNumber",OrderStatusConstants.UNPAID);
                    Order order = orderMapper.selectOne(queryWrapper);
                    if (ObjectUtil.isNotEmpty(order)){
                        //编写支付成功后逻辑 修改订单为已付款。
                        Order upOrder = new Order();
                        upOrder.setId(order.getId());
                        upOrder.setOrderStatus(OrderStatusConstants.PAID);
                        orderMapper.updateById(upOrder);
                        // (通知频率为15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h - 总计 24h4m)
                        // 数据有效期设置25小时
                        stringRedisTemplate.opsForValue().set("ORDER_NO:"+out_trade_no,out_trade_no,25L, TimeUnit.HOURS);
                    }
                }
            }
            //响应微信
            map.put("code", "SUCCESS");
            map.put("message", "成功");
        } catch (Exception e) {
            e.printStackTrace();
        }
        return map;
    }

SpringBoot 整合微信小程序微信支付V3 jsapi (支付、退款) 第5张

至此支付就完成了。

10.退款 说明(项目中用到了Sa-token,@SaIgnore注解标识这个方法无需鉴权,可以匿名访问)

 /**
     * 申请退款
     */
    @SaIgnore
    @PostMapping("/vxPayRefund")
    public Result payRefund(@RequestBody @Validated WxPayRefundReqParam param){
       String s = vxPayService.payRefund(param);
       if (ObjectUtil.isNotEmpty(s)){
           return Result.ok(s);
       } else {
           return Result.fail("退款失败");
       }
    }
 /**
     * 申请退款
     * @param param param
     * @return string
     */
    @Override
    public String payRefund(WxPayRefundReqParam param) {
        Map map = WxPayRefundUtil.refundPay(param, wxPayClient);
        Order upOrder = new Order();
        upOrder.setRefundNumber(map.get("refundNumber"));
        upOrder.setOrderStatus(OrderStatusConstants.REFUND_PROCESSING);
        UpdateWrapper updateWrapper = new UpdateWrapper();
        updateWrapper.eq("orderNumber",param.getTransactionId());
        orderMapper.update(upOrder,updateWrapper);
        return map.get("refund_id");
    }
 /**
     * 发起微信退款申请
     * @param param 微信支付申请退款请求参数
     * @return 微信支付二维码地址
     */
    public static Map refundPay(WxPayRefundReqParam param,CloseableHttpClient wxPayClient) {
        Map returnMap = new HashMap();
        // 1.获取请求参数的Map格式
        Map paramsMap = getRefundParams(param);
        // 2.获取请求对象
        HttpPost httpPost = WxPayCommon.getHttpPost("https://api.mch.weixin.qq.com"+ WxApiConstants.DOMESTIC_REFUNDS.getType(), paramsMap);
        // 3.完成签名并执行请求
        CloseableHttpResponse response = null;
        try {
            response = wxPayClient.execute(httpPost);
        } catch (IOException e) {
            e.printStackTrace();
            throw new RuntimeException("微信支付请求失败");
        }
        // 4.解析response对象
        HashMap resultMap = WxPayCommon.resolverResponse(response);
        if (resultMap != null) {
            // 返回微信支付退款单号
            returnMap.put("refund_id",resultMap.get("refund_id"));
            returnMap.put("refundNumber",paramsMap.get("out_refund_no").toString());
            return returnMap;
        }
        return null;
    }

退款请求参数:

package com.cx.sasmc.vxpay.vxpayutil;
import com.cx.sasmc.utils.OrderNumberGenerate;
import com.cx.sasmc.vxpay.config.WxPayCommon;
import com.cx.sasmc.vxpay.reqparam.WxPayRefundReqParam;
import com.cx.sasmc.vxpay.vxapienum.WxApiConstants;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
 * 

* 微信支付退款 *

* * @author Lch * @dateTime 2024/2/27 9:49 */ public class WxPayRefundUtil { /** * 封装微信支付申请退款请求参数 * @param param 微信支付申请退款请求参数 * @return 封装后的map微信支付申请退款请求参数对象 */ private static Map getRefundParams(WxPayRefundReqParam param) { String out_refund_no = OrderNumberGenerate.orderNo("100001"); Map paramsMap = new HashMap(); if (StringUtils.isNoneBlank(param.getTransactionId())) { paramsMap.put("out_trade_no", param.getTransactionId()); } paramsMap.put("out_refund_no", out_refund_no); paramsMap.put("notify_url", "https://你自己的回调域名.cn/vxPay/refundWechatCallback"); Map amountMap = new HashMap(); amountMap.put("refund", Long.valueOf(param.getRefundMoney())); amountMap.put("total", Long.valueOf(param.getTotalMoney())); amountMap.put("currency", "CNY"); paramsMap.put("amount", amountMap); return paramsMap; } /** * 发起微信退款申请 * @param param 微信支付申请退款请求参数 * @return 微信支付二维码地址 */ public static Map refundPay(WxPayRefundReqParam param,CloseableHttpClient wxPayClient) { Map returnMap = new HashMap(); // 1.获取请求参数的Map格式 Map paramsMap = getRefundParams(param); // 2.获取请求对象 HttpPost httpPost = WxPayCommon.getHttpPost("https://api.mch.weixin.qq.com"+ WxApiConstants.DOMESTIC_REFUNDS.getType(), paramsMap); // 3.完成签名并执行请求 CloseableHttpResponse response = null; try { response = wxPayClient.execute(httpPost); } catch (IOException e) { e.printStackTrace(); throw new RuntimeException("微信支付请求失败"); } // 4.解析response对象 HashMap resultMap = WxPayCommon.resolverResponse(response); if (resultMap != null) { // 返回微信支付退款单号 returnMap.put("refund_id",resultMap.get("refund_id")); returnMap.put("refundNumber",paramsMap.get("out_refund_no").toString()); return returnMap; } return null; } }

退款回调的请求参数

import cn.hutool.core.date.DateUtil;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
/**
 * 

* 微信退款回调参数 *

* * @author Lch * @dateTime 2024/2/27 13:40 */ @Data public class WxChatCallbackRefundReqParam { /** * 商户订单号 */ private String orderId; /** * 商户退款单号,out_refund_no */ private String refundId; /** * 微信支付系统生成的订单号 */ private String transactionId; /** * 微信支付系统生成的退款订单号 */ private String transactionRefundId; /** * 退款渠道 1.ORIGINAL:原路退款 2.BALANCE:退回到余额 * 3.OTHER_BALANCE:原账户异常退到其他余额账户 * 4.OTHER_BANKCARD:原银行卡异常退到其他银行卡 */ private String channel; /** * 退款成功时间 当前退款成功时才有此返回值 */ private Date successTime; /** * 退款状态 退款到银行发现用户的卡作废或者冻结了,导致原路退款银行卡失败,可前往商户平台-交易中心,手动处理此笔退款。 * 1.SUCCESS:退款成功 2.CLOSED:退款关闭 3.PROCESSING:退款处理中 4.ABNORMAL:退款异常 */ private String status; /** * 退款金额 */ private BigDecimal refundMoney; public Date getSuccessTime() { return successTime; } public void setSuccessTime(String successTime) { // Hutool工具包的方法,自动识别一些常用格式的日期字符串 this.successTime = DateUtil.parse(successTime); } }

 退款业务处理接口

import com.cx.sasmc.vxpay.reqparam.WxChatCallbackRefundReqParam;
/**
 * 

* 退款回调 *

* * @author Lch * @dateTime 2024/2/27 13:54 */ public interface WechatRefundCallback { /** * 退款成功处理情况 */ void success(WxChatCallbackRefundReqParam refundData); /** * 退款失败处理情况 */ void fail(WxChatCallbackRefundReqParam refundData); }

 微信退款回调方法

import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
/**
 * 

* 将通知参数转化为字符串 *

* * @author Lch * @dateTime 2024/2/27 13:43 */ public class HttpUtils { /** * 将通知参数转化为字符串 * @param request * @return */ public static String readData(HttpServletRequest request) { BufferedReader br = null; try { StringBuilder result = new StringBuilder(); br = request.getReader(); for (String line; (line = br.readLine()) != null; ) { if (result.length() > 0) { result.append("\n"); } result.append(line); } return result.toString(); } catch (IOException e) { throw new RuntimeException(e); } finally { if (br != null) { try { br.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.DateTimeException;
import java.time.Duration;
import java.time.Instant;
import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.*;
import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.WECHAT_PAY_SERIAL;
/**
 * 

* 微信支付 退款回调请求验证 *

* * @author Lch * @dateTime 2024/2/27 13:47 */ public class WechatPayValidatorForRequest { private final Logger logger = LoggerFactory.getLogger(WechatPayValidatorForRequest.class); /** * 应答超时时间,单位为分钟 */ protected static final long RESPONSE_EXPIRED_MINUTES = 5; protected final Verifier verifier; protected final String body; protected final String requestId; public WechatPayValidatorForRequest(Verifier verifier, String body, String requestId) { this.verifier = verifier; this.body = body; this.requestId = requestId; } protected static IllegalArgumentException parameterError(String message, Object... args) { message = String.format(message, args); return new IllegalArgumentException("parameter error: " + message); } protected static IllegalArgumentException verifyFail(String message, Object... args) { message = String.format(message, args); return new IllegalArgumentException("signature verify fail: " + message); } public final boolean validate(HttpServletRequest request) throws IOException { try { validateParameters(request); String message = buildMessage(request); String serial = request.getHeader(WECHAT_PAY_SERIAL); String signature = request.getHeader(WECHAT_PAY_SIGNATURE); if (!verifier.verify(serial, message.getBytes(StandardCharsets.UTF_8), signature)) { throw verifyFail("serial=[%s] message=[%s] sign=[%s], request-id=[%s]", serial, message, signature, request.getHeader(REQUEST_ID)); } } catch (IllegalArgumentException e) { logger.warn(e.getMessage()); return false; } return true; } protected final void validateParameters(HttpServletRequest request) { // NOTE: ensure HEADER_WECHAT_PAY_TIMESTAMP at last String[] headers = {WECHAT_PAY_SERIAL, WECHAT_PAY_SIGNATURE, WECHAT_PAY_NONCE, WECHAT_PAY_TIMESTAMP}; String header = null; for (String headerName : headers) { header = request.getHeader(headerName); if (header == null) { throw parameterError("empty [%s], request-id=[%s]", headerName, requestId); } } String timestampStr = header; try { Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestampStr)); // 拒绝过期应答 if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= RESPONSE_EXPIRED_MINUTES) { throw parameterError("timestamp=[%s] expires, request-id=[%s]", timestampStr, requestId); } } catch (DateTimeException | NumberFormatException e) { throw parameterError("invalid timestamp=[%s], request-id=[%s]", timestampStr, requestId); } } protected final String buildMessage(HttpServletRequest request) throws IOException { String timestamp = request.getHeader(WECHAT_PAY_TIMESTAMP); String nonce = request.getHeader(WECHAT_PAY_NONCE); return timestamp + "\n" + nonce + "\n" + body + "\n"; } }
import com.cx.sasmc.vxpay.config.WxPayConfig;
import com.cx.sasmc.vxpay.reqparam.WxChatCallbackRefundReqParam;
import com.cx.sasmc.vxpay.service.WechatRefundCallback;
import com.google.gson.Gson;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import com.wechat.pay.contrib.apache.httpclient.util.AesUtil;
import org.apache.commons.lang3.StringUtils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.util.HashMap;
import java.util.Map;
/**
 * 

* 微信支付申请退款回调 *

* * @author Lch * @dateTime 2024/2/27 13:51 */ public class WxPayRefundCallbackUtil { /** * 微信支付申请退款回调方法 * * @param verifier 证书 * @param wxPayConfig 微信配置 * @param refundCallback 回调方法,用于处理业务逻辑,包含退款成功处理于退款失败处理 * @return json格式的string数据,直接返回给微信 */ public static String wxPayRefundCallback(HttpServletRequest request, HttpServletResponse response, Verifier verifier, WxPayConfig wxPayConfig, WechatRefundCallback refundCallback) { Gson gson = new Gson(); // 1.处理通知参数 final String body = HttpUtils.readData(request); HashMap bodyMap = gson.fromJson(body, HashMap.class); // 2.签名验证 WechatPayValidatorForRequest wechatForRequest = new WechatPayValidatorForRequest(verifier, body, (String) bodyMap.get("id")); try { if (!wechatForRequest.validate(request)) { // 通知验签失败 response.setStatus(500); final HashMap map = new HashMap(); map.put("code", "ERROR"); map.put("message", "通知验签失败"); return gson.toJson(map); } } catch (Exception e) { e.printStackTrace(); } // 3.获取明文数据 String plainText = decryptFromResource(bodyMap, wxPayConfig); HashMap plainTextMap = gson.fromJson(plainText, HashMap.class); // log.info("退款plainTextMap:{}", plainTextMap); // 4.封装微信返回的数据 WxChatCallbackRefundReqParam refundData = getRefundCallbackData(plainTextMap); if ("SUCCESS".equals(refundData.getStatus())) { // 执行业务逻辑 refundCallback.success(refundData); } else { // 特殊情况退款失败业务处理,退款到银行发现用户的卡作废或者冻结了,导致原路退款银行卡失败,可前往商户平台-交易中心,手动处理此笔退款 refundCallback.fail(refundData); } // 5.成功应答 response.setStatus(200); final HashMap resultMap = new HashMap(); resultMap.put("code", "SUCCESS"); resultMap.put("message", "成功"); return gson.toJson(resultMap); } private static WxChatCallbackRefundReqParam getRefundCallbackData(HashMap plainTextMap) { Gson gson = new Gson(); WxChatCallbackRefundReqParam refundData = new WxChatCallbackRefundReqParam(); String successTime = String.valueOf(plainTextMap.get("success_time")); if (StringUtils.isNoneBlank(successTime)) { refundData.setSuccessTime(successTime); } refundData.setOrderId(String.valueOf(plainTextMap.get("out_trade_no"))); refundData.setRefundId(String.valueOf(plainTextMap.get("out_refund_no"))); refundData.setTransactionId(String.valueOf(plainTextMap.get("transaction_id"))); refundData.setTransactionRefundId(String.valueOf(plainTextMap.get("refund_id"))); refundData.setChannel(String.valueOf(plainTextMap.get("channel"))); final String status = String.valueOf(plainTextMap.get("refund_status")); refundData.setStatus(status); String amount = String.valueOf(plainTextMap.get("amount")); HashMap amountMap = gson.fromJson(amount, HashMap.class); String refundMoney = String.valueOf(amountMap.get("refund")); refundData.setRefundMoney(new BigDecimal(refundMoney).movePointLeft(2)); // log.info("refundData:{}", refundData); return refundData; } /** * 对称解密 */ private static String decryptFromResource(HashMap bodyMap, WxPayConfig wxPayConfig) { // 通知数据 Map resourceMap = (Map) bodyMap.get("resource"); // 数据密文 String ciphertext = resourceMap.get("ciphertext"); // 随机串 String nonce = resourceMap.get("nonce"); // 附加数据 String associateData = resourceMap.get("associated_data"); AesUtil aesUtil = new AesUtil(wxPayConfig.getApiKey().getBytes(StandardCharsets.UTF_8)); try { return aesUtil.decryptToString(associateData.getBytes(StandardCharsets.UTF_8), nonce.getBytes(StandardCharsets.UTF_8), ciphertext); } catch (GeneralSecurityException e) { e.printStackTrace(); throw new RuntimeException("解密失败"); } } }
    /**
     * 退款回调
     * @param request r
     * @param response rp
     * @return s
     */
    @Override
    public String refundWechatCallback(HttpServletRequest request, HttpServletResponse response) {
        return WxPayRefundCallbackUtil.wxPayRefundCallback(request, response, verifier, wxPayConfiga, new WechatRefundCallback() {
            /**
             * 退款成功
             */
            @Override
            public void success(WxChatCallbackRefundReqParam refundData) {
                Order upOrder = new Order();
                upOrder.setId(Long.valueOf(refundData.getOrderId()));
                upOrder.setOrderStatus(OrderStatusConstants.CANCELED);
                orderMapper.updateById(upOrder);
            }
            /**
             * 退款失败
             */
            @Override
            public void fail(WxChatCallbackRefundReqParam refundData) {
                Order order = orderMapper.selectById(Long.valueOf(refundData.getOrderId()));
                if (ObjectUtil.isNotEmpty(order.getOperatorName()) && ObjectUtil.isNotEmpty(order.getOperatorId())){
                    Order upOrder = new Order();
                    upOrder.setId(Long.valueOf(refundData.getOrderId()));
                    upOrder.setOrderStatus(OrderStatusConstants.OPERATOR_ACCEPTED);
                    orderMapper.updateById(upOrder);
                } else {
                    Order upOrder = new Order();
                    upOrder.setId(Long.valueOf(refundData.getOrderId()));
                    upOrder.setOrderStatus(OrderStatusConstants.PAID);
                    orderMapper.updateById(upOrder);
                }
            }
        });
    }

也是真乱啊 如果大家需要demo就留个言或者私信我一下我就弄一个,或者在在博文里按照图片的顺序把所有代码全部贴进来。


免责声明
1、本网站属于个人的非赢利性网站,转载的文章遵循原作者的版权声明。
2、本网站转载文章仅为传播更多信息之目的,凡在本网站出现的信息,均仅供参考。本网站将尽力确保所
提供信息的准确性及可靠性,但不保证信息的正确性和完整性,且不对因信息的不正确或遗漏导致的任何
损失或损害承担责任。
3、任何透过本网站网页而链接及得到的资讯、产品及服务,本网站概不负责,亦不负任何法律责任。
4、本网站所刊发、转载的文章,其版权均归原作者所有,如其他媒体、网站或个人从本网下载使用,请在
转载有关文章时务必尊重该文章的著作权,保留本网注明的“稿件来源”,并白负版权等法律责任。

手机扫描二维码访问

文章版权声明:除非注明,否则均为主机测评原创文章,转载或复制请以超链接形式并注明出处。

发表评论

快捷回复: 表情:
评论列表 (暂无评论,2457人围观)

还没有评论,来说两句吧...

目录[+]