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());
|
}
|
}
|
}
|