springboot集成微信支付V3(小程序)

2022-10-30 15:03:37 浏览数 (1)

准备工作

小程序开通微信支付

  1. 首先需要在微信支付的官网点击跳转上注册一个服务商
  2. 在服务商的管理页面中申请关联小程序,通过小程序的 appid 进行关联
  3. 进入微信公众平台,功能-微信支付中确认关联(如果服务商和小程序的注册主体不一样,还要经过微信的审核)

获取各种证书、密钥文件

这里比较麻烦,需要认真点。

目前微信支付的 api 有 V2 和 V3 两个版本,V2 是 xml 的数据结构不建议用了,很麻烦(虽然 V3 也不简单).

以下内容全部基于微信支付 V3 的版本

你需要获取如下东西:

  • 商户 id:这个可以在小程序微信公众平台-功能-微信支付 页面中的已关联商户号中得到
  • 商户密钥:这个需要在微信支付的管理后台中申请获取
  • 证书编号: 同样在微信支付的管理后台中申请证书,申请证书后就会看到证书编号
  • 证书私钥:上一步申请证书的时候同时也会获取到证书的公钥、私钥文件。开发中只需要私钥文件

代码开发

由于支付属于比较敏感的操作,所以建议将参数配置放在后台,前端请求获取到参数后直接调起微信支付。

整个微信支付流程如下:

  1. 小程序端请求后台获取统一支付参数
  2. 后台调用微信 api(官方文档)生成预订单,并构造统一下单接口的参数返回小程序
  3. 小程序根据参数调用统一下单接口(官方文档)

实际开发中,小程序端的开发内容很少,98%的工作量都在后台。

支付相关信息:wxpay.properties

代码语言:javascript复制
##服务商/直连商户平台 关联的 公众号appid
v3.appId=
#商户id
v3.merchantId=
#证书序列号
v3.merchantSerialNumber=
#私钥路径
v3.certKeyPath=/cert/apiclient_key.pem
#商户秘钥
v3.apiV3Key=
#回调通知地址
v3.wxnotify=

配置文件:WxPayConfig

代码语言:javascript复制
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;

@Component
@Data
@PropertySource("wxpay.properties")
@ConfigurationProperties(prefix = "v3")
public class WxPayConfig {
    //appId
    private String appId;

    //商户id
    private String merchantId;

    //证书序列号
    private String merchantSerialNumber;

    //私钥路径
    private String certKeyPath;

    //商户秘钥
    private String apiV3Key;

    //回调通知地址
    private String wxnotify;



}

准备请求 httpClient

虽然微信官方目前没有正式放出官方 sdk,但是 github 上已经有了 sdk 的仓库,目前正在开发中,地址.这个工具做了一定程度的封装,极大简化了加密解密证书配置等繁琐的操作。

和微信的请求需要做双向加密,因此要在系统启动时创建一个专用的 httpClient,用来调用微信支付 api.代码如下:

代码语言:javascript复制
import cn.hutool.core.codec.Base64;
import cn.hutool.core.util.IdUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.asymmetric.Sign;
import cn.hutool.crypto.asymmetric.SignAlgorithm;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
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.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 lombok.extern.slf4j.Slf4j;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.io.*;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import java.util.*;

@Autowired
private WxPayConfig wxPayConfig;
//微信专业httpClient
private static CloseableHttpClient httpClient;
//生成签名用
private static Sign sign;
//证书管理器
private static CertificatesManager certificatesManager;
    
@PostConstruct
public void init() throws Exception {

    //获取商户私钥
    PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(getFileInputStream(wxPayConfig.getCertKeyPath()));
    // 获取证书管理器实例
    certificatesManager = CertificatesManager.getInstance();
    sign = SecureUtil.sign(SignAlgorithm.SHA256withRSA, merchantPrivateKey.getEncoded(), null);
    // 向证书管理器增加需要自动更新平台证书的商户信息
    certificatesManager.putMerchant(wxPayConfig.getMerchantId(), new WechatPay2Credentials(wxPayConfig.getMerchantId(),
        new PrivateKeySigner(wxPayConfig.getMerchantSerialNumber(), merchantPrivateKey)), wxPayConfig.getApiV3Key().getBytes(StandardCharsets.UTF_8));
    // 从证书管理器中获取verifier
    Verifier verifier = certificatesManager.getVerifier(wxPayConfig.getMerchantId());
    //用于构造HttpClient
    WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
        .withMerchant(wxPayConfig.getMerchantId(), wxPayConfig.getMerchantSerialNumber(), merchantPrivateKey)
        .withValidator(new WechatPay2Validator(verifier));
    httpClient = builder.build();
}
/**
 * 使用getResourceAsStream直接从resources根路径下获取文件流
 * @param path
 */
private InputStream getFileInputStream(String path) {
    InputStream in = this.getClass().getResourceAsStream(path);
    return in;
}

预订单生成返回给小程序请求参数

代码语言:javascript复制
@Override
public Map<String, String> wxPay(WxPayDto dto) {
    JSONObject obj = new JSONObject();
    obj.put("mchid", wxPayConfig.getMerchantId());
    obj.put("appid", wxPayConfig.getAppId());
    obj.put("description", dto.getDescription());
    obj.put("out_trade_no", dto.getSn());
    obj.put("notify_url", wxPayConfig.getWxnotify());
    Map<String, String> attach = new HashMap<>();
    attach.put("sn", dto.getSn());
    obj.put("attach", JSON.toJSONString(attach));
    JSONObject amount = new JSONObject();
    amount.put("total", dto.getPrice().multiply(BigDecimal.valueOf(100)).intValue());
    obj.put("amount", amount);
    JSONObject payer = new JSONObject();
    //支付者的openId
    payer.put("openid", dto.getOpenId());
    obj.put("payer", payer);
    log.info("请求参数为:"   JSON.toJSONString(obj));
    HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi");
    httpPost.addHeader("Accept", "application/json");
    httpPost.addHeader("Content-type", "application/json; charset=utf-8");
    httpPost.setEntity(new StringEntity(obj.toJSONString(), "UTF-8"));
    try {
        //执行请求
        CloseableHttpResponse response = httpClient.execute(httpPost);
        String bodyAsString = EntityUtils.toString(response.getEntity());
        String prePayId = JSONObject.parseObject(bodyAsString).getString("prepay_id");
        if (prePayId == null){
            String message = JSONObject.parseObject(bodyAsString).getString("message");
            throw new Exception(message,-1);
        }
        //准备小程序端的请求参数
        Map<String, String> map = new HashMap<>(6);
        map.put("appId", wxPayConfig.getAppId());
        String timeStamp = String.valueOf(System.currentTimeMillis() / 1000);
        map.put("timeStamp", timeStamp);
        String nonceStr = IdUtil.fastSimpleUUID();
        map.put("nonceStr", nonceStr);
        String packageStr = "prepay_id="   prePayId;
        map.put("package", packageStr);
        map.put("signType", "RSA");
        String signString =  wxPayConfig.getAppId()   "n"   timeStamp   "n"   nonceStr   "n"   packageStr   "n";
        map.put("paySign", Base64.encode(sign.sign(signString.getBytes())));
        return map;
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}
代码语言:javascript复制
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;
import java.math.BigDecimal;

/**
 * @author adu
 * @date 2022/9/29.
 */
@Data
@Accessors(chain = true)
public class WxPayDto {

    @ApiModelProperty(value = "订单号")
    private String sn;

    @ApiModelProperty(value = "支付金额")
    private BigDecimal price;

    @ApiModelProperty(value = "商品描述")
    private String description;

    @ApiModelProperty(value = "支付用户oppnid")
    private String openId;


}

小程序端请求

小程序端获取请求参数后,直接调用wx.requestPayment(后台返回的参数),即可调起支付

回调通知

微信支付成功后,会通知服务端支付成功,通过之前配置的回调接口。注意回调接口必须为 https。

微信回调参数也是加密的,必须要经过解密后才能获取,代码如下:

注意:部分参数是通过请求头提供的,nginx 等代理在转发请求时可能会将请求头过滤掉,导致无法获取对应参数

代码语言:javascript复制
@Override
public void notify(HttpServletRequest servletRequest) {

String timeStamp = servletRequest.getHeader("Wechatpay-Timestamp");
String nonce = servletRequest.getHeader("Wechatpay-Nonce");
String signature = servletRequest.getHeader("Wechatpay-Signature");
String certSn = servletRequest.getHeader("Wechatpay-Serial");

try (BufferedReader reader = new BufferedReader(new InputStreamReader(servletRequest.getInputStream()))) {
    StringBuilder stringBuilder = new StringBuilder();
    String line;
    while ((line = reader.readLine()) != null) {
        stringBuilder.append(line);
    }
    String obj = stringBuilder.toString();
    log.info("回调数据:{},{},{},{},{}", obj, timeStamp, nonce, signature, certSn);
    Verifier verifier = certificatesManager.getVerifier(merchantId);
    String sn = verifier.getValidCertificate().getSerialNumber().toString(16).toUpperCase(Locale.ROOT);
    NotificationRequest request = new NotificationRequest.Builder().withSerialNumber(sn)
        .withNonce(nonce)
        .withTimestamp(timeStamp)
        .withSignature(signature)
        .withBody(obj)
        .build();
    NotificationHandler handler = new NotificationHandler(verifier, apiV3Key.getBytes(StandardCharsets.UTF_8));
    // 验签和解析请求体
    Notification notification = handler.parse(request);
    JSONObject res = JSON.parseObject(notification.getDecryptData());
    //做一些操作
    } catch (Exception e) {
        log.error("微信支付回调失败", e);
    }
}

实操 :购买优惠劵

1、新建支付记录表

2、购买接口:新增记录(未支付状态)--> 生成支付预订单 --> 返回小程序需要的请求参数

3、回调通知:根据订单id获取记录表记录,更改交易场状态,处理业务。

4、小程序端不断获取支付记录,判断状态是否支付成功。

0 人点赞