微信支付——小程序

注意: 个人无法实现微信支付,需是商家或企业

微信文档写的比较乱:

看文档顺序:
1.开发指引
2.JSAPI下单 (因为小程序调用的是JSAPI支付类型)
3.接口规则

左边下拉框的 证书/密钥/签名介绍 可以看一下
一篇特别好的文章 : 公钥,私钥和数字签名这样最好理解

4.签名生成

5.SDK
这里使用的是这个 wechatpay-apache-httpclient 工具包
使用它构造HttpClient。得到的HttpClient在执行请求时将自动携带身份认证信息,并检查应答的微信支付签名。且此SDK还能定时更新平台证书
下面maven导入的也是其依赖包
6.查询订单
7.小程序调起支付API
8.支付通知

小程序微信支付流程图
在这里插入图片描述

由上图可知需写三个API

1.获取支付信息

前端请求下单支付,后端返回支付参数
详情见: JSAPI下单

2.查询订单

通过微信支付订单号商户订单号获取其支付数据
详情见: 查询订单API

微信支付商户号:注册公众号时有的,唯一的
微信支付订单号:微信自动生成的订单号
商户订单号:自己生成的订单号

3.支付通知API

注: yaml文件里配置的 notify-url 为自己写的支付通知API的 @PostMapping 里的地址
详情见: 小程序调起支付API

签名和各类数据的加密解密是微信支付的难点

code

用到的依赖

		<!--微信支付-->
        <dependency>
            <groupId>com.github.wechatpay-apiv3</groupId>
            <artifactId>wechatpay-apache-httpclient</artifactId>
            <version>0.4.2</version>
        </dependency>
        
		<!--Hutool-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.7.16</version>
        </dependency>

配置文件 .yaml
多加如下配置

wechat-payment:
  #微信支付商户号
  merchant-id: "xxxxxx"
  #微信支付证书序列号
  cert-serial-number: "xxxxxx"
  #微信支付API V3秘钥
  api-v3-key: "xxxxxx"
  #微信支付小程序APP ID
  app-id: "xxxxxx"
  #我的证书放在resources下wechat-payment-cert文件夹里
  #微信支付公钥证书位置
  public-cert-location: "/wechat-payment-cert/apiclient_cert.pem"
  #微信支付私钥证书位置
  private-key-location: "/wechat-payment-cert/apiclient_key.pem"
  #支付回调地址
  notify-url: "http://127.0.0.1/wechat-payment/paymentNotify"

调用到的工具类:
用来对签名和数据进行加密解密

import org.apache.commons.codec.binary.Base64;
import org.springframework.util.Base64Utils;

import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.security.*;

public class SignUtil {

    //编码方式
    private static final String ENCODING = "UTF-8";
    //加密方式
    private static final String SIGNATURE_ALGORITHM = "SHA256withRSA";

    //加密
    //调用顺序:先加密后编码:先 sign256 后 encodeBase64
    public static byte[] sign256(String data, PrivateKey privateKey) throws NoSuchAlgorithmException, InvalidKeyException,
            SignatureException, UnsupportedEncodingException {

        Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);

        signature.initSign(privateKey);

        signature.update(data.getBytes(ENCODING));

        return signature.sign();
    }

    //验证签名正确与否
    //调用顺序:先解码后验证:先decodeBase64后verify256
    public static boolean verify256(String data, byte[] sign, PublicKey publicKey) {
        if (data == null || sign == null || publicKey == null) {
            return false;
        }

        try {
            Signature signCheck = Signature.getInstance(SIGNATURE_ALGORITHM);
            signCheck.initVerify(publicKey);
            signCheck.update(data.getBytes(StandardCharsets.UTF_8));
            return signCheck.verify(sign);
        } catch (Exception e) {
            return false;
        }
    }

    //编码
    public static String encodeBase64(byte[] bytes) {
        return new String(Base64.encodeBase64(bytes));
    }

    //解码
    public static byte[] decodeBase64(String data) {
        byte[] result = null;
        try {
            result = Base64.decodeBase64(data);
        } catch (Exception e) {
            return null;
        }
        return result;
    }


    /**
     * 解密请求回调
     * 参考https://developers.weixin.qq.com/community/develop/article/doc/000eeac2ba4898a9fd6be8b175bc13
     *
     * @param apiV3Key       微信API V3 秘钥
     * @param associatedData 附加数据
     * @param nonce          随机串
     * @param ciphertext     数据密文
     * @return 解密后的数据密文
     */
    public static String decryptWechatPaymentResponseBody(String apiV3Key, String associatedData, String nonce, String ciphertext) {
        try {
            Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");

            SecretKeySpec key = new SecretKeySpec(apiV3Key.getBytes(StandardCharsets.UTF_8), "AES");
            GCMParameterSpec spec = new GCMParameterSpec(128, nonce.getBytes(StandardCharsets.UTF_8));

            cipher.init(Cipher.DECRYPT_MODE, key, spec);
            cipher.updateAAD(associatedData.getBytes(StandardCharsets.UTF_8));

            byte[] bytes;
            try {
                bytes = cipher.doFinal(Base64Utils.decodeFromString(ciphertext));
            } catch (GeneralSecurityException e) {
                throw new IllegalArgumentException(e);
            }
            return new String(bytes, StandardCharsets.UTF_8);
        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
            throw new IllegalStateException(e);
        } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
            throw new IllegalArgumentException(e);
        }
    }
}

用到的实体类

@Data
@AllArgsConstructor
@NoArgsConstructor
public class WeChatPaymentOrder {

    /**
     * 商品描述 string[1,127]
     */
    String description;

    /**
     * 商户订单号 string[6,32]
     */
    String outTradeNumber;

    /**
     * 商户订单号 string[6,32]
     * 商户系统内部订单号,只能是数字、大小写字母_-*且在同一个商户号下唯一
     */
    String notifyUrl;

    /**
     * 总金额,单位为分
     */
    Integer totalAmount;

    /**
     * 支付者
     */
    String userOpenId;

}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class WechatPaymentData {
    /**
     * 时间戳
     * 标准北京时间,时区为东八区,自1970年1月1日 0点0分0秒以来的秒数。注意:部分系统取到的值为毫秒级,需要转换成秒(10位数字)。
     */
    String timeStamp;

    /**
     * 随机字符串
     */
    String nonceStr;

    /**
     * 订单详情扩展字符串
     * 小程序下单接口返回的prepay_id参数值,提交格式如:prepay_id=***
     */
    String orderPackage;

    /**
     * 签名方式
     * 签名类型,默认为RSA,仅支持RSA。
     */
    String signType;

    /**
     * 签名
     * 签名,使用字段appId、timeStamp、nonceStr、package计算得出的签名值
     */
    String paySign;
}

controller层

@Slf4j
@CrossOrigin
@RestController
public class WechatPaymentController {

    @Resource
    private WechatPaymentService wechatPaymentService;

    @Resource
    private WechatUserService wechatUserService;

	/**
     * 1.获取支付信息
     *
     * @param loginCode   登录Code(与userOpenId二选一)
     * @param userOpenId  用户的微信OpenId (与loginCode二选一)
     * @param description 商品描述
     * @param totalAmount 总价
     * @return 支付数据(返回支付参数)
     */
    @PostMapping("/wechat-payment/getWechatPaymentData")
    public WechatPaymentData getWechatPaymentData(String loginCode,
                                                  String userOpenId,
                                                  @RequestParam String description,
                                                  @RequestParam Integer totalAmount) {
        //验证描述长度
        int descriptionLength = description.length();
        if (descriptionLength < 1 || descriptionLength > 127) {
            log.warn("description字段长度限制为[1,126]");
            return null;
        }

        //如果是loginCode的话就需要转化为OpenId
        if (loginCode != null) {
            userOpenId = wechatUserService.getUserWechatOpenId(loginCode);

        }
        //如果为空则不继续执行了
        if (userOpenId == null) {
            log.warn("无法获取用户标识符");
            return null;
        } else {
            log.info("已获取用户OpenId:" + userOpenId);
        }

        return wechatPaymentService.getWechatPaymentData(userOpenId, description, totalAmount);
    }

    /**
     * 2.查询支付记录
     *
     * @param transactionId  微信支付订单号(与商户订单号二选一)
     * @param outTradeNumber 商户订单号(与微信支付订单号二选一)
     * @return 支付数据
     */
    @GetMapping("/wechat-payment/getTransaction")
    public JSONObject getTransaction(String transactionId, String outTradeNumber) {

        if (transactionId == null && outTradeNumber == null) {
            log.warn("参数错误");
            return null;
        }

        return wechatPaymentService.getTransaction(transactionId, outTradeNumber);
    }

	/**
     * 3.微信支付回调
     *
     * @param body 微信提供的支付回调数据
     */
    @PostMapping("/wechat-payment/paymentNotify")
    public void paymentNotify(
            @RequestHeader Map<String, String> headers,
            @RequestBody String body) {

        //验证签名
        boolean signResult = wechatPaymentService.verifyHttpSign(headers, body);
        if (!signResult) {
            log.warn("签名验证失败");
            return;
        }

        //数据处理
        JSONObject notifyObj = JSON.parseObject(body);
        wechatPaymentService.paymentNotify(notifyObj);

		
        //通过body数据判断是否支付成功
        //将paymentNotify方法改为JSONObject返回值,返回数据处理(解密)过后的notifyObj
        //做支付成功判断并插入数据库
        // ...
    }

}

service层:

@Slf4j
@Service
public class WechatPaymentService {

    /**
     * 微信支付商户号
     */
    @Value("${wechat-payment.merchant-id}")
    String paymentMerchantId;
    /**
     * 微信支付API V3秘钥
     */
    @Value("${wechat-payment.api-v3-key}")
    String paymentApiV3Key;
    /**
     * 微信支付小程序APP ID
     */
    @Value("${wechat-payment.app-id}")
    String paymentAppId;
    /**
     * 支付回调地址
     */
    @Value("${wechat-payment.notify-url}")
    String paymentNotifyUrl;

    /**
     * 验证器
     */
    @Resource
    Verifier merchantVerifier;

    /**
     * 私钥
     */
    @Resource
    PrivateKey merchantPrivateKey;

    /**
     * Http客户端
     */
    @Resource
    CloseableHttpClient httpClient;


    /**
     * 验证签名请求头
     *
     * @param headers      HTTP请求头
     * @param responseBody 应答主体
     * @return 是否通过验证
     */
    public Boolean verifyHttpSign(Map<String, String> headers, String responseBody) {

        /*
         * 注意,所有的HTTP请求的header都是小写字母
         * 微信文档中给的不对
         * */

        log.info("获取到的平台Header");
        log.info(String.valueOf(headers));

        //获取可用的微信平台证书
        X509Certificate wechatCertificate = merchantVerifier.getValidCertificate();
        //获取微信平台证书的序列号(16进制转换)
        String wechatCertificateSerialNumber = wechatCertificate.getSerialNumber().toString(16);

        //获取传递的证书序列号
        String certSerial = headers.get("wechatpay-serial");
        //验证证书序列号(忽略大小写)
        if (!certSerial.equalsIgnoreCase(wechatCertificateSerialNumber)) {
            log.error("证书序列号不一致");
            return false;
        }

        //应答时间戳(这里一定要写)
        String timeStamp = headers.get("wechatpay-timestamp");
        //应答随机串
        String nonce = headers.get("wechatpay-nonce");

        //构建应答的验签名串
        String signText = timeStamp + "\n" + nonce + "\n" + responseBody + "\n";
        log.info("应答的验签名串");
        log.info(signText);

        //微信支付的应答签名(BASE64加密后)
        String base64Signature = headers.get("wechatpay-signature");

        //获取签名
        byte[] sign = SignUtil.decodeBase64(base64Signature);

        //验证
        return SignUtil.verify256(signText, sign, wechatCertificate.getPublicKey());
    }

    /**
     * 查询支付记录
     *
     * @param transactionId  微信支付订单号
     * @param outTradeNumber 商户订单号
     * @return 支付记录
     */
    public JSONObject getTransaction(String transactionId, String outTradeNumber) {
        String url;

        if (transactionId != null) {
            //微信支付订单号模式
            url = "https://api.mch.weixin.qq.com/v3/pay/transactions/id/transaction_id?mchid=merchantId";
            url = url.replace("transaction_id", transactionId);
            //微信支付商户号
            url = url.replace("merchantId", paymentMerchantId);
        } else if (outTradeNumber != null) {
            //商户订单号模式
            url = "https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/out_trade_no?mchid=merchantId";
            url = url.replace("out_trade_no", outTradeNumber);
            url = url.replace("merchantId", paymentMerchantId);
        } else {
            log.error("错误的数据请求");
            return null;
        }

        //组装请求参数
        HttpGet httpGet = new HttpGet(url);
        httpGet.addHeader("Accept", "application/json");
        httpGet.addHeader("Content-type", "application/json; charset=utf-8");

        //发送数据
        CloseableHttpResponse response = null;
        try {
            response = httpClient.execute(httpGet);
        } catch (IOException e) {
            e.printStackTrace();
        }

        //解析数据
        try {
            if (response != null) {
                String bodyAsString = EntityUtils.toString(response.getEntity());
                return JSON.parseObject(bodyAsString);
            } else {
                log.error("数据为空");
                return null;
            }
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }


    /**
     * 处理支付回调
     *
     * @param jsonObject 数据
     */
    public void paymentNotify(JSONObject jsonObject) {
        //获取通知数据
        JSONObject resource = jsonObject.getJSONObject("resource");
        //附加数据
        String associatedData = resource.getString("associated_data");
        //随机串
        String nonce = resource.getString("nonce");
        //数据密文
        String ciphertext = resource.getString("ciphertext");

        //获得解密数据
        String decryptString = SignUtil.decryptWechatPaymentResponseBody(paymentApiV3Key, associatedData, nonce, ciphertext);
        //解析数据
        JSONObject decryptData = JSONObject.parseObject(decryptString);

        log.info("获得解析数据:");
        log.info(String.valueOf(decryptData));
    }

    /**
     * 获取微信支付数据
     *
     * @param userOpenId  用户微信OpenId
     * @param description 描述
     * @param totalAmount 总价
     * @return 支付数据(返回支付参数)
     */
    public WechatPaymentData getWechatPaymentData(String userOpenId, String description, Integer totalAmount) {
        //生成订单号
        String outTradeNumber = IdUtil.simpleUUID();

        //组装订单数据:商品描述、商户订单号、商户订单号、支付金额、支付者
        WeChatPaymentOrder paymentOrder = new WeChatPaymentOrder(
                description,
                outTradeNumber,
                paymentNotifyUrl,
                totalAmount,
                userOpenId
        );

        //获取PrePayId
        String prePayID = generatePrePayId(paymentOrder);
        if (prePayID == null) {
            log.error("无法获取PrePayID");
            return null;
        }

        //获取当前时间戳(微信要求10位)
        String timeStamp = String.valueOf(System.currentTimeMillis() / 1000);
        //订单详情扩展字符串
        String orderPackage = "prepay_id=" + prePayID;
        //签名方式
        String signType = "RSA";
        //待签名字符串
        String paySignText = paymentAppId + "\n" + timeStamp + "\n" + outTradeNumber + "\n" + orderPackage + "\n";
        log.info("待签名字符串:");
        log.info(paySignText);

        //签名结果
        String signResult;

        try {
            //获取加密后的签名/对签名进行加密
            byte[] sign256 = SignUtil.sign256(paySignText, merchantPrivateKey);
            signResult = SignUtil.encodeBase64(sign256);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }

        log.info("签名结果:");
        log.info(signResult);

        return new WechatPaymentData(
                timeStamp,
                outTradeNumber,
                orderPackage,
                signType,
                signResult
        );
    }

    /**
     * 生成PrePayId
     *
     * @param data 支付数据
     * @return 获取PrePayId
     */
    private String generatePrePayId(WeChatPaymentOrder data) {

        try {

            //组装数据
            JSONObject rootNode = new JSONObject();

            //应用ID
            rootNode.put("appid", paymentAppId);
            //直连商户号
            rootNode.put("mchid", paymentMerchantId);
            //商品描述 string[1,127]
            rootNode.put("description", data.getDescription());
            //商户订单号 string[6,32]
            //商户系统内部订单号,只能是数字、大小写字母_-*且在同一个商户号下唯一
            rootNode.put("out_trade_no", data.getOutTradeNumber());
            //通知地址
            rootNode.put("notify_url", data.getNotifyUrl());

            //订单金额
            JSONObject amountObject = new JSONObject();
            amountObject.put("total", data.getTotalAmount());
            rootNode.put("amount", amountObject);

            //支付者
            JSONObject payerObject = new JSONObject();
            payerObject.put("openid", data.getUserOpenId());
            rootNode.put("payer", payerObject);

            //组装请求参数
            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(rootNode.toString(), "UTF-8"));

            //发送数据
            CloseableHttpResponse response = httpClient.execute(httpPost);

            //解析数据
            String bodyAsString = EntityUtils.toString(response.getEntity());
            JSONObject jsonObject = JSON.parseObject(bodyAsString);
            String prePayId = jsonObject.getString("prepay_id");

            if (prePayId != null) {
                log.info("已生成PrePayId:" + prePayId);
                return prePayId;
            } else {
                log.error("无法获取PrePayId,错误信息:" + bodyAsString);
                return null;
            }

        } catch (Exception exception) {
            exception.printStackTrace();
            return null;
        }

    }

}
  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值