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

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

这是整体的一个微信支付+退款的项目结构。
如果大家需要的话,我可以等确认回调没有问题以后可以把我这个项目中完整的jsapi支付及退款做一个demo。或者按照图片的顺序把代码贴进来。
--------------------------------------------------2024.6.18------------------------------------------------------------------
demo已经做好了,详情见主页另一篇文章。
---------------------------------------------------------------------------------------------------------------------------------

1.微信支付-准备工作 

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

2.开干!!!!

1.引入pom.xml

        <!-- 微信支付 -->
        <dependency>
            <groupId>com.github.wechatpay-apiv3</groupId>
            <artifactId>wechatpay-apache-httpclient</artifactId>
            <version>0.4.2</version>
        </dependency>
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.8.9</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.12.0</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.78</version>
        </dependency>

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证书?

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;

/**
 * <p>
 * 配置信息实体
 * </p>
 *
 * @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;

/**
 * <p>
 * 请求地址枚举类
 * </p>
 *
 * @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;

/**
 * <p>
 * 预支付参数
 * </p>
 *
 * @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;

/**
 * <p>
 * 将请求参数封装成Map集合
 * </p>
 *
 * @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<String, Object> getBasePayParams(WxPayConfig wxPayConfig, WxPayReqParam basePayData) {
        Map<String, Object> 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<String, Integer> 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<String, Object> 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<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)) {
                    logger.error("微信支付请求失败,提示信息:{}", bodyAsString);
                    // 4.请求码显示失败,则尝试获取提示信息
                    HashMap<String, String> 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<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("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<String, String> resultMap = resolverResponse(response);
        if (resultMap != null) {
            // native请求返回的是二维码链接,前端将链接转换成二维码即可
            return resultMap.get("prepay_id");
        }
        return null;
    }

}

7.创建实体存储前端微信支付所需参数(WxChatPayDto)
 小程序内需要多个参数才可以唤起微信支付,就是输入密码支付的那个支付页面。

import lombok.Data;

/**
 * <p>
 *  前端微信支付所需参数
 * </p>
 *
 * @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;

/**
 * <p>
 * 微信支付回调工具类
 * </p>
 *
 * @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<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 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<String, String> wxOrderCallBack(HttpServletRequest request, HttpServletResponse response) {
        Map<String, String> map = new HashMap<>(2);
        try {
            Map<String, String> 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<Order> 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;
    }

至此支付就完成了。

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<String, String> map = WxPayRefundUtil.refundPay(param, wxPayClient);
        Order upOrder = new Order();
        upOrder.setRefundNumber(map.get("refundNumber"));
        upOrder.setOrderStatus(OrderStatusConstants.REFUND_PROCESSING);
        UpdateWrapper<Order> updateWrapper = new UpdateWrapper<>();
        updateWrapper.eq("orderNumber",param.getTransactionId());
        orderMapper.update(upOrder,updateWrapper);
        return map.get("refund_id");
    }
 /**
     * 发起微信退款申请
     * @param param 微信支付申请退款请求参数
     * @return 微信支付二维码地址
     */
    public static Map<String,String> refundPay(WxPayRefundReqParam param,CloseableHttpClient wxPayClient) {
        Map<String,String> returnMap = new HashMap<>();

        // 1.获取请求参数的Map格式
        Map<String, Object> 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<String, String> 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;

/**
 * <p>
 * 微信支付退款
 * </p>
 *
 * @author Lch
 * @dateTime 2024/2/27 9:49
 */
public class WxPayRefundUtil {
    /**
     * 封装微信支付申请退款请求参数
     * @param param 微信支付申请退款请求参数
     * @return 封装后的map微信支付申请退款请求参数对象
     */
    private static Map<String, Object> getRefundParams(WxPayRefundReqParam param) {
        String out_refund_no = OrderNumberGenerate.orderNo("100001");
        Map<String, Object> 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<String, Object> 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<String,String> refundPay(WxPayRefundReqParam param,CloseableHttpClient wxPayClient) {
        Map<String,String> returnMap = new HashMap<>();

        // 1.获取请求参数的Map格式
        Map<String, Object> 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<String, String> 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;

/**
 * <p>
 * 微信退款回调参数
 * </p>
 *
 * @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;

/**
 * <p>
 * 退款回调
 * </p>
 *
 * @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;

/**
 * <p>
 * 将通知参数转化为字符串
 * </p>
 *
 * @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;

/**
 * <p>
 * 微信支付 退款回调请求验证
 * </p>
 *
 * @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;

/**
 * <p>
 * 微信支付申请退款回调
 * </p>
 *
 * @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<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.封装微信返回的数据
        WxChatCallbackRefundReqParam 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 WxChatCallbackRefundReqParam getRefundCallbackData(HashMap<String, Object> 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<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, WxPayConfig 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.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就留个言或者私信我一下我就弄一个,或者在在博文里按照图片的顺序把所有代码全部贴进来。

  • 26
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 36
    评论
首先,你需要在微信公众平台申请开通JSAPI支付,并获取到商户号、密钥、证书等信息。 接着,在Spring Boot项目中添加微信支付SDK的依赖,比如: ```xml <dependency> <groupId>com.github.binarywang</groupId> <artifactId>weixin-java-pay</artifactId> <version>3.6.0</version> </dependency> ``` 然后,创建一个配置类来配置微信支付相关的参数,比如: ```java @Configuration public class WxPayConfig { @Value("${wxpay.appid}") private String appId; @Value("${wxpay.mchid}") private String mchId; @Value("${wxpay.key}") private String key; @Value("${wxpay.certPath}") private String certPath; @Bean public WxPayService wxPayService() throws Exception { WxPayConfig payConfig = new WxPayConfig(); payConfig.setAppId(appId); payConfig.setMchId(mchId); payConfig.setMchKey(key); payConfig.setKeyPath(certPath); return new WxPayServiceImpl(payConfig); } } ``` 其中,`appId`、`mchId`、`key`和`certPath`是申请支付时所获取到的信息。然后,通过`WxPayServiceImpl`创建一个`WxPayService`的实例,用于后续的支付操作。 接下来,编写控制器处理支付请求,比如: ```java @RestController @RequestMapping("/wxpay") public class WxPayController { @Autowired private WxPayService wxPayService; @PostMapping("/unifiedorder") public Map<String, String> unifiedOrder(@RequestBody WxPayUnifiedOrderRequest request) throws WxPayException { WxPayUnifiedOrderResult result = wxPayService.unifiedOrder(request); Map<String, String> resultMap = new HashMap<>(); resultMap.put("appId", result.getAppid()); resultMap.put("timeStamp", String.valueOf(System.currentTimeMillis() / 1000)); resultMap.put("nonceStr", result.getNonceStr()); resultMap.put("package", "prepay_id=" + result.getPrepayId()); resultMap.put("signType", "MD5"); resultMap.put("paySign", wxPayService.createSign(resultMap)); return resultMap; } } ``` 其中,`WxPayUnifiedOrderRequest`是支付请求参数,包括订单号、金额、回调地址等信息。`wxPayService.unifiedOrder(request)`方法返回的是支付下单结果,包括预支付ID等信息。最后,将这些信息组装成JSAPI支付所需的数据格式,返回给前端即可。 注意,在进行支付之前,需要先通过微信公众平台获取用户的openid,然后将其作为支付请求参数的一个字段传递给微信支付。另外,JSAPI支付还需要在页面上引入微信JSAPI的SDK,同时配置好微信公众平台的授权域名等信息。
评论 36
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值