| 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.nio.file.Files; | 
| import java.nio.file.Paths; | 
| 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 privateCertFileName = PayInfo.privateCertFileName; | 
|     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 filename 私钥文件路径 | 
|      * @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("无效的密钥格式"); | 
|         } | 
|     } | 
|   | 
|     /** | 
|      * 检验登录态 | 
|      * @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 certFileName 私钥证书文件路径 | 
|      * @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()); | 
|     } | 
|   | 
|     /** | 
|      * 获取签名信息 | 
|      * @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 + "\""; | 
|     } | 
|   | 
|     /** | 
|      * 构造验造签名串 | 
|      * @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() throws GeneralSecurityException, IOException { | 
|         String method = "GET"; | 
|         String httpUrl = "/v3/certificates"; | 
|         String nonceStr = generateRandomString(); | 
|         Long timestamp = System.currentTimeMillis() / 1000; | 
|   | 
|         String header = PayInfo.schema + " " + getToken(method, httpUrl, "", nonceStr, timestamp, PayInfo.privateCertFileName); | 
|   | 
|         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) throws GeneralSecurityException, IOException { | 
|         if(CERTIFICATE_MAP.isEmpty() || !CERTIFICATE_MAP.containsKey(wechatpaySerial)) { | 
|             CERTIFICATE_MAP.clear(); | 
|             refreshCertificate(); | 
|         } | 
|         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) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeySpecException, IOException, SignatureException, InvalidKeyException { | 
|         String tradeNo = po.getTradeNo(); | 
|         String refundNo = po.getRefundNo(); | 
|         Integer refund = po.getRefund(); | 
|   | 
|         // 生成body | 
|         Integer 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, privateCertFileName); | 
|   | 
|         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()); | 
|         } | 
|     } | 
| } |