SrpingBoot对接微信JsApi支付(微信公众号支付 V3版本)

一 前期准备

1 公众号平台设置

  • 地址:https://mp.weixin.qq.com 设置与开发->基本配置 获取开发者ID 密钥和设置白名单(获取本地公网出口IP命令: curl ip.sb)
    在这里插入图片描述

2 微信支付平台设置

  • 详细信息见:https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_1.shtml

二 开发过程

1 引入依赖

        <dependency>
            <groupId>com.github.wechatpay-apiv3</groupId>
            <artifactId>wechatpay-apache-httpclient</artifactId>
            <version>0.4.7</version>
        </dependency>

2 将密钥文件放到项目目录中

在这里插入图片描述

3 注入微信支付config

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 lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpHost;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;

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;
import java.util.Properties;


@Configuration
@Data //使用set方法将wxpay节点中的值填充到当前类的属性中
@Slf4j
public class WxPayConfig {

    // 商户号
    @Value("${wechatPay.mch-id}")
    private String mchId;

    // 商户API证书序列号
    @Value("${wechatPay.mch-serial-no}")
    private String mchSerialNo;

    // 商户私钥文件
    @Value("${wechatPay.private-key-path}")
    private String privateKeyPath;

    // APIv3密钥
    @Value("${wechatPay.api-v3-key}")
    private String apiV3Key;

    // APPID
    @Value("${wechatPay.appid}")
    private String appid;

    // 微信服务器地址
    @Value("${wechatPay.domain}")
    private String domain;

    // 接收结果通知地址
    @Value("${wechatPay.notify-domain}")
    private String notifyDomain;

    // 前端 url
    @Value("${wechatPay.front-domain}")
    private String frontDomain;

    @Autowired
    private ApplicationValues appValues;

    /**
     * 获取商户的私钥文件
     *
     * @param filename
     * @return
     */
    private PrivateKey getPrivateKey(String filename) {
        log.info("filename ==============> {}",filename);
        log.info("this.getClass() ==============> {}",this.getClass().getClassLoader().getResourceAsStream(filename));
        return PemUtil.loadPrivateKey(this.getClass().getClassLoader().getResourceAsStream(filename));
    }

    /**
     * 获取 http 请求对象
     *
     * @param verifier
     * @return
     */
    @Bean
    public CloseableHttpClient getWxPayClient(Verifier verifier) {

        RequestConfig requestConfig;
        requestConfig = RequestConfig.custom()
                //服务器返回数据(response)的时间,超过抛出read timeout
                .setSocketTimeout(appValues.getSocketTimeout())
                //连接上服务器(握手成功)的时间,超出抛出connect timeout
                .setConnectTimeout(appValues.getConnTimeOut())
                //从连接池中获取连接的超时时间,超时间未拿到可用连接,会抛出org.apache.http.conn.ConnectionPoolTimeoutException: Timeout waiting for connection from pool
                .setConnectionRequestTimeout(appValues.getConnReqTimeOut())
                .build();
        // 获取商户私钥
        PrivateKey privateKey = getPrivateKey(privateKeyPath);

        HttpClientBuilder builder;
        builder = WechatPayHttpClientBuilder.create()
                .withMerchant(mchId, mchSerialNo, privateKey)
                .withValidator(new WechatPay2Validator(verifier))
                .setDefaultRequestConfig(requestConfig);
        // ... 接下来,你仍然可以通过builder设置各种参数,来配置你的HttpClient

        // 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
        CloseableHttpClient httpClient = builder
                .build();
        return httpClient;
    }

    /**
     * 获取签名验证器
     *
     * @return
     */
    @Bean
    public Verifier getVerifier() {

        // 获取商户私钥
        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, apiV3Key.getBytes(StandardCharsets.UTF_8));
        } catch (IOException | GeneralSecurityException | HttpCodeException e) {
            log.warn(e.getMessage());
            throw new RuntimeException(e);
        }
        // ... 若有多个商户号,可继续调用putMerchant添加商户信息
        // 从证书管理器中获取 verifier
        Verifier verifier = null;
        try {
            // 替换系统代理
            verifier = certificatesManager.getVerifier(mchId);
        } catch (NotFoundException e) {
            e.printStackTrace();
            log.warn(e.getMessage());
//            throw new RuntimeException(e);
        }
        return verifier;
    }

}

4 Controller

import com.alibaba.fastjson.JSONObject;
import com.google.gson.JsonSyntaxException;
import com.test.weixin.common.CommonResult;
import com.test.weixin.jieshun.request.JhtDiscountReq;
import com.test.weixin.jieshun.service.JhtCommService;
import com.test.weixin.service.WechatPayService;
import com.test.weixin.utils.HttpUtils;
import com.test.weixin.utils.WechatPay2ValidatorForRequest;
import com.test.weixin.vo.WechatPayVo;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.Builder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;

/**
 * @author huyao
 * @version 1.0
 * @title
 * @date 2022/8/3 14:02
 * @description
 */

@RestController
@RequestMapping("/wechatPay")
@Api(tags = "网站微信支付API")
@Slf4j
public class WechatPayController {

    @Resource
    private WechatPayService wechatPayService;

    @Resource
    private Verifier verifier;

    /**
     * 调用统一下单API,请求微信支付
     * @param wechatPayVo
     * @return
     * @throws Exception
     */
    @PostMapping("/jsApiPay")
    public CommonResult jsApiPay(@RequestBody WechatPayVo wechatPayVo) throws Exception {
        log.info("发起支付请求");
        // 返回支付信息 包括验签和预支付id
        return CommonResult.success(wechatPayService.createPayOrder(wechatPayVo));
    }


    /**
     * 回调接口
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @PostMapping("/notify")
    public String jsApiNotify(HttpServletRequest request, HttpServletResponse response) throws Exception {
        JSONObject res = new JSONObject();
        try {
            // 处理通知参数
            String body = HttpUtils.readData(request);
            HashMap<String, Object> bodyMap = JSONObject.parseObject(body, HashMap.class);
            String requestId = (String) bodyMap.get("id");
            log.info("支付通知的id ====> {}", requestId);
            log.info("支付通知的完整数据 ====> {}", body);

            // 签名的验证
            WechatPay2ValidatorForRequest wechatPay2ValidatorForRequest = new WechatPay2ValidatorForRequest(verifier, body, requestId);
            if (!wechatPay2ValidatorForRequest.validate(request)) {
                log.info("通知验签失败");
                // 失败应答
                response.setStatus(500);
                res.put("code", "ERROR");
                res.put("message", "通知验签失败");
                return res.toString();
            }
            log.info("通知验签成功");

          	// 处理回调 
            Boolean notify =  wechatPayService.processPayNotify(bodyMap);

            if (notify) {
                // 成功应答
                response.setStatus(200);
                res.put("code", "SUCCESS");
                res.put("message", "成功");
                return res.toString();
            } else {
                // 失败应答
                response.setStatus(500);
                res.put("code", "ERROR");
                res.put("message", "失败");
                return res.toString();
            }
        } catch (JsonSyntaxException e) {
            e.printStackTrace();
            // 失败应答
            response.setStatus(500);
            res.put("code", "ERROR");
            res.put("message", "失败");
            return res.toString();
        }
    }
}

5 Service and ServiceImpl

import com.alibaba.fastjson.JSONObject;
import com.test.weixin.vo.WechatPayVo;

import java.util.HashMap;

/**
 * @author huyao
 * @version 1.0
 * @title
 * @date 2022/8/3 14:05
 * @description
 */
public interface  WechatPayService {
    JSONObject createPayOrder(WechatPayVo wechatPayVo) throws Exception ;

    boolean processPayNotify(HashMap<String, Object> bodyMap) throws Exception;
}
package com.test.weixin.service.impl;

import com.alibaba.fastjson.JSONObject;
import com.test.weixin.common.ParkException;
import com.test.weixin.common.ParkResult;
import com.test.weixin.common.WxPayConfig;
import com.test.weixin.constants.ErrorCode;
import com.test.weixin.constants.PayStatus;
import com.test.weixin.constants.wxpay.WxApiType;
import com.test.weixin.constants.wxpay.WxTradeState;
import com.test.weixin.jieshun.request.JhtPayNotifyReq;
import com.test.weixin.jieshun.service.JhtCommService;
import com.test.weixin.model.TWxPayResult;
import com.test.weixin.service.*;
import com.test.weixin.utils.TimeTransformUtil;
import com.test.weixin.utils.WXPayUtil;
import com.test.weixin.vo.WechatPayVo;
import com.wechat.pay.contrib.apache.httpclient.util.AesUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
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.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.io.*;
import java.math.BigDecimal;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * @author huyao
 * @version 1.0
 * @title
 * @date 2022/8/3 14:06
 * @description
 */
@Service
@Slf4j
public class WechatPayServiceImpl implements WechatPayService {

    @Resource
    private WxPayConfig wxPayConfig;

    @Resource
    @Qualifier("getWxPayClient")
    private CloseableHttpClient wxPayClient;

    @Resource
    private RedissonClient redissonClient;

    @Value("${lock.key.wechat_pay_change_status}")
    private String wechatPayChangeStatus;

    // 商户私钥文件
    @Value("${wechatPay.private-key-path}")
    private String privateKeyPath;

    @Autowired
    WxPayResultIService wxPayResultIService;


    /**
     * 创建订单
     * @param wechatPayVo
     * @return
     * @throws Exception
     */
    @Override
    public JSONObject createPayOrder(WechatPayVo wechatPayVo) throws Exception {

        log.info("WechatPayVo:"+wechatPayVo);

        //请求URL
        HttpPost httpPost = new HttpPost(wxPayConfig.getDomain().concat(WxApiType.JSAPI_PAY.getType()));

        // 请求body参数
        JSONObject jsonParams = new JSONObject();
        jsonParams.put("appid", wxPayConfig.getAppid());
        jsonParams.put("mchid", wxPayConfig.getMchId());
        jsonParams.put("description", wechatPayVo.getProduct());
        jsonParams.put("out_trade_no", wechatPayVo.getOrderId());
        // 微信APi要求结束时间为 rfc 时间
        jsonParams.put("time_expire", TimeTransformUtil.timeToRfc(wechatPayVo.getTimeExpire()));
        jsonParams.put("notify_url", wxPayConfig.getNotifyDomain());

        // 订单金额信息
        JSONObject amount = new JSONObject();
        // 微信支付已分为单位
        amount.put("total", wechatPayVo.getAmount().multiply(new BigDecimal("100")).intValue());
        amount.put("currency", "CNY");
        jsonParams.put("amount", amount);

        // 支付者
        JSONObject payer = new JSONObject();
        payer.put("openid", wechatPayVo.getOpenId());
        jsonParams.put("payer", payer);

        log.info("请求参数:" + jsonParams);

        String reqdata = jsonParams.toString();

        StringEntity entity = new StringEntity(reqdata, "utf-8");
        entity.setContentType("application/json");
        httpPost.setEntity(entity);
        httpPost.setHeader("Accept", "application/json");

        //完成签名并执行请求
        CloseableHttpResponse response = wxPayClient.execute(httpPost);

        try {
            String bodyAsString = EntityUtils.toString(response.getEntity());
            int statusCode = response.getStatusLine().getStatusCode();
            if (statusCode == 200) {
                log.info("成功, 返回结果 = {}", bodyAsString);
            } else if (statusCode == 204) {
                log.info("成功");
            } else {
                log.info("失败, 响应码 = {}, 返回结果 = {}", statusCode, bodyAsString);
                throw new IOException(JSONObject.parseObject(bodyAsString).getString("message"));
            }
            JSONObject body = JSONObject.parseObject(bodyAsString);
            String prepayId = (String) body.get("prepay_id");

            long timestamp = WXPayUtil.getCurrentTimestamp();
            String nonceStr = WXPayUtil.generateNonceStr();

            String str = "%s\n%s\n%s\nprepay_id=%s\n";
            str = String.format(str,wxPayConfig.getAppid(),timestamp,nonceStr,prepayId);
            log.info("构造签名串:\n"+str);

            // 密钥文件也要参与签名
            String paySign = WXPayUtil.getSign(str,this.getClass().getClassLoader().getResourceAsStream(privateKeyPath));

            JSONObject result = new JSONObject(new LinkedHashMap<>());
            result.put("appId", wxPayConfig.getAppid());
            result.put("timeStamp", timestamp);
            result.put("nonceStr", nonceStr);
            result.put("package", "prepay_id=".concat(prepayId));
            result.put("signType", "RSA");
            result.put("paySign", paySign);
            return result;
        } finally {
            response.close();
        }
    }


    /**
     * 处理支付回调通知
     *
     * @param bodyMap
     * @throws Exception
     */
    @Override
    public boolean processPayNotify(HashMap<String, Object> bodyMap) throws Exception {
        log.info("订单处理");

        String plainText = decryptFromResource(bodyMap);

        log.info("返回明文数据:"+plainText);

        // 将明文转换成 map
        HashMap plainTextMap = JSONObject.parseObject(plainText, HashMap.class);
        // 商户订单号
        String orderId = (String) plainTextMap.get("out_trade_no");
				// 微信订单号
        String transactionId = (String) plainTextMap.get("transaction_id");

        // 交易状态
        String tradeState = (String) plainTextMap.get("trade_state");

      	String addLockKey = wechatPayChangeStatus + orderId;
        // 加锁 防止重复处理 
        RLock rLock = redissonClient.getLock(addLockKey);
        try {
          if (rLock.tryLock(10, 10, TimeUnit.SECONDS)) {
            // 支付成功
            if (WxTradeState.SUCCESS.getType().equals(tradeState)) {
              // 执行自己的业务逻辑
                  return true;
                }
          }
        } catch (Exception e) {
          log.error("更改 id = {} 订单状态时 锁分配失败,10秒后再试", orderId);
        }
      return false;
    }
  
    /**
     * 对称解密
     *
     * @param bodyMap
     * @return
     */
    private String decryptFromResource(HashMap<String, Object> bodyMap) throws GeneralSecurityException {
        log.info("密文解密");

        // 通知数据
        Map<String, String> resourceMap = (Map<String, String>) bodyMap.get("resource");
        // 数据密文
        String ciphertext = resourceMap.get("ciphertext");
        // 随机串
        String nonce = resourceMap.get("nonce");
        // 附加数据
        String associated_data = resourceMap.get("associated_data");

        log.info("密文 ===> {}", ciphertext);

        AesUtil aesUtil = new AesUtil(wxPayConfig.getApiV3Key().getBytes(StandardCharsets.UTF_8));
        String plainText = aesUtil.decryptToString(
                associated_data.getBytes(StandardCharsets.UTF_8),
                nonce.getBytes(StandardCharsets.UTF_8),
                ciphertext);

        log.info("明文 ===> {}", plainText);
        return plainText;
    }
}

6 WXPayUtil

import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.Base64Utils;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.util.*;

/** 签名工具
 */
public class WXPayUtil {

    private static final String SYMBOLS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";

    private static final Random RANDOM = new SecureRandom();


    public static String getSign(String signatureStr, InputStream privateKey) throws InvalidKeyException, NoSuchAlgorithmException, SignatureException, IOException, URISyntaxException {
        PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(privateKey);
        Signature sign = Signature.getInstance("SHA256withRSA");
        sign.initSign(merchantPrivateKey);
        sign.update(signatureStr.getBytes(StandardCharsets.UTF_8));
        return Base64Utils.encodeToString(sign.sign());
    }

    /**
     * 获取随机字符串 Nonce Str
     *
     * @return String 随机字符串
     */
    public static String generateNonceStr() {
        char[] nonceChars = new char[32];
        for (int index = 0; index < nonceChars.length; ++index) {
            nonceChars[index] = SYMBOLS.charAt(RANDOM.nextInt(SYMBOLS.length()));
        }
        return new String(nonceChars);
    }


    /**
     * 日志
     * @return
     */
    public static Logger getLogger() {
        Logger logger = LoggerFactory.getLogger("wxpay java sdk");
        return logger;
    }

    /**
     * 获取当前时间戳,单位秒
     * @return
     */
    public static long getCurrentTimestamp() {
        return System.currentTimeMillis()/1000;
    }

    /**
     * 获取当前时间戳,单位毫秒
     * @return
     */
    public static long getCurrentTimestampMs() {
        return System.currentTimeMillis();
    }

}

7 备注

  • 微信支付回调不一定能成功通知业务方 所以需要自己在项目中开定时任务扫描你的支付数据 定时去查询支付状态是否是未成功支付的订单

三 接口调用

在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值