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 index 75d6420d..39089a45 100644 --- 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 @@ -1,8 +1,9 @@ package com.ruoyi.cai.trdpay.handle; import com.alibaba.fastjson.JSONArray; -import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.alipay.api.domain.BizData; import com.ruoyi.cai.domain.PayTrdConfig; import com.ruoyi.cai.pay.PayManager; import com.ruoyi.cai.pay.PayOrderInfoDTO; @@ -14,13 +15,27 @@ 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.cai.trdpay.handle.v12new.enums.*; +import com.ruoyi.cai.trdpay.handle.v12new.request.TradeCreateBizRequest; +import com.ruoyi.cai.trdpay.handle.v12new.request.TradeRequest; +import com.ruoyi.cai.trdpay.handle.v12new.request.inner.PayerInfo; +import com.ruoyi.cai.trdpay.handle.v12new.request.inner.RiskmgtInfo; +import com.ruoyi.cai.trdpay.handle.v12new.response.TradeCreateBizResponse; +import com.ruoyi.cai.trdpay.handle.v12new.response.TradeResponse; +import com.ruoyi.cai.trdpay.handle.v12new.utils.AESUtil; +import com.ruoyi.cai.trdpay.handle.v12new.utils.JacksonUtil; +import com.ruoyi.cai.trdpay.handle.v12new.utils.OKHttp3Util; +import com.ruoyi.cai.trdpay.handle.v12new.utils.RSAUtil; import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.utils.ServletUtils; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.RandomStringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import java.util.Map; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.util.*; @Service @Slf4j @@ -31,24 +46,22 @@ public class PayTrdV12Service implements PayTrdService { @Autowired private PayManager payManager; - private SandPayClient sandPayClient = null; + private static final String PRIVATE_KEY_PATH = "/home/server/api/sign/6888806128148.pfx"; +// private static final String PRIVATE_KEY_PATH = "D:\\mmm\\6888806128148.pfx"; + private static final String PRIVATE_KEY_PASSWORD = "926645"; + private static final String PUBLIC_KEY_PATH = "/home/server/api/sign/sand_pro.cer"; +// private static final String PUBLIC_KEY_PATH = "D:\\mmm\\sand_pro.cer"; + + + private boolean init = false; + private static PublicKey sandPublicKey = null; + private static PrivateKey merchantPrivateKey = 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.setPrivateKeyPath("/home/server/api/sign/6888806128148.pfx");//存放目录:src\main\resources\cert\prod - config.setPrivateKeyPassword("926645"); -// config.setSandPublicKeyPath("D:\\mmm\\sand_pro.cer");//存放目录:src\main\resources\cert\prod - config.setSandPublicKeyPath("/home/server/api/sign/sand_pro.cer");//存放目录:src\main\resources\cert\prod - // 请求版本号 - config.setVersion("4.0.0"); - sandPayClient = new SandPayClient(config); + if(!init){ + sandPublicKey = RSAUtil.getPublicKeyByPath(PUBLIC_KEY_PATH); + merchantPrivateKey = RSAUtil.getPrivateKeyByPath(PRIVATE_KEY_PATH, PRIVATE_KEY_PASSWORD); + init = true; } } @@ -60,69 +73,106 @@ public class PayTrdV12Service implements PayTrdService { @Override public PayReturnResp createOrderAli(PayOrderInfoDTO payOrderInfoDTO, PayTrdConfig payTrdConfig,boolean wx) { init(payTrdConfig.getMchId()); + Date now = new Date(); 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(); + // 组装业务请求参数 + TradeCreateBizRequest bizRequest = new TradeCreateBizRequest(); + bizRequest.setMid(payTrdConfig.getMchId()); + bizRequest.setOutOrderNo(payOrderInfoDTO.getOrderNo()); + bizRequest.setDescription("知予-"+payOrderInfoDTO.getSubject()); + bizRequest.setGoodsClass(GoodsClassEnum.GC99.getCode()); + bizRequest.setAmount(payOrderInfoDTO.getPrice()); + PayerInfo payerInfo = new PayerInfo(); + List payExtraList = new ArrayList<>(); if(wx){ - bizData.put("funcCodeList", new String[]{"02010005"}); + bizRequest.setFuncCodeList(Collections.singletonList(FuncCodeEnum.FC02010005.getCode())); /** * 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("subAppId","wxae39c7eed3221d26"); // 我们的APPID - 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"}); + PayerInfo.PayExtra payExtra = new PayerInfo.PayExtra(); + payExtra.setFuncCode(FuncCodeEnum.FC02010005.getCode()); // 收银台功能编码 + payExtra.setSubAppId("wxae39c7eed3221d26"); // 移动应用appid,微信开放平台获取,wx开头 + payExtra.setGhOriId("gh_40fd947b7d3f"); // 小程序原始id,微信公众平台获取,gh_开头 + payExtra.setPathUrl("pages/zf/index?"); // 拉起小程序页面,默认地址:pages/zf/index? + payExtra.setMiniProgramType("0"); // 小程序版本,0-正式版 1-开发版 2-体验版 + payExtraList.add(payExtra); + payerInfo.setPayExtra(payExtraList); + }else { + bizRequest.setFuncCodeList(Collections.singletonList(FuncCodeEnum.FC02020004.getCode())); } 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 riskmgtInfo = new JSONObject(); - riskmgtInfo.put("sourceIp", ServletUtils.getClientIP()); - bizData.put("sdCashierType", "SDK"); - bizData.put("metaOption", "[{\"s\":\"Android\",\"n\":\"\",\"id\":\"\",\"sc\":\"\"},{\"s\":\"IOS\",\"n\":\"\",\"id\":\"\",\"sc\":\"\"}]"); - bizData.put("riskmgtInfo",riskmgtInfo); + payerInfo.setFrontUrl(notifyUrl); + payerInfo.setPayExtra(payExtraList); + bizRequest.setPayerInfo(payerInfo); + bizRequest.setNotifyUrl(notifyUrl); + RiskmgtInfo riskmgtInfo = new RiskmgtInfo(); + riskmgtInfo.setSourceIp(ServletUtils.getClientIP()); + bizRequest.setRiskmgtInfo(riskmgtInfo); + bizRequest.setSdCashierType(SdCashierTypeEnum.SDK); + bizRequest.setMetaOption("[{\"s\":\"Android\",\"n\":\"\",\"id\":\"\",\"sc\":\"\"},{\"s\":\"IOS\",\"n\":\"\",\"id\":\"\",\"sc\":\"\"}]"); + String bizRequestJson = JacksonUtil.objToJson(bizRequest); + String requestKey = RandomStringUtils.randomAlphanumeric(16); // 生成随机AES-key + String bizData = AESUtil.encrypt(bizRequestJson, requestKey); // AES-报文加密 + // 组装公共请求参数 + TradeRequest request = new TradeRequest(); + request.setAccessMid(payTrdConfig.getMchId()); + request.setTimestamp(now); + request.setVersion("4.0.0"); + request.setSignType(AsymmetricTypeEnum.RSA); + request.setSign(RSAUtil.sign(bizData, merchantPrivateKey, request.getSignType())); // 商户私钥-签名 + request.setEncryptType(SymmetricTypeEnum.AES); + request.setEncryptKey(RSAUtil.encrypt(requestKey, sandPublicKey)); // 杉德公钥-加密AES-Key + // request.setCertNo(null); + request.setBizData(bizData); + String requestJson = JacksonUtil.objToJson(request); + log.info("公共请求参数:{}", requestJson); + // 发送请求 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(bizData), 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)); + String responseJson = OKHttp3Util.postJson(createOrderUrl, requestJson); + log.info("V12支付请求:url:{},request:{},body:{}", createOrderUrl,bizRequestJson,responseJson); + TradeResponse response = JacksonUtil.jsonToObj(responseJson, TradeResponse.class); + if (!OutRespCodeEnum.SUCCESS.getCode().equals(response.getRespCode())) { + log.info("第三方支付失败 V12 统一支付失败失败 url={} request={} body={}, payTrdConfig={}", createOrderUrl, bizRequestJson, responseJson, JSON.toJSONString(payTrdConfig)); throw new ServiceException("调用支付失败"); - }else{ - log.info("第三方支付成功 V12 url={} params={} body={}, payTrdConfig={}", createOrderUrl, JSON.toJSONString(bizData), JSON.toJSONString(responseJson), JSON.toJSONString(payTrdConfig)); - orderLogsService.createAliPayLogs(payOrderInfoDTO.getOrderNo(), createOrderUrl+JSON.toJSONString(bizData), com.alibaba.fastjson2.JSONObject.from(responseJson), true, type, getStepName(wx)); } - return PayReturnResp.createDesan(responseJson.getString("cashierUrl")); + boolean verifySign = RSAUtil.verifySign(response.getBizData(), response.getSign(), sandPublicKey, response.getSignType()); // 杉德公钥-验签 + if (!verifySign) { + log.info("第三方支付失败 V12 统一支付验签失败 url={} request={} body={}, payTrdConfig={}", createOrderUrl, bizRequestJson, responseJson, JSON.toJSONString(payTrdConfig)); + throw new ServiceException("调用支付失败"); + } + String responseKey = RSAUtil.decrypt(response.getEncryptKey(), merchantPrivateKey); // 商户私钥-解密AES-key + String bizResponseJson = AESUtil.decrypt(response.getBizData(), responseKey); // AES解密Data + TradeCreateBizResponse bizResponse = JacksonUtil.jsonToObj(bizResponseJson, TradeCreateBizResponse.class); + log.info("V12支付请求22222:url:{},request:{},body:{}", createOrderUrl,bizRequestJson,JSON.toJSONString(bizResponse)); + if (ResultStatusEnum.FAIL.getCode().equals(bizResponse.getResultStatus())) { + log.info("第三方支付失败 V12 统一支付状态失败 url={} request={} body={}, payTrdConfig={}", createOrderUrl, bizRequestJson, responseJson, JSON.toJSONString(payTrdConfig)); + throw new ServiceException("调用支付失败"); + } + return PayReturnResp.createDesan(bizResponse.getCashierUrl()); } @Override public NotifyResp getNotifyResp(Map sourceData) { - String mchOrderNo = sourceData.get("orderid"); - String payOrderId = sourceData.get("transaction_id"); - String status = sourceData.get("returncode"); + String bizData = sourceData.get("bizData"); + String sign = sourceData.get("sign"); + boolean verifySign = RSAUtil.verifySign(bizData, sign, sandPublicKey, AsymmetricTypeEnum.RSA); + if(verifySign) { + return null; + } + JSONObject bizDataJson = JSON.parseObject(bizData); + String outOrderNo = bizDataJson.getString("outOrderNo"); + String channelOrderNo = bizDataJson.getString("channelOrderNo"); + String resultStatus = bizDataJson.getString("resultStatus"); NotifyResp resp = new NotifyResp(); - resp.setOrderNo(mchOrderNo); - resp.setTrdOrderNo(payOrderId); + resp.setOrderNo(outOrderNo); + resp.setTrdOrderNo(channelOrderNo); resp.setPayTypeEnum(getType()); resp.setSourceData(sourceData); - resp.setSuccess("00".equals(status)); + resp.setSuccess(ResultStatusEnum.SUCCESS.getCode().equals(resultStatus)); return resp; } @@ -137,3 +187,4 @@ public class PayTrdV12Service implements PayTrdService { } } + diff --git a/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/constant/DatePattern.java b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/constant/DatePattern.java new file mode 100644 index 00000000..9fdf2aa6 --- /dev/null +++ b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/constant/DatePattern.java @@ -0,0 +1,18 @@ +package com.ruoyi.cai.trdpay.handle.v12new.constant; + +/** + * 日期格式常量 + */ +public class DatePattern { + + public static final String TIME_ZONE = "GMT+8"; + public static final String DATE = "yyyyMMdd"; + public static final String DATE_SEP = "yyyy-MM-dd"; + public static final String TIME = "HHmmss"; + public static final String TIME_SEP = "HH:mm:ss"; + public static final String DATE_TIME = "yyyyMMddHHmmss"; + public static final String DATE_TIME_SEP = "yyyy-MM-dd HH:mm:ss"; + public static final String MILLIS = "yyyyMMddHHmmssSSS"; + public static final String MILLIS_SEP = "yyyy-MM-dd HH:mm:ss.SSS"; + +} diff --git a/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/enums/AccessTypeEnum.java b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/enums/AccessTypeEnum.java new file mode 100644 index 00000000..89b68a8e --- /dev/null +++ b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/enums/AccessTypeEnum.java @@ -0,0 +1,18 @@ +package com.ruoyi.cai.trdpay.handle.v12new.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * API接入类型枚举 + */ +@AllArgsConstructor +@Getter +public enum AccessTypeEnum { + + AGENCY("agency", "服务商接入"); + + private final String code; + private final String desc; + +} diff --git a/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/enums/AsymmetricTypeEnum.java b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/enums/AsymmetricTypeEnum.java new file mode 100644 index 00000000..55ba86c3 --- /dev/null +++ b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/enums/AsymmetricTypeEnum.java @@ -0,0 +1,19 @@ +package com.ruoyi.cai.trdpay.handle.v12new.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 非对称加密类型枚举 + */ +@AllArgsConstructor +@Getter +public enum AsymmetricTypeEnum { + + RSA("SHA256WithRSA"); + + /** + * 签名算法 + */ + private final String signAlgorithm; +} diff --git a/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/enums/FuncCodeEnum.java b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/enums/FuncCodeEnum.java new file mode 100644 index 00000000..8048bf47 --- /dev/null +++ b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/enums/FuncCodeEnum.java @@ -0,0 +1,29 @@ +package com.ruoyi.cai.trdpay.handle.v12new.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 收银台功能编码枚举 + */ +@AllArgsConstructor +@Getter +public enum FuncCodeEnum { + + FC02010005("02010005", "APP包装微信小程序"), + FC02020004("02020004", "APP包装支付宝生活号"), + FC02020006("02020006", "APP包装支付宝码付"), + FC02030001("02030001", "银联线上收银台"), + FC06030003("06030003", "快捷充值(推荐)"), + FC06030001("06030001", "H5快捷(WAP支付)"), + FC05030001("05030001", "一键快捷"), + FC02040001("02040001", "杉德宝SDK/杉德宝扫码"), + FC02010002("02010002", "微信公众号"), + FC02010006("02010006", "H5包装云函数"), + FC02020002("02020002", "H5包装支付宝生活号"), + FC02020005("02020005", "H5包装支付宝码付"); + + private final String code; + private final String desc; + +} diff --git a/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/enums/GoodsClassEnum.java b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/enums/GoodsClassEnum.java new file mode 100644 index 00000000..7cb00d41 --- /dev/null +++ b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/enums/GoodsClassEnum.java @@ -0,0 +1,28 @@ +package com.ruoyi.cai.trdpay.handle.v12new.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 商品服务类型枚举 + */ +@AllArgsConstructor +@Getter +public enum GoodsClassEnum { + + GC01("01", "一般类实物商品、现场服务"), + GC02("02", "一般类咨询类服务"), + GC10("10", "游戏点卡装备充值购买类"), + GC11("11", "NFT数字藏品类"), + GC12("12", "比特币等虚拟货币类"), + GC19("19", "其他预付型储值卡充值类卡券购买类"), + GC20("20", "公共事业费缴费"), + GC21("21", "手机充值、通讯费缴费"), + GC31("31", "医美、整容"), + GC99("99", "其他"); + + private final String code; + + private final String desc; + +} diff --git a/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/enums/IdcardTypeEnum.java b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/enums/IdcardTypeEnum.java new file mode 100644 index 00000000..42d8ab16 --- /dev/null +++ b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/enums/IdcardTypeEnum.java @@ -0,0 +1,25 @@ +package com.ruoyi.cai.trdpay.handle.v12new.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 证件类型枚举 + * + * @author su.sp + */ +@AllArgsConstructor +@Getter +public enum IdcardTypeEnum { + + ID_CARD("1", "中国大陆居民-身份证"), + PASSPORT("2", "其他国家或地区居民护照"), + HK_PASS("3", "中国香港居民–来往内地通行证"), + MACAO_PASS("4", "中国澳门居民–来往内地通行证"), + TW_PASS("5", "中国台湾居民–来往大陆通行证"); + + private final String code; + + private final String desc; + +} diff --git a/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/enums/MarketProductEnum.java b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/enums/MarketProductEnum.java new file mode 100644 index 00000000..b1dbc29f --- /dev/null +++ b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/enums/MarketProductEnum.java @@ -0,0 +1,18 @@ +package com.ruoyi.cai.trdpay.handle.v12new.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 市场产品枚举 + */ +@AllArgsConstructor +@Getter +public enum MarketProductEnum { + + QZF("标准线上收款"), + CSDB("企业杉德宝"); + + private final String desc; + +} diff --git a/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/enums/OutRespCodeEnum.java b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/enums/OutRespCodeEnum.java new file mode 100644 index 00000000..9100c4eb --- /dev/null +++ b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/enums/OutRespCodeEnum.java @@ -0,0 +1,18 @@ +package com.ruoyi.cai.trdpay.handle.v12new.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 杉德系统应答码枚举 + */ +@AllArgsConstructor +@Getter +public enum OutRespCodeEnum { + + SUCCESS("success", "系统响应成功"), // 系统已经完成接入验证/报文格式验证,系统生成业务订单 + FAIL("fail", "系统响应失败"); // 在接入验证/报文格式验证时发生错误,不会生成业务订单 + + private final String code; + private final String desc; +} diff --git a/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/enums/PayAccLimitEnum.java b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/enums/PayAccLimitEnum.java new file mode 100644 index 00000000..179f8b26 --- /dev/null +++ b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/enums/PayAccLimitEnum.java @@ -0,0 +1,18 @@ +package com.ruoyi.cai.trdpay.handle.v12new.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 限定外部付款账户类型枚举 + */ +@AllArgsConstructor +@Getter +public enum PayAccLimitEnum { + + NO_CREDIT("debit", "不支持贷记卡"), + NO_HUABEI("bal_alipay", "不支持花呗"); + + private final String code; + private final String desc; +} diff --git a/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/enums/PayerLimitFlagEnum.java b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/enums/PayerLimitFlagEnum.java new file mode 100644 index 00000000..aa145f69 --- /dev/null +++ b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/enums/PayerLimitFlagEnum.java @@ -0,0 +1,18 @@ +package com.ruoyi.cai.trdpay.handle.v12new.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 限定买家标识枚举 + */ +@AllArgsConstructor +@Getter +public enum PayerLimitFlagEnum { + + NO_LIMIT("0", "默认,不限制买家"), + LIMIT("1", "限制买家"); + + private final String code; + private final String desc; +} diff --git a/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/enums/ProfitSharingFlagEnum.java b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/enums/ProfitSharingFlagEnum.java new file mode 100644 index 00000000..653334c3 --- /dev/null +++ b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/enums/ProfitSharingFlagEnum.java @@ -0,0 +1,18 @@ +package com.ruoyi.cai.trdpay.handle.v12new.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 分账交易标识枚举 + */ +@AllArgsConstructor +@Getter +public enum ProfitSharingFlagEnum { + + NO_SHARE("0", "不分账(默认)"), + FOLLOW_UP("1", "后续发起分账交易"); + + private final String code; + private final String desc; +} diff --git a/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/enums/ResponseCodeEnum.java b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/enums/ResponseCodeEnum.java new file mode 100644 index 00000000..3ea1a57a --- /dev/null +++ b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/enums/ResponseCodeEnum.java @@ -0,0 +1,42 @@ +package com.ruoyi.cai.trdpay.handle.v12new.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 返回码枚举 + */ +@AllArgsConstructor +@Getter +public enum ResponseCodeEnum { + + SYSTEM_BUSY("P01003", "系统繁忙,请稍后重试"), + SYSTEM_ERROR("P01098", "系统异常"), + DATABASE_ERROR("P01104", "数据库操作异常"), + REDIS_ERROR("P01106", "Redis操作异常"), + CHANNEL_ERROR("P01108", "渠道请求异常"), + INVALID_FORMAT("P03002", "请求报文格式有误"), + INVALID_PARAMS("P03003", "请求报文参数有误"), + SIGN_VERIFY_ERROR("P03004", "交易验签失败"), + DECRYPT_ERROR("P03005", "解密失败"), + BUILD_RESP_ERROR("P03006", "响应报文生成失败"), + INVALID_IP("P03012", "请求IP不合法"), + INVALID_PAY_TYPE("P05002", "交易商户未开通该市场产品或功能产品或不支持该类业务或交易商户无此API接口权限"), + BIZ_NO_REPEAT("P05004", "订单号/流水号/参考号重复"), + BIZ_NOT_SUPPORT("P05015", "交易的付款方账户不支持该类业务"), + CODE_SEND_FAIL("P05020", "验证码(验证授权)/收款码下发失败或无法下发"), + OTHER_BIZ_ERROR("P05998", "业务验证类的其他原因"), + ORDER_EXPIRED("P05999", "订单超时失败或超时未支付"), + ONE_TRADE_LIMIT("P06001", "商户单笔交易限额超限"), + STATUS_NOT_ALLOW("P07002", "协议状态不支持该操作"); + + /** + * 返回码 + */ + private final String code; + + /** + * 返回码描述 + */ + private final String desc; +} diff --git a/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/enums/ResultStatusEnum.java b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/enums/ResultStatusEnum.java new file mode 100644 index 00000000..19bb29b7 --- /dev/null +++ b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/enums/ResultStatusEnum.java @@ -0,0 +1,20 @@ +package com.ruoyi.cai.trdpay.handle.v12new.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 交易结果状态枚举 + */ +@AllArgsConstructor +@Getter +public enum ResultStatusEnum { + + SUCCESS("success", "交易成功"), + FAIL("fail", "交易失败"), + PROCESS("process", "交易处理中"), + ACCEPT("accept", "受理成功"); + + private final String code; + private final String desc; +} diff --git a/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/enums/SdCashierTypeEnum.java b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/enums/SdCashierTypeEnum.java new file mode 100644 index 00000000..b8e5c620 --- /dev/null +++ b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/enums/SdCashierTypeEnum.java @@ -0,0 +1,15 @@ +package com.ruoyi.cai.trdpay.handle.v12new.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 杉德收银台类型枚举 + */ +@AllArgsConstructor +@Getter +public enum SdCashierTypeEnum { + + H5, SDK, PC + +} diff --git a/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/enums/SettleCurrencyEnum.java b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/enums/SettleCurrencyEnum.java new file mode 100644 index 00000000..0502869a --- /dev/null +++ b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/enums/SettleCurrencyEnum.java @@ -0,0 +1,16 @@ +package com.ruoyi.cai.trdpay.handle.v12new.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 结算币种枚举 + */ +@AllArgsConstructor +@Getter +public enum SettleCurrencyEnum { + + CNY("人民币"); + + private final String desc; +} diff --git a/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/enums/SymmetricTypeEnum.java b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/enums/SymmetricTypeEnum.java new file mode 100644 index 00000000..5a909bdf --- /dev/null +++ b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/enums/SymmetricTypeEnum.java @@ -0,0 +1,15 @@ +package com.ruoyi.cai.trdpay.handle.v12new.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 对称加密类型枚举 + */ +@AllArgsConstructor +@Getter +public enum SymmetricTypeEnum { + + AES + +} diff --git a/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/enums/TransBlockFlagEnum.java b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/enums/TransBlockFlagEnum.java new file mode 100644 index 00000000..67feb6c3 --- /dev/null +++ b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/enums/TransBlockFlagEnum.java @@ -0,0 +1,18 @@ +package com.ruoyi.cai.trdpay.handle.v12new.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 营销户资金不足阻断交易标识枚举 + */ +@AllArgsConstructor +@Getter +public enum TransBlockFlagEnum { + + NONE("none", "默认为营销户资金不足,用户不享受指定营销活动的优惠,按无优惠模式继续当前收款交易"), + BLOCK("block", "营销户资金不足,阻断当前收款交易,交易失败"); + + private final String code; + private final String desc; +} diff --git a/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/enums/TransFlagEnum.java b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/enums/TransFlagEnum.java new file mode 100644 index 00000000..90faa942 --- /dev/null +++ b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/enums/TransFlagEnum.java @@ -0,0 +1,18 @@ +package com.ruoyi.cai.trdpay.handle.v12new.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 特定交易标识枚举 + */ +@AllArgsConstructor +@Getter +public enum TransFlagEnum { + + NONE("00", "无(默认)"), + S0("S0", "申请S0结算"); + + private final String code; + private final String desc; +} diff --git a/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/request/BaseBizRequest.java b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/request/BaseBizRequest.java new file mode 100644 index 00000000..a2ce8835 --- /dev/null +++ b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/request/BaseBizRequest.java @@ -0,0 +1,28 @@ +package com.ruoyi.cai.trdpay.handle.v12new.request; + +import com.ruoyi.cai.trdpay.handle.v12new.enums.AccessTypeEnum; +import com.ruoyi.cai.trdpay.handle.v12new.enums.MarketProductEnum; +import lombok.Getter; +import lombok.Setter; + +import java.io.Serializable; + +/** + * 业务请求参数基类 + */ +@Getter +@Setter +public class BaseBizRequest implements Serializable { + + /** + * 市场产品,默认为:QZF标准线上收款 + */ + private MarketProductEnum marketProduct; + + /** + * API接入类型,若为服务商接入,则必填,agency-服务商接入 + * {@link AccessTypeEnum} + */ + private String accessType; + +} diff --git a/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/request/TradeCreateBizRequest.java b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/request/TradeCreateBizRequest.java new file mode 100644 index 00000000..14cb71c1 --- /dev/null +++ b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/request/TradeCreateBizRequest.java @@ -0,0 +1,125 @@ +package com.ruoyi.cai.trdpay.handle.v12new.request; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.OptBoolean; +import com.ruoyi.cai.trdpay.handle.v12new.constant.DatePattern; +import com.ruoyi.cai.trdpay.handle.v12new.enums.FuncCodeEnum; +import com.ruoyi.cai.trdpay.handle.v12new.enums.GoodsClassEnum; +import com.ruoyi.cai.trdpay.handle.v12new.enums.SdCashierTypeEnum; +import com.ruoyi.cai.trdpay.handle.v12new.request.inner.*; +import lombok.Getter; +import lombok.Setter; + +import java.math.BigDecimal; +import java.util.Date; +import java.util.List; + +/** + * 统一下单业务请求参数 + * + * @author su.sp + */ +@Getter +@Setter +public class TradeCreateBizRequest extends BaseBizRequest { + + /** + * 交易商户号,实际收款商户的商户号(在杉德的市场产品侧注册的),收款交易资金清算给此商户 + */ + private String mid; + + /** + * 商户订单号,商户向杉德发起交易提交的商户自定义的的唯一单号,要求50个字符内,只能是数字、大小写字母、_、-,建议包含下单日期 + */ + private String outOrderNo; + + /** + * 收款订单描述,收款订单适用,该信息描述收款订单的摘要,会提交给通道,也将出现在对账单中。记录在收款方交易明细中,若付款方使用杉德内部账户的,会记录在付款方的账单明细的"订单描述"字段 + * 例如:腾讯充值中心-QQ会员充值 + */ + private String description; + + /** + * 商品服务类型,用于收款交易的风险管理,参见商品服务类型码表 + * {@link GoodsClassEnum} + */ + private String goodsClass; + + /** + * 商户订单金额,单位元,订单总金额 + */ + private BigDecimal amount; + + /** + * 可用收银台功能列表,指在调起杉德收银台时限定的前端支付方式(实际展示的商户的签约功能产品、客户在当前收银台类型下可用的功能产品和本参数指定的收银台功能的交集) + * 支持传入多个,编码不得重复,(支付宝,微信功能各仅限传1个),参考收银台功能编码列表 + * {@link FuncCodeEnum} + */ + private List funcCodeList; + + /** + * 付款方信息域 + */ + private PayerInfo payerInfo; + + /** + * 手续费账户账号,用于支付手续费的账号,在商家中心等平台对商户展示。不填写则默认使用配置的手续费账户 + * 预留,实际目前不用传。开放平台接口无此字段对接 + */ + private String feeSdaccSubId; + + /** + * 订单超时时间,格式:yyyyMMddHHmmss,不填时默认2个小时,最低5分钟,不超过30天 + */ + @JsonFormat(pattern = DatePattern.DATE_TIME, timezone = DatePattern.TIME_ZONE, lenient = OptBoolean.FALSE) + private Date timeOut; + + /** + * 异步通知地址,杉德支付主动通知商户订单支付结果的https路径,通知地址必须为直接可以访问的URL,该地址需向杉德报备 + */ + private String notifyUrl; + + /** + * 结算信息域 + */ + private SettleInfo settleInfo; + + /** + * 商户门店信息 + */ + private StoreInfo storeInfo; + + /** + * 交易优惠信息域,仅适合商户在杉德营销平台建立的营销活动 + */ + private DiscountInfo discountInfo; + + /** + * 请求方保留域 + */ + private ReqReserved reqReserved; + + /** + * 风险监控信息域,商户上送的风险监控信息 + */ + private RiskmgtInfo riskmgtInfo; + + /** + * 杉德收银台类型 + */ + private SdCashierTypeEnum sdCashierType; + + /** + * 终端/网站参数,[{"s":"Android","n":"","id":"","sc":""},{"s":"IOS","n":"","id":"","sc":""}] + * 本参数填示例值就可以 + */ + private String metaOption; + + /** + * 跳转scheme,安卓支付宝SDK跳转所需参数 + * 格式:"sandh5payres://自定义小写字符串",需要和客户端工程配置保持一致 + * 例:android:scheme = "sandh5payres",android:host = "xxx",则jumpScheme需填"sandh5payres://xxx" + */ + private String jumpScheme; + +} diff --git a/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/request/TradeRequest.java b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/request/TradeRequest.java new file mode 100644 index 00000000..1db07273 --- /dev/null +++ b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/request/TradeRequest.java @@ -0,0 +1,65 @@ +package com.ruoyi.cai.trdpay.handle.v12new.request; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.OptBoolean; +import com.ruoyi.cai.trdpay.handle.v12new.constant.DatePattern; +import com.ruoyi.cai.trdpay.handle.v12new.enums.AsymmetricTypeEnum; +import com.ruoyi.cai.trdpay.handle.v12new.enums.SymmetricTypeEnum; +import lombok.Getter; +import lombok.Setter; + +import java.util.Date; + +/** + * 网关交易公共请求参数 + */ +@Getter +@Setter +public class TradeRequest { + + /** + * 接入商户号,验签和加解密使用接入商户号绑定的证书或密钥。平台接入传平台mid,普通商户传自己的mid + */ + private String accessMid; + + /** + * 时间戳,发送请求的时间,格式"yyyy-MM-dd HH:mm:ss" + */ + @JsonFormat(pattern = DatePattern.DATE_TIME_SEP, timezone = DatePattern.TIME_ZONE, lenient = OptBoolean.FALSE) + private Date timestamp; + + /** + * 版本号,调用的接口版本 + */ + private String version; + + /** + * 签名方式 + */ + private AsymmetricTypeEnum signType; + + /** + * 签名,请求参数的签名串 + */ + private String sign; + + /** + * 加密方式,AES:AES加密 + */ + private SymmetricTypeEnum encryptType; + + /** + * 加密key,使用杉德公钥加密后的加密key + */ + private String encryptKey; + + /** + * 证书序列号,预留字段,一个接入商户多证书时使用 + */ + private String certNo; + + /** + * 报文体,使用随机数加密后的请求参数 + */ + private String bizData; +} diff --git a/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/request/inner/DiscountInfo.java b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/request/inner/DiscountInfo.java new file mode 100644 index 00000000..42541a7a --- /dev/null +++ b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/request/inner/DiscountInfo.java @@ -0,0 +1,29 @@ +package com.ruoyi.cai.trdpay.handle.v12new.request.inner; + +import lombok.Getter; +import lombok.Setter; + +import java.math.BigDecimal; + +/** + * 交易优惠信息域 + */ +@Getter +@Setter +public class DiscountInfo { + + /** + * 参加营销活动编号,收款商户(交易商户号)在杉德营销平台上建立的营销活动 + */ + private String activityNo; + + /** + * 优惠金额,单位元,仅立减营销活动模式适用,交易时会从交易商户或平台商户指定的营销出资账户扣除商户优惠金额 + */ + private BigDecimal discountAmt; + + /** + * 营销户资金不足阻断交易标识,仅对杉德营销活动有效{@link TransBlockFlagEnum} + */ + private String transBlockFlag; +} diff --git a/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/request/inner/PayerInfo.java b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/request/inner/PayerInfo.java new file mode 100644 index 00000000..077d4259 --- /dev/null +++ b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/request/inner/PayerInfo.java @@ -0,0 +1,150 @@ +package com.ruoyi.cai.trdpay.handle.v12new.request.inner; + +import com.ruoyi.cai.trdpay.handle.v12new.enums.FuncCodeEnum; +import com.ruoyi.cai.trdpay.handle.v12new.enums.IdcardTypeEnum; +import com.ruoyi.cai.trdpay.handle.v12new.enums.PayAccLimitEnum; +import com.ruoyi.cai.trdpay.handle.v12new.enums.PayerLimitFlagEnum; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +/** + * 付款方信息域 + */ +@Getter +@Setter +public class PayerInfo { + + /** + * 支付扩展域,根据收银台功能编码确定,详见支付扩展域填写说明 + */ + private List payExtra; + + /** + * 前台通知地址,支付结束后跳转的网页地址 + */ + private String frontUrl; + + /** + * 付款方实名域,目前只对支付宝付款生效 + */ + private PayerIdentityInfo payerIdentityInfo; + + /** + * 限定外部付款账户类型,在商户签约了该类账户可支付的前提下,限制用户只可使用某些账户进行支付{@link PayAccLimitEnum} + */ + private List payAccLimit; + + @Getter + @Setter + public static class PayExtra { + + /** + * 收银台功能编码,参考收银台功能编码列表{@link FuncCodeEnum} + */ + private String funcCode; + + /** + * 微信公众号或小程序appid,微信支付必须上送,请人工报备,所属收银台功能编码如下: + * APP包装微信小程序02010005 + * 微信公众号02010002 + * H5包装云函数02010006 + */ + private String subAppId; + + /** + * 小程序原始id,微信公众平台获取,gh_开头,所属收银台功能编码如下: + * APP包装微信小程序02010005 + */ + private String ghOriId; + + /** + * 拉起小程序页面默认地址:pages/zf/index?,所属收银台功能编码如下: + * APP包装微信小程序02010005 + */ + private String pathUrl; + + /** + * 开发时根据小程序是开发版、体验版或正式版自行选择。正式版:0; 开发版:1; 体验版:2,所属收银台功能编码如下: + * APP包装微信小程序02010005 + */ + private String miniProgramType; + + /** + * 微信支付时,要求上送用户在合作方subAppId下唯一标识,获取流程请参考文档说明:https://developers.weixin.qq.com/miniprogram/dev/api/open-api/user-info/wx.getUserInfo.html,获取openid建议使用静默授权,所属收银台功能编码如下: + * 微信公众号02010002 + */ + private String subUserId; + + /** + * 云开发环境ID,所属收银台功能编码如下: + * H5包装云函数02010006 + */ + private String resourceEnv; + + /** + * 用户ID,持卡用户在商户侧的唯一标识,所属收银台功能编码如下: + * 快捷充值(推荐)06030003 + * 一键快捷05030001 + */ + private String userId; + + /** + * 付款卡号,填写卡号则直接跳转到银联H5支付页面,且卡号不允许修改;不填写卡号则跳转至杉德H5支付页面,卡号由用户手工输入,提交后再跳转到银联H5支付页面,且卡号允许修改;建议填写卡号,所属收银台功能编码如下: + * 快捷充值(推荐)06030003 + * H5快捷06030001 + */ + private String cardNo; + + /** + * 银行预留手机号,绑定银行卡对应银行预留手机号,所属收银台功能编码如下: + * 快捷充值(推荐)06030003 + */ + private String phoneNo; + + /** + * 姓名,所属收银台功能编码如下: + * 快捷充值(推荐)06030003 + */ + private String name; + + /** + * 证件类型,所属收银台功能编码如下: + * 快捷充值(推荐)06030003 + * {@link IdcardTypeEnum} + */ + private String idcardType; + + /** + * 证件号码,所属收银台功能编码如下: + * 快捷充值(推荐)06030003 + */ + private String idcardNumber; + } + + @Getter + @Setter + public static class PayerIdentityInfo { + + /** + * 限定买家标识{@link PayerLimitFlagEnum} + */ + private String payerLimitFlag; + + /** + * 姓名 + */ + private String name; + + /** + * 证件类型{@link IdcardTypeEnum} + */ + private String idcardType; + + /** + * 证件号码 + */ + private String idcardNumber; + } +} diff --git a/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/request/inner/ReqReserved.java b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/request/inner/ReqReserved.java new file mode 100644 index 00000000..0ecd13d2 --- /dev/null +++ b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/request/inner/ReqReserved.java @@ -0,0 +1,18 @@ +package com.ruoyi.cai.trdpay.handle.v12new.request.inner; + +import lombok.Getter; +import lombok.Setter; + +/** + * 请求方保留域 + */ +@Getter +@Setter +public class ReqReserved { + + /** + * 请求方自用交易摘要,请求方自用,杉德不做处理原样返回,交易对账单中可选出现,结算单明细中不出现 + */ + private String reqMemo; + +} diff --git a/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/request/inner/RiskmgtInfo.java b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/request/inner/RiskmgtInfo.java new file mode 100644 index 00000000..45b8990b --- /dev/null +++ b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/request/inner/RiskmgtInfo.java @@ -0,0 +1,18 @@ +package com.ruoyi.cai.trdpay.handle.v12new.request.inner; + +import lombok.Getter; +import lombok.Setter; + +/** + * 风险监控信息域 + */ +@Getter +@Setter +public class RiskmgtInfo { + + /** + * 客户发起交易的设备IP + */ + private String sourceIp; + +} diff --git a/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/request/inner/SettleInfo.java b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/request/inner/SettleInfo.java new file mode 100644 index 00000000..732dd0c1 --- /dev/null +++ b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/request/inner/SettleInfo.java @@ -0,0 +1,48 @@ +package com.ruoyi.cai.trdpay.handle.v12new.request.inner; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.OptBoolean; +import com.ruoyi.cai.trdpay.handle.v12new.constant.DatePattern; +import com.ruoyi.cai.trdpay.handle.v12new.enums.ProfitSharingFlagEnum; +import com.ruoyi.cai.trdpay.handle.v12new.enums.SettleCurrencyEnum; +import com.ruoyi.cai.trdpay.handle.v12new.enums.TransFlagEnum; +import lombok.Getter; +import lombok.Setter; + +import java.util.Date; + +/** + * 结算信息域 + */ +@Getter +@Setter +public class SettleInfo { + + /** + * 特定交易标识,需要商户签约时开通了才可以使用 + * {@link TransFlagEnum} + */ + private String transFlag; + + /** + * S0划款附言,transFlag值为"S0"时有效,该信息将出现在S0结算交易的付款附言中 + */ + private String s0Remark; + + /** + * 分账交易标识,0-不分账(默认) 1-后续发起分账交易 + * {@link ProfitSharingFlagEnum} + */ + private String profitSharingFlag; + + /** + * 分账超时时间,格式:yyyyMMddHHmmss,到超时时间后,若未完结的会自动完结,剩余未分账金额结算给收款商户,对于profitSharingFlag="1"时可填写,未填写默认为30天,最长60天 + */ + @JsonFormat(pattern = DatePattern.DATE_TIME, timezone = DatePattern.TIME_ZONE, lenient = OptBoolean.FALSE) + private Date profitSharingTimeout; + + /** + * 结算币种,默认为:CNY人民币 + */ + private SettleCurrencyEnum settleCurrency; +} diff --git a/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/request/inner/StoreInfo.java b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/request/inner/StoreInfo.java new file mode 100644 index 00000000..ad1b16dc --- /dev/null +++ b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/request/inner/StoreInfo.java @@ -0,0 +1,40 @@ +package com.ruoyi.cai.trdpay.handle.v12new.request.inner; + +import lombok.Getter; +import lombok.Setter; + +/** + * 商户门店信息 + * + * @author su.sp + */ +@Getter +@Setter +public class StoreInfo { + + /** + * 收银台号,用于对账单展示 + */ + private String cashierNo; + + /** + * 收银员号,用于对账单展示 + */ + private String operatorNo; + + /** + * 门店编号,用于对账单展示 + */ + private String storeNo; + + /** + * 门店名称,用于对账单展示 + */ + private String storeName; + + /** + * 门店区域,用于对账单展示 + */ + private String storeArea; + +} diff --git a/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/response/BaseBizResponse.java b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/response/BaseBizResponse.java new file mode 100644 index 00000000..6a023968 --- /dev/null +++ b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/response/BaseBizResponse.java @@ -0,0 +1,45 @@ +package com.ruoyi.cai.trdpay.handle.v12new.response; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.OptBoolean; +import com.ruoyi.cai.trdpay.handle.v12new.constant.DatePattern; +import com.ruoyi.cai.trdpay.handle.v12new.enums.ResponseCodeEnum; +import com.ruoyi.cai.trdpay.handle.v12new.enums.ResultStatusEnum; +import lombok.Getter; +import lombok.Setter; + +import java.io.Serializable; +import java.util.Date; + +/** + * 业务响应参数基类 + * + * @author su.sp + */ +@Getter +@Setter +public class BaseBizResponse implements Serializable { + + /** + * 返回客户应答时间,杉德系统返回给外部交易请求方的应答时间,格式"yyyyMMddHHmmss" + */ + @JsonFormat(pattern = DatePattern.DATE_TIME, timezone = DatePattern.TIME_ZONE, lenient = OptBoolean.FALSE) + private Date outRespTime; + + /** + * 交易结果状态,杉德返回给外部客户的交易结果状态 + * {@link ResultStatusEnum} + */ + private String resultStatus; + + /** + * 错误码,交易结果状态为失败时返回 + * {@link ResponseCodeEnum} + */ + private String errorCode; + + /** + * 错误描述,交易结果状态为失败时返回,错误描述必须是明确的,方便客户定位问题和解决问题 + */ + private String errorDesc; +} diff --git a/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/response/TradeCreateBizResponse.java b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/response/TradeCreateBizResponse.java new file mode 100644 index 00000000..a39af2c7 --- /dev/null +++ b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/response/TradeCreateBizResponse.java @@ -0,0 +1,25 @@ +package com.ruoyi.cai.trdpay.handle.v12new.response; + +import lombok.Getter; +import lombok.Setter; + +/** + * 统一下单业务响应参数 + * + * @author su.sp + */ +@Getter +@Setter +public class TradeCreateBizResponse extends BaseBizResponse { + + /** + * 交易商户号,实际收款商户的商户号(在杉德的市场产品侧注册的),收款交易资金清算给此商户 + */ + private String mid; + + /** + * 前台跳转页面,收银台页面的链接地址 + */ + private String cashierUrl; + +} diff --git a/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/response/TradeResponse.java b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/response/TradeResponse.java new file mode 100644 index 00000000..99d56254 --- /dev/null +++ b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/response/TradeResponse.java @@ -0,0 +1,72 @@ +package com.ruoyi.cai.trdpay.handle.v12new.response; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.OptBoolean; +import com.ruoyi.cai.trdpay.handle.v12new.constant.DatePattern; +import com.ruoyi.cai.trdpay.handle.v12new.enums.AsymmetricTypeEnum; +import com.ruoyi.cai.trdpay.handle.v12new.enums.OutRespCodeEnum; +import com.ruoyi.cai.trdpay.handle.v12new.enums.SymmetricTypeEnum; +import lombok.Getter; +import lombok.Setter; + +import java.util.Date; + +/** + * 网关交易公共响应参数 + */ +@Getter +@Setter +public class TradeResponse { + + /** + * 接入商户号 + */ + private String accessMid; + + /** + * 版本号,调用的接口版本 + */ + private String version; + + /** + * 签名方式 + */ + private AsymmetricTypeEnum signType; + + /** + * 签名,响应参数的签名串 + */ + private String sign; + + /** + * 加密方式 + */ + private SymmetricTypeEnum encryptType; + + /** + * 加密key,使用商户公钥加密后的加密key + */ + private String encryptKey; + + /** + * 杉德系统应答码,杉德系统返回给外部交易请求方的应答码,此字段是通信标识,非交易标识 + * {@link OutRespCodeEnum} + */ + private String respCode; + + /** + * 杉德系统应答描述,杉德系统返回给外部交易请求方的系统响应失败的原因,如非空,为错误原因 + */ + private String respDesc; + + /** + * 响应时间戳,处理完成的时间,格式"yyyy-MM-dd HH:mm:ss" + */ + @JsonFormat(pattern = DatePattern.DATE_TIME_SEP, timezone = DatePattern.TIME_ZONE, lenient = OptBoolean.FALSE) + private Date respTime; + + /** + * 报文体,使用随机数加密后的响应参数 + */ + private String bizData; +} diff --git a/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/utils/AESUtil.java b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/utils/AESUtil.java new file mode 100644 index 00000000..afd1e334 --- /dev/null +++ b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/utils/AESUtil.java @@ -0,0 +1,60 @@ +package com.ruoyi.cai.trdpay.handle.v12new.utils; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +/** + * AES工具类 + */ +public class AESUtil { + + /** + * 密钥算法 + */ + public static final String KEY_ALGORITHM = "AES"; + + /** + * 算法/模式/填充方式 + */ + public static final String CIPHER_ALGORITHM = "AES/ECB/PKCS5Padding"; + + public static String encrypt(String plainText, String key) { + try { + byte[] data = plainText.getBytes(StandardCharsets.UTF_8); + Cipher cipher = getAESCipher(Cipher.ENCRYPT_MODE, key); + return Base64.getEncoder().encodeToString(cipher.doFinal(data)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static String decrypt(String cipherTextBase64, String key) { + try { + byte[] data = Base64.getDecoder().decode(cipherTextBase64); + Cipher cipher = getAESCipher(Cipher.DECRYPT_MODE, key); + return new String(cipher.doFinal(data), StandardCharsets.UTF_8); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static Cipher getAESCipher(int mode, String key) { + try { + // AES密钥长度为128bit、192bit、256bit,默认为128bit + byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8); + if (keyBytes.length % 8 != 0 || keyBytes.length < 16 || keyBytes.length > 32) { + throw new RuntimeException("AES密钥长度不合法"); + } + SecretKey secretKey = new SecretKeySpec(keyBytes, KEY_ALGORITHM); + Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM); + cipher.init(mode, secretKey); + return cipher; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + +} diff --git a/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/utils/JacksonUtil.java b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/utils/JacksonUtil.java new file mode 100644 index 00000000..04486d1e --- /dev/null +++ b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/utils/JacksonUtil.java @@ -0,0 +1,125 @@ +package com.ruoyi.cai.trdpay.handle.v12new.utils; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.ruoyi.cai.trdpay.handle.v12new.constant.DatePattern; +import org.apache.commons.lang3.StringUtils; + +import java.text.SimpleDateFormat; +import java.util.List; +import java.util.Map; + +/** + * Jackson工具类:Jackson + * + * @author su.sp + */ +public class JacksonUtil { + + private static final ObjectMapper objectMapper; + + static { + objectMapper = new ObjectMapper() + .setDateFormat(new SimpleDateFormat(DatePattern.DATE_TIME_SEP)) // 日期输出格式 + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) // 忽略不能识别的属性 + .configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true) // 空字符串转NULL + .configure(JsonParser.Feature.ALLOW_UNQUOTED_CONTROL_CHARS, true) // 允许转义字符 + .setSerializationInclusion(JsonInclude.Include.NON_NULL); // 忽略NULL字段 + } + + /** + * 该方法返回ObjectMapper对象实例 + */ + public static ObjectMapper getObjectMapper() { + return objectMapper; + } + + /** + * Json字符串转换成对象 + */ + public static T jsonToObj(String json, Class clazz) { + try { + return objectMapper.readValue(json, clazz); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * Json字符串转换成对象 + */ + public static T jsonToObj(String json, Class parametrized, Class... parameterClasses) { + try { + JavaType javaType = objectMapper.getTypeFactory().constructParametricType(parametrized, parameterClasses); + return objectMapper.readValue(json, javaType); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * Json字符串转换成List + */ + public static List jsonToList(String json) { + try { + return objectMapper.readValue(json, new TypeReference>() { + }); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * Json字符串转换成Map + */ + public static Map jsonToMap(String json) { + if (StringUtils.isBlank(json)) { + return null; + } + try { + return objectMapper.readValue(json, new TypeReference>() { + }); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * 对象转换成Json字符串 + */ + public static String objToJson(Object src) { + try { + return objectMapper.writeValueAsString(src); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * 对象转换成对象 + */ + public static T objToObj(Object src, Class clazz) { + try { + return objectMapper.convertValue(src, clazz); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * 对象转换成Map + */ + public static Map objToMap(Object src) { + try { + return objectMapper.convertValue(src, new TypeReference>() { + }); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + +} diff --git a/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/utils/OKHttp3Util.java b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/utils/OKHttp3Util.java new file mode 100644 index 00000000..14e99800 --- /dev/null +++ b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/utils/OKHttp3Util.java @@ -0,0 +1,165 @@ +package com.ruoyi.cai.trdpay.handle.v12new.utils; + +import lombok.extern.slf4j.Slf4j; +import okhttp3.*; +import org.apache.commons.collections4.MapUtils; +import org.apache.commons.lang3.StringUtils; + +import java.net.Proxy; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +/** + * 请求工具类 + */ +@Slf4j +public class OKHttp3Util { + + private static final OkHttpClient okHttpClient = new OkHttpClient.Builder() + .proxy(Proxy.NO_PROXY) + .connectTimeout(15, TimeUnit.SECONDS) + .readTimeout(15, TimeUnit.SECONDS) + .writeTimeout(15, TimeUnit.SECONDS) + .build(); + + public static String get(String url, Map paramMap) { + return get(url, paramMap, null); + } + + public static String get(String url, Map paramMap, Map headerMap) { + log.info("===============OkHttp3Client-get==============="); + log.info("===============请求地址:{}", url); + log.info("===============请求内容:{}", paramMap); + log.info("===============请求头:{}", headerMap); + HttpUrl.Builder httpUrlBuilder = Objects.requireNonNull(HttpUrl.parse(url)).newBuilder(); + if (MapUtils.isNotEmpty(paramMap)) { + paramMap.forEach(httpUrlBuilder::addQueryParameter); + } + Request.Builder requestBuilder = new Request.Builder().url(httpUrlBuilder.build()); + if (MapUtils.isNotEmpty(headerMap)) { + headerMap.forEach(requestBuilder::header); + } + Request request = requestBuilder.build(); + return executeForBody(request); + } + + public static String postJson(String url, String jsonData) { + return postJson(url, jsonData, null); + } + + public static String postJson(String url, String jsonData, Map headerMap) { + log.info("===============OkHttp3Client-postJson==============="); + log.info("===============请求地址:{}", url); + log.info("===============请求内容:{}", jsonData); + log.info("===============请求头:{}", headerMap); + RequestBody requestBody = RequestBody.create(MediaType.get("application/json; charset=utf-8"), jsonData); + Request.Builder requestBuilder = new Request.Builder().url(url).post(requestBody); + if (MapUtils.isNotEmpty(headerMap)) { + headerMap.forEach(requestBuilder::header); + } + Request request = requestBuilder.build(); + return executeForBody(request); + } + + public static String postFormForBody(String url, Map paramMap) { + return postForm(url, paramMap, null, ResultTypeEnum.BODY); + } + + public static String postFormForUrl(String url, Map paramMap, Map headerMap) { + return postForm(url, paramMap, headerMap, ResultTypeEnum.URL); + } + + public static String postForm(String url, Map paramMap, Map headerMap, ResultTypeEnum resultTypeEnum) { + log.info("===============OkHttp3Client-postForm==============="); + log.info("===============请求地址:{}", url); + log.info("===============请求内容:{}", paramMap); + log.info("===============请求头:{}", headerMap); + log.info("===============ResultType:{}", resultTypeEnum); + FormBody.Builder formBodyBuilder = new FormBody.Builder(); + if (MapUtils.isNotEmpty(paramMap)) { + paramMap.forEach(formBodyBuilder::add); + } + Request.Builder requestBuilder = new Request.Builder().url(url).post(formBodyBuilder.build()); + if (MapUtils.isNotEmpty(headerMap)) { + headerMap.forEach(requestBuilder::header); + } + Request request = requestBuilder.build(); + if (ResultTypeEnum.BODY == resultTypeEnum) { + String body = executeForBody(request); + try { + body = URLDecoder.decode(body, StandardCharsets.UTF_8.name()); + } catch (Exception ignored) { + + } + log.info("===============解码后:{}", body); + return body; + } + if (ResultTypeEnum.URL == resultTypeEnum) { + return executeForUrl(request); + } + return StringUtils.EMPTY; + } + + public static String postMultipart(String url, Map paramMap, String fileParamName, String fileName, byte[] files) { + return postMultipart(url, paramMap, null, fileParamName, fileName, files); + } + + public static String postMultipart(String url, Map paramMap, Map headerMap, String fileParamName, String fileName, byte[] files) { + log.info("===============OkHttp3Client-postMultipart==============="); + log.info("===============请求地址:{}", url); + log.info("===============请求内容:{}", paramMap); + log.info("===============请求头:{}", headerMap); + MultipartBody.Builder multipartBodyBuilder = new MultipartBody.Builder().setType(MultipartBody.FORM); + if (MapUtils.isNotEmpty(paramMap)) { + paramMap.forEach(multipartBodyBuilder::addFormDataPart); + } + if (files != null) { + multipartBodyBuilder.addFormDataPart(fileParamName, fileName, RequestBody.create(MediaType.parse("application/octet-stream"), files)); + } + Request.Builder requestBuilder = new Request.Builder().url(url).post(multipartBodyBuilder.build()); + if (MapUtils.isNotEmpty(headerMap)) { + headerMap.forEach(requestBuilder::header); + } + Request request = requestBuilder.build(); + return executeForBody(request); + } + + private static String executeForBody(Request request) { + try { + Response response = okHttpClient.newCall(request).execute(); + if (response.isSuccessful()) { + ResponseBody responseBody = response.body(); + String body = responseBody == null ? StringUtils.EMPTY : responseBody.string(); + log.info("===============响应成功:{}", body); + return body; + } + throw new RuntimeException("响应失败:" + response.code() + "," + response.message()); + } catch (Exception e) { + log.error("===============请求异常", e); + throw new RuntimeException("请求异常:" + e.getMessage()); + } + } + + private static String executeForUrl(Request request) { + try { + Response response = okHttpClient.newCall(request).execute(); + if (response.isSuccessful()) { + String url = response.request().url().toString(); + log.info("===============响应成功:{}", url); + return url; + } + throw new RuntimeException("响应失败:" + response.code() + "," + response.message()); + } catch (Exception e) { + log.error("===============请求异常", e); + throw new RuntimeException("请求异常:" + e.getMessage()); + } + } + + private enum ResultTypeEnum { + BODY, URL + } + +} diff --git a/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/utils/RSAUtil.java b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/utils/RSAUtil.java new file mode 100644 index 00000000..c12da286 --- /dev/null +++ b/ruoyi-cai/src/main/java/com/ruoyi/cai/trdpay/handle/v12new/utils/RSAUtil.java @@ -0,0 +1,310 @@ +package com.ruoyi.cai.trdpay.handle.v12new.utils; + +import com.ruoyi.cai.trdpay.handle.v12new.enums.AsymmetricTypeEnum; +import lombok.Getter; +import lombok.Setter; +import org.apache.commons.lang3.StringUtils; + +import javax.crypto.Cipher; +import java.io.IOException; +import java.io.InputStream; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.security.*; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.RSAPublicKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.*; + +/** + * RSA工具类 + */ +public class RSAUtil { + + /** + * 密钥算法 + */ + public static final String KEY_ALGORITHM = "RSA"; + + /** + * 算法/模式/填充方式 + */ + public static final String CIPHER_ALGORITHM = "RSA/ECB/PKCS1Padding"; + + /** + * 数据加密 + * + * @param plainText 原始数据 + * @param publicKey 公钥Base64字符串 + */ + public static String encrypt(String plainText, String publicKey) { + return encrypt(plainText, getPublicKey(publicKey)); + } + + /** + * 数据加密 + * + * @param plainText 原始数据 + * @param publicKey 公钥 + */ + public static String encrypt(String plainText, PublicKey publicKey) { + try { + Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM); + cipher.init(Cipher.ENCRYPT_MODE, publicKey); + byte[] data = plainText.getBytes(StandardCharsets.UTF_8); + // 使用公钥对数据进行加密 + byte[] cipherData = cipher.doFinal(data); + // 对加密后的数据进行BASE64编码 + return Base64.getEncoder().encodeToString(cipherData); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * 数据解密 + * + * @param cipherTextBase64 密文 + * @param privateKey 私钥Base64字符串 + */ + public static String decrypt(String cipherTextBase64, String privateKey) { + return decrypt(cipherTextBase64, getPrivateKey(privateKey)); + } + + /** + * 数据解密 + * + * @param cipherTextBase64 密文 + * @param privateKey 私钥 + */ + public static String decrypt(String cipherTextBase64, PrivateKey privateKey) { + try { + Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM); + cipher.init(Cipher.DECRYPT_MODE, privateKey); + // 对密文进行BASE64解码 + byte[] cipherData = Base64.getDecoder().decode(cipherTextBase64); + // 使用私钥对解码后的数据进行解密 + byte[] data = cipher.doFinal(cipherData); + return new String(data, StandardCharsets.UTF_8); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * 获取签名内容 + */ + public static String getSignContent(Map params) { + params.remove("sign"); + StringBuilder content = new StringBuilder(); + List keys = new ArrayList<>(params.keySet()); + Collections.sort(keys); + for (int i = 0; i < keys.size(); i++) { + String key = keys.get(i); + String value = params.get(key); + if (StringUtils.isNotBlank(key) && StringUtils.isNotBlank(value)) { + content.append(i == 0 ? "" : "&").append(key).append("=").append(value); + } + } + return content.toString(); + } + + /** + * 数据签名 + * + * @param content 原始数据 + * @param privateKey 私钥Base64字符串 + * @param asymmetricTypeEnum 签名类型 + */ + public static String sign(String content, String privateKey, AsymmetricTypeEnum asymmetricTypeEnum) { + return sign(content, getPrivateKey(privateKey), asymmetricTypeEnum); + } + + /** + * 数据签名 + * + * @param content 原始数据 + * @param privateKey 私钥 + * @param asymmetricTypeEnum 签名类型 + */ + public static String sign(String content, PrivateKey privateKey, AsymmetricTypeEnum asymmetricTypeEnum) { + try { + Signature signature = Signature.getInstance(asymmetricTypeEnum.getSignAlgorithm()); + signature.initSign(privateKey); + signature.update(content.getBytes(StandardCharsets.UTF_8)); + return Base64.getEncoder().encodeToString(signature.sign()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * 数据验签 + * + * @param content 原始数据 + * @param sign 签名 + * @param publicKey 公钥Base64字符串 + * @param asymmetricTypeEnum 签名类型 + */ + public static boolean verifySign(String content, String sign, String publicKey, AsymmetricTypeEnum asymmetricTypeEnum) { + return verifySign(content, sign, getPublicKey(publicKey), asymmetricTypeEnum); + } + + /** + * 数据验签 + * + * @param content 原始数据 + * @param sign 签名 + * @param publicKey 公钥 + * @param asymmetricTypeEnum 签名类型 + */ + public static boolean verifySign(String content, String sign, PublicKey publicKey, AsymmetricTypeEnum asymmetricTypeEnum) { + try { + Signature signature = Signature.getInstance(asymmetricTypeEnum.getSignAlgorithm()); + signature.initVerify(publicKey); + signature.update(content.getBytes(StandardCharsets.UTF_8)); + return signature.verify(Base64.getDecoder().decode(sign.getBytes())); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * 获取私钥 + * + * @param privateKey 私钥Base64字符串 + */ + public static PrivateKey getPrivateKey(String privateKey) { + try { + byte[] keyBytes = Base64.getDecoder().decode(privateKey); + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes); + KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM); + return keyFactory.generatePrivate(keySpec); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * 获取私钥 + * + * @param privateKeyPath 私钥.pfx文件的classpath路径或者绝对路径 + * @param password 私钥.pfx文件的密码 + */ + public static PrivateKey getPrivateKeyByPath(String privateKeyPath, String password) { + try (InputStream keyStream = getKeyStream(privateKeyPath)) { + KeyStore keyStore = KeyStore.getInstance("PKCS12"); + char[] pwdChar = null; + if (StringUtils.isNotBlank(password)) { + pwdChar = password.toCharArray(); + } + keyStore.load(keyStream, pwdChar); + + Enumeration aliases = keyStore.aliases(); + String alias = null; + if (aliases.hasMoreElements()) { + alias = aliases.nextElement(); + } + return (PrivateKey) keyStore.getKey(alias, pwdChar); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * 获取公钥 + * + * @param publicKey 公钥Base64字符串 + */ + public static PublicKey getPublicKey(String publicKey) { + try { + byte[] keyBytes = Base64.getDecoder().decode(publicKey); + X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes); + KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM); + return keyFactory.generatePublic(keySpec); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * 获取公钥 + * + * @param keyModulus 公钥-系数 + * @param keyExponent 公钥-指数 + */ + public static PublicKey getPublicKey(String keyModulus, String keyExponent) { + try { + BigInteger modulus = new BigInteger(keyModulus, 16); + BigInteger publicExponent = new BigInteger(keyExponent); + RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec(modulus, publicExponent); + KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM); + return keyFactory.generatePublic(publicKeySpec); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * 获取公钥 + * + * @param publicKeyPath 公钥.cer文件的classpath路径或者绝对路径 + */ + public static PublicKey getPublicKeyByPath(String publicKeyPath) { + try (InputStream keyStream = getKeyStream(publicKeyPath)) { + CertificateFactory factory = CertificateFactory.getInstance("X.509"); + X509Certificate x509 = (X509Certificate) factory.generateCertificate(keyStream); + return x509.getPublicKey(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static InputStream getKeyStream(String keyPath) throws IOException { + String classpathPrefix = "classpath:"; + InputStream keyStream; + if (keyPath.startsWith(classpathPrefix)) { + keyStream = RSAUtil.class.getClassLoader().getResourceAsStream(keyPath.substring(classpathPrefix.length())); + } else { + keyStream = Files.newInputStream(Paths.get(keyPath)); + } + return keyStream; + } + + /** + * 生成公私密钥对 + * + * @param keyLength 密钥长度 + */ + public static RSAKeyPair buildRSAKey(int keyLength) { + try { + // RSA密钥长度:512-65536(64的倍数) + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(KEY_ALGORITHM); + keyPairGenerator.initialize(keyLength, new SecureRandom()); + // 生成RSA密钥对 + KeyPair keyPair = keyPairGenerator.generateKeyPair(); + PrivateKey privateKey = keyPair.getPrivate(); + PublicKey publicKey = keyPair.getPublic(); + + RSAKeyPair rsaKeyPair = new RSAKeyPair(); + rsaKeyPair.setPrivateKey(Base64.getEncoder().encodeToString(privateKey.getEncoded())); + rsaKeyPair.setPublicKey(Base64.getEncoder().encodeToString(publicKey.getEncoded())); + return rsaKeyPair; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Getter + @Setter + public static class RSAKeyPair { + + private String privateKey; // 私钥 + + private String publicKey; // 公钥 + } +}