接入支付平台测试

This commit is contained in:
墨大叔 2024-08-02 11:05:36 +08:00
parent 43af667f20
commit 8990eb3297
23 changed files with 1239 additions and 1 deletions

View File

@ -0,0 +1,23 @@
package com.ruoyi.common.pay.yst.config;
import com.ruoyi.common.pay.yst.util.YstClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author wjh
* 2024/8/1
*/
@Configuration
public class YstBeanConfig {
@Autowired
private YstConfig ystConfig;
@Bean
public YstClient ystClient() throws Exception {
return new YstClient(ystConfig);
}
}

View File

@ -0,0 +1,48 @@
package com.ruoyi.common.pay.yst.config;
import com.ruoyi.common.pay.yst.util.YstClient;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
/**
* @author wjh
* 2024/8/1
*/
@Component
@ConfigurationProperties(prefix = "yst")
@Data
public class YstConfig {
// APP ID
private String appId;
// 云商通二代分配的服务商应用ID
private String spAppId;
// 秘钥
private String secretKey;
// 域名
private String host;
// 格式化
private String format;
// 字符集
private String charset;
// 签名方式
private String signType;
// API版本
private String version;
// 私钥
private String privateKey;
// 公钥
private String publicKey;
}

View File

@ -0,0 +1,27 @@
package com.ruoyi.common.pay.yst.constants;
/**
* @author wjh
* 2024/8/1
*/
public class YstApi {
// 会员及账户类接口
public static final String TM = "/yst-service-api/tm/handle";
// 交易类接口
public static final String TX = "/yst-service-api/tx/handle";
// 交易结果查询类接口
public static final String TQ = "/yst-service-api/tq/handle";
// 对账文件下载类接口
public static final String DOWNLOAD = "/yst-service-api/file/download";
// 文件上传
public static final String UPLOAD = "/yst-service-api/file/upload";
}

View File

@ -0,0 +1,100 @@
package com.ruoyi.common.pay.yst.constants;
/**
* 云商通接口代码
* @author wjh
* 2024/8/1
*/
public class YstTransCode {
// 企业会员实名开户
public static final String ENTERPRISE_MEMBER_REAL_NAME = "1020";
// 会员资料补录
public static final String MEMBER_INFO_SUPPLEMENT = "1022";
// 个人会员实名及绑卡申请
public static final String INDIVIDUAL_MEMBER_REAL_NAME_AND_BIND_CARD_APPLY = "1010";
// 个人会员实名及绑卡确认
public static final String INDIVIDUAL_MEMBER_REAL_NAME_AND_BIND_CARD_CONFIRM = "1011";
// 会员绑定手机号申请
public static final String MEMBER_BIND_MOBILE_APPLY = "1030";
// 会员解绑手机号原手机号申请
public static final String MEMBER_UNBIND_MOBILE_APPLY = "1031";
// 确认绑定/解绑手机号
public static final String CONFIRM_BIND_UNBIND_MOBILE = "1032";
// 会员线上协议签约申请
public static final String MEMBER_ONLINE_AGREEMENT_SIGN_APPLY = "1050";
// 线下协议文件上传
public static final String OFFLINE_AGREEMENT_FILE_UPLOAD = "1051";
// 会员协议签约结果通知
public static final String MEMBER_AGREEMENT_SIGN_RESULT_NOTIFY = "1052";
// 企业会员支付账户开户
public static final String ENTERPRISE_MEMBER_PAYMENT_ACCOUNT_OPEN = "1025";
// 查询会员信息
public static final String QUERY_MEMBER_INFO = "1027";
// 账户余额查询
public static final String ACCOUNT_BALANCE_QUERY = "1023";
// 平台资金查询
public static final String PLATFORM_FUND_QUERY = "1026";
// 会员绑定收银宝商户
public static final String MEMBER_BIND_SHOUYINBAO_MERCHANT = "1024";
// 终端信息管理
public static final String TERMINAL_INFO_MANAGEMENT = "4001";
// 消费申请
public static final String CONSUMPTION_APPLY = "2085";
// 担保消费申请
public static final String GUARANTEE_CONSUMPTION_APPLY = "2089";
// 单订单担保确认
public static final String SINGLE_ORDER_GUARANTEE_CONFIRM = "2090";
// 单会员担保确认
public static final String SINGLE_MEMBER_GUARANTEE_CONFIRM = "2091";
// 划款入账通知
public static final String TRANSFER_INTO_ACCOUNT_NOTIFY = "2080";
// 转账申请
public static final String TRANSFER_APPLY = "2084";
// 提现申请
public static final String WITHDRAW_APPLY = "2290";
// 退款申请
public static final String REFUND_APPLY = "2294";
// 订单关闭
public static final String ORDER_CLOSE = "2295";
// 确认支付后台+短信
public static final String CONFIRM_PAYMENT_BACK_SMS = "3010";
// 订单状态查询
public static final String ORDER_STATUS_QUERY = "3001";
// 订单详情查询
public static final String ORDER_DETAILS_QUERY = "3002";
// 电子回单下载
public static final String ELECTRONIC_RECEIPT_DOWNLOAD = "4003";
// 应用集合对账文件下载
public static final String APPLICATION_SET_RECONCILIATION_FILE_DOWNLOAD = "4002";
}

View File

@ -0,0 +1,36 @@
package com.ruoyi.common.pay.yst.domain;
import com.alibaba.fastjson2.JSONObject;
import java.util.List;
import java.util.Map;
/**
* 业务参数
*
* @author gejunqing
* @version 1.0
* @date 2024/1/11
*/
public class BizParameter extends JSONObject
{
public void addParam(String paramName, String paramValue)
{
this.put(paramName, paramValue);
}
public void addParam(String paramName, int paramValue)
{
this.put(paramName, paramValue);
}
public void addMapParam(String paramName, Map<String, Object> paramMap)
{
this.put(paramName, paramMap);
}
public void addListParam(String paramName, List paramValue)
{
this.put(paramName, paramValue);
}
}

View File

@ -0,0 +1,68 @@
package com.ruoyi.common.pay.yst.domain;
import com.alibaba.fastjson2.JSON;
import lombok.Data;
/**
* 云商通通用请求数据结构
* 字段 字段类型 必填 字段名称 说明
* appId String 云商通二代分配的应用ID 如上送服务商应用ID验证服务商证书
* spAppId String 云商通二代分配的服务商应用ID
* transCode String 接口代码
* format String 仅支持JSON json
* charset String 请求使用的编码格式utf-8 UTF-8
* signType String 商户生成签名字符串所使用的签名
* 算法类型 比如SM3withSM2
* sign String 商户请求参数的签名串
* transDate String 发送请求的日期格式"yyyyMMdd" 20240202
* transTime String 请求时间 141333
* version String 调用的接口版本 1.0
* bizData String 请求参数的集合最大长度不限除公共参数外所有请求参数都必须放在这个参数中传递具体参照各产品快速接入文档
*
* @author wjh
* 2024/8/1
*/
@Data
public class YstRequest {
// 云商通二代分配的应用ID
private String appId;
// 云商通二代分配的服务商应用ID
private String spAppId;
// 接口代码
private String transCode;
// 仅支持JSON
private String format;
// 请求使用的编码格式
private String charset;
// 商户生成签名字符串所使用的签名算法类型
private String signType;
// 签名串
private String sign;
// 发送请求的日期格式"yyyyMMdd"
private String transDate;
// 请求时间
private String transTime;
// 调用的接口版本
private String version;
// 请求参数的集合
private String bizData;
@Override
public String toString()
{
return JSON.toJSONString(this);
}
}

View File

@ -0,0 +1,32 @@
package com.ruoyi.common.pay.yst.domain;
import lombok.Data;
/**
* 字段 字段类型 必填 字段名称 说明
* code String 调用结果返回码 00000-成功
* msg String 调用结果返回码描述
* sign String 商户请求参数的签名串
* bizData String 返回参数的集合最大长度不限除公共参数外所有返回参数都必须放在这个参数中传递具体参照各产品快速接入文档
*
* @author wjh
* 2024/8/1
*/
@Data
public class YstResponse<T> {
// 调用结果返回码
private String code;
// 调用结果返回码描述
private String msg;
// 商户请求参数的签名串
private String sign;
// 返回参数的集合
private T bizData;
public boolean isSuccess() {
return this.code != null && this.code.equals("00000");
}
}

View File

@ -0,0 +1,86 @@
package com.ruoyi.common.pay.yst.domain.bizRes.comsumptionApply;
import com.alibaba.fastjson2.JSONObject;
import lombok.Data;
/**
* @author wjh
* 2024/8/2
*/
@Data
public class ChannelParamInfo {
/**
* 收银宝渠道交易流水号
*/
private String chnlTradeCode;
/**
* 收银宝渠道返回的交易类型针对退款订单透传退款订单落地的渠道交易类型
*/
private String chnlTransCode;
/**
* 支付人账号根据不同支付渠道返回不同的账号信息如微信的openid支付宝的user_id
*/
private String payAcctNo;
/**
* 渠道实际交易金额单位为分
*/
private Long chnlAmount;
/**
* 渠道手续费单位为分
*/
private Long chnlFee;
/**
* 透传渠道活动参数包含云闪付微信支付宝的活动参数
*/
private JSONObject chnlData;
/**
* 收银宝接口交易完成时间
*/
private String chnlFinTime;
/**
* 收银宝商户号
*/
private String chnlMchtNo;
/**
* 卡类型如借记卡信用卡或其他
*/
private String acctType;
/**
* 后侧渠道流水号如支付宝订单号微信交易单号
*/
private String chnlTrxid;
/**
* 收付通银行流水号透传收付通渠道的银行流水号
*/
private String sftChnlBankTradeCode;
/**
* 终端编号
*/
private String termNo;
/**
* 银行借贷标记仅微信支付返回
*/
private String termAuthCode;
/**
* 终端参考号收银宝订单POS支付时有值
*/
private String termRefnum;
/**
* 终端流水号对应收银宝接口字段traceno
*/
private String traceNo;
}

View File

@ -0,0 +1,41 @@
package com.ruoyi.common.pay.yst.domain.bizRes.comsumptionApply;
import com.alibaba.fastjson2.JSONObject;
import lombok.Data;
/**
* 消费申请响应
* @author wjh
* 2024/8/2
*/
@Data
public class ConsumptionApplyRes {
// 订单状态仅当交易验证方式为0时返回表示订单是否成功
private String result;
// 订单失败时的错误信息
private String respMsg;
// 通联支付系统生成的唯一订单号
private String respTraceNum;
// 商户系统内部的订单号用于商户识别订单
private String reqTraceNum;
// 扩展参数用于传递额外的信息
private String extendParams;
// 渠道参数信息包含支付详情如扫码支付信息JS支付串信息等
private ChannelParamInfo channelParamInfo;
// 前端支付参数信息用于前端展示和支付操作
private JSONObject chnlFrontParamInfo;
// 业务返回码用于标识业务处理的结果
private String respCode;
// 详细的业务返回说明提供更多关于业务处理情况的信息
private String respMsgDetailed;
}

View File

@ -0,0 +1,10 @@
package com.ruoyi.common.pay.yst.domain.params;
/**
* 消费申请 参数
* @author wjh
* 2024/8/1
*/
public class AddOrderParam {
}

View File

@ -0,0 +1,18 @@
package com.ruoyi.common.pay.yst.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author wjh
* 2024/8/1
*/
@Getter
@AllArgsConstructor
public enum YstRespCode {
SUCCESS("00000", "成功");
private final String code;
private final String msg;
}

View File

@ -0,0 +1,21 @@
package com.ruoyi.common.pay.yst.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 交易验证方式
* @author wjh
* 2024/8/2
*/
@Getter
@AllArgsConstructor
public enum YstVerifyType {
NONE("0", "无验证"),
SMS("1", "短信验证码"),
;
private final String type;
private final String msg;
}

View File

@ -0,0 +1,9 @@
package com.ruoyi.common.pay.yst.service;
/**
* 云商通账户接口
* @author wjh
* 2024/8/1
*/
public interface YstAccountService {
}

View File

@ -0,0 +1,15 @@
package com.ruoyi.common.pay.yst.service;
/**
* 云商通支付服务
* @author wjh
* 2024/8/1
*/
public interface YstPayService {
/**
* 调起支付
*/
void pay() throws Exception;
}

View File

@ -0,0 +1,12 @@
package com.ruoyi.common.pay.yst.service.impl;
import com.ruoyi.common.pay.yst.service.YstAccountService;
import org.springframework.stereotype.Service;
/**
* @author wjh
* 2024/8/1
*/
@Service
public class YstAccountServiceImpl implements YstAccountService {
}

View File

@ -0,0 +1,150 @@
package com.ruoyi.common.pay.yst.service.impl;
import com.ruoyi.common.pay.yst.config.YstConfig;
import com.ruoyi.common.pay.yst.constants.YstApi;
import com.ruoyi.common.pay.yst.constants.YstTransCode;
import com.ruoyi.common.pay.yst.domain.BizParameter;
import com.ruoyi.common.pay.yst.domain.YstResponse;
import com.ruoyi.common.pay.yst.domain.bizRes.comsumptionApply.ConsumptionApplyRes;
import com.ruoyi.common.pay.yst.domain.params.AddOrderParam;
import com.ruoyi.common.pay.yst.service.YstPayService;
import com.ruoyi.common.pay.yst.util.SM4Utils;
import com.ruoyi.common.pay.yst.util.YstClient;
import com.ruoyi.common.utils.SnowFlakeUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.text.SimpleDateFormat;
import java.util.*;
import static jdk.nashorn.internal.runtime.regexp.joni.Config.log;
/**
* @author wjh
* 2024/8/1
*/
@Service
@Slf4j
@RestController
@RequestMapping("/yst")
public class YstPayServiceImpl implements YstPayService {
@Autowired
private YstClient ystClient;
@Autowired
private YstConfig ystConfig;
/**
* 调起支付
*/
@Override
@GetMapping
public void pay() throws Exception {
// 支付模式
String bankCardNo= SM4Utils.encryptEcb(ystConfig.getSecretKey(), "6212261001029054530");
//微信正扫
Map<String, Object> SCAN_WEIXIN = new HashMap<>();
SCAN_WEIXIN.put("SCAN_WEIXIN","{\"limitPay\":\"no_credit\"}");
SCAN_WEIXIN.put("SCAN_WEIXIN","{\"vspCusid\":\"55058404816VQJW\"}");
// SCAN_WEIXIN.put("SCAN_WEIXIN","{\"extendParams\":\"渠道扩展参数\"}");
// SCAN_WEIXIN.put("SCAN_WEIXIN","{\"idNo\":\"\"}");
// SCAN_WEIXIN.put("SCAN_WEIXIN","{\"name\":\"\"}");
// SCAN_WEIXIN.put("SCAN_WEIXIN","{\"cardThype\":\"\"}");
//收银宝快捷
Map<String, Object> QUICKPAY_VSP = new HashMap<>();
System.out.println("{\"bankCardNo\":\""+bankCardNo+"\"}");
QUICKPAY_VSP.put("QUICKPAY_VSP","{\"bankCardNo\":\""+bankCardNo+"\"}");
//收付通快捷
Map<String, Object> QUICKPAY_SFT = new HashMap<>();
System.out.println("{\"bankCardNo\":\""+bankCardNo+"\"}");
QUICKPAY_SFT.put("QUICKPAY_SFT","{\"bankCardNo\":\""+bankCardNo+"\"}");
//银联正扫
Map<String, Object> SCAN_UNIONPAY = new HashMap<>();
SCAN_UNIONPAY.put("SCAN_UNIONPAY","{\"limitPay\":\"no_credit\"}");
//支付宝正扫
Map<String, Object> SCAN_ALIPAY = new HashMap<>();
SCAN_ALIPAY.put("SCAN_ALIPAY","{\"limitPay\":\"no_credit\"}");
//收银宝POS
Map<String, Object> ORDER_VSPPAY = new HashMap<>();
ORDER_VSPPAY.put("ORDER_VSPPAY","{\"vspCusid\":\"6602900601500JK\"}");
//付款码支付
Map<String, Object> CODEPAY_VSP = new HashMap<>();
CODEPAY_VSP.put("CODEPAY_VSP","{\"vspCusid\":\"\"}");
CODEPAY_VSP.put("CODEPAY_VSP","{\"authcode\":\"280310687633511560\"}");
//分账规则
List sepDetail = new ArrayList();
Map<String, Object> map1 = new HashMap<>();
map1.put("signNum", "2705wxl00002");
map1.put("amount", 1);
sepDetail.add(map1);
Map<String, Object> map2 = new HashMap<>();
map2.put("signNum", "9665wxl202400004");
map2.put("amount", 1);
// sepDetail.add(map2);
//订单过期时间
final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
final Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.MINUTE, 720);
final Date date = calendar.getTime();
final String orderValidTime = sdf.format(date);
// 组装参数
BizParameter bizParameter = new BizParameter();
bizParameter.addParam("signNum", "111111");//TESTWC10011
bizParameter.addParam("receiverSignNum", "2705wxl00001");
bizParameter.addParam("reqTraceNum", "wxl"+ SnowFlakeUtil.newId());//商户订单号
bizParameter.addParam("orderAmount", 3);//订单金额
bizParameter.addParam("payAmount", 2);//支付金额
bizParameter.addParam("promotionAmount", 1);//营销金额
bizParameter.addParam("couponAmount",1 );//平台抽佣金额
bizParameter.addParam("verifyType", 1);//交易验证方式-0无验证1短信验证码默认-1短信验证码
//支付模式
bizParameter.addMapParam("payMode", SCAN_WEIXIN);//微信正扫
// bizParameter.addMapParam("payMode", QUICKPAY_VSP);//收银宝快捷
// bizParameter.addMapParam("payMode", QUICKPAY_SFT);//收付通快捷
// bizParameter.addMapParam("payMode", SCAN_UNIONPAY);//银联正扫
// bizParameter.addMapParam("payMode", SCAN_ALIPAY);//支付宝正扫
// bizParameter.addMapParam("payMode", ORDER_VSPPAY);//收银宝POS
// bizParameter.addMapParam("payMode", CODEPAY_VSP);//付款码支付
bizParameter.addListParam("sepDetail", sepDetail);//分账规则
bizParameter.addParam("reqsUrl", "http://www.baidu.com");//前台通知地址
bizParameter.addParam("respUrl", "http://test.allinpay.com/open/testNotify");//后台通知地址
bizParameter.addParam("orderValidTime", orderValidTime);//订单过期时间
// bizParameter.addParam("goodsType", "goodsType");//商品类型
// bizParameter.addParam("bizGoodsNo", "bizGoodsNo123456");//商户商品编号
bizParameter.addParam("goodsName", "goodsName123456");//商品名称
bizParameter.addParam("goodsDesc", "goodsDesc123456");//商品描述
bizParameter.addParam("industryCode", "111111");//行业代码
bizParameter.addParam("industryName", "222222");//行业名称
bizParameter.addParam("summary", "摘要");
bizParameter.addParam("extendParams", "{\"extend\":\"abcdefghiii\"}");
// 发送请求
YstResponse<ConsumptionApplyRes> res = ystClient.sendRequest(YstApi.TX, YstTransCode.CONSUMPTION_APPLY, bizParameter, ConsumptionApplyRes.class);
res.getBizData().getChannelParamInfo();
// 打印响应结果
log.info(res.getBizData().toString());
}
}

View File

@ -0,0 +1,92 @@
package com.ruoyi.common.pay.yst.util;
import com.alibaba.fastjson2.JSONObject;
import org.apache.commons.lang3.StringUtils;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.util.encoders.Base64;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Arrays;
/**
* Created with IntelliJ IDEA.
*
* @author gejunqing
* @version 1.0
* @date 2024/1/11
*/
public class SM2Utils
{
/** 算法常量:SM3withSM2 */
public static final String ALGORITHM_SM3SM2_BCPROV = "SM3withSM2";
static
{
Security.addProvider(new BouncyCastleProvider());
}
public static String jsonMapToStr(JSONObject jsonObject)
{
String[] keys = jsonObject.keySet().toArray(new String[0]);
Arrays.sort(keys);
StringBuilder raw = new StringBuilder();
for (String key : keys)
{
if (!StringUtils.isBlank(jsonObject.getString(key)))
{
raw.append(key).append("=").append(jsonObject.getString(key)).append("&");
}
}
if (raw.length() > 0)
{
raw.deleteCharAt(raw.length() - 1);
}
return raw.toString();
}
/** 从字符串读取私钥-目前支持PKCS8(keystr为BASE64格式) */
public static PrivateKey privKeySM2FromBase64Str(String keystr) throws Exception
{
KeyFactory keyFactory = KeyFactory.getInstance("EC");
return keyFactory.generatePrivate(new PKCS8EncodedKeySpec(Base64.decode(keystr)));
}
/** 从字符串读取RSA公钥(keystr为BASE64格式) */
public static PublicKey pubKeySM2FromBase64Str(String keystr) throws Exception
{
KeyFactory keyFactory = KeyFactory.getInstance("EC");
return keyFactory.generatePublic(new X509EncodedKeySpec(Base64.decode(keystr)));
}
public static String sign(PrivateKey privateKey, String text) throws Exception
{
Signature signature = Signature.getInstance(ALGORITHM_SM3SM2_BCPROV, "BC");
signature.initSign(privateKey);
byte[] plainText = text.getBytes(StandardCharsets.UTF_8);
signature.update(plainText);
byte[] signatureValue = signature.sign();
return Base64.toBase64String(signatureValue);
}
public static boolean verify(PublicKey publicKey, String text, String sign) throws Exception
{
if (isEmpty(sign))
{
return false;
}
Signature signature = Signature.getInstance(ALGORITHM_SM3SM2_BCPROV, "BC");
signature.initVerify(publicKey);
signature.update(text.getBytes(StandardCharsets.UTF_8));
byte[] signed = Base64.decode(sign);
return signature.verify(signed);
}
public static boolean isEmpty(String str)
{
return str == null || "".equals(str) || "".equals(str.trim());
}
}

View File

@ -0,0 +1,184 @@
package com.ruoyi.common.pay.yst.util;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.pqc.math.linearalgebra.ByteUtils;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.spec.SecretKeySpec;
import java.security.*;
import java.util.Arrays;
/**
* Created with IntelliJ IDEA.
*
* @author gejunqing
* @version 1.0
* @date 2024/1/25
*/
public class SM4Utils
{
static
{
Security.addProvider(new BouncyCastleProvider());
}
private static final String ENCODING = "UTF-8";
public static final String ALGORITHM_NAME = "SM4";
// 加密算法/分组加密模式/分组填充方式
// PKCS5Padding-以8个字节为一组进行分组加密
// 定义分组加密模式使用PKCS5Padding
public static final String ALGORITHM_NAME_ECB_PADDING = "SM4/ECB/PKCS5Padding";
// 128-32位16进制256-64位16进制
public static final int DEFAULT_KEY_SIZE = 128;
/**
* 生成ECB暗号
*
* @explain ECB模式电子密码本模式Electronic codebook
* @param algorithmName 算法名称
* @param mode 模式
* @param key
* @return
* @throws Exception
*/
private static Cipher generateEcbCipher(String algorithmName, int mode, byte[] key) throws Exception
{
Cipher cipher = Cipher.getInstance(algorithmName, BouncyCastleProvider.PROVIDER_NAME);
Key sm4Key = new SecretKeySpec(key, ALGORITHM_NAME);
cipher.init(mode, sm4Key);
return cipher;
}
/**
* 自动生成密钥
*
* @explain
* @return
* @throws NoSuchAlgorithmException
* @throws NoSuchProviderException
*/
public static byte[] generateKey() throws Exception
{
return generateKey(DEFAULT_KEY_SIZE);
}
/**
* @explain
* @param keySize
* @return
* @throws Exception
*/
public static byte[] generateKey(int keySize) throws Exception
{
KeyGenerator kg = KeyGenerator.getInstance(ALGORITHM_NAME, BouncyCastleProvider.PROVIDER_NAME);
kg.init(keySize, new SecureRandom());
return kg.generateKey().getEncoded();
}
/**
* sm4加密
*
* @explain 加密模式ECB 密文长度不固定会随着被加密字符串长度的变化而变化
* @param hexKey 16进制密钥忽略大小写
* @param paramStr 待加密字符串
* @return 返回16进制的加密字符串
* @throws Exception
*/
public static String encryptEcb(String hexKey, String paramStr) throws Exception
{
String cipherText = "";
// 16进制字符串-->byte[]
byte[] keyData = ByteUtils.fromHexString(hexKey);
// String-->byte[]
byte[] srcData = paramStr.getBytes(ENCODING);
// 加密后的数组
byte[] cipherArray = encrypt_Ecb_Padding(keyData, srcData);
// byte[]-->hexString
cipherText = ByteUtils.toHexString(cipherArray);
return cipherText;
}
/**
* 加密模式之Ecb
*
* @explain
* @param key
* @param data
* @return
* @throws Exception
*/
public static byte[] encrypt_Ecb_Padding(byte[] key, byte[] data) throws Exception
{
Cipher cipher = generateEcbCipher(ALGORITHM_NAME_ECB_PADDING, Cipher.ENCRYPT_MODE, key);
return cipher.doFinal(data);
}
/**
* sm4解密
*
* @explain 解密模式采用ECB
* @param hexKey 16进制密钥
* @param cipherText 16进制的加密字符串忽略大小写
* @return 解密后的字符串
* @throws Exception
*/
public static String decryptEcb(String hexKey, String cipherText) throws Exception
{
// 用于接收解密后的字符串
String decryptStr = "";
// hexString-->byte[]
byte[] keyData = ByteUtils.fromHexString(hexKey);
// hexString-->byte[]
byte[] cipherData = ByteUtils.fromHexString(cipherText);
// 解密
byte[] srcData = decrypt_Ecb_Padding(keyData, cipherData);
// byte[]-->String
decryptStr = new String(srcData, ENCODING);
return decryptStr;
}
/**
* 解密
*
* @explain
* @param key
* @param cipherText
* @return
* @throws Exception
*/
public static byte[] decrypt_Ecb_Padding(byte[] key, byte[] cipherText) throws Exception
{
Cipher cipher = generateEcbCipher(ALGORITHM_NAME_ECB_PADDING, Cipher.DECRYPT_MODE, key);
return cipher.doFinal(cipherText);
}
/**
* 校验加密前后的字符串是否为同一数据
*
* @explain
* @param hexKey 16进制密钥忽略大小写
* @param cipherText 16进制加密后的字符串
* @param paramStr 加密前的字符串
* @return 是否为同一数据
* @throws Exception
*/
public static boolean verifyEcb(String hexKey, String cipherText, String paramStr) throws Exception
{
// 用于接收校验结果
boolean flag = false;
// hexString-->byte[]
byte[] keyData = ByteUtils.fromHexString(hexKey);
// 将16进制字符串转换成数组
byte[] cipherData = ByteUtils.fromHexString(cipherText);
// 解密
byte[] decryptData = decrypt_Ecb_Padding(keyData, cipherData);
// 将原字符串转换成byte[]
byte[] srcData = paramStr.getBytes(ENCODING);
// 判断2个数组是否一致
flag = Arrays.equals(decryptData, srcData);
return flag;
}
}

View File

@ -0,0 +1,67 @@
package com.ruoyi.common.pay.yst.util;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.X509TrustManager;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
/**
* Created with IntelliJ IDEA.
*
* @author gejunqing
* @version 1.0
* @date 2024/1/11
*/
public class TxTrustManager implements X509TrustManager
{
private static volatile TxTrustManager instance;
private SSLSocketFactory sslFactory;
@Override
public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException
{
}
@Override
public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException
{
}
@Override
public X509Certificate[] getAcceptedIssuers()
{
return new X509Certificate[0];
}
public SSLSocketFactory getSSLSocketFactory()
{
return this.sslFactory;
}
private TxTrustManager()
{
}
public static TxTrustManager instance() throws NoSuchAlgorithmException, KeyManagementException
{
if (instance == null)
{
synchronized (TxTrustManager.class)
{
if (instance == null)
{
instance = new TxTrustManager();
SSLContext sc = SSLContext.getInstance("TLSv1.2");
sc.init(null, new TxTrustManager[] { instance }, null);
instance.sslFactory = sc.getSocketFactory();
}
}
}
return instance;
}
}

View File

@ -0,0 +1,166 @@
package com.ruoyi.common.pay.yst.util;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.alibaba.fastjson2.TypeReference;
import com.ruoyi.common.pay.yst.config.YstConfig;
import com.ruoyi.common.pay.yst.domain.BizParameter;
import com.ruoyi.common.pay.yst.domain.YstRequest;
import com.ruoyi.common.pay.yst.domain.YstResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import javax.net.ssl.HttpsURLConnection;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* 云商通请求客户端
* @author wjh
* 2024/8/1
*/
@Slf4j
public class YstClient {
private static final String YYYY_MM_DD = "yyyyMMdd";
private static final String HH_MM_SS = "HHmmss";
private YstConfig config;
private final PrivateKey privateKey;
private final PublicKey tlPublicKey;
public YstClient(YstConfig config) throws Exception
{
this.config = config;
this.privateKey = SM2Utils.privKeySM2FromBase64Str(config.getPrivateKey());
this.tlPublicKey = SM2Utils.pubKeySM2FromBase64Str(config.getPublicKey());
}
/**
* 发送请求
*/
public <T> YstResponse<T> sendRequest(String api, String transCode, BizParameter param, Class<T> type) throws Exception
{
YstRequest request = assembleRequest(transCode, param.toString());
log.info("request:{}", request);
String respStr = post(request.toString(), config.getHost() + api);
log.info("response:{}", respStr);
verify(respStr);
log.info("验签成功");
YstResponse<T> resp = JSON.parseObject(respStr, new TypeReference<YstResponse<T>>() {});
String bizDataStr = resp.getBizData() != null ? resp.getBizData().toString() : null;
if (StringUtils.isNotBlank(bizDataStr)) {
resp.setBizData(JSON.parseObject(bizDataStr, type));
}
return resp;
}
private void verify(String respStr) throws Exception
{
JSONObject map = JSON.parseObject(respStr);
String sign = (String) map.remove("sign");
String signType = (String) map.remove("signType");
String srcSignMsg = SM2Utils.jsonMapToStr(map);
if (!SM2Utils.verify(this.tlPublicKey, srcSignMsg, sign))
{
throw new Exception("响应报文验签失败");
}
}
private <T> YstRequest assembleRequest(String transCode, String param) throws Exception
{
YstRequest request = new YstRequest();
request.setAppId(this.config.getAppId());
request.setSpAppId(this.config.getSpAppId());
request.setTransCode(transCode);
request.setFormat(this.config.getFormat());
request.setCharset(this.config.getCharset());
request.setTransDate(new SimpleDateFormat(YYYY_MM_DD).format(new Date()));
request.setTransTime(new SimpleDateFormat(HH_MM_SS).format(new Date()));
request.setVersion(this.config.getVersion());
request.setBizData(param);
String signedValue = SM2Utils.jsonMapToStr((JSONObject) JSON.toJSON(request));
log.info("待签名源串:{}", signedValue);
String sign = SM2Utils.sign(this.privateKey, signedValue);
request.setSignType(this.config.getSignType());
request.setSign(sign);
return request;
}
/**
* 发送请求
*
* @param param
* @return
* @throws IOException
*/
private String post(String param, String url) throws IOException
{
if (url == null) {
throw new RuntimeException("url不允许为空");
}
StringBuilder result = new StringBuilder();
BufferedWriter writer = null;
BufferedReader reader = null;
try
{
URL httpUrl = new URL(url);
HttpURLConnection connection = (HttpURLConnection) httpUrl.openConnection();
if (connection instanceof HttpsURLConnection)
{
((HttpsURLConnection) connection).setSSLSocketFactory(TxTrustManager.instance().getSSLSocketFactory());
}
connection.setRequestProperty("Connection", "keep-alive");
connection.setRequestProperty("Content-Type", "application/json;charset=utf-8");
connection.setRequestMethod("POST");
connection.setDoOutput(true);
connection.setDoInput(true);
writer = new BufferedWriter(new OutputStreamWriter(connection.getOutputStream(), StandardCharsets.UTF_8));
writer.write(param);
writer.flush();
// 响应
reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8));
while (true)
{
String line = reader.readLine();
if (line == null)
{
break;
}
result.append(line);
}
}
catch (Exception e2)
{
this.log.error(e2.getMessage(), e2);
}
finally
{
if (writer != null)
{
try
{
writer.close();
}
catch (IOException e)
{
this.log.error(e.getMessage(), e);
}
}
if (reader != null)
{
reader.close();
}
}
return result.toString();
}
}

View File

@ -380,7 +380,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
set store_id = null,
user_id = null,
device_name = '未命名',
activation_time = null
activation_time = null,
custom_picture = null
where device_id = #{deviceId}
</update>

View File

@ -56,3 +56,26 @@ spring:
max-active: 8
# #连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms
# 云商通配置
yst:
# appid
appId: 21762000921804636162
# 云商通二代分配的服务商应用ID
spAppId: ""
# 域名
host: http://116.228.64.55:28082
# 格式化
format: json
# 字符集
charset: UTF-8
# 签名方式
signType: SM3withSM2
# API版本
version: 1.0
# 秘钥
secret-key: 878427523d3525e070298d44481b8d2e
# 私钥
private-key: MIGTAgEAMBMGByqGSM49AgEGCCqBHM9VAYItBHkwdwIBAQQgiaZmB+feACtziE8SYjVZsaQwLNLRiyO8ebSupeoWIF2gCgYIKoEcz1UBgi2hRANCAATwEo0zq6KaB992PToWeJH52LmfS0sFovnB8/LMaoIAOTlFJtA3YgjWXKlO3KT+GqOCfCC4xE60isCr28tqy7hM
# 公钥
public-key: MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAEu9LNkJlyLtjJxtQWIGlcZ/hyHt5eZ7LEH1nfOiK1H9HsE1cMPu5KK5jZVTtAyc7lPMXixUMirf6A3tMbuMbgqg==

View File

@ -56,3 +56,12 @@ spring:
max-active: 8
# #连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms
# 云商通配置
yst:
# appid
appId: xx
# 云商通二代分配的服务商应用ID
spAppId: xx
# 域名
host: https://ibsapi.allinpay.com