zhubaomin
6 天以前 026447959141110afd24d18b61da953c62aa2209
pipIrr-platform/pipIrr-web/pipIrr-web-wechat/src/main/java/com/dy/pipIrrWechat/util/PayHelper.java
New file
@@ -0,0 +1,532 @@
package com.dy.pipIrrWechat.util;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.dy.common.webUtil.BaseResponse;
import com.dy.common.webUtil.BaseResponseUtils;
import com.dy.pipIrrGlobal.pojoSe.SeVirtualCard;
import com.dy.pipIrrGlobal.voSe.VoOrders;
import com.dy.pipIrrWechat.result.WechatResultCode;
import com.dy.pipIrrWechat.virtualCard.VirtualCardSv;
import com.dy.pipIrrWechat.wechatpay.PayInfo;
import com.dy.pipIrrWechat.wechatpay.dto.Refund;
import com.dy.pipIrrWechat.wechatpay.dto.RefundRequest;
import com.dy.pipIrrWechat.wechatpay.dto.RefundResponse;
import com.dy.pipIrrWechat.wechatpay.dto.ToRefund;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import javax.crypto.NoSuchPaddingException;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.*;
/**
 * @author ZhuBaoMin
 * @date 2024-07-15 10:11
 * @LastEditTime 2024-07-15 10:11
 * @Description
 */
@Component
@RequiredArgsConstructor
public class PayHelper {
    private final VirtualCardSv virtualCardSv;
    private final RestTemplateUtil restTemplateUtil;
    private static final String CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
    private String checkSessionUrl = PayInfo.checkSessionUrl;
    private String tokenUrl = PayInfo.tokenUrl;
    private String resetUserSessionKeyUrl = PayInfo.resetUserSessionKeyUrl;
    private String notifyUrl = PayInfo.notifyUrl;
    private String schema = PayInfo.schema;
    private String refundUrl = PayInfo.refundUrl;
    // 平台证书公钥
    public Map<String, Certificate> CERTIFICATE_MAP = new HashMap();
    /**
     * 获取32位随机字符串
     * @return 随机串
     */
    public String generateRandomString() {
        Random random = new Random();
        StringBuilder sb = new StringBuilder(32);
        for (int i = 0; i < 32; i++) {
            int index = random.nextInt(CHARACTERS.length());
            sb.append(CHARACTERS.charAt(index));
        }
        return sb.toString();
    }
    /**
     * 获取商户证书私钥对象
     * @param certFileBs 私钥文件内容
     * @return 私钥对象
     * @throws IOException
    public 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("无效的密钥格式");
        }
    }*/
    public PrivateKey getPrivateKey(byte[] certFileBs) throws IOException {
        String content = new String(certFileBs, "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("无效的密钥格式");
        }
    }
    /**
     * 检验登录态
     * @param appid 小程序 appId
     * @param secret 小程序 appSecret
     * @param openid 用户唯一标识符
     * @param sessionKey 会话密钥
     * @return
     * @throws IOException
     */
    public JSONObject checkSessionKey(String appid, String secret, String openid, String sessionKey) throws IOException, NoSuchAlgorithmException, InvalidKeyException {
        String accessToken = "";
        Integer expiresIn = 0;
        String signature = HmacSha256.getSignature(sessionKey, "");
        String sigMethod = "hmac_sha256";
        JSONObject job_token = getAccessToken(appid, secret);
        if(job_token != null) {
            accessToken = job_token.getString("access_token");
            expiresIn = job_token.getInteger("expires_in");
        }
        Map<String, Object> queryParams = new HashMap<>();
        queryParams.put("access_token", accessToken);
        queryParams.put("openid", openid);
        queryParams.put("signature", signature);
        queryParams.put("sig_method", sigMethod);
        Map<String, String> headerParams = new HashMap<>();
        JSONObject result = restTemplateUtil.get(checkSessionUrl, queryParams, headerParams);
        return result;
    }
    /**
     * 重置登录态
     * @param appid 小程序 appId
     * @param secret 小程序 appSecret
     * @param openid 用户唯一标识符
     * @param sessionKey 会话密钥
     * @return
     * @throws NoSuchAlgorithmException
     * @throws InvalidKeyException
     * @throws IOException
     */
    public JSONObject resetUserSessionKey(String appid, String secret, String openid, String sessionKey) throws NoSuchAlgorithmException, InvalidKeyException, IOException {
        String accessToken = "";
        Integer expiresIn = 0;
        String signature = HmacSha256.getSignature(sessionKey, "");
        String sigMethod = "hmac_sha256";
        JSONObject job_token = getAccessToken(appid, secret);
        if(job_token != null) {
            accessToken = job_token.getString("access_token");
            expiresIn = job_token.getInteger("expires_in");
        }
        Map<String, Object> queryParams = new HashMap<>();
        queryParams.put("access_token", accessToken);
        queryParams.put("openid", openid);
        queryParams.put("signature", signature);
        queryParams.put("sig_method", sigMethod);
        Map<String, String> headerParams = new HashMap<>();
        JSONObject result = restTemplateUtil.get(resetUserSessionKeyUrl, queryParams, headerParams);
        return result;
    }
    /**
     * 获取接口调用凭据
     * @param appid 小程序 appId
     * @param secret 小程序 appSecret
     * @return 凭据及凭据有效时间
     * @throws IOException
     */
    public JSONObject getAccessToken(String appid, String secret) throws IOException {
        Map<String, Object> queryParams = new HashMap<>();
        queryParams.put("grant_type", "client_credential");
        queryParams.put("appid", appid);
        queryParams.put("secret", secret);
        Map<String, String> headerParams = new HashMap<>();
        JSONObject job_result = restTemplateUtil.get(tokenUrl, queryParams, headerParams);
        return job_result;
    }
    /**
     * 构造签名串_下单
     * @param method HTTP请求方法
     * @param url URL
     * @param timestamp 时间戳
     * @param nonceStr 随机串
     * @param body 报文主题
     * @return 签名串
     */
    public String buildMessage_order(String method, String url, long timestamp, String nonceStr, String body) {
        return method + "\n"
                + url + "\n"
                + timestamp + "\n"
                + nonceStr + "\n"
                + body + "\n";
    }
    /**
     * 构造签名串_再次下单
     * @param appid 小程序唯一标识
     * @param timestamp 时间戳
     * @param nonceStr 随机串
     * @param pkg package
     * @return 签名串
     */
    public String buildMessage_signAgain(String appid, String timestamp, String nonceStr, String pkg) {
        return appid + "\n"
                + timestamp + "\n"
                + nonceStr + "\n"
                + pkg + "\n";
    }
    /**
     * 签名
     * @param message 被签名信息
     * @param certBs 私钥证书文件内容
     * @return signature签名值,签名信息中的一项,参与生成签名信息
     * @throws NoSuchAlgorithmException
     * @throws InvalidKeyException
     * @throws SignatureException
     * @throws IOException
    public String sign(byte[] message, String certFileName) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException, IOException {
        Signature sign = Signature.getInstance("SHA256withRSA");
        sign.initSign(getPrivateKey(certFileName));
        sign.update(message);
        return Base64.getEncoder().encodeToString(sign.sign());
    }*/
    public String sign(byte[] message, byte[] certBs) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException, IOException, Exception {
        Signature sign = Signature.getInstance("SHA256withRSA");
        sign.initSign(getPrivateKey(certBs));
        sign.update(message);
        return Base64.getEncoder().encodeToString(sign.sign());
    }
    /**
     * 获取签名信息
     * @param method
     * @param url
     * @param body
     * @return 签名信息,HTTP头中的签名信息
     * HTTP头:Authorization: 认证类型 签名信息
     * 认证类型,WECHATPAY2-SHA256-RSA2048
    public String getToken(String method, String url, String body, String nonceStr, Long timestamp, String certFileName) throws NoSuchAlgorithmException, InvalidKeySpecException, IOException, SignatureException, InvalidKeyException, NoSuchPaddingException {
        String message = buildMessage_order(method, url, timestamp, nonceStr, body);
        String signature = sign(message.getBytes("utf-8"), certFileName);
        return "mchid=\"" + PayInfo.mchid + "\","
                + "nonce_str=\"" + nonceStr + "\","
                + "timestamp=\"" + timestamp + "\","
                + "serial_no=\"" + PayInfo.serial_no + "\","
                + "signature=\"" + signature + "\"";
    }*/
    public String getToken(String method, String url, String body, String nonceStr, Long timestamp, byte[] certFileBs) throws NoSuchAlgorithmException, InvalidKeySpecException, IOException, SignatureException, InvalidKeyException, Exception {
        String message = buildMessage_order(method, url, timestamp, nonceStr, body);
        String signature = sign(message.getBytes("utf-8"), certFileBs);
        return "mchid=\"" + PayInfo.mchid + "\","
                + "nonce_str=\"" + nonceStr + "\","
                + "timestamp=\"" + timestamp + "\","
                + "serial_no=\"" + PayInfo.serial_no + "\","
                + "signature=\"" + signature + "\"";
    }
    /**
     * 构造验造签名串
     * @param wechatpayTimestamp 请求头中返回的时间戳
     * @param wechatpayNonce 请求头中返回的随机串
     * @param boey 请求返回的body
     * @return signatureStr构造的验签名串
     */
    public String responseSign(String wechatpayTimestamp, String wechatpayNonce, String boey) {
        String signatureStr = wechatpayTimestamp + "\n"
                + wechatpayNonce + "\n"
                + boey + "\n";
        return signatureStr;
    }
    /**
     * 重新下载证书
     */
    public void refreshCertificate(byte[] certFileBs) throws GeneralSecurityException, IOException , Exception {
        String method = "GET";
        String httpUrl = "/v3/certificates";
        String nonceStr = generateRandomString();
        Long timestamp = System.currentTimeMillis() / 1000;
        String header = PayInfo.schema + " " + getToken(method, httpUrl, "", nonceStr, timestamp, certFileBs);
        Map<String, String> headers = new HashMap<>();
        headers.put("Authorization", header);
        headers.put("Accept", "application/json");
        //headers.put("User-Agent", "https://zh.wikipedia.org/wiki/User_agent");
        JSONObject job_result = restTemplateUtil.getHeaders(PayInfo.certificates,null, headers);
        JSONObject job_headers = job_result.getJSONObject("headers");
        String wechatpayNonce = job_headers.getJSONArray("Wechatpay-Nonce").getString(0);
        String wechatpaySerial = job_headers.getJSONArray("Wechatpay-Serial").getString(0);
        String signature_h = job_headers.getJSONArray("Wechatpay-Signature").getString(0);
        String signatureType_h = job_headers.getJSONArray("Wechatpay-Signature-Type").getString(0);
        String wechatpayTimestamp = job_headers.getJSONArray("Wechatpay-Timestamp").getString(0);
        JSONObject job_body = job_result.getJSONObject("body");
        if(job_body != null) {
            JSONArray array = job_body.getJSONArray("data");
            if(array != null && array.size() > 0) {
                for(int i = 0; i < array.size(); i++) {
                    JSONObject job_data = array.getJSONObject(i);
                    String certificateSerial = job_data.getString("serial_no");
                    String effective_time  = job_data.getString("effective_time");
                    String expire_time  = job_data.getString("expire_time");
                    JSONObject job_certificate = job_data.getJSONObject("encrypt_certificate");
                    String algorithm = job_certificate.getString("algorithm");
                    String nonce  = job_certificate.getString("nonce");
                    String associated_data  = job_certificate.getString("associated_data");
                    String ciphertext  = job_certificate.getString("ciphertext");
                    //对证书密文进行解密得到平台证书公钥
                    String publicKey = AesUtil.decryptToString(PayInfo.key.getBytes("utf-8"), associated_data.getBytes("utf-8"), nonce.getBytes("utf-8"), ciphertext);
                    // 将平台公钥字符串转成Certificate对象
                    final CertificateFactory cf = CertificateFactory.getInstance("X509");
                    ByteArrayInputStream inputStream = new ByteArrayInputStream(publicKey.getBytes(StandardCharsets.UTF_8));
                    Certificate certificate = null;
                    try {
                        certificate = cf.generateCertificate(inputStream);
                    } catch (CertificateException e) {
                        e.printStackTrace();
                    }
                    // 响应头证书序号与响应体证书序列号一致,且时间差小于5分钟时才将证书存储map
                    Long timeDiff = (System.currentTimeMillis() / 1000 - Long.parseLong(wechatpayTimestamp))/60;
                    if(wechatpaySerial.equals(certificateSerial) && timeDiff <= 5) {
                        // 证书放入MAP
                        CERTIFICATE_MAP.put(certificateSerial, certificate);
                    }
                }
            }
        }
    }
    /**
     * 使用微信平台证书进行响应验签
     * @param wechatpaySerial 来自响应头的微信平台证书序列号
     * @param signatureStr 构造的验签名串
     * @param wechatpaySignature 来自响应头的微信平台签名
     * @return
     * @throws NoSuchAlgorithmException
     * @throws InvalidKeyException
     * @throws SignatureException
     */
    public Boolean responseSignVerify(String wechatpaySerial, String signatureStr, String wechatpaySignature, byte[] certFileBs) throws Exception {
        if(CERTIFICATE_MAP.isEmpty() || !CERTIFICATE_MAP.containsKey(wechatpaySerial)) {
            CERTIFICATE_MAP.clear();
            refreshCertificate(certFileBs);
        }
        Certificate certificate = (Certificate)CERTIFICATE_MAP.get(wechatpaySerial);
        if(certificate == null) {
            return false;
        }
        // 获取公钥
        PublicKey publicKey = certificate.getPublicKey();
        // 初始化SHA256withRSA前面器
        Signature signature = Signature.getInstance("SHA256withRSA");
        // 用微信平台公钥对前面器进行初始化
        signature.initVerify(certificate);
        // 将构造的验签名串更新到签名器中
        signature.update(signatureStr.getBytes(StandardCharsets.UTF_8));
        // 请求头中微信服务器返回的签名用Base64解码,使用签名器进行验证
        boolean valid = signature.verify(Base64.getDecoder().decode(wechatpaySignature));
        return valid;
    }
    /**
     * 获取待退款对象列表
     *      待退款对象包含订单号和可退款金额
     *      订单对象包含订单号、充值金额、充值完成时间
     *      1. 根据虚拟卡号到虚拟卡表中取出该卡余额
     *      2. 根据虚拟卡号到充值表取出订单对象列表
     * @param virtualId
     * @param refundAmount
     * @return
     */
    public List<ToRefund> getToRefunds(Long virtualId, Integer refundAmount) {
        ToRefund toRefund = new ToRefund();
        List<ToRefund> list = new ArrayList<>();
        Double money = 0d;
        // 根据虚拟卡号获取当前虚拟卡余额
        SeVirtualCard seVirtualCard = virtualCardSv.selectVirtuCardById(virtualId);
        if(seVirtualCard != null) {
            money = seVirtualCard.getMoney();
        }
        // 要退金额大于该卡余额,返回空列表
        if(refundAmount > money) {
            return list;
        }
        // 根据虚拟卡号获取订单列表(仅限充值成功的)
        List<VoOrders> list_Orders = virtualCardSv.selectOrders(virtualId);
        // 遍历订单列表,获取
        if(list_Orders != null && list_Orders.size() > 0) {
            JSONArray array_Orders = (JSONArray) JSON.toJSON(list_Orders);
            for(int i = 0; i < array_Orders.size(); i++) {
                JSONObject job_order = array_Orders.getJSONObject(i);
                String orderNumber = job_order.getString("orderNumber");
                Integer rechargeAmount = job_order.getInteger("rechargeAmount");
                Date rechargeTime = job_order.getDate("rechargeTime");
                // 计算充值至今时间差(分钟)
                Long timestamp_Recharge = rechargeTime.getTime() / 1000;
                Long timestamp_Current = System.currentTimeMillis() / 1000;
                Long timeDiff_Minute = (timestamp_Current - timestamp_Recharge)/60;
                // 获取该订单已退款笔数
                Integer refundCount = 0;
                List<Integer> list_RefundAmount = virtualCardSv.selectRefundAmount(orderNumber);
                if(list_RefundAmount != null && list_RefundAmount.size() > 0) {
                    refundCount = list_RefundAmount.size();
                }
                // 充值至今未超过一年且该订单退款总次数未超过50次
                if(timeDiff_Minute/(365*24*60) >= 1 && (refundCount + 1) > 50)
                    return list;
                /**
                 * 1. 如果要退金额小于当前订单的充值金额,要退金额即为应退金额并返回
                 * 2. 如果要推金额大于当前订单充值金额,当前订单充值金额即为应退金额
                 *      a. 生成应退款对象
                 *      b. 计算新的余额
                 *      c. 金蒜新的要退款金额
                 *      d. 如果要退金额大于0,遍历下一个订单
                 */
                if(refundAmount <= rechargeAmount) {
                    toRefund = new ToRefund();
                    toRefund.setOrderNumber(orderNumber);
                    toRefund.setRefundAmount(refundAmount);
                    list.add(toRefund);
                    // 计算新的余额和新的要退金额
                    money = money - refundAmount;
                    refundAmount = refundAmount - refundAmount;
                    return list;
                }else {
                    toRefund = new ToRefund();
                    toRefund.setOrderNumber(orderNumber);
                    toRefund.setRefundAmount(rechargeAmount);
                    list.add(toRefund);
                    // 计算新的余额和新的要退金额
                    money = money - rechargeAmount;
                    refundAmount = refundAmount - rechargeAmount;
                    if(refundAmount > 0) {
                        continue;
                    }else {
                        return list;
                    }
                }
            }
        }
        return list;
    }
    /**
     * 退款申请,调用微信支付退款申请接口
     * @param po 退款请求对象,包含订单号、退款单号、退款金额
     * @return
     * @throws NoSuchPaddingException
     * @throws NoSuchAlgorithmException
     * @throws InvalidKeySpecException
     * @throws IOException
     * @throws SignatureException
     * @throws InvalidKeyException
     */
    public BaseResponse<Boolean> refunds(Refund po, byte[] certFileBs) throws SignatureException, InvalidKeyException, Exception{
        String tradeNo = po.getTradeNo();
        String refundNo = po.getRefundNo();
        Integer refund = po.getRefund();
        // 生成body
        Double total = virtualCardSv.getRechargeAmountByOrderNumber(tradeNo);
        RefundRequest.Amount amount = new RefundRequest.Amount();
        amount.setRefund(refund);
        amount.setTotal(total);
        amount.setCurrency("CNY");
        RefundRequest refundRequest = new RefundRequest();
        refundRequest.setOut_trade_no(tradeNo);
        refundRequest.setOut_refund_no(refundNo);
        refundRequest.setNotify_url(notifyUrl);
        refundRequest.setAmount(amount);
        // 生成header
        String nonceStr = generateRandomString();
        Long timestamp = System.currentTimeMillis() / 1000;
        String method = "POST";
        String httpUrl = "/v3/refund/domestic/refunds";
        String body = JSONObject.toJSONString(refundRequest);
        String header = schema + " " + getToken(method, httpUrl, body, nonceStr, timestamp, certFileBs);
        Map<String, String> headers = new HashMap<>();
        headers.put("Authorization", header);
        headers.put("Accept", "application/json");
        headers.put("Content-Type", "application/json");
        JSONObject job_refundResponse = restTemplateUtil.post(PayInfo.refundUrl, body, headers);
        RefundResponse refundResponse = JSON.parseObject(job_refundResponse.toJSONString(), RefundResponse.class);
        String status = refundResponse.getStatus();
        if(status != null && status.equals("SUCCESS")) {
            // 退款申请已受理
            return BaseResponseUtils.buildSuccess(true) ;
        } else if(status != null && status.equals("PROCESSING")) {
            // 退款处理中
            return BaseResponseUtils.buildFail(WechatResultCode.PROCESSING.getMessage());
        } else {
            // 退款异常
            return BaseResponseUtils.buildError(WechatResultCode.ABNORMAL.getMessage());
        }
    }
}