JAVA微信小程序支付(V3)

JAVA微信小程序支付(V3)

1.yml配置

spring:
  application:
    name: jeecg-system
  profiles:
    active: prod

small_wechat:
  sessionHost: https://api.weixin.qq.com/sns/jscode2session
  appId: 小程序APPId
  secret: 秘钥
  grantType: authorization_code  
  pay:
    info:
      # 统一下单接口
      payUrl: https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi
      # 微信商户id
      mchId: xxxx
      # 支付回调
      notifyUrl: xxx - 必须是https
      # 证书序列号
      certificateSerialNo: xxx
      # 私钥存放路径
      # privateKeyPath: D:/xxx     -- windows
      privateKeyPath: xxxx         # linux  - 存放证书文件的路径
      # v3秘钥key
      wechatV3Key: xxx
      # 微信获取平台证书列表地址
      wechatCertificatesUrl: https://api.mch.weixin.qq.com/v3/certificates

2 . 小程序支付配置类

package org.jeecg.modules.wxpay.config;

import org.jeecg.common.exception.RequestWechatException;
import org.jeecg.modules.wxpay.utils.PayResponseUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;

import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.text.ParseException;
import java.util.Map;

/**
 * 微信支付配置类
 *
 * @Author 王超
 * @Version V1.0.0
 * @Date 2021/8/23 17:57
 */
@Component
@PropertySource("classpath:application.yml")
public class WeixinConfig implements InitializingBean {

    @Value("${small_wechat.pay.info.payUrl}")
    private String payUrl;

    @Value("${small_wechat.pay.info.mchId}")
    private String mchId;

    @Value("${small_wechat.pay.info.notifyUrl}")
    private String notifyUrl;

    @Value("${small_wechat.appId}")
    private String appId;

    @Value("${small_wechat.pay.info.certificateSerialNo}")
    private String certificateSerialNo;

    @Value("${small_wechat.pay.info.privateKeyPath}")
    private String privateKeyPath;

    @Value("${small_wechat.pay.info.wechatV3Key}")
    private String wechatV3Key;

    @Value("${small_wechat.pay.info.wechatCertificatesUrl}")
    private String wechatCertificatesUrl;

    /**
     * 统一支付接口地址
     */
    public static String PAY_URL;

    /**
     * 商户id
     */
    public static String MCH_ID;

    /**
     * 回调地址
     */
    public static String NOTIFY_URL;

    /**
     * 微信APPid
     */
    public static String WX_APP_ID;

    /**
     * 私钥地址
     */
    public static String PRIVATE_KEY_PATH;

    /**
     * 证书序列号
     */
    public static String CERTI_SERIAL_NO;

    /**
     * 微信获取平台证书列表地址
     */
    public static String CERTI_FICATES_URL;

    /**
     * V3秘钥
     */
    public static String WECHATV3KEY;

    /**
     * 证书map
     */
    public static Map<String, X509Certificate> CERTI_MAP;

    @Override
    public void afterPropertiesSet() throws ParseException, RequestWechatException, CertificateException {
        WX_APP_ID = appId;
        PAY_URL = payUrl;
        MCH_ID = mchId;
        NOTIFY_URL = notifyUrl;
        CERTI_SERIAL_NO = certificateSerialNo;
        PRIVATE_KEY_PATH = privateKeyPath;
        WECHATV3KEY = wechatV3Key;
        CERTI_FICATES_URL = wechatCertificatesUrl;
        CERTI_MAP = PayResponseUtils.refreshCertificate();
    }
}

3 .定义常量类

package org.jeecg.modules.wxpay.constant;

/**
 * @Author 王超
 * @Version V1.0.0
 * @Date 2021/8/23 17:07
 */
public class PayConstant {

    /**
     * 通知类型 支付成功通知的类型为TRANSACTION.SUCCESS
     */
    public static final String TRANSACTION_SUCCESS = "TRANSACTION.SUCCESS";
}

4 . 回调和签名的工具类

package org.jeecg.modules.wxpay.utils;

import org.jeecg.common.exception.RequestWechatException;
import org.jeecg.modules.wxpay.config.WeixinConfig;
import org.springframework.util.Base64Utils;

import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.Signature;
import java.security.SignatureException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.text.ParseException;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * 回调工具类
 *
 * @Author 王超
 * @Version V1.0.0
 * @Date 2021/8/23 18:06
 */
public class CallBackUtil {

    /**
     * 验证微信签名
     *
     * @param request request
     * @param body    body
     * @return boolean
     */
    public static boolean verifiedSign(HttpServletRequest request, String body) throws ParseException, RequestWechatException, CertificateException, NoSuchAlgorithmException, InvalidKeyException, SignatureException {
        //微信返回的证书序列号
        String serialNo = request.getHeader("Wechatpay-Serial");
        //微信返回的随机字符串
        String nonceStr = request.getHeader("Wechatpay-Nonce");
        //微信返回的时间戳
        String timestamp = request.getHeader("Wechatpay-Timestamp");
        //微信返回的签名
        String wechatSign = request.getHeader("Wechatpay-Signature");
        //组装签名字符串
        String signStr = Stream.of(timestamp, nonceStr, body)
                .collect(Collectors.joining("\n", "", "\n"));
        //当证书容器为空 或者 响应提供的证书序列号不在容器中时  就应该刷新了
        if (WeixinConfig.CERTI_MAP.isEmpty() || !WeixinConfig.CERTI_MAP.containsKey(serialNo)) {
            WeixinConfig.CERTI_MAP = PayResponseUtils.refreshCertificate();
        }
        //根据序列号获取平台证书
        X509Certificate certificate = WeixinConfig.CERTI_MAP.get(serialNo);
        //获取失败 验证失败
        if (certificate == null) {
            return false;
        }
        //SHA256withRSA签名
        Signature signature = Signature.getInstance("SHA256withRSA");
        signature.initVerify(certificate);
        signature.update(signStr.getBytes());
        //返回验签结果
        return signature.verify(Base64Utils.decodeFromString(wechatSign));
    }

    /**
     * 获取请求文体
     *
     * @param request request
     * @return 返回请求问
     * @throws IOException io
     */
    public static String getRequestBody(HttpServletRequest request) throws IOException {
        ServletInputStream stream;
        BufferedReader reader = null;
        StringBuffer sb = new StringBuffer();
        try {
            stream = request.getInputStream();
            // 获取响应
            reader = new BufferedReader(new InputStreamReader(stream));
            String line;
            while ((line = reader.readLine()) != null) {
                sb.append(line);
            }
        } catch (IOException e) {
            throw new IOException("读取返回支付接口数据流出现异常!");
        } finally {
            if (reader != null) {
                reader.close();
            }
        }
        return sb.toString();
    }
}
// =============================================请求验签工具类=======================================================
package org.jeecg.modules.wxpay.utils;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.apache.http.HttpResponse;
import org.apache.http.client.config.RequestConfig;
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.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.jeecg.common.exception.RequestWechatException;
import org.jeecg.modules.wxpay.config.WeixinConfig;
import org.springframework.util.Base64Utils;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.*;
import java.security.cert.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * @Author 王超
 * @Version V1.0.0
 * @Date 2021/8/23 17:46
 */
public class PayRequestUtils {

    /**
     * V3  SHA256withRSA 移动端请求签名.
     *
     * @param timestamp 当前时间戳
     * @param nonceStr  随机字符串
     * @param prepayId  统一下单接口返回的prepay_id参数值,提交格式如:prepay_id=***
     * @return 签名
     */
    public static String appSign(long timestamp, String nonceStr, String prepayId) throws InvalidKeyException, NoSuchAlgorithmException, SignatureException, IOException {
        String signatureStr = Stream.of(WeixinConfig.WX_APP_ID, String.valueOf(timestamp), nonceStr, prepayId)
                .collect(Collectors.joining("\n", "", "\n"));
        return getSign(signatureStr);
    }

    /**
     * V3  SHA256withRSA http请求签名.
     *
     * @param method       请求方法  GET  POST PUT DELETE 等
     * @param canonicalUrl 请求地址
     * @param timestamp    当前时间戳   因为要配置到TOKEN 中所以 签名中的要跟TOKEN 保持一致
     * @param nonceStr     随机字符串  要和TOKEN中的保持一致
     * @param body         请求体 GET 为 "" POST 为JSON
     * @return the string
     */
    private static String httpSign(String method, String canonicalUrl, String body, long timestamp, String nonceStr) throws InvalidKeyException, NoSuchAlgorithmException, SignatureException, IOException {
        URL url = new URL(canonicalUrl);
        String signUrl;
        if ("GET".equals(method) && url.getQuery() != null) {
            signUrl = url.getPath() + "?" + url.getQuery();
        } else {
            signUrl = url.getPath();
        }
        String signatureStr = Stream.of(method, signUrl, String.valueOf(timestamp), nonceStr, body)
                .collect(Collectors.joining("\n", "", "\n"));
        return getSign(signatureStr);
    }

    /**
     * 获取签名
     *
     * @param signatureStr 签名串
     * @return 签名
     * @throws InvalidKeyException      InvalidKeyException
     * @throws NoSuchAlgorithmException NoSuchAlgorithmException
     * @throws SignatureException       SignatureException
     * @throws IOException              IOException
     */
    public static String getSign(String signatureStr) throws InvalidKeyException, NoSuchAlgorithmException, SignatureException, IOException {
        Signature sign = Signature.getInstance("SHA256withRSA");
        sign.initSign(SignUtil.getPrivateKey(WeixinConfig.PRIVATE_KEY_PATH));
        sign.update(signatureStr.getBytes(StandardCharsets.UTF_8));
        return Base64Utils.encodeToString(sign.sign());
    }

    /**
     * 生成Token http请求
     *
     * @param method       请求方法  GET  POST PUT DELETE 等
     * @param canonicalUrl 请求地址
     * @param body         请求体 GET 为 "" POST 为JSON
     * @return the string
     */
    private static String httpToken(String method, String canonicalUrl, String body) throws IOException, NoSuchAlgorithmException, InvalidKeyException, SignatureException {
        Long timestamp = System.currentTimeMillis() / 1000;
        String nonceStr = UUID.randomUUID().toString().replace("-", "");
        String signature = httpSign(method, canonicalUrl, body, timestamp, nonceStr);
        final String TOKEN_PATTERN = "WECHATPAY2-SHA256-RSA2048 mchid=\"%s\",nonce_str=\"%s\",timestamp=\"%d\",serial_no=\"%s\",signature=\"%s\"";
        // 生成token
        return String.format(TOKEN_PATTERN,
                WeixinConfig.MCH_ID,
                nonceStr, timestamp, WeixinConfig.CERTI_SERIAL_NO, signature);
    }

    public static <T> T wechatHttpPost(String url, String jsonStr, Class<T> t) throws RequestWechatException {
        //创建httpClient实例
        CloseableHttpClient httpClient = HttpClients.createDefault();
        T instance = null;
        try {
            instance = t.newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }
        //生成Token http请求
        String token = null;
        try {
            token = httpToken("POST", url, jsonStr);
        } catch (Exception e) {
            e.printStackTrace();
        }
        HttpPost httppost = new HttpPost(url);
        httppost.addHeader("Content-Type", "application/json;charset=UTF-8");
        httppost.addHeader("Accept", "application/json");
        httppost.addHeader("Authorization", token);
        //设置连接超时时间和数据获取超时时间--单位:ms
        RequestConfig requestConfig = RequestConfig.custom()
                .setConnectTimeout(5000000).setConnectionRequestTimeout(5000000)
                .setSocketTimeout(5000000).build();
        httppost.setConfig(requestConfig);
        //设置http request body请求体
        if (null != jsonStr) {
            //解决中文乱码问题
            StringEntity myEntity = new StringEntity(jsonStr, "UTF-8");
            httppost.setEntity(myEntity);
        }
        HttpResponse response;
        try {
            response = httpClient.execute(httppost);
        } catch (IOException e) {
            e.printStackTrace();
            throw new RequestWechatException();
        }
        String result = null;
        try {
            result = EntityUtils.toString(response.getEntity(), "UTF-8");
        } catch (IOException e) {
            e.printStackTrace();
        }
        //得到返回的字符串
        JSONObject jsonObject = JSON.parseObject(result);
        if (instance instanceof JSONObject) {
            return (T) jsonObject;
        }

        T resultBean = (T) JSONObject.parseObject(jsonObject.toString(), t);
        try {
            httpClient.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return resultBean;
    }


    /**
     * httpget调用http接口
     *
     * @param url     地址
     * @param jsonStr json穿
     * @param t       t
     * @param <T>     <T>
     * @return 结果
     */
    public static <T> T wechatHttpGet(String url, String jsonStr, Class<T> t) throws RequestWechatException {
        String result = "";
        T instance = null;
        try {
            instance = t.newInstance();
        } catch (InstantiationException | IllegalAccessException e) {
            e.printStackTrace();
        }
        //生成Token http请求
        String token = null;
        try {
            token = httpToken("GET", url, jsonStr);
        } catch (Exception e) {
            e.printStackTrace();
        }
        //创建httpClient实例
        CloseableHttpClient httpClient = HttpClients.createDefault();
        //创建实例方法
        HttpGet httpget = new HttpGet(url);
        httpget.addHeader("Content-Type", "application/json;charset=UTF-8");
        httpget.addHeader("Accept", "application/json");
        httpget.addHeader("Authorization", token);
        HttpResponse response;
        try {
            response = httpClient.execute(httpget);
        } catch (IOException e) {
            throw new RequestWechatException();
        }

        //如果状态码为200,就是正常返回
        if (response.getStatusLine().getStatusCode() == 200) {
            try {
                result = EntityUtils.toString(response.getEntity(), "UTF-8");
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        JSONObject jsonObject = JSON.parseObject(result);
        if (instance instanceof JSONObject) {
            return (T) jsonObject;
        }

        T resultBean = (T) JSONObject.parseObject(jsonObject.toString(), t);
        try {
            httpClient.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return resultBean;
    }

    /**
     * 获取证书。
     *
     * @param fis 证书文件流
     * @return X509证书
     */
    public static X509Certificate getCertificate(InputStream fis) throws IOException {
        BufferedInputStream bis = new BufferedInputStream(fis);
        try {
            CertificateFactory cf = CertificateFactory.getInstance("X509");
            X509Certificate cert = (X509Certificate) cf.generateCertificate(bis);
            cert.checkValidity();
            return cert;
        } catch (CertificateExpiredException e) {
            throw new RuntimeException("证书已过期", e);
        } catch (CertificateNotYetValidException e) {
            throw new RuntimeException("证书尚未生效", e);
        } catch (CertificateException e) {
            throw new RuntimeException("无效的证书文件", e);
        } finally {
            bis.close();
        }
    }

    /**
     * 获取私钥。
     *
     * @param filename 私钥文件路径  (required)
     * @return 私钥对象
     */
    public static PrivateKey getPrivateKey(String filename) throws IOException {

        String content = new String(Files.readAllBytes(Paths.get(filename)), "utf-8");
        try {
            String privateKey = content.replace("-----BEGIN PRIVATE KEY-----", "")
                    .replace("-----END PRIVATE KEY-----", "")
                    .replaceAll("\\s+", "");

            KeyFactory kf = KeyFactory.getInstance("RSA");
            return kf.generatePrivate(
                    new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey)));
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("当前Java环境不支持RSA", e);
        } catch (InvalidKeySpecException e) {
            throw new RuntimeException("无效的密钥格式");
        }
    }
}
// ============================================微信付款返回数据工具类==========================================================
package org.jeecg.modules.wxpay.utils;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.jeecg.common.exception.RequestWechatException;
import org.jeecg.modules.wxpay.config.WeixinConfig;
import org.jeecg.modules.wxpay.model.CertificateVO;
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.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 微信付款返回数据工具类
 *
 * @Author 王超
 * @Version V1.0.0
 * @Date 2021/8/24 9:21
 */
public class PayResponseUtils {

    /**
     * 用微信V3密钥解密响应体.
     *
     * @param associatedData response.body.data[i].encrypt_certificate.associated_data
     * @param nonce          response.body.data[i].encrypt_certificate.nonce
     * @param ciphertext     response.body.data[i].encrypt_certificate.ciphertext
     * @return the string
     */
    public static String decryptResponseBody(String associatedData, String nonce, String ciphertext) {
        try {
            Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");

            SecretKeySpec key = new SecretKeySpec(WeixinConfig.WECHATV3KEY.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);
        }
    }

    /**
     * 获取平台证书Map
     *
     * @return 证书Map
     */
    public static Map<String, X509Certificate> refreshCertificate() throws ParseException, CertificateException, RequestWechatException {
        //获取平台证书json
        JSONObject jsonObject = PayRequestUtils.wechatHttpGet(WeixinConfig.CERTI_FICATES_URL, "", JSONObject.class);
        List<CertificateVO> certificateList = JSON.parseArray(jsonObject.getString("data"), CertificateVO.class);
        //最新证书响应实体类
        CertificateVO newestCertificate = null;
        //最新时间
        Date newestTime = null;
        for (CertificateVO certificate : certificateList) {
            SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
            //如果最新时间是null
            if (newestTime == null) {
                newestCertificate = certificate;
                //设置最新启用时间
                newestTime = formatter.parse(certificate.getEffective_time());
            } else {
                Date effectiveTime = formatter.parse(certificate.getEffective_time());
                //如果启用时间大于最新时间
                if (effectiveTime.getTime() > newestTime.getTime()) {
                    //更换最新证书响应实体类
                    newestCertificate = certificate;
                }
            }
        }

        CertificateVO.EncryptCertificate encryptCertificate = newestCertificate.getEncrypt_certificate();
        //获取证书字公钥
        String publicKey = decryptResponseBody(encryptCertificate.getAssociated_data(), encryptCertificate.getNonce(), encryptCertificate.getCiphertext());
        CertificateFactory cf = CertificateFactory.getInstance("X509");

        //获取证书
        ByteArrayInputStream inputStream = new ByteArrayInputStream(publicKey.getBytes(StandardCharsets.UTF_8));
        X509Certificate certificate = null;
        try {
            certificate = (X509Certificate) cf.generateCertificate(inputStream);
        } catch (CertificateException e) {
            e.printStackTrace();
        }
        //保存微信平台证书公钥
        Map<String, X509Certificate> certificateMap = new ConcurrentHashMap<>();
        // 清理HashMap
        certificateMap.clear();
        // 放入证书
        certificateMap.put(newestCertificate.getSerial_no(), certificate);
        return certificateMap;
    }
}
// ===============================这有一个官方文档上的例子,我把他整理了一下,可用于签名和验签======================================================================
package org.jeecg.modules.wxpay.utils;

import okhttp3.HttpUrl;
import org.jeecg.modules.wxpay.config.WeixinConfig;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;

public class SignUtil {
    private static String SCHEMA = "WECHATPAY2-SHA256-RSA2048 ";

    public static String getToken(String urlPath, String method, String param, long timestamp, String nonceStr) throws IOException, NoSuchAlgorithmException, SignatureException, InvalidKeyException {
        HttpUrl url = HttpUrl.parse(urlPath);
        if (url == null) {
            throw new RuntimeException("url = null");
        }
        String message = buildMessage(method, url, timestamp, nonceStr, param);
        String signature = sign(message.getBytes(StandardCharsets.UTF_8));

        return SignUtil.SCHEMA + "mchid=\"" + WeixinConfig.MCH_ID + "\","
                + "nonce_str=\"" + nonceStr + "\","
                + "timestamp=\"" + timestamp + "\","
                + "serial_no=\"" + WeixinConfig.CERTI_SERIAL_NO + "\","
                + "signature=\"" + signature + "\"";
    }

    public static String getTokenTow(String packageStr, String nonceStr, long timestamp) throws IOException, NoSuchAlgorithmException, InvalidKeyException, SignatureException {

        // 拼接签名字符串
        String signStr = WeixinConfig.WX_APP_ID + "\n" + timestamp + "\n" + nonceStr + "\n" + packageStr + "\n";

        return sign(signStr.getBytes(StandardCharsets.UTF_8));
    }

    public static String sign(byte[] message) throws NoSuchAlgorithmException, SignatureException, IOException, InvalidKeyException {
        Signature sign = Signature.getInstance("SHA256withRSA");
        sign.initSign(SignUtil.getPrivateKey(WeixinConfig.PRIVATE_KEY_PATH));
        sign.update(message);

        return Base64.getEncoder().encodeToString(sign.sign());
    }

    private static String buildMessage(String method, HttpUrl url, long timestamp, String nonceStr, String body) {
        String canonicalUrl = url.encodedPath();
        if (url.encodedQuery() != null) {
            canonicalUrl += "?" + url.encodedQuery();
        }

        return method + "\n"
                + canonicalUrl + "\n"
                + timestamp + "\n"
                + nonceStr + "\n"
                + body + "\n";
    }

    /**
     * 获取私钥。
     *
     * @param filename 私钥文件路径  (required)
     * @return 私钥对象
     */
    public static PrivateKey getPrivateKey(String filename) throws IOException {

        String content = new String(Files.readAllBytes(Paths.get(filename)), StandardCharsets.UTF_8);
        try {
            String privateKey = content.replace("-----BEGIN PRIVATE KEY-----", "")
                    .replace("-----END PRIVATE KEY-----", "")
                    .replaceAll("\\s+", "");

            KeyFactory kf = KeyFactory.getInstance("RSA");
            return kf.generatePrivate(
                    new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey)));
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("当前Java环境不支持RSA", e);
        } catch (InvalidKeySpecException e) {
            throw new RuntimeException("无效的密钥格式");
        }
    }
}
//============================================================================================================
package org.jeecg.modules.wxpay.utils;

import java.math.BigDecimal;
import java.math.RoundingMode;
/**
 * 元转分工具类
 * @Author 王超
 * @Version V1.0.0
 * @Date 2021/8/24 9:32
 */
public class AmountUtil {

    public static String converAmount(BigDecimal fee) {
        //  四舍五入,精确2位小数
        fee = fee.setScale(2, BigDecimal.ROUND_HALF_UP);
        //  转换单位为:分
        fee = fee.multiply(new BigDecimal(100));
        //  取整,过滤小数部分
        fee = fee.setScale(0, RoundingMode.DOWN);
        return String.valueOf(fee);
    }
}

5 . 各种VO

package org.jeecg.modules.wxpay.model;

/**
 * 微信平台证书VO
 *
 * @Author 王超
 * @Version V1.0.0
 * @Date 2021/8/23 17:58
 */
public class CertificateVO {

    /**
     * 平台证书序列号
     */
    private String serial_no;

    /**
     * 平台证书有效时间
     */
    private String effective_time;

    /**
     * 平台证书过期时间
     */
    private String expire_time;

    /**
     * 加密证书
     */
    private EncryptCertificate encrypt_certificate;

    /**
     * 加密证书
     */
    public class EncryptCertificate {

        /**
         * 算法
         */
        private String algorithm;

        /**
         * 随机字符串
         */
        private String nonce;

        /**
         * 相关数据
         */
        private String associated_data;

        /**
         * 密文
         */
        private String ciphertext;

        public String getAlgorithm() {
            return algorithm;
        }

        public void setAlgorithm(String algorithm) {
            this.algorithm = algorithm;
        }

        public String getNonce() {
            return nonce;
        }

        public void setNonce(String nonce) {
            this.nonce = nonce;
        }

        public String getAssociated_data() {
            return associated_data;
        }

        public void setAssociated_data(String associated_data) {
            this.associated_data = associated_data;
        }

        public String getCiphertext() {
            return ciphertext;
        }

        public void setCiphertext(String ciphertext) {
            this.ciphertext = ciphertext;
        }
    }

    public String getSerial_no() {
        return serial_no;
    }

    public void setSerial_no(String serial_no) {
        this.serial_no = serial_no;
    }

    public String getEffective_time() {
        return effective_time;
    }

    public void setEffective_time(String effective_time) {
        this.effective_time = effective_time;
    }

    public String getExpire_time() {
        return expire_time;
    }

    public void setExpire_time(String expire_time) {
        this.expire_time = expire_time;
    }

    public EncryptCertificate getEncrypt_certificate() {
        return encrypt_certificate;
    }

    public void setEncrypt_certificate(EncryptCertificate encrypt_certificate) {
        this.encrypt_certificate = encrypt_certificate;
    }
}
// ==========================================================================================================
package org.jeecg.modules.wxpay.model;

/**
 * 微信通知数据解密后对象
 *
 * @Author 王超
 * @Version V1.0.0
 * @Date 2021/8/23 17:24
 */
public class NotifyResourceVO {

    /**
     * 公众号ID
     */
    private String appid;

    /**
     * 直连商户号
     */
    private String mchid;

    /**
     * 商户订单号
     */
    private String out_trade_no;

    /**
     * 微信支付订单号
     */
    private String transaction_id;

    /**
     * 交易类型
     */
    private String trade_type;

    /**
     * 交易状态
     */
    private String trade_state;

    /**
     * 交易状态描述
     */
    private String trade_state_desc;

    /**
     * 付款银行
     */
    private String bank_type;

    /**
     * 支付完成时间
     */
    private String success_time;

    /**
     * 支付者
     */
    private Payer payer;

    /**
     * 订单金额
     */
    private Amount amount;


    /**
     * 支付者
     */
    public class Payer {
        /**
         * 用户标识
         */
        private String openid;

        public String getOpenid() {
            return openid;
        }

        public void setOpenid(String openid) {
            this.openid = openid;
        }
    }

    /**
     * 订单金额
     */
    public class Amount {
        /**
         * 总金额
         */
        private Integer total;

        /**
         * 用户支付金额
         */
        private Integer payer_total;

        /**
         * 货币类型
         */
        private String currency;

        /**
         * 用户支付币种
         */
        private String payer_currency;

        public Integer getTotal() {
            return total;
        }

        public void setTotal(Integer total) {
            this.total = total;
        }

        public Integer getPayer_total() {
            return payer_total;
        }

        public void setPayer_total(Integer payer_total) {
            this.payer_total = payer_total;
        }

        public String getCurrency() {
            return currency;
        }

        public void setCurrency(String currency) {
            this.currency = currency;
        }

        public String getPayer_currency() {
            return payer_currency;
        }

        public void setPayer_currency(String payer_currency) {
            this.payer_currency = payer_currency;
        }
    }

    public String getAppid() {
        return appid;
    }

    public void setAppid(String appid) {
        this.appid = appid;
    }

    public String getMchid() {
        return mchid;
    }

    public void setMchid(String mchid) {
        this.mchid = mchid;
    }

    public String getOut_trade_no() {
        return out_trade_no;
    }

    public void setOut_trade_no(String out_trade_no) {
        this.out_trade_no = out_trade_no;
    }

    public String getTransaction_id() {
        return transaction_id;
    }

    public void setTransaction_id(String transaction_id) {
        this.transaction_id = transaction_id;
    }

    public String getTrade_type() {
        return trade_type;
    }

    public void setTrade_type(String trade_type) {
        this.trade_type = trade_type;
    }

    public String getTrade_state() {
        return trade_state;
    }

    public void setTrade_state(String trade_state) {
        this.trade_state = trade_state;
    }

    public String getTrade_state_desc() {
        return trade_state_desc;
    }

    public void setTrade_state_desc(String trade_state_desc) {
        this.trade_state_desc = trade_state_desc;
    }

    public String getBank_type() {
        return bank_type;
    }

    public void setBank_type(String bank_type) {
        this.bank_type = bank_type;
    }

    public String getSuccess_time() {
        return success_time;
    }

    public void setSuccess_time(String success_time) {
        this.success_time = success_time;
    }

    public Payer getPayer() {
        return payer;
    }

    public void setPayer(Payer payer) {
        this.payer = payer;
    }

    public Amount getAmount() {
        return amount;
    }

    public void setAmount(Amount amount) {
        this.amount = amount;
    }
}
// ==========================================================================================================
package org.jeecg.modules.wxpay.model;

/**
 * 接收微信支付通知VO
 *
 * @Author 王超
 * @Version V1.0.0
 * @Date 2021/8/23 16:57
 */
public class PayNotifyVO {
    /**
     * 通知的唯一ID
     */
    private String id;

    /**
     * 通知创建时间
     */
    private String create_time;

    /**
     * 通知类型 支付成功通知的类型为TRANSACTION.SUCCESS
     */
    private String event_type;

    /**
     * 通知数据类型 支付成功通知为encrypt-resource
     */
    private String resource_type;

    /**
     * 通知资源数据
     */
    private Resource resource;

    /**
     * 回调摘要
     */
    private String summary;

    /**
     * 通知资源数据
     */
    public class Resource {
        /**
         * 加密算法类型
         */
        private String algorithm;

        /**
         * 数据密文
         */
        private String ciphertext;

        /**
         * 附加数据
         */
        private String associated_data;

        /**
         * 随机串
         */
        private String nonce;

        public String getAlgorithm() {
            return algorithm;
        }

        public void setAlgorithm(String algorithm) {
            this.algorithm = algorithm;
        }

        public String getCiphertext() {
            return ciphertext;
        }

        public void setCiphertext(String ciphertext) {
            this.ciphertext = ciphertext;
        }

        public String getAssociated_data() {
            return associated_data;
        }

        public void setAssociated_data(String associated_data) {
            this.associated_data = associated_data;
        }

        public String getNonce() {
            return nonce;
        }

        public void setNonce(String nonce) {
            this.nonce = nonce;
        }
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getCreate_time() {
        return create_time;
    }

    public void setCreate_time(String create_time) {
        this.create_time = create_time;
    }

    public String getEvent_type() {
        return event_type;
    }

    public void setEvent_type(String event_type) {
        this.event_type = event_type;
    }

    public String getResource_type() {
        return resource_type;
    }

    public void setResource_type(String resource_type) {
        this.resource_type = resource_type;
    }

    public Resource getResource() {
        return resource;
    }

    public void setResource(Resource resource) {
        this.resource = resource;
    }

    public String getSummary() {
        return summary;
    }

    public void setSummary(String summary) {
        this.summary = summary;
    }
}
// ==========================================================================================================
package org.jeecg.modules.wxpay.model;

import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

import java.io.Serializable;
import java.util.Map;

/**
 * @Description: mec_user
 * @Author: chao
 * @Date: 2021-07-22
 * @Version: V1.0
 */
@Data
public class WxParticipate implements Serializable {
    private static final long serialVersionUID = 1L;

    /**
     * 应用ID
     */
    @ApiModelProperty(value = "应用ID")
    private String appid;

    /**
     * 直连商户号
     */
    @ApiModelProperty(value = "直连商户号")
    private String mchid;

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

    /**
     * 商户订单号
     */
    @ApiModelProperty(value = "商户订单号")
    private String out_trade_no;

    /**
     * 附加数据
     */
    @ApiModelProperty(value = "附加数据")
    private String attach;

    /**
     * 通知地址
     */
    @ApiModelProperty(value = "通知地址")
    private String notify_url;

    /**
     * 订单金额
     */
    @ApiModelProperty(value = "订单金额")
    private Map<String, Object> amount;

    /**
     * 支付者
     */
    @ApiModelProperty(value = "支付者")
    private Map<String, Object> payer;

    /**
     * 场景信息
     */
    @ApiModelProperty(value = "场景信息")
    private Map<String, Object> scene_info;

}

6 . controller

package org.jeecg.modules.wxpay.controller;

import com.alibaba.fastjson.JSONObject;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.api.vo.Result;
import org.jeecg.modules.mecorder.service.IMecOrderService;
import org.jeecg.modules.wxpay.constant.PayConstant;
import org.jeecg.modules.wxpay.model.NotifyResourceVO;
import org.jeecg.modules.wxpay.model.PayNotifyVO;
import org.jeecg.modules.wxpay.service.PaymentService;
import org.jeecg.modules.wxpay.utils.CallBackUtil;
import org.jeecg.modules.wxpay.utils.PayResponseUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;

/**
 * @Author 王超
 * @Version V1.0.0
 * @Date 2021/8/23 17:26
 */

@Api(tags = "微信预支付")
@RestController
@Slf4j
@RequestMapping(value = "/wxPay")
public class PaymentController {

    @Autowired
    private PaymentService paymentService;

    @Autowired
    private IMecOrderService iMecOrderService;


    @ResponseBody
    @PostMapping(value = "/prepaid")
    @ApiOperation(value = "微信预支付接口", notes = "微信预支付接口")
    public Result<?> toPay(@RequestParam(name = "orderId") String orderId, HttpServletRequest request) {
        try {
            return Result.OK(paymentService.toPay(orderId, request));
        } catch (Exception e) {
            e.printStackTrace();
            return Result.error(e.getMessage());
        }
    }

    /**
     * 微信支付结果回调地址
     *
     * @param request request
     */
    @PostMapping(value = "/payNotify")
    @Transactional(rollbackFor = Exception.class)
    public Map<String, String> payNotify(HttpServletRequest request) {
        Map<String, String> map = new HashMap<>(2);
        try {
            log.info("接受到微信回调 == > 准备验签");
            //微信返回的请求体
            String body = CallBackUtil.getRequestBody(request);
            //如果验证签名序列号通过
            if (CallBackUtil.verifiedSign(request, body)) {
                log.info("验证签名成功,准备校验通知类型");
                //微信支付通知实体类
                PayNotifyVO payNotifyVO = JSONObject.parseObject(body, PayNotifyVO.class);
                //如果支付成功
                if (PayConstant.TRANSACTION_SUCCESS.equals(payNotifyVO.getEvent_type())) {
                    log.info("通知类型为==>支付成功,开始执行业务代码");
                    // 通知资源数据
                    PayNotifyVO.Resource resource = payNotifyVO.getResource();
                    // 解密后资源数据
                    String notifyResourceStr = PayResponseUtils.decryptResponseBody(resource.getAssociated_data(), resource.getNonce(), resource.getCiphertext());
                    // 通知资源数据对象
                    NotifyResourceVO notifyResourceVO = JSONObject.parseObject(notifyResourceStr, NotifyResourceVO.class);
                    // 处理业务代码 --- > 这个地方根据需求不同自行修改
                    paymentService.callbackHandler(notifyResourceVO);
                } else {
                    log.info("微信返回支付错误摘要:" + payNotifyVO.getSummary());
                }
                //通知微信正常接收到消息,否则微信会轮询该接口
                map.put("code", "SUCCESS");
                map.put("message", "");
                return map;
            }
            log.info("验证签名失败");
        } catch (Exception e) {
            e.printStackTrace();
        }
        return map;
    }

}

7. service

package org.jeecg.modules.wxpay.service;


import org.jeecg.modules.wxpay.model.NotifyResourceVO;

import javax.servlet.http.HttpServletRequest;
import java.util.Map;

/**
 * @Author 王超
 * @Version V1.0.0
 * @Date 2021/8/21 9:30
 */
public interface PaymentService {

    /**
     * 预支付接口
     *
     * @param orderId 订单id
     * @param request request
     * @return prepay_id
     */
    Map toPay(String orderId, HttpServletRequest request);

    /**
     * 回调的业务代码
     *
     * @param notifyResourceVO vo
     */
    void callbackHandler(NotifyResourceVO notifyResourceVO);

}

8.serviceImpl

package org.jeecg.modules.wxpay.service.impl;

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.http.Header;
import cn.hutool.http.HttpRequest;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.TypeReference;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.jeecg.common.constant.MecCategoryConstants;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.util.CheckUtils;
import org.jeecg.common.util.IPUtils;
import org.jeecg.modules.mecorder.constant.OrderStatusConstant;
import org.jeecg.modules.mecorder.entity.MecOrder;
import org.jeecg.modules.mecorder.service.IMecOrderService;
import org.jeecg.modules.wxpay.config.WeixinConfig;
import org.jeecg.modules.wxpay.model.NotifyResourceVO;
import org.jeecg.modules.wxpay.model.WxParticipate;
import org.jeecg.modules.wxpay.service.PaymentService;
import org.jeecg.modules.wxpay.utils.AmountUtil;
import org.jeecg.modules.wxpay.utils.SignUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.servlet.http.HttpServletRequest;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

@Slf4j
@Service
public class WeChatServiceImpl implements PaymentService {

    @Autowired
    private IMecOrderService iMecOrderService;

    @SneakyThrows
    @Override
    public Map toPay(String orderId, HttpServletRequest request) {

        // 根据orderid查询订单
        MecOrder mecOrder = Optional.ofNullable(iMecOrderService.getById(orderId))
                .orElseThrow(() -> new JeecgBootException("预支付失败,订单id : " + orderId + "不存在"));

        log.info("准备预支付,openid is {} , orderid is {}", mecOrder.getOpenId(), orderId);

        String ipAddr = IPUtils.getIpAddr(request);

        // 获取金额
        BigDecimal price = CheckUtils.checkBigDecimal(mecOrder.getPrice(), mecOrder.getCardCount());

        String converPrice = AmountUtil.converAmount(price);

        log.info("调用结束,准备返回参数");
        return packageParameters(mecOrder, ipAddr, converPrice);

    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void callbackHandler(NotifyResourceVO notifyResourceVO) {
        MecOrder order = iMecOrderService.getById(notifyResourceVO.getOut_trade_no());
        if (order != null) {
            // 如果订单状态是未支付状态,改成支付成功
            if ("自己订单的状态".equals(order.getStatus())) {
                //如果付款成功
                MecOrder update = new MecOrder();
                update.setId(order.getId());
                log.info("当前订单:" + order.getId() + " 符合内部校验,准备验证微信返回的交易状态");
                if ("SUCCESS".equals(notifyResourceVO.getTrade_state())) {
                   // 处理成功后的业务
                } else {
                    // 处理失败业务
                }
            }
        }
    }

    @SneakyThrows
    private Map packageParameters(MecOrder mecOrder, String ipAddr, String converPrice) {

        // 定义附加数据
        Map<String, Object> attach = new HashMap<>();
        attach.put("userId", mecOrder.getUserId());

        // 定义订单金额
        Map<String, Object> amount = new HashMap<>();
        amount.put("total", Integer.valueOf(converPrice));

        // 定义支付者
        Map<String, Object> payer = new HashMap<>();
        payer.put("openid", mecOrder.getOpenId());

        // 定义场景信息 -- ip地址
        Map<String, Object> sceneInfo = new HashMap<>();
        sceneInfo.put("payer_client_ip", ipAddr);

        WxParticipate wxParticipate = new WxParticipate();
        wxParticipate.setAppid(WeixinConfig.WX_APP_ID);
        wxParticipate.setMchid(WeixinConfig.MCH_ID);
        wxParticipate.setDescription(mecOrder.getDescription());
        wxParticipate.setOut_trade_no(mecOrder.getId());
        wxParticipate.setAttach(JSONObject.toJSONString(attach));
        wxParticipate.setNotify_url(WeixinConfig.NOTIFY_URL);
        wxParticipate.setAmount(amount);
        wxParticipate.setPayer(payer);
        wxParticipate.setScene_info(sceneInfo);

        // 转换请求参数
        String param = JSON.toJSONString(wxParticipate);

        String nonceStr = RandomUtil.randomStringUpper(32);
        long timestamp = System.currentTimeMillis() / 1000;

        String post = HttpRequest.post(WeixinConfig.PAY_URL)
                .header(Header.AUTHORIZATION, SignUtil.getToken(WeixinConfig.PAY_URL, "POST", param, timestamp, nonceStr))
                .header(Header.CONTENT_TYPE, "application/json")
                .body(param)
                .execute()
                .body();

        if (StringUtils.isBlank(post)) {
            return null;
        }

        Map<String, Object> map = JSON.parseObject(post, new TypeReference<Map<String, Object>>() {
        });

        if (MapUtil.isEmpty(map) || StringUtils.isBlank(MapUtil.get(map, "prepay_id", String.class))) {
            throw new JeecgBootException("预支付订单生成失败,原因:" + post);
        }

        setResultMap(map, timestamp, nonceStr);

        return map;
    }

    @SneakyThrows
    private void setResultMap(Map<String, Object> map, long timestamp, String nonceStr) {

        // 获取微信返回的必要参数
        String prepayId = MapUtil.get(map, "prepay_id", String.class);

        // 拼接package
        String packageStr = "prepay_id=" + prepayId;

        // 获取二次签名
        String tokenTow = SignUtil.getTokenTow(packageStr, nonceStr, timestamp);


        map.put("timeStamp", timestamp + "");
        map.put("nonceStr", nonceStr);
        map.put("package", packageStr);
        map.put("paySign", tokenTow);
    }
}

我自己写的时候参考了下面这个大佬的文章
参考链接-> https://blog.csdn.net/qq_39706128/article/details/111558994

  • 6
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 10
    评论
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值