diff --git a/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/TrdPayTypeEnum.java b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/TrdPayTypeEnum.java index 21ded77b..02dda45e 100644 --- a/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/TrdPayTypeEnum.java +++ b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/TrdPayTypeEnum.java @@ -128,6 +128,7 @@ public enum TrdPayTypeEnum { * 支付宝 8 微信 4 */ V11("http://bgxa.peiqi.zhifusg.com","/Pay_Index.html","/Pay_Trade_query.html","/api/pay/trd/notify/v11","OK"), + V12("https://cashapi.sandpay.com.cn","/gateway/trade","/Pay_Trade_query.html","/api/pay/trd/notify/v11","OK"), ; private final String gatewayUrl; diff --git a/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/PayTrdV12Service.java b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/PayTrdV12Service.java new file mode 100644 index 00000000..9e6b9497 --- /dev/null +++ b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/PayTrdV12Service.java @@ -0,0 +1,136 @@ +package com.ruoyi.cai.trdpay.handle; + +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.alibaba.fastjson2.JSON; +import com.ruoyi.cai.domain.PayTrdConfig; +import com.ruoyi.cai.pay.PayManager; +import com.ruoyi.cai.pay.PayOrderInfoDTO; +import com.ruoyi.cai.pay.PayReturnResp; +import com.ruoyi.cai.service.OrderLogsService; +import com.ruoyi.cai.trdpay.PayTrdService; +import com.ruoyi.cai.trdpay.TrdPayTypeEnum; +import com.ruoyi.cai.trdpay.dto.NotifyResp; +import com.ruoyi.cai.trdpay.handle.v12.SandPayClient; +import com.ruoyi.cai.trdpay.handle.v12.SandPayConfig; +import com.ruoyi.cai.trdpay.handle.v12.SandpayConfigUtil; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.ServletUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Map; + +@Service +@Slf4j +public class PayTrdV12Service implements PayTrdService { + + @Autowired + private OrderLogsService orderLogsService; + @Autowired + private PayManager payManager; + + private SandPayClient sandPayClient = null; + + public synchronized void init(String accessMid){ + if(sandPayClient == null){ + // 初始化sandpay参数(全局设置一次) + SandPayConfig config = new SandPayConfig(); + // 接入商户号 + config.setAccessMid(accessMid); + // 加签 加密参数设置 + config.setEncryptType(SandpayConfigUtil.AES_STR); + config.setPrivateKeyPath("D:\\mmm\\6888806128148.pfx");//存放目录:src\main\resources\cert\prod + config.setPrivateKeyPassword("926645"); + config.setSandPublicKeyPath("D:\\mmm\\sand_pro.cer");//存放目录:src\main\resources\cert\prod + // 请求版本号 + config.setVersion("4.0.0"); + sandPayClient = new SandPayClient(config); + } + } + + @Override + public TrdPayTypeEnum getType() { + return TrdPayTypeEnum.V12; + } + + @Override + public PayReturnResp createOrderAli(PayOrderInfoDTO payOrderInfoDTO, PayTrdConfig payTrdConfig,boolean wx) { + init(payTrdConfig.getMchId()); + TrdPayTypeEnum type = getType(); + JSONObject bizData = new JSONObject(); + bizData.put("mid","6888806128148"); + bizData.put("outOrderNo",payOrderInfoDTO.getOrderNo()); + bizData.put("description","知予-"+payOrderInfoDTO.getSubject()); + bizData.put("goodsClass","99"); + bizData.put("amount", payOrderInfoDTO.getPrice()); + JSONArray payExtra = new JSONArray(); + if(wx){ + bizData.put("funcCodeList", new String[]{"02010005"}); + /** + * subAppId : wxe78b5fdab7ddebbc + * ghOriId : gh_40fd947b7d3f + * pathUrl : pages/zf/index? + * miniProgramType : 0 + */ + JSONObject exPayExtra = new JSONObject(); + exPayExtra.put("funcCode","02010005"); + exPayExtra.put("subAppId","wxe78b5fdab7ddebbc"); + exPayExtra.put("ghOriId","gh_40fd947b7d3f"); + exPayExtra.put("pathUrl","pages/zf/index?"); + exPayExtra.put("miniProgramType","0"); + payExtra.add(exPayExtra); + }else{ + bizData.put("funcCodeList", new String[]{"02020004"}); + } + String notifyUrl = type.getNotifyUrl(payTrdConfig.getNotifyUrl(), wx); + JSONObject payerInfo = new JSONObject(); + payerInfo.put("frontUrl", notifyUrl); + payerInfo.put("payExtra",payExtra); + bizData.put("payerInfo",payerInfo); + bizData.put("notifyUrl", notifyUrl); + JSONObject reqReserved = new JSONObject(); + reqReserved.put("sourceIp", ServletUtils.getClientIP()); + bizData.put("sdCashierType", "SDK"); + bizData.put("metaOption", "[{\"s\":\"Android\",\"n\":\"\",\"id\":\"\",\"sc\":\"\"},{\"s\":\"IOS\",\"n\":\"\",\"id\":\"\",\"sc\":\"\"}]"); + bizData.put("reqReserved",reqReserved); + + String gatewayUrl = getGatewayUrl(payTrdConfig); + String createOrderUrl = gatewayUrl + type.getCreateOrderUrl(); + JSONObject responseJson = sandPayClient.execute(createOrderUrl, bizData); + if(responseJson == null || !"accept".equals(responseJson.getString("resultStatus"))){ + orderLogsService.createAliPayLogs(payOrderInfoDTO.getOrderNo(), createOrderUrl+JSON.toJSONString(responseJson), com.alibaba.fastjson2.JSONObject.from(responseJson), false, type, getStepName(wx)); + log.info("第三方支付失败 V12 统一支付失败失败 url={} params={} body={}, payTrdConfig={}", createOrderUrl, JSON.toJSONString(bizData), JSON.toJSONString(responseJson), JSON.toJSONString(payTrdConfig)); + throw new ServiceException("调用支付失败"); + }else{ + orderLogsService.createAliPayLogs(payOrderInfoDTO.getOrderNo(), createOrderUrl+JSON.toJSONString(responseJson), com.alibaba.fastjson2.JSONObject.from(responseJson), true, type, getStepName(wx)); + } + return PayReturnResp.createH5(responseJson.getString("cashierUrl")); + } + + @Override + public NotifyResp getNotifyResp(Map sourceData) { + String mchOrderNo = sourceData.get("orderid"); + String payOrderId = sourceData.get("transaction_id"); + String status = sourceData.get("returncode"); + NotifyResp resp = new NotifyResp(); + resp.setOrderNo(mchOrderNo); + resp.setTrdOrderNo(payOrderId); + resp.setPayTypeEnum(getType()); + resp.setSourceData(sourceData); + resp.setSuccess("00".equals(status)); + return resp; + } + + @Override + public com.alibaba.fastjson2.JSONObject queryOrder(String orderNo, PayTrdConfig payTrdConfig) { + throw new RuntimeException("暂时不支持订单查询"); + } + + @Override + public com.alibaba.fastjson2.JSONObject resetOrder(String orderNo, PayTrdConfig payTrdConfig, boolean updateData) { + throw new RuntimeException("暂时不支持订单充值"); + } + +} diff --git a/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/PayTrdV7Service.java b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/PayTrdV7Service.java index c6a026aa..2534dd9d 100644 --- a/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/PayTrdV7Service.java +++ b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/PayTrdV7Service.java @@ -68,7 +68,7 @@ public class PayTrdV7Service implements PayTrdService { boolean success = checkSuccess(jsonObject); orderLogsService.createAliPayLogs(payOrderInfoDTO.getOrderNo(), createOrderUrl+JSON.toJSONString(map), jsonObject, success, type, getStepName(wx)); if(!success){ - log.info("第三方支付失败 V7 统一支付失败失败 url={} params={} body={}, payTrdConfig={}, typeEnum={}", createOrderUrl,JSON.toJSONString(map), body, JSON.toJSONString(payTrdConfig), JSON.toJSONString(jsonObject)); + log.info("第三方支付失败 V7 统一支付失败失败 url={} params={} body={}, payTrdConfig={}", createOrderUrl,JSON.toJSONString(map), body, JSON.toJSONString(payTrdConfig)); throw new ServiceException("调用支付失败"); } String payUrl = jsonObject.getJSONObject("data").getString("payUrl"); diff --git a/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12/AESUtils.java b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12/AESUtils.java new file mode 100644 index 00000000..8f537bd6 --- /dev/null +++ b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12/AESUtils.java @@ -0,0 +1,43 @@ +package com.ruoyi.cai.trdpay.handle.v12; + +import com.ruoyi.common.utils.StringUtils; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + + +public class AESUtils { + private static final String KEY_ALGORITHM = "AES"; + private static final String CIPHER_ALGORITHM = "AES/ECB/PKCS5Padding"; + + public AESUtils() { + } + + public static byte[] encrypt(byte[] plainBytes, byte[] keyBytes, String IV) throws Exception { + Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); + SecretKey secretKey = new SecretKeySpec(keyBytes, "AES"); + if (StringUtils.isNotBlank(IV)) { + IvParameterSpec ips = new IvParameterSpec(IV.getBytes()); + cipher.init(1, secretKey, ips); + } else { + cipher.init(1, secretKey); + } + + return cipher.doFinal(plainBytes); + } + + public static byte[] decrypt(byte[] encryptedBytes, byte[] keyBytes, String IV) throws Exception { + Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); + SecretKey secretKey = new SecretKeySpec(keyBytes, "AES"); + if (StringUtils.isNotBlank(IV)) { + IvParameterSpec ips = new IvParameterSpec(IV.getBytes()); + cipher.init(2, secretKey, ips); + } else { + cipher.init(2, secretKey); + } + + return cipher.doFinal(encryptedBytes); + } +} diff --git a/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12/FileUtils.java b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12/FileUtils.java new file mode 100644 index 00000000..e1b1da34 --- /dev/null +++ b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12/FileUtils.java @@ -0,0 +1,76 @@ +package com.ruoyi.cai.trdpay.handle.v12; + + +import com.ruoyi.common.utils.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; + +public class FileUtils { + + public static final Logger logger = LoggerFactory.getLogger(FileUtils.class); + + public FileUtils() { + } + + public static InputStream loadFile(String fileName) { + if (StringUtils.isNotBlank(fileName)) { + return loadFromAbsoluteFile(fileName); +// return absolutePathStart(fileName) ? loadFromAbsoluteFile(fileName) : loadFromClasspathFile(fileName); + } else { + return null; + } + } + + private static InputStream loadFromAbsoluteFile(String fileName) { + try { + File f = new File(fileName); + return !f.exists() ? null : new FileInputStream(f); + } catch (Throwable var3) { + logger.warn("load file[" + fileName + "] fail", var3); + return null; + } + } + + private static boolean absolutePathStart(String path) { + File[] files = File.listRoots(); + File[] var2 = files; + int var3 = files.length; + + for(int var4 = 0; var4 < var3; ++var4) { + File file = var2[var4]; + if (path.startsWith(file.getPath())) { + return true; + } + } + + return false; + } + + private static InputStream loadFromClasspathFile(String fileName) { + try { +// return ClassLoader.getSystemResourceAsStream(fileName); + return FileUtils.class.getClassLoader().getResourceAsStream(fileName); + } catch (Throwable var2) { + logger.warn("load file[" + fileName + "] fail", var2); + return null; + } + } + + private static InputStream loadFromRelativeFile(String fileName) { + String userDir = System.getProperty("user.dir"); + String realFilePath = addSeparator(userDir) + fileName; + return loadFromAbsoluteFile(realFilePath); + } + + private static String addSeparator(String dir) { + if (!dir.endsWith(File.separator)) { + dir = dir + File.separator; + } + + return dir; + } +} diff --git a/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12/HttpClientUtils.java b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12/HttpClientUtils.java new file mode 100644 index 00000000..a7821177 --- /dev/null +++ b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12/HttpClientUtils.java @@ -0,0 +1,65 @@ +package com.ruoyi.cai.trdpay.handle.v12; + + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.net.URL; +import java.net.URLConnection; + +public class HttpClientUtils { + public static final Logger logger = LoggerFactory.getLogger(HttpClientUtils.class); + + public HttpClientUtils() { + } + + public static String sendPost(String url, String param, int connectTimeout, int readTimeout) { + PrintWriter out = null; + BufferedReader in = null; + + try { + URL realUrl = new URL(url); + URLConnection conn = realUrl.openConnection(); + conn.setRequestProperty("Content-Type", "application/json"); + conn.setRequestProperty("accept", "*/*"); + conn.setRequestProperty("connection", "Keep-Alive"); + conn.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)"); + conn.setDoOutput(true); + conn.setDoInput(true); + conn.setConnectTimeout(connectTimeout); + conn.setReadTimeout(readTimeout); + out = new PrintWriter(conn.getOutputStream()); + out.print(param); + out.flush(); + in = new BufferedReader(new InputStreamReader(conn.getInputStream())); + StringBuilder sb = new StringBuilder(); + String line = null; + + while((line = in.readLine()) != null) { + sb.append(line); + } + + String var10 = sb.toString(); + return var10; + } catch (Exception var19) { + throw new SandPayException("请求服务器失败,url:" + url + " err:" + var19.getMessage(), var19); + } finally { + try { + if (out != null) { + out.close(); + } + + if (in != null) { + in.close(); + } + } catch (IOException var18) { + logger.error("发送POST方法的请求,关闭流异常:", var18); + } + + } + } +} diff --git a/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12/RSAUtils.java b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12/RSAUtils.java new file mode 100644 index 00000000..88e5f218 --- /dev/null +++ b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12/RSAUtils.java @@ -0,0 +1,174 @@ +package com.ruoyi.cai.trdpay.handle.v12; + +import com.ruoyi.common.utils.StringUtils; + +import javax.crypto.Cipher; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.Signature; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.Base64; +import java.util.Enumeration; +import java.util.Objects; + +public class RSAUtils { + + private static final String ALGORITHM = "RSA/ECB/PKCS1Padding"; + private static final int KEY_LENGTH = 2048; + private static final int RESERVE_SIZE = 11; + + public RSAUtils() { + } + + public static String encryptToBase64Text(String plainTextBytes, PublicKey publicKey) throws Exception { + byte[] aesKeyBytes = plainTextBytes.getBytes(); + byte[] encryptKeyBytes = encrypt(aesKeyBytes, publicKey); + return Base64.getEncoder().encodeToString(encryptKeyBytes); + } + + public static String decryptBase64ToText(String encText, PrivateKey privateKey) throws Exception { + byte[] encTextBytes = Base64.getDecoder().decode(encText); + byte[] contentBytes = decrypt(encTextBytes, privateKey); + String decryptText = new String(contentBytes, StandardCharsets.UTF_8); + return decryptText; + } + + public static byte[] encrypt(byte[] plainTextBytes, PublicKey publicKey) throws Exception { + boolean keyByteSize = true; + boolean encryptBlockSize = true; + int nBlock = plainTextBytes.length / 245; + if (plainTextBytes.length % 245 != 0) { + ++nBlock; + } + + Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); + cipher.init(1, publicKey); + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(nBlock * 256); + + for(int offset = 0; offset < plainTextBytes.length; offset += 245) { + int inputLen = plainTextBytes.length - offset; + if (inputLen > 245) { + inputLen = 245; + } + + byte[] decryptedBlock = cipher.doFinal(plainTextBytes, offset, inputLen); + byteArrayOutputStream.write(decryptedBlock); + } + + byteArrayOutputStream.flush(); + byteArrayOutputStream.close(); + return byteArrayOutputStream.toByteArray(); + } + + public static byte[] decrypt(byte[] cipherTextBytes, PrivateKey privateKey) throws Exception { + boolean keyByteSize = true; + boolean decryptBlockSize = true; + int nBlock = cipherTextBytes.length / 256; + Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); + cipher.init(2, privateKey); + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(nBlock * 245); + + for(int offset = 0; offset < cipherTextBytes.length; offset += 256) { + int inputLen = cipherTextBytes.length - offset; + if (inputLen > 256) { + inputLen = 256; + } + + byte[] decryptedBlock = cipher.doFinal(cipherTextBytes, offset, inputLen); + byteArrayOutputStream.write(decryptedBlock); + } + + byteArrayOutputStream.flush(); + byteArrayOutputStream.close(); + return byteArrayOutputStream.toByteArray(); + } + + public static String generateSign(String content, PrivateKey privateKey, String signType, String charset) throws Exception { + Signature signature = null; + if (!"RSA".equals(signType) && !"SHA256WithRSA".equals(signType)) { + throw new SandPayException("不是支持的签名类型 : signType=" + signType); + } else { + signature = Signature.getInstance("SHA256WithRSA"); + signature.initSign(privateKey); + if (StringUtils.isBlank(charset)) { + signature.update(content.getBytes()); + } else { + signature.update(content.getBytes(charset)); + } + + byte[] signed = signature.sign(); + return Base64.getEncoder().encodeToString(signed); + } + } + /** + * @Description: 验签 + * @param content 响应报文bizData + * @param sign 响应报文中的sign + * @param signType 响应报文中的signType + * @param publicKey 杉德公钥 + * @param charset 编码格式 + * @return: boolean + */ + public static boolean verifySign(String content, String sign, String signType, PublicKey publicKey, String charset) throws Exception { + Signature signature = Signature.getInstance(signType); + signature.initVerify(publicKey); + if (StringUtils.isBlank(charset)) { + signature.update(content.getBytes()); + } else { + signature.update(content.getBytes(charset)); + } + return signature.verify(Base64.getDecoder().decode(sign.getBytes())); + } + + public static PublicKey getPublicKeyByStr(String publicKeyStr) throws Exception { + return loadPublicKey(new ByteArrayInputStream(publicKeyStr.getBytes())); + } + + public static PrivateKey getPrivateKeyByStr(String priKeyStr) throws Exception { + return loadPrivateKey(new ByteArrayInputStream(priKeyStr.getBytes()), (String)null); + } + + public static PrivateKey loadPrivateKey(InputStream ins, String password) throws Exception { + KeyStore ks = KeyStore.getInstance("PKCS12"); + char[] nPassword; + if (password != null && !password.trim().equals("")) { + nPassword = password.toCharArray(); + } else { + nPassword = null; + } + + ks.load(ins, nPassword); + Enumeration enumas = ks.aliases(); + String keyAlias = null; + if (enumas.hasMoreElements()) { + keyAlias = (String)enumas.nextElement(); + } + + return (PrivateKey)ks.getKey(keyAlias, nPassword); + } + + + public static PublicKey getSandPublicKey(String sandPublicKeyPath) throws Exception { + InputStream publicIns = FileUtils.loadFile(sandPublicKeyPath); + if (Objects.isNull(publicIns)) { + throw new SandPayException("获取公钥失败"); + } else { + return RSAUtils.loadPublicKey(publicIns); + } + } + + + public static PublicKey loadPublicKey(InputStream ins) throws Exception { + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + X509Certificate oCert = (X509Certificate)cf.generateCertificate(ins); + PublicKey publicKey = oCert.getPublicKey(); + return publicKey; + } + +} diff --git a/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12/RandomV12Util.java b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12/RandomV12Util.java new file mode 100644 index 00000000..72c34f32 --- /dev/null +++ b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12/RandomV12Util.java @@ -0,0 +1,10 @@ +package com.ruoyi.cai.trdpay.handle.v12; + +import cn.hutool.core.util.RandomUtil; + +public class RandomV12Util { + + public static String genRandomString(int length) { + return RandomUtil.randomString("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",length); + } +} diff --git a/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12/SandPayClient.java b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12/SandPayClient.java new file mode 100644 index 00000000..6f5ede52 --- /dev/null +++ b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12/SandPayClient.java @@ -0,0 +1,305 @@ +package com.ruoyi.cai.trdpay.handle.v12; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.ruoyi.common.utils.StringUtils; +import lombok.extern.slf4j.Slf4j; + +import java.nio.charset.StandardCharsets; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.*; + +/** + * @ClassName : SandPayClient + * @Description : 加密、签名、验签 + **/ +@Slf4j +public class SandPayClient { + + private SandpayConfigUtil sandpayConfigUtil = new SandpayConfigUtil(); + + public SandPayClient() { + } + + public SandPayClient(SandPayConfig sandPayConfig) { + if (sandPayConfig != null) { + try { + this.sandpayConfigUtil.setBaseUrl(sandPayConfig.getBaseUrl()); + this.sandpayConfigUtil.setAccessMid(sandPayConfig.getAccessMid()); + this.sandpayConfigUtil.setMid(sandPayConfig.getMid()); + this.sandpayConfigUtil.setPlMid(sandPayConfig.getPlMid()); + this.sandpayConfigUtil.setVersion(sandPayConfig.getVersion()); +// this.sandpayConfigUtil.setCertNo(sandPayConfig.getCertNo());//预留 + String privateKeyPath = sandPayConfig.getPrivateKeyPath(); + String sandPublicKeyPath = sandPayConfig.getSandPublicKeyPath(); + if (!StringUtils.isBlank(privateKeyPath) || !StringUtils.isBlank(sandPublicKeyPath)) { + this.sandpayConfigUtil.setPrivateKey(this.sandpayConfigUtil.getPrivateKey(sandPayConfig.getPrivateKeyPath(), sandPayConfig.getPrivateKeyPassword())); + this.sandpayConfigUtil.setSandPublicKey(this.sandpayConfigUtil.getSandPublicKey(sandPayConfig.getSandPublicKeyPath())); + this.sandpayConfigUtil.setEncryptType(sandPayConfig.getEncryptType()); + this.sandpayConfigUtil.setSignType(sandPayConfig.getSignType()); + Integer connectTimeout = sandPayConfig.getConnectTimeout(); + if (Objects.nonNull(connectTimeout)) { + this.sandpayConfigUtil.setConnectTimeout(connectTimeout); + } + Integer readTimeout = sandPayConfig.getReadTimeout(); + if (Objects.nonNull(readTimeout)) { + this.sandpayConfigUtil.setReadTimeout(readTimeout); + } + } + } catch (Exception var6) { + log.error(var6.getMessage()); + throw new SandPayException("初始化SandPayClient异常", var6); + } + } + } + + public JSONObject execute(String url, JSONObject bizData) { + return (JSONObject) this.requestCoreReturnObject(url, bizData, JSONObject.class); + } + + public JSONObject execute(String url, JSONObject bizData, String encryptType) { + return (JSONObject) this.requestCoreReturnObject(url, bizData, JSONObject.class, encryptType); + } + + private T requestCoreReturnObject(String url, Object bizRequest, Class returnClass, String encryptType) { + String bizRequestJson = JSON.toJSONString(bizRequest); + JSONObject sandPayCommonRequest = this.buildRequest(bizRequestJson, encryptType); + log.info("请求Sand报文:{}", sandPayCommonRequest); + String responseStr = HttpClientUtils.sendPost(url, JSON.toJSONString(sandPayCommonRequest), this.sandpayConfigUtil.getConnectTimeout(), this.sandpayConfigUtil.getReadTimeout()); + log.info("Sand返回报文:{}", responseStr); + return this.handleResponse(responseStr, returnClass, encryptType); + } + + private T requestCoreReturnObject(String url, Object bizRequest, Class returnClass) throws SandPayException { + String encryptType = this.sandpayConfigUtil.getEncryptType(); + return this.requestCoreReturnObject(url, bizRequest, returnClass, encryptType); + } + + /** + * @Description: 加密、签名接口请求报文并组装公共请求报文 + * @param content 接口请求报文明文 + * @param encryptType 加密方式 + * @return: JSONObject 公共请求报文 + */ + public JSONObject buildRequest(String content, String encryptType) throws SandPayException { + String signType = "RSA"; + String encryptKey; + String bizData; + String sign; + if (StringUtils.isBlank(content)) { + throw new SandPayException("签名内容为空 : content=" + content); + } else if (!"AES".equals(encryptType)) { + throw new SandPayException("不是支持的加密类型 : encryptType=" + encryptType); + } else { + //1.生成一个16位的随机字符串aesKey,该字符串仅包含大小写字母及数字。 + String aesKey = RandomV12Util.genRandomString(16); + //2.将随机字符串转为byte数组aesKeyBytes,编码格式为UTF_8。 + //3.将请求报文中明文bizData域转为byte数组,编码格式为UTF_8,并用aesKeyBytes用AES算法(AES/ECB/PKCS5Padding)对其加密,并对结果进行base64转码,得到加密报文体bizData。 + bizData = this.aesEncrypt(content, aesKey); + //4.将随机字符串byte数组aesKeyBytes使用杉德公钥进行RSA算法(RSA/ECB/PKCS1Padding)加密,并将结果进行base64转码即得到encryptKey。 + encryptKey = rsaEncrypt(aesKey, this.sandpayConfigUtil.getSandPublicKey()); + //5.将加密报文体使用商户私钥进行RSA算法(SHA256WithRSA)签名,得到sign。 + sign = this.generateSign(bizData, signType, this.sandpayConfigUtil.getPrivateKey()); + + } + + //6.组装公共请求报文并返回 + JSONObject commonRequest = new JSONObject(); + commonRequest.put("accessMid", this.sandpayConfigUtil.getAccessMid()); + commonRequest.put("encryptType", encryptType); + commonRequest.put("encryptKey", encryptKey); +// commonRequest.put("certNo", this.sandpayConfigUtil.getCertNo());//预留 + commonRequest.put("version", this.sandpayConfigUtil.getVersion()); + commonRequest.put("timestamp", this.getTimestamp()); + commonRequest.put("signType", signType); + commonRequest.put("sign", sign); + commonRequest.put("bizData", bizData); + return commonRequest; + } + + //解析响应报文 + public JSONObject parseResponse(String content, String encryptType) throws SandPayException { + return (JSONObject) this.handleResponse(content, JSONObject.class, encryptType); + } + + //验签回调 + public boolean verifySign(String data, String sign, String signType, String charset) throws SandPayException { + try { + return RSAUtils.verifySign(data, sign, signType, this.sandpayConfigUtil.getSandPublicKey(), charset); + } catch (Exception var6) { + throw new SandPayException("验证回调签名异常", var6); + } + } + + //处理响应报文 + private T handleResponse(String responseStr, Class returnClass, String encryptType) throws SandPayException { + JSONObject commonResponse = (JSONObject) JSON.parseObject(responseStr, JSONObject.class); + String respCode = (String) commonResponse.get("respCode"); + if (respCode == null) { + throw new SandPayException("处理返回报文失败,原始报文" + responseStr); + } else { + String bizData; + if (!"success".equals(respCode)) { + bizData = (String) commonResponse.get("respDesc"); + throw new SandPayException(respCode, bizData, responseStr); + } else { + bizData = commonResponse.get("bizData").toString(); + String sign = (String) commonResponse.get("sign"); + String signType = (String) commonResponse.get("signType"); + this.verifySign(bizData, sign, signType, this.sandpayConfigUtil.getSandPublicKey()); + if (!"AES".equals(encryptType)) { + throw new SandPayException("不是支持的加密类型: encryptType=" + encryptType); + } else { + String encryptKey = (String) commonResponse.get("encryptKey"); + String respAesKey = this.rsaDecrypt(encryptKey, this.sandpayConfigUtil.getPrivateKey()); + bizData = this.aesDecrypt(bizData, respAesKey); + + if (returnClass == String.class) { + return (T) bizData; + } else { + try { + return JSON.parseObject(bizData, returnClass); + } catch (Exception var11) { + throw new SandPayException("解析报文失败," + responseStr, var11); + } + } + } + } + } + } + + //AES加密 + private String aesEncrypt(String content, String aesKey) { + byte[] aesKeyBytes = aesKey.getBytes(StandardCharsets.UTF_8); + + try { + byte[] encryptValueBytes = AESUtils.encrypt(content.getBytes(StandardCharsets.UTF_8), aesKeyBytes, (String) null); + return Base64.getEncoder().encodeToString(encryptValueBytes); + } catch (Exception var5) { + throw new SandPayException("AES加密异常", var5); + } + } + + //RSA加密 + public static String rsaEncrypt(String content, PublicKey publicKey) { + try { + byte[] aesKeyBytes = content.getBytes(); + byte[] encryptKeyBytes = RSAUtils.encrypt(aesKeyBytes, publicKey); + return Base64.getEncoder().encodeToString(encryptKeyBytes); + } catch (Exception var4) { + throw new SandPayException("RSA加密异常", var4); + } + } + + //RSA解密响应参数 + private String rsaDecrypt(String content, PrivateKey privateKey) { + byte[] decryptKeyBytes = Base64.getDecoder().decode(content); + + try { + byte[] contentBytes = RSAUtils.decrypt(decryptKeyBytes, privateKey); + String decryptKey = new String(contentBytes, StandardCharsets.UTF_8); + return decryptKey; + } catch (Exception var6) { + throw new SandPayException("RSA解密随机加密串失败", var6); + } + } + + //AES解密响应参数 + private String aesDecrypt(String content, String aesKey) { + byte[] decryptDataBase64 = Base64.getDecoder().decode(content); + + try { + byte[] decryptDataBytes = AESUtils.decrypt(decryptDataBase64, aesKey.getBytes(StandardCharsets.UTF_8), (String) null); + String decryptData = new String(decryptDataBytes); + return decryptData; + } catch (Exception var6) { + throw new SandPayException("AES解密返回参数失败", var6); + } + } + + //签名 + public String generateSign(String content, String signType, PrivateKey privateKey) { + try { + if ("RSA".equals(signType)) { + signType = "SHA256WithRSA"; + } + + return RSAUtils.generateSign(content, privateKey, signType, "UTF-8"); + } catch (Exception var5) { + throw new SandPayException("请求签名异常", var5); + } + } + + /** + * @Description: 验签响应报文 + * @param content 响应报文中的bizData + * @param sign 响应报文中的sign + * @param signType 响应报文中的signType + * @param publicKey 杉德公钥 + * @return: boolean 验签结果 + */ + private boolean verifySign(String content, String sign, String signType, PublicKey publicKey) { + try { + if ("RSA".equals(signType)) { + signType = "SHA256WithRSA"; + } + return RSAUtils.verifySign(content, sign, signType, publicKey, "UTF-8"); + } catch (Exception var6) { + throw new SandPayException("验证响应报文签名异常", var6); + } + } + + //生成时间戳 yyyy-MM-dd HH:mm:ss + private String getTimestamp() { + DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + df.setTimeZone(TimeZone.getTimeZone("GMT+8")); + return df.format(new Date(System.currentTimeMillis())); + } + + //请求时间 yyyyMMddHHmmss + public String getCurrentTime() { + SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss"); + sdf.setTimeZone(TimeZone.getTimeZone("GMT+8")); + return sdf.format(new Date()); + } + + //获取当前时间*小时后的时间 yyyyMMddHHmmss + public String getTimeOutTime(int hour) { + Calendar calendar = Calendar.getInstance(); + calendar.add(Calendar.HOUR, hour); + SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss"); + sdf.setTimeZone(TimeZone.getTimeZone("GMT+8")); + return sdf.format(calendar.getTime()); + } + + //生成订单号 + public String getOutOrderNo() { + return this.getOutOrderNo("", 25); + } + + public String getOutOrderNo(String prefix, int length) { + if (prefix == null) { + prefix = ""; + } + + if (length < 25) { + length = 25; + } + + DateFormat df = new SimpleDateFormat("yyyyMMddHHmmsss"); + df.setTimeZone(TimeZone.getTimeZone("GMT+8")); + String currentTime = df.format(new Date(System.currentTimeMillis())); + String outOrderNo = prefix.concat(currentTime); + int randomLen = length - outOrderNo.length(); + outOrderNo = prefix.concat(currentTime).concat(RandomV12Util.genRandomString(randomLen)); + return outOrderNo; + } + + public SandpayConfigUtil getSandpayConfigUtil() { + return this.sandpayConfigUtil; + } + +} diff --git a/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12/SandPayConfig.java b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12/SandPayConfig.java new file mode 100644 index 00000000..afc51e51 --- /dev/null +++ b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12/SandPayConfig.java @@ -0,0 +1,27 @@ +package com.ruoyi.cai.trdpay.handle.v12; + + +import lombok.Data; + +import java.security.PrivateKey; +import java.security.PublicKey; + +@Data +public class SandPayConfig { + private String baseUrl; + private String version = "4.0.0"; + private String accessMid; + private String plMid; + private String mid; + private PrivateKey privateKey; + private String privateKeyPassword; + private String privateKeyPath; + private String certNo; + private PublicKey sandPublicKey; + private String sandPublicKeyPath; + private String signType = "RSA"; + private String encryptType = "AES"; + private int connectTimeout = 5000; + private int readTimeout = 5000; + +} diff --git a/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12/SandPayException.java b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12/SandPayException.java new file mode 100644 index 00000000..bb22e52f --- /dev/null +++ b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12/SandPayException.java @@ -0,0 +1,59 @@ +package com.ruoyi.cai.trdpay.handle.v12; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; + + +public class SandPayException extends RuntimeException { + private String respCode; + private String respDesc; + private String responseMsg; + + public SandPayException() { + } + + public SandPayException(String message) { + super(message); + this.respDesc = message; + } + + public SandPayException(String message, Throwable cause) { + super(message, cause); + this.respDesc = message; + } + + public SandPayException(String respCode, String respDesc, String responseMsg) { + super(respCode + ":" + respDesc); + this.respCode = respCode; + this.respDesc = respDesc; + this.responseMsg = responseMsg; + } + + public String toString() { + return this.toJSON().toString(); + } + + public JSONObject toJSON() { + JSONObject sandResp = new JSONObject(); + sandResp.put("respCode", this.getRespCode()); + sandResp.put("respDesc", this.getRespDesc()); + if (this.getResponseMsg() != null && this.getResponseMsg().length() > 0) { + JSONObject responseMsg = (JSONObject) JSON.parseObject(this.getResponseMsg(), JSONObject.class); + sandResp.put("responseMsg", responseMsg); + } + + return sandResp; + } + + public String getRespCode() { + return this.respCode; + } + + public String getRespDesc() { + return this.respDesc; + } + + public String getResponseMsg() { + return this.responseMsg; + } +} diff --git a/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12/SandpayConfigUtil.java b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12/SandpayConfigUtil.java new file mode 100644 index 00000000..973714b4 --- /dev/null +++ b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12/SandpayConfigUtil.java @@ -0,0 +1,158 @@ +package com.ruoyi.cai.trdpay.handle.v12; + + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.InputStream; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.util.Objects; + +public class SandpayConfigUtil { + public static final Logger logger = LoggerFactory.getLogger(SandpayConfigUtil.class); + private String baseUrl; + private String version; + private String accessMid; + private String plMid; + private String mid; + private PrivateKey privateKey; + private String certNo; + private PublicKey sandPublicKey; + private String signType = "SHA256WithRSA"; + private String encryptType = "AES"; + private int connectTimeout = 10000; + private int readTimeout = 10000; + public static final String CHARSET = "UTF-8"; + public static final String AES_STR = "AES"; + public static final String RSA_STR = "RSA"; + public static final String SHA_256_WITH_RSA = "SHA256WithRSA"; + public static final String SUCCESS_STR = "success"; + public static final String RESP_CODE_STR = "respCode"; + public static final String RESP_DESC_STR = "respDesc"; + public static final String BIZ_DATA_STR = "bizData"; + public static final String SIGN_STR = "sign"; + public static final String SIGN_TYPE_STR = "signType"; + public static final String ENCRYPT_TYPE_STR = "encryptType"; + public static final String ENCRYPT_KEY_STR = "encryptKey"; + + public SandpayConfigUtil() { + } + + public PrivateKey getPrivateKey(String privateKeyPath, String privateKeyPassword) throws Exception { + logger.debug("商户私钥路径 => "+privateKeyPath); + logger.debug("商户私钥密码 => "+privateKeyPassword); + InputStream privateIns = FileUtils.loadFile(privateKeyPath); + if (Objects.isNull(privateIns)) { + throw new SandPayException("获取私钥失败"); + } else { + return RSAUtils.loadPrivateKey(privateIns, privateKeyPassword); + } + } + + public PublicKey getSandPublicKey(String sandPublicKeyPath) throws Exception { + logger.debug("杉德公钥路径 => "+ sandPublicKeyPath); + InputStream publicIns = FileUtils.loadFile(sandPublicKeyPath); + if (Objects.isNull(publicIns)) { + throw new SandPayException("获取公钥失败"); + } else { + return RSAUtils.loadPublicKey(publicIns); + } + } + + public String getBaseUrl() { + return this.baseUrl; + } + + public void setBaseUrl(String baseUrl) { + this.baseUrl = baseUrl; + } + + public String getVersion() { + return this.version; + } + + public void setVersion(String version) { + this.version = version; + } + + public String getAccessMid() { + return this.accessMid; + } + + public void setAccessMid(String accessMid) { + this.accessMid = accessMid; + } + + public String getPlMid() { + return this.plMid; + } + + public void setPlMid(String plMid) { + this.plMid = plMid; + } + + public String getMid() { + return this.mid; + } + + public void setMid(String mid) { + this.mid = mid; + } + + public PrivateKey getPrivateKey() { + return this.privateKey; + } + + public void setPrivateKey(PrivateKey privateKey) { + this.privateKey = privateKey; + } + + public String getCertNo() { + return this.certNo; + } + + public void setCertNo(String certNo) { + this.certNo = certNo; + } + + public PublicKey getSandPublicKey() { + return this.sandPublicKey; + } + + public void setSandPublicKey(PublicKey sandPublicKey) { + this.sandPublicKey = sandPublicKey; + } + + public String getSignType() { + return this.signType; + } + + public void setSignType(String signType) { + this.signType = signType; + } + + public String getEncryptType() { + return this.encryptType; + } + + public void setEncryptType(String encryptType) { + this.encryptType = encryptType; + } + + public int getConnectTimeout() { + return this.connectTimeout; + } + + public void setConnectTimeout(int connectTimeout) { + this.connectTimeout = connectTimeout; + } + + public int getReadTimeout() { + return this.readTimeout; + } + + public void setReadTimeout(int readTimeout) { + this.readTimeout = readTimeout; + } +}