springboot集成微信小程序支付

890 阅读6分钟

准备工作

1、微信公众平台接入微信支付

商户号mchid、appid a492034179d4ce1d08d3a95f6ccb40a.png

开发 ---> 开发管理 ---> 开发设置

appSecret 管理员可设置、查看 86735a72408c25e362fe5a37a75a3a7.png

证书相关参数 image.png 点击查看跳转到微信支付(管理员账号登录)

申请API证书和AIPv3证书

API证书申请成功后可以下载一个压缩包,压缩包内容 7fccace8f110c37c9d2b7cf6b260f7f.png 把私钥放在服务器上,记录地址privateKeyPath

serialNo:api证书序列号,从api证书点击查看进去可以看到序列号

apiV3Key:AIPv3证书私钥,申请AIPv3证书时设置的32位私钥

2、相关代码

引入依赖

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

微信支付工具类

import cn.hutool.core.util.IdUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.sdecloud.common.utils.StringUtils;
import com.sdecloud.modules.collectdata.util.HttpClientUtil;
import com.sdecloud.modules.hswjordermanage.util.dto.WxPayParam;
import com.sdecloud.modules.hswjordermanage.util.dto.WxRefundParam;
import com.sdecloud.modules.hswjordermanage.util.enums.PayConfig;
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.NotFoundException;
import com.wechat.pay.contrib.apache.httpclient.exception.ParseException;
import com.wechat.pay.contrib.apache.httpclient.exception.ValidationException;
import com.wechat.pay.contrib.apache.httpclient.notification.Notification;
import com.wechat.pay.contrib.apache.httpclient.notification.NotificationHandler;
import com.wechat.pay.contrib.apache.httpclient.notification.NotificationRequest;
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
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.client.utils.URIBuilder;
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 org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.SignatureException;
import java.util.Base64;
import java.util.Objects;

/**
 * 微信支付工具类
 *
 * @author jhx
 */
@Component
public class WxPayUtils {

    /**
     * 支付相关参数
     */
    private PayConfig config;
    /**
     * 证书管理器
     */
    private CertificatesManager certificatesManager;
    private CloseableHttpClient httpClient;

    private static WxPayUtils wxPayUtils;

    private static final Logger log = LoggerFactory.getLogger(WxPayUtils.class);

    /**
     * 项目地址
     */
    private final String realPath = System.getProperty("user.dir") + File.separator;
    /**
     * 微信app支付相关接口地址
     */
    private final String WX_TRADE_URL = "https://api.mch.weixin.qq.com/v3/pay/transactions/";
    private final String WX_QUERY_TRADE_URL = "https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/";
    private final String WX_REFUND_URL = "https://api.mch.weixin.qq.com/v3/refund/domestic/refunds";
    private final String WX_QUERY_REFUND_URL = "https://api.mch.weixin.qq.com/v3/refund/domestic/refunds/";
    private final String WX_QUERY_OPENID_URL = "https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code";
    
    /**
     * 微信支付相关配置
     */
    private final String wxAppid = "wxc*************fb";
    private final String wxAppSecret = "f615ee4*************627dc4a30e31";
    private final String wxMchid = "16******64";
    private final String wxPrivateKeyPath = "wxCrt/wxPrivateKey.pem";
    private final String wxSerialNo = "7C9CD9294F*************1F0F252775BEA5008";
    private final String wxApiV3Key = "3538ab7354*************7b1f480c2";

    @PostConstruct
    public void init() {
        wxPayUtils = this;
        setVerifier();
        wxPayUtils.certificatesManager = this.certificatesManager;
        wxPayUtils.httpClient = this.httpClient;
    }

    public void setVerifier() {
        // 获取证书管理器实例
        this.certificatesManager = CertificatesManager.getInstance();
        try {
            // 加载商户私钥(privateKey:私钥字符串)
            PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(new FileInputStream(realPath + wxPrivateKeyPath));
            // 向证书管理器增加需要自动更新平台证书的商户信息
            this.certificatesManager.putMerchant(wxMchid, new WechatPay2Credentials(wxMchid, new PrivateKeySigner(wxSerialNo, merchantPrivateKey)), wxApiV3Key.getBytes(StandardCharsets.UTF_8));
            Verifier verifier = wxPayUtils.certificatesManager.getVerifier(wxMchid);
            wxPayUtils.httpClient = WechatPayHttpClientBuilder.create()
                    .withMerchant(wxMchid, wxSerialNo, merchantPrivateKey)
                    .withValidator(new WechatPay2Validator(verifier)).build();
        } catch (IOException e) {
            e.printStackTrace();
            log.error("加载秘钥文件失败");
        } catch (GeneralSecurityException e) {
            e.printStackTrace();
            log.error("获取平台证书失败");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 获取预支付id
     * @param param {@link WxPayParam}
     * @return JSONObject
     */
    public JSONObject prePay(WxPayParam param) {
        // 请求URL
        HttpPost httpPost = new HttpPost(WX_TRADE_URL + param.getPayPath());
        httpPost.addHeader("Accept", "application/json");
        httpPost.addHeader("Content-type","application/json; charset=utf-8");

        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectMapper objectMapper = new ObjectMapper();

        ObjectNode rootNode = objectMapper.createObjectNode();
        rootNode.put("mchid", wxMchid)
                .put("appid", wxAppid)
                .put("description", "商品售卖")
                .put("notify_url", param.getNotifyUrl())
                .put("out_trade_no", param.getOutTradeNo());
        rootNode.putObject("amount").put("total", param.getTotalFee());
        rootNode.putObject("payer").put("openid", param.getOpenid());

        try {
            objectMapper.writeValue(bos, rootNode);
            httpPost.setEntity(new StringEntity(bos.toString("UTF-8"), "UTF-8"));
            CloseableHttpResponse response = wxPayUtils.httpClient.execute(httpPost);
            log.info("微信发起支付返回结果:" + response);

            String bodyAsString = EntityUtils.toString(response.getEntity());
            JSONObject result = JSONObject.parseObject(bodyAsString);
            // 返回给前端的参数
            JSONObject jsonObject = new JSONObject();
            long timeStamp = System.currentTimeMillis() / 1000;
            String uuid = IdUtil.simpleUUID();
            String paySign = getPaySign(timeStamp, uuid, result.getString("prepay_id"));
            jsonObject.put("prepayid", result.getString("prepay_id"));
            jsonObject.put("timestamp", timeStamp);
            jsonObject.put("nonceStr", uuid);
            jsonObject.put("paySign", paySign);
            jsonObject.put("appid", wxAppid);
            jsonObject.put("partnerid", wxMchid);
            jsonObject.put("package", "Sign=WXPay");
            jsonObject.put("outTradeNo", param.getOutTradeNo());
            return jsonObject;
        } catch (IOException e) {
            log.error("微信发起支付失败,报错信息:" + e.getMessage());
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 查询订单是否支付成功,用于订单超时检测,返回SUCCESS则支付成功
     *
     * @param outTradeNo 订单号
     * @return 返回交易状态 SUCCESS:支付成功 * REFUND:转入退款 * NOTPAY:未支付 * CLOSED:已关闭 * REVOKED:已撤销(仅付款码支付会返回) * USERPAYING:用户支付中(仅付款码支付会返回)     * PAYERROR:支付失败(仅付款码支付会返回)
     */
    public String queryTrade(String outTradeNo) {
        try {
            URIBuilder uriBuilder = new URIBuilder(WX_QUERY_TRADE_URL + outTradeNo + "?mchid=" + wxMchid);
            HttpGet httpGet = new HttpGet(uriBuilder.build());
            httpGet.addHeader("Accept", "application/json");

            CloseableHttpResponse response = wxPayUtils.httpClient.execute(httpGet);
            log.info("微信查询支付状态返回结果:" + response);
            int statusCode = response.getStatusLine().getStatusCode();
            if (statusCode == 200) {
                return EntityUtils.toString(response.getEntity());
            } else {
                return "";
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 退款
     */
    public JSONObject dpRefund(WxRefundParam param) {
        // 请求URL
        HttpPost httpPost = new HttpPost(WX_REFUND_URL);
        JSONObject paramJson = new JSONObject();
        paramJson.put("transaction_id", param.getTradeNo());
        paramJson.put("out_refund_no", param.getOutRefundNo());
        paramJson.put("reason", param.getReason());
        JSONObject refundAmount = new JSONObject();
        refundAmount.put("refund", param.getRefundFee());
        refundAmount.put("total", param.getTotalFee());
        refundAmount.put("currency", "CNY");
        paramJson.put("amount", refundAmount);

        StringEntity entity = new StringEntity(paramJson.toString(), StandardCharsets.UTF_8);
        entity.setContentType("application/json");
        httpPost.setEntity(entity);
        httpPost.setHeader("Accept", "application/json");
        try {
            // 完成签名并执行请求
            CloseableHttpResponse response = wxPayUtils.httpClient.execute(httpPost);
            log.info("微信发起退款返回结果:" + response);
            int statusCode = response.getStatusLine().getStatusCode();
            // 处理成功
            if (statusCode == 200) {
                return JSONObject.parseObject(EntityUtils.toString(response.getEntity()));
            }
        } catch (Exception e) {
            log.error("微信发起退款失败,报错信息:" + e.getMessage());
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 微信查询退款结果
     * @param outRefundNo 商户退款单号
     * @return String
     */
    public String queryRefund(String outRefundNo) {
        try {
            URIBuilder uriBuilder = new URIBuilder(WX_QUERY_REFUND_URL + outRefundNo);
            HttpGet httpGet = new HttpGet(uriBuilder.build());
            httpGet.addHeader("Accept", "application/json");
            
            CloseableHttpResponse response = wxPayUtils.httpClient.execute(httpGet);
            log.info("微信查询退款状态返回结果:" + response);
            int statusCode = response.getStatusLine().getStatusCode();
            if (statusCode == 200) {
                return EntityUtils.toString(response.getEntity());
            } else {
                return "";
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "";
    }


    /**
     * 支付回调
     *
     * @param request  request
     * @return JSONObject
     */
    public JSONObject callBack(HttpServletRequest request) {
        JSONObject resultJson = new JSONObject();

        // 从请求头获取验签字段
        JSONObject requestJson = new JSONObject();
        requestJson.put("nonce", request.getHeader("Wechatpay-Nonce"));
        requestJson.put("signature", request.getHeader("Wechatpay-Signature"));
        requestJson.put("timestamp", request.getHeader("Wechatpay-Timestamp"));
        requestJson.put("serial", request.getHeader("Wechatpay-Serial"));

        try {
            ServletInputStream inputStream = request.getInputStream();
            StringBuilder sb = new StringBuilder();
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
            String s;
            // 读取回调请求体
            while ((s = bufferedReader.readLine()) != null) {
                sb.append(s);
            }
            requestJson.put("body", sb.toString());
            Notification notification = this.notificationHandler(requestJson);
            if (Objects.isNull(notification)) {
                resultJson.put("code", "FAIL");
                resultJson.put("message", "解析请求体失败");
                return resultJson;
            }
            String data = notification.getDecryptData();
            log.info("微信回调返回结果:" + data);

            resultJson.put("code", "SUCCESS");
            resultJson.put("message", "成功");
            resultJson.put("data", data);

        } catch (IOException e) {
            resultJson.put("code", "FAIL");
            resultJson.put("message", e.getMessage());
            e.printStackTrace();
        }
        return resultJson;
    }

    public Notification notificationHandler(JSONObject jsonObject) {
        try {
            Verifier verifier = wxPayUtils.certificatesManager.getVerifier(wxMchid);
            // 构建request,传入必要参数
            NotificationRequest request = new NotificationRequest.Builder()
                    .withSerialNumber(jsonObject.getString("serial"))
                    .withNonce(jsonObject.getString("nonce"))
                    .withTimestamp(jsonObject.getString("timestamp"))
                    .withSignature(jsonObject.getString("signature"))
                    .withBody(jsonObject.getString("body"))
                    .build();
            NotificationHandler handler = new NotificationHandler(verifier, wxApiV3Key.getBytes(StandardCharsets.UTF_8));
            // 验签和解析请求体
            return handler.parse(request);
        } catch (NotFoundException | ValidationException | ParseException e) {
            e.printStackTrace();
            log.error("解析请求体失败,报错信息:" + e.getMessage());
        }
        return null;
    }

    public String getOpenid(String code) {
        String url = String.format(WX_QUERY_OPENID_URL, wxAppid, wxAppSecret, code);
        String response = HttpClientUtil.doGet(url);
        JSONObject resultJson = JSON.parseObject(response);
        return resultJson.getString("openid");
    }

    public String getPaySign(long timestamp, String randString, String prepayid) {
        String data = wxAppid + "\n" +
                timestamp + "\n" +
                randString + "\n" +
                "prepay_id=" + prepayid + "\n";
        String signature = null;
        try {
            signature = sign(data.getBytes(StandardCharsets.UTF_8));
        } catch (SignatureException e) {
            e.printStackTrace();
        }
        return signature;
    }

    public String sign(byte[] message) throws SignatureException {
        Signature sign = null;
        try {
            sign = Signature.getInstance("SHA256withRSA");
            PrivateKey privateKey = PemUtil
                    .loadPrivateKey(new FileInputStream(realPath + wxPrivateKeyPath));
            sign.initSign(privateKey);
            sign.update(message);
        } catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException | IOException e) {
            e.printStackTrace();
        }
        return Base64.getEncoder().encodeToString(sign.sign());
    }

}

sevice调用

/**
 * 微信预支付
 * @param orderId 订单id
 * @param payPath 支付方式,app,jsapi(小程序支付)
 * @return JSONObject
 */
@Override
public R<JSONObject> wxPrePay(String orderId, String payPath) {
    // 校验是否有不可支付订单
    String errorMsg = this.checkPay(orderId);
    if (StringUtils.isNotBlank(errorMsg)) {
        return R.failed(errorMsg);
    }
    // 计算订单总金额
    double totalFee = baseMapper.getTotalFee(orderId);
    // todo 添加支付记录
    // todo 支付记录id
    String outTradeNo = "";
    // expire 超时时间,自定义
    LocalDateTime expireTime = LocalDateTime.now().plusMinutes(expire);
    WxPayParam payParam = new WxPayParam();
    payParam.setOutTradeNo(outTradeNo);
    payParam.setTimeExpire(expireTime);
    // backUrl为服务域名,https
    payParam.setNotifyUrl(backUrl + "/orderInfo/wxCallBack");
    // 微信支付金额单位为分
    payParam.setTotalFee((int) (totalFee * 100));
    payParam.setPayPath(payPath);
    // 获取当前登录人,自定义
    SysUser sysUser = Objects.requireNonNull(SecurityUtils.getLoginUser()).getUser();
    // 获取openid:进入小程序调用wx.login()方法获取code,code传到后端使用支付工具类中的getOpenid()方法获取到openid,存到用户信息表中
    payParam.setOpenid(sysUser.getWxOpenId());
    log.info("微信发起支付参数:" + payParam);
    JSONObject jsonObject = wxPayUtils.prePay(payParam);
    if (Objects.isNull(jsonObject)) {
        return R.failed("拉起支付失败");
    }
    return R.ok(jsonObject);
}
/**
 * 微信支付回调
 */
@Override
public JSONObject wxCallBack() {
    HttpServletRequest request = ServletUtils.getRequest();
    JSONObject result = wxPayUtils.callBack(request);
    if ("SUCCESS".equals(result.getString("code"))) {
        JSONObject dataJson = JSON.parseObject(result.getString("data"));
        result.remove("data");
        String appId = dataJson.getString("appid");
        // 商户id
        String mchId = dataJson.getString("mchid");
        // 商户支付流水号
        String outTradeNo = dataJson.getString("out_trade_no");
        // 微信支付流水号
        String tradeNo = dataJson.getString("transaction_id");
        // 交易方式
        String tradeType = dataJson.getString("trade_type");
        // 交易状态
        String tradeState = dataJson.getString("trade_state");
        // 交易状态描述
        String tradeStateDesc = dataJson.getString("trade_state_desc");
        // 商户添加的附加属性
        String attach = dataJson.getString("attach");
        // 交易成功时间
        String successTime = dataJson.getString("success_time");
        JSONObject payer = dataJson.getJSONObject("payer");
        // 根据交易状态获取系统订单状态(自定义方法)
        String payStatus = this.getWxTradeStatus(tradeState);
        // 支付记录
        OrderPayRecord record = orderPayRecordService.getById(outTradeNo);
        // 如果微信支付流水号存在则表示已经接收到回调,不需要再次处理回调信息
        if (StringUtils.isBlank(record.getTradeNo()) || record.getTradeStatus().equals(tradeState)) {
            // 更新支付记录
            record.setTradeStatus(tradeState);
            record.setPayTime(successTime);
            record.setTradeNo(tradeNo);
            record.setBuyerLogonId(payer.getString("openid"));
            orderPayRecordService.updateById(record);

            String[] orderIdList = StringUtils.split(record.getOrderId(), ",");
            // 修改订单支付状态
            baseMapper.update(null, new UpdateWrapper<OrderInfo>()
                    .set("pay_status", payStatus)
                    .set("trade_no", tradeNo)
                    .set("out_trade_no", outTradeNo)
                    .eq("id", record.getOrderId()));
            // todo 其他处理订单相关逻辑
        }
    }
    result.remove("data");
    return result;
}
/**
 * 主动发起支付状态查询
 * @param outTradeNo 交易id
 * @return 订单状态
 */
@Override
public R<String> queryPayStatus(String outTradeNo) {
    OrderPayRecord record = orderPayRecordService.getById(outTradeNo);
    String tradeStatus = "";
    String payStatus = "";
    String tradeNo = "";
    switch (record.getPayType()) {
        case "1":
            // todo 支付宝处理逻辑
            break;
        case "2":
            String response = wxPayUtils.queryTrade(outTradeNo);
            if (StringUtils.isBlank(response)) {
                return R.failed("查询失败");
            }
            JSONObject responseJson = JSON.parseObject(response);
            tradeStatus = responseJson.getString("trade_state");
            payStatus = this.getWxTradeStatus(tradeStatus);
            tradeNo = responseJson.getString("transaction_id");
            break;
        default:
            break;
    }
    // 支付记录的支付宝/微信交易流水号为空表示未收到回调
    if (StringUtils.isBlank(record.getTradeNo()) || !record.getTradeStatus().equals(tradeStatus)) {
        // 修改订单支付状态
        baseMapper.update(null, new UpdateWrapper<OrderInfo>()
                .set("pay_status", payStatus)
                .set("trade_no", tradeNo)
                .eq("id", record.getOrderId()));
        // 修改支付记录
        record.setTradeStatus(tradeStatus);
        record.setTradeNo(tradeNo);
        orderPayRecordService.updateById(record);
        // todo 其他订单相关处理逻辑
    }
    return R.ok(payStatus);
}
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

import java.io.Serializable;
import java.time.LocalDateTime;

/**
 * 微信支付参数
 * @author jhx
 */
@Data
public class WxPayParam implements Serializable {

    @ApiModelProperty("微信openid")
    private String openid;

    @ApiModelProperty("订单号")
    private String outTradeNo;

    @ApiModelProperty("支付金额,单位分")
    private Integer totalFee;

    @ApiModelProperty("微信服务器调用支付结果通知路径")
    private String notifyUrl;

    @ApiModelProperty("交易结束时间")
    private LocalDateTime timeExpire;

    @ApiModelProperty("支付方式,app,jsapi(小程序支付)")
    private String payPath;
}

注:原代码中带有多租户逻辑,已删除;拉起支付、查询支付状态、回调部分已调通,退款及查询待更新