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

一、微信支付-准备工作

微信支付开发前,需要先获取商家信息,包括商户号、AppId、证书和密钥。

  1. 获取商户号
    微信商户平台 申请成为商户 => 提交资料 => 签署协议 => 获取商户号
  2. 获取AppID
    微信公众平台 注册服务号 => 服务号认证 => 获取APPID => 绑定商户号
  3. 申请商户证书
    登录商户平台 => 选择 账户中心 => 安全中心 => API安全 => 申请API证书 包括商户证书和商户私钥
  4. 获取微信的证书
  5. 获取APIv3秘钥(在微信支付回调通知和商户获取平台证书使用APIv3密钥)
    登录商户平台 => 选择 账户中心 => 安全中心 => API安全 => 设置APIv3密钥

二、微信支付-基本配置

1.引入pom.xml

<!--  微信支付 -->
<dependency>
    <groupId>com.github.wechatpay-apiv3</groupId>
    <artifactId>wechatpay-apache-httpclient</artifactId>
    <version>0.4.2</version>
</dependency>

2.配置商户信息、证书、密钥等。将客户端对象构建到Bean中,方便后续使用。
可以采用两种方式
①.配置application.yml

weixin:
  appid: wx*************acx # appid
  mch-serial-no: 3FB18E2*******0127B3*****0053E2 # 证书序列号
  private-key-path: D:\wx\pem\apiclient_key.pem # 证书路径
  mch-id: 16*****801 # 商户号
  key: F8CDeHBc***********2t5nvVeh1 # api秘钥
  domain: https://api.mch.weixin.qq.com # 微信服务器地址
  notify-domain: https://xw666.mynatapp.cc # 回调,自己的回调地址

然后创建对应的实体

@Configuration
@PropertySource("classpath:application.yml") //读取配置文件
@ConfigurationProperties(prefix = "weixin") //读取wxpay节点
@Data //使用set方法将wxpay节点中的值填充到当前类的属性中
@ApiModel("微信支付静态常量类")
public class WxPayConfig {
    
    

    @ApiModelProperty("商户号")
    private String mchId;

    @ApiModelProperty("商户API证书序列号")
    private String mchSerialNo;

    @ApiModelProperty("商户私钥文件")
    private String privateKeyPath;

    @ApiModelProperty("APIv3密钥")
    private String key;

    @ApiModelProperty("APPID")
    private String appid;

    @ApiModelProperty("微信服务器地址")
    private String domain;

    @ApiModelProperty("接收结果通知地址")
    private String notifyDomain;
}

②使用数据库存储,在项目启动时加载到redis中,然后从redis中获取

@Configuration
@Data
@ApiModel("微信支付静态常量类")
public class WxPayConstants {
    
    

    @Resource
    private RedisCache redisCache;

    @ApiModelProperty("APPID")
    public String appid;

    @ApiModelProperty("商户API证书序列号")
    public String mchSerialNo;

    @ApiModelProperty("商户私钥文件")
    public String privateKeyPath;

    @ApiModelProperty("商户号")
    public String mchId;

    @ApiModelProperty("APIv3密钥")
    public String key;

    @ApiModelProperty("微信服务器地址")
    public String domain;

    @ApiModelProperty("接收结果通知地址")
    public String notifyDomain;

    @Resource
    public void getParam(RedisCache redisCache){
    
    
        appid = redisCache.getCacheObject("WX_PAY_SAVE_WX_APPID");
        mchSerialNo = redisCache.getCacheObject("WX_PAY_SAVE_MCH_SERIAL_NO");
        privateKeyPath = redisCache.getCacheObject("WX_PAY_SAVE_PRIVATE_KEY_PATH");
        mchId = redisCache.getCacheObject("WX_PAY_SAVE_MCH_ID");
        key = redisCache.getCacheObject("WX_PAY_SAVE_KEY");
        domain = redisCache.getCacheObject("WX_PAY_SAVE_DOMAIN");
        notifyDomain = redisCache.getCacheObject("WX_PAY_SAVE_NOTIFY_DOMAIN");
    }
}

这两个实体有几个共同的方法,无论使用哪一个,放到下面即可

    /**
     * 获取商户的私钥文件
     *
     * @param filename 证书地址
     * @return 私钥文件
     */
    public PrivateKey getPrivateKey(String filename) {
    
    
        try {
    
    
            return PemUtil.loadPrivateKey(new FileInputStream(filename));
        } catch (FileNotFoundException e) {
    
    
            throw new ServiceException("私钥文件不存在");
        }
    }


    /**
     * 获取签名验证器
     */
    @Bean
    public Verifier getVerifier() {
    
    
        // 获取商户私钥
        final PrivateKey privateKey = getPrivateKey(privateKeyPath);

        // 私钥签名对象
        PrivateKeySigner privateKeySigner = new PrivateKeySigner(mchSerialNo, privateKey);

        // 身份认证对象
        WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, privateKeySigner);

        // 获取证书管理器实例
        CertificatesManager certificatesManager = CertificatesManager.getInstance();

        try {
    
    
            // 向证书管理器增加需要自动更新平台证书的商户信息
            certificatesManager.putMerchant(mchId, wechatPay2Credentials, key.getBytes(StandardCharsets.UTF_8));
        } catch (IOException | GeneralSecurityException | HttpCodeException e) {
    
    
            e.printStackTrace();
        }

        try {
    
    
            return certificatesManager.getVerifier(mchId);
        } catch (NotFoundException e) {
    
    
            e.printStackTrace();
            throw new ServiceException("获取签名验证器失败");
        }
    }

    /**
     * 获取微信支付的远程请求对象
     * @return Http请求对象
     */
    @Bean
    public CloseableHttpClient getWxPayClient() {
    
    

        // 获取签名验证器
        Verifier verifier = getVerifier();

        // 获取商户私钥
        final PrivateKey privateKey = getPrivateKey(privateKeyPath);

        WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create().withMerchant(mchId, mchSerialNo, privateKey)
                .withValidator(new WechatPay2Validator(verifier));

        return builder.build();
    }

3.请求地址枚举类(WxApiConstants)

为了防止微信支付的请求地址前缀发生变化,因此请求前缀存储在application.yml中,请求时进行拼接即可。

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Getter;

//为了防止微信支付的请求地址前缀发生变化,因此请求前缀存储在mysql,redis中,请求时进行拼接即可。
@AllArgsConstructor
@Getter
@ApiModel("请求地址")
public enum WxApiConstants {
    
    
    
    @ApiModelProperty("Native下单")
    NATIVE_PAY("/v3/pay/transactions/native"),

    @ApiModelProperty("jsapi下单")
    JSAPI_PAY("/v3/pay/transactions/jsapi"),

    @ApiModelProperty("jsapi下单")
    H5_PAY("/v3/pay/transactions/h5"),

    @ApiModelProperty("APP下单")
    APP_PAY("/v3/pay/transactions/app"),

    @ApiModelProperty("查询订单")
    ORDER_QUERY_BY_NO("/v3/pay/transactions/out-trade-no/%s"),

    @ApiModelProperty("关闭订单")
    CLOSE_ORDER_BY_NO("/v3/pay/transactions/out-trade-no/%s/close"),

    @ApiModelProperty("申请退款")
    DOMESTIC_REFUNDS("/v3/refund/domestic/refunds"),

    @ApiModelProperty("查询单笔退款")
    DOMESTIC_REFUNDS_QUERY("/v3/refund/domestic/refunds/%s"),

    @ApiModelProperty("申请交易账单")
    TRADE_BILLS("/v3/bill/tradebill"),

    @ApiModelProperty("申请资金账单")
    FUND_FLOW_BILLS("/v3/bill/fundflowbill");

    @ApiModelProperty("类型")
    private final String type;
}

4.回调地址枚举类(WxChatBasePayDto)

发生请求后微信官方会回调我们传递的地址,这里通过枚举统一管理我们的回调地址,回调地址由application.yml中的
weixin.notify-domain拼接组成。

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
@ApiModel("微信回调地址,根据自己的项目来,不要直接照搬")
public enum WxNotifyConstants {
    
    
    
    @ApiModelProperty("订单支付通知")
    RUN_ERRANDS_NOTIFY("/wx/order/wxOrderCallBack"),

    @ApiModelProperty("卡支付成功通知")
    CAMPUS_CARD_NOTIFY("/wx/campusCardOrder/wxCampusCardOrderCallBack"),

    @ApiModelProperty("卡退款成功通知")
    CAMPUS_CARD_REFUND_NOTIFY("/wx/campusCardOrder/refundWechatCallback");

    @ApiModelProperty("类型")
    private final String type;
}

5.微信支付基础请求数据对象(WxChatBasePayDto)

import com.xxx.project.wx.constants.WxNotifyConstants;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

import java.math.BigDecimal;

@Data
@ApiModel("微信支付基础请求数据对象")
public class WxChatBasePayDto {
    
    

    @ApiModelProperty("商品描述")
    private String title;

    @ApiModelProperty("商家订单号,对应 out_trade_no")
    private String orderId;

    @ApiModelProperty("订单金额")
    private BigDecimal price;

    @ApiModelProperty("回调地址")
    private WxNotifyConstants notify;

    @ApiModelProperty("支付用户的openid")
    private String openId;
}

6.将请求参数封装成Map集合(WxPayCommon)

封装完枚举类后,首先就是请求参数的封装,支付类请求参数都非常相近,我们将都需要的参数提取出来以map的方式进行返回。这里的参数,指每个支付类请求都用到的参数,个别支付需要额外添加数据

import com.google.gson.Gson;
import com.xxx.common.exception.ServiceException;
import com.xxx.project.wx.constants.WxPayConstants;
import com.xxx.project.wx.constants.WxApiConstants;
import com.xxx.project.wx.dto.WxChatBasePayDto;
import lombok.extern.slf4j.Slf4j;
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 java.io.IOException;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;
@Slf4j
public class WxPayCommon {
    
    

    /**
     * 封装基础通用请求数据
     * @param wxPayConfig 微信的配置文件
     * @param basePayData 微信支付基础请求数据
     * @return 封装后的map对象
     */
    public static Map<String, Object> getBasePayParams(WxPayConstants wxPayConfig, WxChatBasePayDto basePayData) {
    
    
        Map<String, Object> paramsMap = new HashMap<>();
        paramsMap.put("appid", wxPayConfig.getAppid());
        paramsMap.put("mchid", wxPayConfig.getMchId());
        // 如果商品名称过长则截取
        String title = basePayData.getTitle().length() > 62 ? basePayData.getTitle().substring(0, 62) : basePayData.getTitle();
        paramsMap.put("description",title);
        paramsMap.put("out_trade_no", basePayData.getOrderId());
        paramsMap.put("notify_url", wxPayConfig.getNotifyDomain().concat(basePayData.getNotify().getType()));
        Map<String, Integer> amountMap = new HashMap<>();
        amountMap.put("total", basePayData.getPrice().multiply(new BigDecimal("100")).intValue());
        paramsMap.put("amount", amountMap);
        return paramsMap;
    }

	/**
     * 获取请求对象(Post请求)
     * @param wxPayConfig 微信配置类
     * @param apiType 接口请求地址
     * @param paramsMap 请求参数
     * @return Post请求对象
     */
    public static HttpPost getHttpPost(WxPayConstants wxPayConfig, WxApiConstants apiType, Map<String, Object> paramsMap) {
    
    
        // 1.设置请求地址
        HttpPost httpPost = new HttpPost(wxPayConfig.getDomain().concat(apiType.getType()));

        // 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<String, String> 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<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class);
                return resultMap;
            } else {
    
    
                if (StringUtils.isNoneBlank(bodyAsString)) {
    
    
                    log.error("微信支付请求失败,提示信息:{}", bodyAsString);
                    // 4.请求码显示失败,则尝试获取提示信息
                    HashMap<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class);
                    throw new ServiceException(resultMap.get("message"));
                }
                log.error("微信支付请求失败,未查询到原因,提示信息:{}", response);
                // 其他异常,微信也没有返回数据,这就需要具体排查了
                throw new IOException("request failed");
            }
        } catch (Exception e) {
    
    
            e.printStackTrace();
            throw new ServiceException(e.getMessage());
        } finally {
    
    
            try {
    
    
                response.close();
            } catch (IOException e) {
    
    
                e.printStackTrace();
            }
        }
    }
}

7.创建微信支付订单的三种方式(Native,Jsapi,App)

商户Native支付下单接口,微信后台系统返回链接参数code_url,商户后台系统将code_url值生成二维码图片,用户使用微信客户端扫码后发起支付,也就是说后端只需要返回code_url即可。

官方JSAPI支付开发指引
继续在上方WxPayCommon中加入

 /**
 * 创建微信支付订单-Native方式
 *
 * @param wxPayConfig 微信配置信息
 * @param basePayData 基础请求信息,商品标题、商家订单id、订单价格
 * @param wxPayClient 微信请求客户端()
 * @return 微信支付二维码地址
 */
public static String wxNativePay(WxPayConstants wxPayConfig, WxChatBasePayDto basePayData, CloseableHttpClient wxPayClient) {
    
    
    // 1.获取请求参数的Map格式
    Map<String, Object> paramsMap = getBasePayParams(wxPayConfig, basePayData);

    // 2.获取请求对象
    HttpPost httpPost = getHttpPost(wxPayConfig, WxApiConstants.NATIVE_PAY, paramsMap);

    // 3.完成签名并执行请求
    CloseableHttpResponse response = null;
    try {
    
    
        response = wxPayClient.execute(httpPost);
    } catch (IOException e) {
    
    
        e.printStackTrace();
        throw new ServiceException("微信支付请求失败");
    }

    // 4.解析response对象
    HashMap<String, String> resultMap = resolverResponse(response);
    if (resultMap != null) {
    
    
        // native请求返回的是二维码链接,前端将链接转换成二维码即可
        return resultMap.get("code_url");
    }
    return null;
}

/**
 * 创建微信支付订单-jsapi方式
 * @param wxPayConfig 微信配置信息
 * @param basePayData 基础请求信息,商品标题、商家订单id、订单价格
 * @param openId 通过微信小程序或者公众号获取到用户的openId
 * @param wxPayClient 微信请求客户端()
 * @return 微信支付二维码地址
 */
public static String wxJsApiPay(WxPayConstants wxPayConfig, WxChatBasePayDto basePayData, String openId, CloseableHttpClient wxPayClient) {
    
    
    // 1.获取请求参数的Map格式
    Map<String, Object> paramsMap = getBasePayParams(wxPayConfig, basePayData);
    // 1.1 添加支付者信息
    Map<String,String> payerMap = new HashMap();
    payerMap.put("openid",openId);
    paramsMap.put("payer",payerMap);

    // 2.获取请求对象
    HttpPost httpPost = getHttpPost(wxPayConfig, WxApiConstants.JSAPI_PAY, paramsMap);

    // 3.完成签名并执行请求
    CloseableHttpResponse response = null;
    try {
    
    
        response = wxPayClient.execute(httpPost);
    } catch (IOException e) {
    
    
        e.printStackTrace();
        throw new ServiceException("微信支付请求失败");
    }

    // 4.解析response对象
    HashMap<String, String> resultMap = resolverResponse(response);
    if (resultMap != null) {
    
    
        // native请求返回的是二维码链接,前端将链接转换成二维码即可
        return resultMap.get("prepay_id");
    }
    return null;
}

/**
 * 创建微信支付订单-APP方式
 *
 * @param wxPayConfig 微信配置信息
 * @param basePayData 基础请求信息,商品标题、商家订单id、订单价格
 * @param wxPayClient 微信请求客户端()
 * @return 微信支付二维码地址
 */
public static String wxAppPay(WxPayConstants wxPayConfig, WxChatBasePayDto basePayData, CloseableHttpClient wxPayClient) {
    
    
    // 1.获取请求参数的Map格式
    Map<String, Object> paramsMap = getBasePayParams(wxPayConfig, basePayData);

    // 2.获取请求对象
    HttpPost httpPost = getHttpPost(wxPayConfig, WxApiConstants.APP_PAY, paramsMap);

    // 3.完成签名并执行请求
    CloseableHttpResponse response = null;
    try {
    
    
        response = wxPayClient.execute(httpPost);
    } catch (IOException e) {
    
    
        e.printStackTrace();
        throw new ServiceException("微信支付请求失败");
    }

    // 4.解析response对象
    HashMap<String, String> resultMap = resolverResponse(response);
    if (resultMap != null) {
    
    
        // native请求返回的是二维码链接,前端将链接转换成二维码即可
        return resultMap.get("prepay_id");
    }
    return null;
}

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

因为前端拉起微信支付需要多个参数,直接用一个实体返回更便捷

@Data
@ApiModel("前端微信支付所需参数")
public class WxChatPayDto {
    
    

    @ApiModelProperty("需要支付的小程序id")
    private String appid;

    @ApiModelProperty("时间戳(当前的时间)")
    private String timeStamp;

    @ApiModelProperty("随机字符串,不长于32位。")
    private String nonceStr;

    @ApiModelProperty("小程序下单接口返回的prepay_id参数值,提交格式如:prepay_id=***")
    private String prepayId;

    @ApiModelProperty("签名类型,默认为RSA,仅支持RSA。")
    private String signType;

    @ApiModelProperty("签名,使用字段appId、timeStamp、nonceStr、package计算得出的签名值")
    private String paySign;

三、微信支付-调起微信支付

设置一个公共方法pay,每次支付可以直接调用

	@Resource
    private WxPayConstants wxPayConfig;
    @Resource
    private CloseableHttpClient wxPayClient;
    
	/**
     * 微信用户调用微信支付
     */
    @Override
    public WxChatPayDto pay(WxChatBasePayDto payData) {
    
    

        String prepayId = WxPayCommon.wxJsApiPay(wxPayConfig, payData, payData.getOpenId(), wxPayClient);

        WxChatPayDto wxChatPayDto = new WxChatPayDto();
        wxChatPayDto.setAppid(redisCache.getCacheObject("WX_PAY_SAVE_WX_APPID"));
        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;
    }
    /**
     * 获取签名
     * @param nonceStr  随机数
     * @param appId     微信公众号或者小程序等的appid
     * @param prepay_id 预支付交易会话ID
     * @param timestamp 时间戳 10位
     * @return String 新签名
     */
    String getSign(String nonceStr, String appId, String prepay_id, long timestamp) {
    
    

        //从下往上依次生成
        String message = buildMessage(appId, timestamp, nonceStr, prepay_id);
        //签名
        try {
    
    
            return sign(message.getBytes("utf-8"));
        } catch (IOException e) {
    
    
            throw new RuntimeException("签名异常,请检查参数或商户私钥");
        }
    }

    String sign(byte[] message) {
    
    
        try {
    
    
            //签名方式
            Signature sign = Signature.getInstance("SHA256withRSA");
            //私钥,通过MyPrivateKey来获取,这是个静态类可以接调用方法 ,需要的是_key.pem文件的绝对路径配上文件名
            sign.initSign(PemUtil.loadPrivateKey(new FileInputStream(redisCache.getCacheObject("WX_PAY_SAVE_PRIVATE_KEY_PATH").toString())));
            sign.update(message);
            return Base64.getEncoder().encodeToString(sign.sign());
        } catch (Exception e) {
    
    
            throw new RuntimeException("签名异常,请检查参数或商户私钥");
        }
    }

    /**
     * 按照前端签名文档规范进行排序,\n是换行
     *
     * @param nonceStr  随机数
     * @param appId     微信公众号或者小程序等的appid
     * @param prepay_id 预支付交易会话ID
     * @param timestamp 时间戳 10位
     * @return String 新签名
     */
    String buildMessage(String appId, long timestamp, String nonceStr, String prepay_id) {
    
    
        return appId + "\n"
                + timestamp + "\n"
                + nonceStr + "\n"
                + prepay_id + "\n";
    }

调用pay,获取前端拉起支付所需要的参数

//支付
WxChatBasePayDto payData = new WxChatBasePayDto();
payData.setTitle("订单支付");
payData.setOrderId(runOrder.getOrderNumber());
payData.setPrice(new BigDecimal(0.01));
payData.setNotify(WxNotifyConstants.RUN_ERRANDS_NOTIFY);
payData.setOpenId(runOrder.getOpenId());
WxChatPayDto pay = pay(payData);

给前端直接返回pay这个参数,前端通过里面的参数拉起支付

JSAPI调起支付API

四、微信支付-成功回调

同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。 推荐的做法是,当商户系统收到通知进行处理时,先检查对应业务数据的状态,并判断该通知是否已经处理。如果未处理,则再进行处理;如果已处理,则直接返回结果成功。在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱。
如果在所有通知频率后没有收到微信侧回调,商户应调用查询订单接口确认订单状态。

特别提醒:商户系统对于开启结果通知的内容一定要做签名验证,并校验通知的信息是否与商户侧的信息一致,防止数据泄露导致出现“假通知”,造成资金损失。

该链接是通过基础下单接口中的请求参数“notify_url”来设置的,要求必须为https地址。请确保回调URL是外部可正常访问的,且不能携带后缀参数,否则可能导致商户无法接收到微信的回调通知信息。

import com.alibaba.fastjson.JSONObject;
import com.xxx.framework.redis.RedisCache;
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;

/**
 * 微信支付回调工具类
 */
@Component
public class WxPayCallbackUtil {
    
    

    @Resource
    private RedisCache redisCache;
    @Resource
    private Verifier verifier;

    /**
     * 获取回调数据
     * @param request
     * @param response
     * @return
     */
    public Map<String, String> 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<String, String> 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<String,String> convertWechatPayMsgToMap(String plainBody){
    
    

        Map<String,String> 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
     * @return
     */
    public String decryptBody(String body) throws UnsupportedEncodingException, GeneralSecurityException {
    
    
        AesUtil aesUtil = new AesUtil(redisCache.getCacheObject("WX_PAY_SAVE_KEY").toString().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();

    }
}

支付成功后的回调方法

	 /**
     * 订单支付后回调
     */
    @Override
    public Map<String, String> wxOrderCallBack(HttpServletRequest request, HttpServletResponse response) {
    
    
        Map<String, String> map = new HashMap<>(2);
        try {
    
    
            Map<String, String> stringMap = wxPayCallbackUtil.wxChatPayCallback(request, response);
            //支付成功
            if (stringMap.get("trade_state").equals("SUCCESS")){
    
    
                //编写支付成功后逻辑
            }
            //响应微信
            map.put("code", "SUCCESS");
            map.put("message", "成功");
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
        return map;
    }

五、微信支付-申请退款

  1. 申请退款请求对象

微信支付订单号,微信支付订单号和商家订单号二选一,这个是必不可少的,原订单金额也是必填的,微信会做二次验证。

@Data
@ApiModel("微信退款对象")
public class WxChatRefundDto {
    
    

    @ApiModelProperty("微信支付订单号,微信支付订单号和商家订单号二选一")
    private String transactionId;

    @ApiModelProperty("商家订单号,对应 out_trade_no")
    private String orderId;

    @ApiModelProperty("商户退款单号,对应out_refund_no")
    private String refundOrderId;

    @ApiModelProperty("退款原因,选填")
    private String reason;

    @ApiModelProperty("回调地址")
    private WxNotifyConstants notify;

    @ApiModelProperty("退款金额")
    private BigDecimal refundMoney;

    @ApiModelProperty("原订单金额,必填")
    private BigDecimal totalMoney;
  1. 将请求参数封装成Map集合
import com.xxx.common.exception.ServiceException;
import com.xxx.project.wx.constants.WxApiConstants;
import com.xxx.project.wx.constants.WxPayConstants;
import com.xxx.project.wx.dto.WxChatRefundDto;
import lombok.extern.slf4j.Slf4j;
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;

@Slf4j
public class WxPayRefundUtil {
    
    

    /**
     * 封装微信支付申请退款请求参数
     * @param dto 微信支付申请退款请求参数
     * @return 封装后的map微信支付申请退款请求参数对象
     */
    private static Map<String, Object> getRefundParams(WxPayConstants wxPayConstants, WxChatRefundDto dto) {
    
    
        Map<String, Object> paramsMap = new HashMap<>();
        if (StringUtils.isNoneBlank(dto.getTransactionId())) {
    
    
            paramsMap.put("transaction_id", dto.getTransactionId());
        } else if (StringUtils.isNoneBlank(dto.getOrderId())) {
    
    
            paramsMap.put("out_trade_no", dto.getOrderId());
        } else {
    
    
            throw new ServiceException("微信支付订单号和商户订单号必须填写一个");
        }
        paramsMap.put("out_refund_no", dto.getRefundOrderId());
        if (StringUtils.isNoneBlank(dto.getReason())) {
    
    
            paramsMap.put("reason", dto.getReason());
        }
        paramsMap.put("notify_url", wxPayConstants.getNotifyDomain().concat(dto.getNotify().getType()));
        Map<String, Object> amountMap = new HashMap<>();
        amountMap.put("refund", dto.getRefundMoney());
        amountMap.put("total", dto.getTotalMoney());
        amountMap.put("currency", "CNY");
        paramsMap.put("amount", amountMap);

        return paramsMap;
    }

    /**
     * 发起微信退款申请
     *
     * @param wxPayConfig 微信配置信息
     * @param param       微信支付申请退款请求参数
     * @param wxPayClient 微信请求客户端()
     * @return 微信支付二维码地址
     */
    public static String refundPay(WxPayConstants wxPayConfig, WxChatRefundDto param, CloseableHttpClient wxPayClient) {
    
    
        // 1.获取请求参数的Map格式
        Map<String, Object> paramsMap = getRefundParams(wxPayConfig, param);

        // 2.获取请求对象
        HttpPost httpPost = WxPayCommon.getHttpPost(wxPayConfig, WxApiConstants.DOMESTIC_REFUNDS, paramsMap);

        // 3.完成签名并执行请求
        CloseableHttpResponse response = null;
        try {
    
    
            response = wxPayClient.execute(httpPost);
        } catch (IOException e) {
    
    
            e.printStackTrace();
            throw new ServiceException("微信支付请求失败");
        }

        // 4.解析response对象
        HashMap<String, String> resultMap = WxPayCommon.resolverResponse(response);
        log.info("发起退款参数:{}",resultMap);
        if (resultMap != null) {
    
    
            // 返回微信支付退款单号
            return resultMap.get("refund_id");
        }
        return null;
    }
}
  1. 申请退款使用
@Resource
private WxPayConstants wxPayConstants;
@Resource
private CloseableHttpClient closeableHttpClient;


WxChatRefundDto dto = new WxChatRefundDto();
dto.setOrderId(); //订单号
String refundOrderId = IdWorker.getIdStr();
dto.setRefundOrderId(refundOrderId);
dto.setNotify(WxNotifyConstants.CAMPUS_CARD_REFUND_NOTIFY); //回调地址
dto.setRefundMoney(new BigDecimal(1));//单位为分
dto.setTotalMoney(new BigDecimal(1));//单位为分
String s = WxPayRefundUtil.refundPay(wxPayConstants, dto, closeableHttpClient);

注意:

1、交易时间超过一年的订单无法提交退款
2、微信支付退款支持单笔交易分多次退款(不超50次),多次退款需要提交原支付订单的商户订单号和设置不同的退款单号。申请退款总>金额不能超过订单金额。一笔退款失败后重新提交,请不要更换退款单号,请使用原商户退款单号
3、错误或无效请求频率限制:6qps,即每秒钟异常或错误的退款申请请求不超过6次
4、每个支付订单的部分退款次数不能超过50次
5、如果同一个用户有多笔退款,建议分不同批次进行退款,避免并发退款导致退款失败
6、申请退款接口的返回仅代表业务的受理情况,具体退款是否成功,需要通过退款查询接口获取结果
7、一个月之前的订单申请退款频率限制为:5000/min
8、同一笔订单多次退款的请求需相隔1分钟

六、微信支付-退款成功回调

  1. 退款返回数据对象
import cn.hutool.core.date.DateUtil;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

import java.math.BigDecimal;
import java.util.Date;

@Data
@ApiModel("微信退款回调参数")
public class WxChatCallbackRefundDto {
    
    

    @ApiModelProperty("商户订单号")
    private String orderId;

    @ApiModelProperty("商户退款单号,out_refund_no")
    private String refundId;

    @ApiModelProperty("微信支付系统生成的订单号")
    private String transactionId;

    @ApiModelProperty("微信支付系统生成的退款订单号")
    private String transactionRefundId;

    @ApiModelProperty("退款渠道 1.ORIGINAL:原路退款 2.BALANCE:退回到余额 " +
            "3.OTHER_BALANCE:原账户异常退到其他余额账户 4.OTHER_BANKCARD:原银行卡异常退到其他银行卡")
    private String 	channel;

    @ApiModelProperty("退款成功时间 当前退款成功时才有此返回值")
    private Date successTime;

    @ApiModelProperty("退款状态  退款到银行发现用户的卡作废或者冻结了,导致原路退款银行卡失败,可前往商户平台-交易中心,手动处理此笔退款。" +
            "1.SUCCESS:退款成功 2.CLOSED:退款关闭 3.PROCESSING:退款处理中 4.ABNORMAL:退款异常")
    private String 	status;

    @ApiModelProperty("退款金额")
    private BigDecimal refundMoney;

    public Date getSuccessTime() {
    
    
        return successTime;
    }

    public void setSuccessTime(String successTime) {
    
    
        // Hutool工具包的方法,自动识别一些常用格式的日期字符串
        this.successTime = DateUtil.parse(successTime);
    }

}
  1. 退款业务处理接口
import com.xxx.project.wx.dto.WxChatCallbackRefundDto;

/**
 * 退款处理接口,为了防止项目开发人员,不手动判断退款失败的情况
 * 退款失败:退款到银行发现用户的卡作废或者冻结了,导致原路退款银行卡失败,可前往商户平台-交易中心,手动处理此笔退款
 */
public interface WechatRefundCallback {
    
    

    /**
     * 退款成功处理情况
     */
    void success(WxChatCallbackRefundDto refundData);

    /**
     * 退款失败处理情况
     */
    void fail(WxChatCallbackRefundDto refundData);
}
  1. 微信退款回调方法
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();
                }
            }
        }
    }
}
package com.runerrands.project.wx.util;

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.*;

public class WechatPayValidatorForRequest {
    
    

    protected static final Logger log = 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) {
    
    
            log.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.google.gson.Gson;
import com.xxx.common.exception.ServiceException;
import com.xxx.common.utils.http.HttpUtils;
import com.xxx.project.wx.constants.WxPayConstants;
import com.xxx.project.wx.dto.WxChatCallbackRefundDto;
import com.xxx.project.wx.service.WechatRefundCallback;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import com.wechat.pay.contrib.apache.httpclient.util.AesUtil;
import lombok.extern.slf4j.Slf4j;
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;

@Slf4j
public class WxPayRefundCallbackUtil {
    
    

    /**
     * 微信支付申请退款回调方法
     *
     * @param verifier       证书
     * @param wxPayConfig    微信配置
     * @param refundCallback 回调方法,用于处理业务逻辑,包含退款成功处理于退款失败处理
     * @return json格式的string数据,直接返回给微信
     */
    public static String wxPayRefundCallback(HttpServletRequest request, HttpServletResponse response, Verifier verifier, WxPayConstants wxPayConfig, WechatRefundCallback refundCallback) {
    
    
        Gson gson = new Gson();

        // 1.处理通知参数
        final String body = HttpUtils.readData(request);
        HashMap<String, Object> 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<String, Object> 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<String, Object> plainTextMap = gson.fromJson(plainText, HashMap.class);
//        log.info("退款plainTextMap:{}", plainTextMap);
        // 4.封装微信返回的数据
        WxChatCallbackRefundDto refundData = getRefundCallbackData(plainTextMap);

        if ("SUCCESS".equals(refundData.getStatus())) {
    
    
            // 执行业务逻辑
            refundCallback.success(refundData);
        } else {
    
    
            // 特殊情况退款失败业务处理,退款到银行发现用户的卡作废或者冻结了,导致原路退款银行卡失败,可前往商户平台-交易中心,手动处理此笔退款
            refundCallback.fail(refundData);
        }

        // 5.成功应答
        response.setStatus(200);
        final HashMap<String, Object> resultMap = new HashMap<>();
        resultMap.put("code", "SUCCESS");
        resultMap.put("message", "成功");
        return gson.toJson(resultMap);
    }

    private static WxChatCallbackRefundDto getRefundCallbackData(HashMap<String, Object> plainTextMap) {
    
    
        Gson gson = new Gson();
        WxChatCallbackRefundDto refundData = new WxChatCallbackRefundDto();
        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<String, Object> 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<String, Object> bodyMap, WxPayConstants wxPayConfig) {
    
    
        // 通知数据
        Map<String, String> 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.getKey().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 ServiceException("解密失败");
        }
    }

}

  1. 回调方法
@Resource
private WxPayConstants wxPayConfig;
@Resource
private Verifier verifier;
@ApiOperation("微信退款回调接口")
    @PostMapping("/refundWechatCallback")
    public String refundWechatCallback(HttpServletRequest request, HttpServletResponse response) {
    
    
        return WxPayRefundCallbackUtil.wxPayRefundCallback(request, response, verifier, wxPayConfig, new WechatRefundCallback() {
    
    
            @Override
            public void success(WxChatCallbackRefundDto refundData) {
    
    
                // TODO 退款成功的业务逻辑,例如更改订单状态为退款成功等
               System.out.println("退款成功");
            }

            @Override
            public void fail(WxChatCallbackRefundDto refundData) {
    
    
                // TODO 特殊情况下退款失败业务处理,例如银行卡冻结需要人工退款,此时可以邮件或短信提醒管理员,并携带退款单号等关键信息
                System.out.println("退款失败");
            }
        });
    }

猜你喜欢

转载自blog.csdn.net/whatevery/article/details/132297867