小工具知识网

当前位置: 首页 > 行业知识 > SpringBoot整合微信小程序支付V3(支付、退款)

行业知识

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

2026-11-15 07:53:48 来源:互联网转载

文章目录

    • 一、微信支付-准备工作
    • 二、微信支付-基本配置
    • 三、微信支付-调起微信支付
    • 四、微信支付-成功回调
    • 五、微信支付-申请退款
    • 六、微信支付-退款成功回调
    • 七、demo

      一、微信支付-准备工作

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

      ()
      1. 获取商户号

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

      2. 获取AppID

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

      3. 申请商户证书 ()

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

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

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

      二、微信支付-基本配置

      1.引入pom.xml

          com.github.wechatpay-apiv3    wechatpay-apache-httpclient    0.4.2

      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;@Slf4jpublic class WxPayCommon {    /**     * 封装基础通用请求数据     * @param wxPayConfig 微信的配置文件     * @param basePayData 微信支付基础请求数据     * @return 封装后的map对象     */    public static Map getBasePayParams(WxPayConstants wxPayConfig, WxChatBasePayDto basePayData) {        Map 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 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 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 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)) {                    log.error("微信支付请求失败,提示信息:{}", bodyAsString);                    // 4.请求码显示失败,则尝试获取提示信息                    HashMap 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 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 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 paramsMap = getBasePayParams(wxPayConfig, basePayData);    // 1.1 添加支付者信息    Map 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 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 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 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_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;/** * 微信支付回调工具类 */@Componentpublic class WxPayCallbackUtil {    @Resource    private RedisCache redisCache;    @Resource    private Verifier verifier;    /**     * 获取回调数据     * @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     * @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 wxOrderCallBack(HttpServletRequest request, HttpServletResponse response) {        Map map = new HashMap(2);        try {            Map 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;@Slf4jpublic class WxPayRefundUtil {    /**     * 封装微信支付申请退款请求参数     * @param dto 微信支付申请退款请求参数     * @return 封装后的map微信支付申请退款请求参数对象     */    private static Map getRefundParams(WxPayConstants wxPayConstants, WxChatRefundDto dto) {        Map 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 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 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 resultMap = WxPayCommon.resolverResponse(response);        log.info("发起退款参数:{}",resultMap);        if (resultMap != null) {            // 返回微信支付退款单号            return resultMap.get("refund_id");        }        return null;    }}
      1. 申请退款使用
      @Resourceprivate WxPayConstants wxPayConstants;@Resourceprivate 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;@Slf4jpublic 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 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.封装微信返回的数据        WxChatCallbackRefundDto 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 WxChatCallbackRefundDto getRefundCallbackData(HashMap 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 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, WxPayConstants 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.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. 回调方法
      @Resourceprivate WxPayConstants wxPayConfig;@Resourceprivate 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("退款失败");            }        });    }

      七、demo

      wx_pay_demo

上一篇:烽火服务器怎么进后台?

下一篇:将Echarts图表导出为图片的三种方式