JSAPI微信公众号apiV3文档支付


一.所需参数及maven依赖

官方文档
1.公众号id appid
2.商户号 mchid
3.APIv3秘钥 secret
4.商户api私钥 apiclient_key.pem
5.证书序列号

maven依赖

<!-- 微信支付V3版sdk -->
<dependency>
	<groupId>com.github.wechatpay-apiv3</groupId>
	<artifactId>wechatpay-apache-httpclient</artifactId>
	<version>0.2.1</version>
</dependency>

二.前端vue部分

// 确认支付
onSubmit() {
  // 手机端校验
  if (!navigator.userAgent.match(/(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i)) {
    this.$dialog.alert({ message: '请在手机端微信关注公众号"跃迁赋能中心"进行购买' })
    return
  }

  // TODO 购买商品参数拼接、非空校验

  let that = this
  this.$api.request({
    url: '/wxpay/pay',
    method: 'post',
    params: params
  }).then(response => {
    // 拉起微信支付参数赋值
    that.payMap = response.data
    
    if (typeof WeixinJSBridge === 'undefined') {
      if (document.addEventListener) {
        document.addEventListener('WeixinJSBridgeReady', this.onBridgeReady, false)
      } else if (document.attachEvent) {
        document.attachEvent('WeixinJSBridgeReady', this.onBridgeReady)
        document.attachEvent('onWeixinJSBridgeReady', this.onBridgeReady)
      }
    } else {
      this.onBridgeReady()
    }
  })
},
// 拉起微信支付
onBridgeReady() {
  let payMap = this.payMap
  let that = this
  WeixinJSBridge.invoke(
    'getBrandWCPayRequest', {
      'appId': payMap.appId, // 公众号名称,由商户传入
      'timeStamp': payMap.timeStamp, // 时间戳,自1970年以来的秒数
      'nonceStr': payMap.nonceStr, // 随机串
      'package': payMap.package,
      'signType': payMap.signType, // 微信签名方式:
      'paySign': payMap.paySign // 微信签名
    },
    function(res) {
      if (res.err_msg == 'get_brand_wcpay_request:ok') {
        // 付款成功处理
        that.$router.replace({ path: '/' })
      }
    })
},

三.后端java部分

3.1 获取前端拉起微信支付参数
Controller

@ApiOperation("微信公众号支付")
@PostMapping("/pay")
public AjaxResult pay(HttpServletRequest request, @RequestParam(required = false) String miniIdStr,
    @RequestParam Double payAmount, @RequestParam String couponIdStr,
    @RequestParam(required = false) Long courseId) {
    Long personId = tokenService.getPersonId(request);
    // TODO 购买课程参数校验

    try {
        JSONObject result = officeOrderService.wxPay(personId, miniIdStr, payAmount, couponIdStr, courseId);
        Integer status = result.getInteger("status");
        if (status != null && status == 0) {
        	// 支付失败不反回前端拉起微信支付参数
            return AjaxResult.error("支付失败,请稍后再试");
        }
        return AjaxResult.success(result);
    } catch (Exception e) {
        log.error("微信支付下单接口失败,personId:{},miniIdStr:{},payAmount:{},couponIdStr:{},courseId:{}", personId,
            miniIdStr, payAmount, couponIdStr, courseId, e);
        return AjaxResult.error("支付失败,请稍后再试");
    }
}

Service

@Override
public JSONObject wxPay(Long personId, String miniIdStr, Double payAmount, String couponIdStr, Long courseId) {
    // 根据personId获取公众号openId
    String openId = wxLoginMapper.selectOpenIdByPersonId(personId);

    // 生成商品订单号
    String orderNumber = UUID.randomUUID().toString().replace("-", "");

    // TODO 查询购买课程信息

    // 返回前端数据集合
    JSONObject payMap = new JSONObject();

    // TODO 校验优惠券与购买课程是否相符(不符返回支付失败)
    // TODO 查询优惠券信息
    
    BigDecimal totalVal = BigDecimal.ZERO;
 	// TODO 计算订单金额 填充生成本地订单数据(存入本地数据库的订单)
    Double total = totalVal.doubleValue();

    // 校验订单金额
    if (!total.equals(payAmount)) {
        log.error("订单金额不符,personId:{},miniIdStr:{},payAmount:{},couponIdStr:{},total:{}", personId, miniIdStr,
            payAmount, couponIdStr, total);
        payMap.put("status", 0);
        return payMap;
    }

    // 支付金额大于0时调用微信支付
    if (total > 0) {
        // 拼接统一下单参数
        JSONObject params = new JSONObject();
        params.put("appid", WxConstants.OFFICIAL_APPID);
        params.put("mchid", WxConstants.PAY_MCH_ID);
        params.put("description", "课程购买");
        params.put("notify_url", "http://yq.zdxueqian.com/office/wxpay/payNotify");
        params.put("out_trade_no", orderNumber);
        // 商品金额
        JSONObject amount = new JSONObject();
        amount.put("total", totalVal.multiply(new BigDecimal(100)).intValue());
        log.info("订单金额:" + amount.get("total"));
        params.put("amount", amount);
        // 支付者
        JSONObject payer = new JSONObject();
        payer.put("openid", openId);
        params.put("payer", payer);

        // 发送微信预支付订单请求
        String prepayId = WxPayUtils.sendPay(params);

        if (StringUtils.isBlank(prepayId)) {
            // 生成预支付订单id失败(发送微信下单请求)
            log.error("生成预支付订单id失败,params:{}", params);
            payMap.put("status", 0);
            return payMap;
        }

        // 填充返回前端数据 注:返回前端的timeStamp与nonceStr字段必须与生成签名的一致 否则前端验签失败
        payMap.put("appId", WxConstants.OFFICIAL_APPID);
        long timestamp = System.currentTimeMillis() / 1000;
        payMap.put("timeStamp", timestamp + "");
        String nonceStr = WxPayUtils.generateNonceStr();
        payMap.put("nonceStr", nonceStr);
        payMap.put("package", "prepay_id=" + prepayId);
        payMap.put("signType", "RSA");
        // 生成请求签名
        String paySign = WxPayUtils.getPaySign(nonceStr, timestamp, payMap.getString("package"));
        payMap.put("paySign", paySign);
    } else {
        // 订单金额为0时返回status为1(按支付成功处理)
        payMap.put("status", 1);
    }

    // TODO 添加本地数据库订单信息
    return payMap;
}

WxPayUtils工具类

package com.yueqian.common.utils.wxpay;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.Signature;
import java.security.SignatureException;
import java.security.cert.X509Certificate;
import java.util.Base64;
import java.util.Random;

import javax.servlet.http.HttpServletRequest;

import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.util.EntityUtils;
import org.springframework.core.io.ClassPathResource;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
import com.wechat.pay.contrib.apache.httpclient.auth.AutoUpdateCertificatesVerifier;
import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner;
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.util.PemUtil;
import com.yueqian.common.constant.WxConstants;

import lombok.extern.slf4j.Slf4j;

/**
 * 微信支付工具类
 * 
 * @author suihao
 * @create 2020-10-22 16:51
 */
@Slf4j
public class WxPayUtils {

    /**
     * 发送微信预支付订单请求
     * 
     * @param params
     * @return
     * @throws IOException
     */
    public static String sendPay(JSONObject params) {
        try {
            // 读取证书私钥
            PrivateKey privateKey = PemUtil.loadPrivateKey(new ClassPathResource("apiclient_key.pem").getInputStream());

            // 不需要传入微信支付证书,将会自动更新
            AutoUpdateCertificatesVerifier verifier = new AutoUpdateCertificatesVerifier(
                new WechatPay2Credentials(WxConstants.PAY_MCH_ID,
                    new PrivateKeySigner(WxConstants.PAY_SERIAL_NO, privateKey)),
                WxConstants.PAY_SECRET_V3.getBytes(StandardCharsets.UTF_8.name()));

            // 创建http请求
            HttpClient client = WechatPayHttpClientBuilder.create()
                .withMerchant(WxConstants.PAY_MCH_ID, WxConstants.PAY_SERIAL_NO, privateKey)
                .withValidator(new WechatPay2Validator(verifier)).build();

            // 配置http请求参数
            HttpPost post = new HttpPost("https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi");
            post.setHeader("Content-Type", "application/json");
            post.setHeader("Accept", "application/json");
            post.setEntity(new StringEntity(params.toJSONString(), "utf-8"));

            // 获取请求结果
            HttpResponse response = client.execute(post);
            // System.out.println("响应码:" + response.getStatusLine());
            // System.out.println("响应body体:" + EntityUtils.toString(response.getEntity()));
            JSONObject jo = JSON.parseObject(EntityUtils.toString(response.getEntity()));

            // 预支付订单生成失败(微信端订单)
            if (response.getStatusLine().getStatusCode() != 200) {
                log.error("发送微信预支付订单请求失败:" + jo.getString("message") + ",params:{}", params);
                return null;
            }

            // 获取预支付订单id
            return jo.getString("prepay_id");
        } catch (IOException e) {
            log.error("发送微信预支付订单请求失败,params:{}", params, e);
            return null;
        }
    }

    /**
     * 获取调起微信支付签名
     * 
     * @param nonceStr
     * @param timestamp
     * @param prepayId
     * @return
     */
    public static String getPaySign(String nonceStr, long timestamp, String prepayId) {
        String message = WxConstants.OFFICIAL_APPID + "\n" + timestamp + "\n" + nonceStr + "\n" + prepayId + "\n";
        String signature = null;
        try {
            signature = sign(message.getBytes(StandardCharsets.UTF_8.name()), "apiclient_key.pem");
        } catch (UnsupportedEncodingException e) {
            log.error("生成微信支付签名失败,message:{}", message, e);
        }
        return signature;
    }

    /**
     * 读取请求体中数据
     * 
     * @param request
     * @return
     */
    public static String readData(HttpServletRequest request) {
        BufferedReader br = null;

        try {
            StringBuilder result = new StringBuilder();

            String line;
            for (br = request.getReader(); (line = br.readLine()) != null; result.append(line)) {
                if (result.length() > 0) {
                    result.append("\n");
                }
            }

            line = result.toString();
            return line;
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            if (br != null) {
                try {
                    br.close();
                } catch (IOException e) {
                    log.error("关闭字符输入流失败", e);
                }
            }
        }
    }

    /**
     * 微信支付回调签名验证并对加密请求参数解密
     *
     * @param serialNo
     * @param result
     * @param signatureStr
     * @param nonce
     * @param timestamp
     * @param paySecretV3
     * @return
     */
    public static String verifyNotify(String serialNo, String result, String signatureStr, String nonce,
        String timestamp, String paySecretV3) throws Exception {

        // 读取证书私钥
        PrivateKey privateKey = PemUtil.loadPrivateKey(new ClassPathResource("apiclient_key.pem").getInputStream());

        // 获取平台证书
        AutoUpdateCertificatesVerifier verifier = new AutoUpdateCertificatesVerifier(
            new WechatPay2Credentials(WxConstants.PAY_MCH_ID,
                new PrivateKeySigner(WxConstants.PAY_SERIAL_NO, privateKey)),
            WxConstants.PAY_SECRET_V3.getBytes(StandardCharsets.UTF_8.name()));
        X509Certificate certificate = verifier.getValidCertificate();

        // 证书序列号
        String serialNumber = certificate.getSerialNumber().toString(16).toUpperCase();
        // 证书验证
        if (serialNumber.equals(serialNo)) {
            // 构造验签名串
            String buildSignMessage = timestamp + "\n" + nonce + "\n" + result + "\n";
            // 获取平台公钥
            PublicKey publicKey = certificate.getPublicKey();

            Signature signature = Signature.getInstance("SHA256WithRSA");
            signature.initVerify(publicKey);
            signature.update(buildSignMessage.getBytes(StandardCharsets.UTF_8));
            boolean verifySignature =
                signature.verify(Base64.getDecoder().decode(signatureStr.getBytes(StandardCharsets.UTF_8)));

            if (verifySignature) {
                JSONObject resultObject = JSON.parseObject(result);
                JSONObject resource = resultObject.getJSONObject("resource");
                String cipherText = resource.getString("ciphertext");
                String nonceStr = resource.getString("nonce");
                String associatedData = resource.getString("associated_data");

                AesUtil aesUtil = new AesUtil(paySecretV3.getBytes(StandardCharsets.UTF_8));
                return aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8),
                    nonceStr.getBytes(StandardCharsets.UTF_8), cipherText);
            }
        }
        return null;
    }

    /** 对字节数据进行私钥签名(加密) */
    private static String sign(byte[] message, String serialPath) {
        try {
            // 签名方式(固定SHA256withRSA)
            Signature sign = Signature.getInstance("SHA256withRSA");
            // 使用私钥进行初始化签名(私钥需要从私钥文件【证书】中读取)
            InputStream inputStream = new ClassPathResource(serialPath).getInputStream();
            sign.initSign(PemUtil.loadPrivateKey(inputStream));
            // 签名更新
            sign.update(message);
            // 对签名结果进行Base64编码
            return Base64.getEncoder().encodeToString(sign.sign());
        } catch (SignatureException | InvalidKeyException | NoSuchAlgorithmException | IOException e) {
            log.error("微信支付进行私钥签名失败,message:{}", message, e);
        }
        return null;
    }

    /**
     * 获取随机字符串 Nonce Str
     * 随机字符从symbols获取
     * SecureRandom真随机数
     * 
     * @return String 随机字符串
     */
    public static String generateNonceStr() {
        String symbols = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
        Random random = new SecureRandom();
        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);
    }

}

3.2 用户支付成功回调

@ApiOperation("微信公众号支付回调")
@RequestMapping("/payNotify")
public void payNotify(HttpServletRequest request, HttpServletResponse response) {
    JSONObject resObj = new JSONObject();
    String plainText = null;
    try {
        // 获取回调请求头
        String timestamp = request.getHeader("Wechatpay-Timestamp");
        String nonce = request.getHeader("Wechatpay-Nonce");
        String serialNo = request.getHeader("Wechatpay-Serial");
        String signature = request.getHeader("Wechatpay-Signature");

        // 获取回调支付密文
        String result = WxPayUtils.readData(request);

        // 验证签名并对请求参数解密
        plainText =
            WxPayUtils.verifyNotify(serialNo, result, signature, nonce, timestamp, WxConstants.PAY_SECRET_V3);

        if (StringUtils.isNotBlank(plainText)) {
            JSONObject payResult = JSON.parseObject(plainText);
            // TODO 用户支付成功业务处理(更改本地订单支付状态、添加购买记录、删除购物车内对应商品)
            // 注: 业务处理应先查本地数据库订单支付状态 未处理时再进行后面业务处理 微信支付可能会多次调用回调接口

            response.setStatus(200);
            resObj.put("code", "SUCCESS");
            resObj.put("message", "SUCCESS");
        } else {
            log.warn("签名校验失败");
            response.setStatus(500);
            resObj.put("code", "ERROR");
            resObj.put("message", "签名错误");
        }

        // 响应请求结果
        response.setHeader("Content-type", "application/json");
        response.getOutputStream().write(resObj.toJSONString().getBytes(StandardCharsets.UTF_8));
        response.flushBuffer();
    } catch (Exception e) {
        log.error("微信支付回调接口处理失败,plainText:{}", plainText, e);
    }
}

第一次写博文,去掉了业务处理部分,尽量使其简化,写的不好多见谅。


版权声明:本文为weixin_45847916原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。