33333333333

This commit is contained in:
777
2025-04-07 22:39:07 +08:00
parent ef44bbebf6
commit 4e5cd6f97e
35 changed files with 1805 additions and 60 deletions

View File

@@ -1,8 +1,9 @@
package com.ruoyi.cai.trdpay.handle; package com.ruoyi.cai.trdpay.handle;
import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson2.JSON; 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.domain.PayTrdConfig;
import com.ruoyi.cai.pay.PayManager; import com.ruoyi.cai.pay.PayManager;
import com.ruoyi.cai.pay.PayOrderInfoDTO; 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.SandPayClient;
import com.ruoyi.cai.trdpay.handle.v12.SandPayConfig; import com.ruoyi.cai.trdpay.handle.v12.SandPayConfig;
import com.ruoyi.cai.trdpay.handle.v12.SandpayConfigUtil; 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.exception.ServiceException;
import com.ruoyi.common.utils.ServletUtils; import com.ruoyi.common.utils.ServletUtils;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.Map; import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.*;
@Service @Service
@Slf4j @Slf4j
@@ -31,24 +46,22 @@ public class PayTrdV12Service implements PayTrdService {
@Autowired @Autowired
private PayManager payManager; 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){ public synchronized void init(String accessMid){
if(sandPayClient == null){ if(!init){
// 初始化sandpay参数全局设置一次 sandPublicKey = RSAUtil.getPublicKeyByPath(PUBLIC_KEY_PATH);
SandPayConfig config = new SandPayConfig(); merchantPrivateKey = RSAUtil.getPrivateKeyByPath(PRIVATE_KEY_PATH, PRIVATE_KEY_PASSWORD);
// 接入商户号 init = true;
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);
} }
} }
@@ -60,69 +73,106 @@ public class PayTrdV12Service implements PayTrdService {
@Override @Override
public PayReturnResp createOrderAli(PayOrderInfoDTO payOrderInfoDTO, PayTrdConfig payTrdConfig,boolean wx) { public PayReturnResp createOrderAli(PayOrderInfoDTO payOrderInfoDTO, PayTrdConfig payTrdConfig,boolean wx) {
init(payTrdConfig.getMchId()); init(payTrdConfig.getMchId());
Date now = new Date();
TrdPayTypeEnum type = getType(); TrdPayTypeEnum type = getType();
JSONObject bizData = new JSONObject(); // 组装业务请求参数
bizData.put("mid","6888806128148"); TradeCreateBizRequest bizRequest = new TradeCreateBizRequest();
bizData.put("outOrderNo",payOrderInfoDTO.getOrderNo()); bizRequest.setMid(payTrdConfig.getMchId());
bizData.put("description","知予-"+payOrderInfoDTO.getSubject()); bizRequest.setOutOrderNo(payOrderInfoDTO.getOrderNo());
bizData.put("goodsClass","99"); bizRequest.setDescription("知予-"+payOrderInfoDTO.getSubject());
bizData.put("amount", payOrderInfoDTO.getPrice()); bizRequest.setGoodsClass(GoodsClassEnum.GC99.getCode());
JSONArray payExtra = new JSONArray(); bizRequest.setAmount(payOrderInfoDTO.getPrice());
PayerInfo payerInfo = new PayerInfo();
List<PayerInfo.PayExtra> payExtraList = new ArrayList<>();
if(wx){ if(wx){
bizData.put("funcCodeList", new String[]{"02010005"}); bizRequest.setFuncCodeList(Collections.singletonList(FuncCodeEnum.FC02010005.getCode()));
/** /**
* subAppId : wxe78b5fdab7ddebbc * subAppId : wxe78b5fdab7ddebbc
* ghOriId : gh_40fd947b7d3f * ghOriId : gh_40fd947b7d3f
* pathUrl : pages/zf/index? * pathUrl : pages/zf/index?
* miniProgramType : 0 * miniProgramType : 0
*/ */
JSONObject exPayExtra = new JSONObject(); PayerInfo.PayExtra payExtra = new PayerInfo.PayExtra();
exPayExtra.put("funcCode","02010005"); payExtra.setFuncCode(FuncCodeEnum.FC02010005.getCode()); // 收银台功能编码
// exPayExtra.put("subAppId","wxe78b5fdab7ddebbc"); payExtra.setSubAppId("wxae39c7eed3221d26"); // 移动应用appid微信开放平台获取wx开头
exPayExtra.put("subAppId","wxae39c7eed3221d26"); // 我们的APPID payExtra.setGhOriId("gh_40fd947b7d3f"); // 小程序原始id微信公众平台获取gh_开头
exPayExtra.put("ghOriId","gh_40fd947b7d3f"); payExtra.setPathUrl("pages/zf/index?"); // 拉起小程序页面默认地址pages/zf/index?
exPayExtra.put("pathUrl","pages/zf/index?"); payExtra.setMiniProgramType("0"); // 小程序版本0-正式版 1-开发版 2-体验版
exPayExtra.put("miniProgramType","0"); payExtraList.add(payExtra);
payExtra.add(exPayExtra); payerInfo.setPayExtra(payExtraList);
}else{ }else {
bizData.put("funcCodeList", new String[]{"02020004"}); bizRequest.setFuncCodeList(Collections.singletonList(FuncCodeEnum.FC02020004.getCode()));
} }
String notifyUrl = type.getNotifyUrl(payTrdConfig.getNotifyUrl(), wx); String notifyUrl = type.getNotifyUrl(payTrdConfig.getNotifyUrl(), wx);
JSONObject payerInfo = new JSONObject(); payerInfo.setFrontUrl(notifyUrl);
payerInfo.put("frontUrl", notifyUrl); payerInfo.setPayExtra(payExtraList);
payerInfo.put("payExtra",payExtra); bizRequest.setPayerInfo(payerInfo);
bizData.put("payerInfo",payerInfo); bizRequest.setNotifyUrl(notifyUrl);
bizData.put("notifyUrl", notifyUrl); RiskmgtInfo riskmgtInfo = new RiskmgtInfo();
JSONObject riskmgtInfo = new JSONObject(); riskmgtInfo.setSourceIp(ServletUtils.getClientIP());
riskmgtInfo.put("sourceIp", ServletUtils.getClientIP()); bizRequest.setRiskmgtInfo(riskmgtInfo);
bizData.put("sdCashierType", "SDK"); bizRequest.setSdCashierType(SdCashierTypeEnum.SDK);
bizData.put("metaOption", "[{\"s\":\"Android\",\"n\":\"\",\"id\":\"\",\"sc\":\"\"},{\"s\":\"IOS\",\"n\":\"\",\"id\":\"\",\"sc\":\"\"}]"); bizRequest.setMetaOption("[{\"s\":\"Android\",\"n\":\"\",\"id\":\"\",\"sc\":\"\"},{\"s\":\"IOS\",\"n\":\"\",\"id\":\"\",\"sc\":\"\"}]");
bizData.put("riskmgtInfo",riskmgtInfo); 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 gatewayUrl = getGatewayUrl(payTrdConfig);
String createOrderUrl = gatewayUrl + type.getCreateOrderUrl(); String createOrderUrl = gatewayUrl + type.getCreateOrderUrl();
JSONObject responseJson = sandPayClient.execute(createOrderUrl, bizData); String responseJson = OKHttp3Util.postJson(createOrderUrl, requestJson);
if(responseJson == null || !"accept".equals(responseJson.getString("resultStatus"))){ log.info("V12支付请求url:{},request:{},body:{}", createOrderUrl,bizRequestJson,responseJson);
orderLogsService.createAliPayLogs(payOrderInfoDTO.getOrderNo(), createOrderUrl+JSON.toJSONString(bizData), com.alibaba.fastjson2.JSONObject.from(responseJson), false, type, getStepName(wx)); TradeResponse response = JacksonUtil.jsonToObj(responseJson, TradeResponse.class);
log.info("第三方支付失败 V12 统一支付失败失败 url={} params={} body={}, payTrdConfig={}", createOrderUrl, JSON.toJSONString(bizData), JSON.toJSONString(responseJson), JSON.toJSONString(payTrdConfig)); if (!OutRespCodeEnum.SUCCESS.getCode().equals(response.getRespCode())) {
log.info("第三方支付失败 V12 统一支付失败失败 url={} request={} body={}, payTrdConfig={}", createOrderUrl, bizRequestJson, responseJson, JSON.toJSONString(payTrdConfig));
throw new ServiceException("调用支付失败"); 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支付请求22222url:{},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 @Override
public NotifyResp getNotifyResp(Map<String, String> sourceData) { public NotifyResp getNotifyResp(Map<String, String> sourceData) {
String mchOrderNo = sourceData.get("orderid"); String bizData = sourceData.get("bizData");
String payOrderId = sourceData.get("transaction_id"); String sign = sourceData.get("sign");
String status = sourceData.get("returncode"); 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(); NotifyResp resp = new NotifyResp();
resp.setOrderNo(mchOrderNo); resp.setOrderNo(outOrderNo);
resp.setTrdOrderNo(payOrderId); resp.setTrdOrderNo(channelOrderNo);
resp.setPayTypeEnum(getType()); resp.setPayTypeEnum(getType());
resp.setSourceData(sourceData); resp.setSourceData(sourceData);
resp.setSuccess("00".equals(status)); resp.setSuccess(ResultStatusEnum.SUCCESS.getCode().equals(resultStatus));
return resp; return resp;
} }
@@ -137,3 +187,4 @@ public class PayTrdV12Service implements PayTrdService {
} }
} }

View File

@@ -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";
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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
}

View File

@@ -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;
}

View File

@@ -0,0 +1,15 @@
package com.ruoyi.cai.trdpay.handle.v12new.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 对称加密类型枚举
*/
@AllArgsConstructor
@Getter
public enum SymmetricTypeEnum {
AES
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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<String> 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;
}

View File

@@ -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;
/**
* 加密方式AESAES加密
*/
private SymmetricTypeEnum encryptType;
/**
* 加密key使用杉德公钥加密后的加密key
*/
private String encryptKey;
/**
* 证书序列号,预留字段,一个接入商户多证书时使用
*/
private String certNo;
/**
* 报文体,使用随机数加密后的请求参数
*/
private String bizData;
}

View File

@@ -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;
}

View File

@@ -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> payExtra;
/**
* 前台通知地址,支付结束后跳转的网页地址
*/
private String frontUrl;
/**
* 付款方实名域,目前只对支付宝付款生效
*/
private PayerIdentityInfo payerIdentityInfo;
/**
* 限定外部付款账户类型,在商户签约了该类账户可支付的前提下,限制用户只可使用某些账户进行支付{@link PayAccLimitEnum}
*/
private List<String> 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;
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

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

View File

@@ -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> T jsonToObj(String json, Class<T> clazz) {
try {
return objectMapper.readValue(json, clazz);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* Json字符串转换成对象
*/
public static <T> 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 <T> List<T> jsonToList(String json) {
try {
return objectMapper.readValue(json, new TypeReference<List<T>>() {
});
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* Json字符串转换成Map
*/
public static <K, V> Map<K, V> jsonToMap(String json) {
if (StringUtils.isBlank(json)) {
return null;
}
try {
return objectMapper.readValue(json, new TypeReference<Map<K, V>>() {
});
} 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> T objToObj(Object src, Class<T> clazz) {
try {
return objectMapper.convertValue(src, clazz);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 对象转换成Map
*/
public static <K, V> Map<K, V> objToMap(Object src) {
try {
return objectMapper.convertValue(src, new TypeReference<Map<K, V>>() {
});
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -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<String, String> paramMap) {
return get(url, paramMap, null);
}
public static String get(String url, Map<String, String> paramMap, Map<String, String> 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<String, String> 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<String, String> paramMap) {
return postForm(url, paramMap, null, ResultTypeEnum.BODY);
}
public static String postFormForUrl(String url, Map<String, String> paramMap, Map<String, String> headerMap) {
return postForm(url, paramMap, headerMap, ResultTypeEnum.URL);
}
public static String postForm(String url, Map<String, String> paramMap, Map<String, String> 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<String, String> paramMap, String fileParamName, String fileName, byte[] files) {
return postMultipart(url, paramMap, null, fileParamName, fileName, files);
}
public static String postMultipart(String url, Map<String, String> paramMap, Map<String, String> 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
}
}

View File

@@ -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<String, String> params) {
params.remove("sign");
StringBuilder content = new StringBuilder();
List<String> 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<String> 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; // 公钥
}
}