Commit da69e2e2 by Jony.L

新支付初步修改

parent ff391fee
package com.luhu.computility.module.pay.framework.pay.core.client.impl.wpgj;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.Mode;
import cn.hutool.crypto.Padding;
import cn.hutool.crypto.symmetric.AES;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import javax.crypto.Cipher;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* 旺铺聚合支付加解密工具类
*
* 支持RSA+AES混合加密
* - AES加密请求数据
* - RSA加密AES密钥
*
* @author generated
*/
@Slf4j
public class WpgjCryptoUtils {
private static final String AES_ALGORITHM = "AES";
private static final String RSA_ALGORITHM = "RSA";
private static final String RSA_TRANSFORMATION = "RSA/ECB/PKCS1Padding";
/**
* 生成随机AES密钥(16位)
*
* @return AES密钥
*/
public static String generateAESKey() {
return RandomUtil.randomString("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", 16);
}
/**
* AES加密
*
* @param data 明文数据
* @param aesKey AES密钥
* @return Base64编码的密文
*/
public static String aesEncrypt(String data, String aesKey) {
try {
AES aes = new AES(Mode.ECB, Padding.PKCS5Padding, aesKey.getBytes(StandardCharsets.UTF_8));
byte[] encrypted = aes.encrypt(data.getBytes(StandardCharsets.UTF_8));
return Base64.encode(encrypted);
} catch (Exception e) {
log.error("[aesEncrypt] AES加密失败: {}", e.getMessage(), e);
throw new RuntimeException("AES加密失败", e);
}
}
/**
* AES解密
*
* @param encryptedData Base64编码的密文
* @param aesKey AES密钥
* @return 明文数据
*/
public static String aesDecrypt(String encryptedData, String aesKey) {
try {
AES aes = new AES(Mode.ECB, Padding.PKCS5Padding, aesKey.getBytes(StandardCharsets.UTF_8));
byte[] encrypted = Base64.decode(encryptedData);
byte[] decrypted = aes.decrypt(encrypted);
return new String(decrypted, StandardCharsets.UTF_8);
} catch (Exception e) {
log.error("[aesDecrypt] AES解密失败: {}", e.getMessage(), e);
throw new RuntimeException("AES解密失败", e);
}
}
/**
* RSA公钥加密
*
* @param data 要加密的数据
* @param publicKey RSA公钥
* @return Base64编码的密文
*/
public static String rsaEncrypt(String data, String publicKey) {
try {
// 移除公钥格式
String publicKeyStr = publicKey.replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PUBLIC KEY-----", "")
.replaceAll("\\s", "");
byte[] keyBytes = Base64.decode(publicKeyStr);
X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM);
PublicKey pubKey = keyFactory.generatePublic(spec);
Cipher cipher = Cipher.getInstance(RSA_TRANSFORMATION);
cipher.init(Cipher.ENCRYPT_MODE, pubKey);
byte[] encrypted = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
return Base64.encode(encrypted);
} catch (Exception e) {
log.error("[rsaEncrypt] RSA加密失败: {}", e.getMessage(), e);
throw new RuntimeException("RSA加密失败", e);
}
}
/**
* RSA私钥解密
*
* @param encryptedData Base64编码的密文
* @param privateKey RSA私钥
* @return 明文数据
*/
public static String rsaDecrypt(String encryptedData, String privateKey) {
try {
// 移除私钥格式
String privateKeyStr = privateKey.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\s", "");
byte[] keyBytes = Base64.decode(privateKeyStr);
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM);
PrivateKey privKey = keyFactory.generatePrivate(spec);
Cipher cipher = Cipher.getInstance(RSA_TRANSFORMATION);
cipher.init(Cipher.DECRYPT_MODE, privKey);
byte[] encrypted = Base64.decode(encryptedData);
byte[] decrypted = cipher.doFinal(encrypted);
return new String(decrypted, StandardCharsets.UTF_8);
} catch (Exception e) {
log.error("[rsaDecrypt] RSA解密失败: {}", e.getMessage(), e);
throw new RuntimeException("RSA解密失败", e);
}
}
/**
* 构建请求数据
*
* @param data 业务数据
* @param organizNo 机构号
* @return 完整的请求数据
*/
public static String buildRequestData(JSONObject data, String organizNo) {
JSONObject request = new JSONObject();
request.set("serialNo", generateSerialNo());
request.set("version", "1.0");
request.set("timestamp", generateTimestamp());
request.set("data", data);
request.set("signature", null);
request.set("extras", null);
request.set("organizNo", organizNo);
return JSONUtil.toJsonStr(request);
}
/**
* 加密请求数据
*
* @param requestData 请求数据
* @param publicKey 旺铺公钥
* @return 加密后的请求数据
*/
public static String encryptRequest(String requestData, String publicKey) {
// 生成随机AES密钥
String aesKey = generateAESKey();
// AES加密请求数据
JSONObject requestJson = JSONUtil.parseObj(requestData);
String dataToEncrypt = JSONUtil.toJsonStr(requestJson.get("data"));
String encryptedData = aesEncrypt(dataToEncrypt, aesKey);
// RSA加密AES密钥
String encryptedAesKey = rsaEncrypt(aesKey, publicKey);
// 组装最终请求数据
requestJson.set("data", encryptedData);
requestJson.set("signature", encryptedAesKey);
return JSONUtil.toJsonStr(requestJson);
}
/**
* 解密响应数据
*
* @param responseData 响应数据
* @param privateKey 机构私钥
* @return 解密后的业务数据
*/
public static String decryptResponse(String responseData, String privateKey) {
try {
JSONObject responseJson = JSONUtil.parseObj(responseData);
// 获取加密的AES密钥
String encryptedAesKey = responseJson.getStr("signature");
if (StrUtil.isBlank(encryptedAesKey)) {
throw new RuntimeException("响应中缺少加密的AES密钥");
}
// RSA解密得到AES密钥
String aesKey = rsaDecrypt(encryptedAesKey, privateKey);
// 获取加密的业务数据
String encryptedData = responseJson.getStr("data");
if (StrUtil.isBlank(encryptedData)) {
throw new RuntimeException("响应中缺少加密的业务数据");
}
// AES解密得到业务数据
return aesDecrypt(encryptedData, aesKey);
} catch (Exception e) {
log.error("[decryptResponse] 解密响应数据失败: {}", e.getMessage(), e);
throw new RuntimeException("解密响应数据失败", e);
}
}
/**
* 生成序列号
*/
private static String generateSerialNo() {
return System.currentTimeMillis() + RandomUtil.randomNumbers(6);
}
/**
* 生成时间戳
*/
private static String generateTimestamp() {
return LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS"));
}
}
\ No newline at end of file
package com.luhu.computility.module.pay.framework.pay.core.client.impl.wpgj;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.luhu.computility.module.pay.enums.PayChannelEnum;
import com.luhu.computility.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO;
import com.luhu.computility.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import com.luhu.computility.module.pay.framework.pay.core.client.impl.AbstractPayClient;
import com.luhu.computility.module.pay.enums.order.PayOrderStatusEnum;
import com.luhu.computility.module.pay.framework.pay.core.enums.PayOrderDisplayModeEnum;
import lombok.extern.slf4j.Slf4j;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
/**
* 旺铺聚合支付的 PayClient 实现类
*
* @author generated
*/
@Slf4j
public class WpgjPayClient extends AbstractPayClient<WpgjPayClientConfig> {
public WpgjPayClient(Long channelId, String channelCode, WpgjPayClientConfig config) {
super(channelId, PayChannelEnum.WX_NATIVE.getCode(), config);
}
@Override
protected void doInit() {
// 旺铺支付不需要特殊初始化
}
@Override
protected PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws Throwable {
try {
// 构建业务数据
JSONObject businessData = buildBusinessData(reqDTO);
// 构建完整请求数据
String requestData = WpgjCryptoUtils.buildRequestData(businessData, config.getOrganizNo());
// 加密请求数据
String encryptedRequest = WpgjCryptoUtils.encryptRequest(requestData, config.getPublicKey());
// 发送HTTP请求
String apiUrl = config.getApiUrl() + "/industrial/payment/dynamic";
HttpResponse response = HttpUtil.createPost(apiUrl)
.body(encryptedRequest)
.contentType("application/json")
.timeout(30000)
.execute();
if (!response.isOk()) {
log.error("[doUnifiedOrder] 旺铺支付请求失败: status={}, body={}", response.getStatus(), response.body());
return PayOrderRespDTO.closedOf("HTTP_ERROR", "请求失败: " + response.getStatus(),
reqDTO.getOutTradeNo(), response.body());
}
// 解析响应
String responseBody = response.body();
JSONObject responseJson = JSONUtil.parseObj(responseBody);
// 检查响应状态
String code = responseJson.getStr("code");
if (!"0000".equals(code)) {
String msg = responseJson.getStr("msg", "未知错误");
log.error("[doUnifiedOrder] 旺铺支付返回错误: code={}, msg={}", code, msg);
return PayOrderRespDTO.closedOf(code, msg, reqDTO.getOutTradeNo(), responseBody);
}
// 解密响应数据获取payUrl
String decryptedData = WpgjCryptoUtils.decryptResponse(responseBody, config.getPrivateKey());
JSONObject dataJson = JSONUtil.parseObj(decryptedData);
String payUrl = dataJson.getStr("payUrl");
if (StrUtil.isBlank(payUrl)) {
log.error("[doUnifiedOrder] 旺铺支付响应中缺少payUrl");
return PayOrderRespDTO.closedOf("MISSING_PAY_URL", "响应中缺少支付链接",
reqDTO.getOutTradeNo(), responseBody);
}
// 返回成功结果
Map<String, Object> rawData = new HashMap<>();
rawData.put("serialNo", responseJson.getStr("serialNo"));
rawData.put("payUrl", payUrl);
rawData.put("response", responseBody);
return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.QR_CODE.getMode(), payUrl,
reqDTO.getOutTradeNo(), rawData);
} catch (Exception e) {
log.error("[doUnifiedOrder] 旺铺支付统一下单异常", e);
return PayOrderRespDTO.closedOf("SYSTEM_ERROR", e.getMessage(),
reqDTO.getOutTradeNo(), e.getMessage());
}
}
@Override
protected PayOrderRespDTO doParseOrderNotify(Map<String, String> params, String body, Map<String, String> headers) throws Throwable {
try {
// 解密回调数据
String decryptedData = WpgjCryptoUtils.decryptResponse(body, config.getPrivateKey());
JSONObject dataJson = JSONUtil.parseObj(decryptedData);
// 获取订单状态和相关信息
String orderId = dataJson.getStr("order_id");
String orderStatus = dataJson.getStr("order_status");
String tradeNo = dataJson.getStr("trade_no");
String tradeTime = dataJson.getStr("trade_time");
// 判断支付状态
Integer status;
if ("1".equals(orderStatus)) {
status = PayOrderStatusEnum.SUCCESS.getStatus();
} else {
status = PayOrderStatusEnum.CLOSED.getStatus();
}
// 解析交易时间
LocalDateTime successTime = null;
if (StrUtil.isNotBlank(tradeTime)) {
try {
successTime = LocalDateTime.parse(tradeTime, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
} catch (Exception e) {
log.warn("[doParseOrderNotify] 解析交易时间失败: {}", tradeTime, e);
}
}
// 构建原始数据
Map<String, Object> rawData = new HashMap<>();
rawData.put("decryptedData", decryptedData);
rawData.put("rawBody", body);
rawData.put("dataJson", dataJson);
return PayOrderRespDTO.of(status, tradeNo, null, successTime,
orderId, rawData);
} catch (Exception e) {
log.error("[doParseOrderNotify] 解析旺铺支付回调异常", e);
throw e;
}
}
@Override
protected PayOrderRespDTO doGetOrder(String outTradeNo) throws Throwable {
// 旺铺支付暂时不支持主动查询订单状态
// 可以根据需要实现订单查询接口
log.warn("[doGetOrder] 旺铺支付暂不支持订单查询: {}", outTradeNo);
return null;
}
/**
* 构建业务数据
*
* @param reqDTO 统一下单请求
* @return 业务数据
*/
private JSONObject buildBusinessData(PayOrderUnifiedReqDTO reqDTO) {
JSONObject data = new JSONObject();
// 必填字段
data.set("organiz_no", config.getOrganizNo());
data.set("mer_no", config.getMerNo());
data.set("mer_code", config.getMerCode());
data.set("term_code", config.getTermCode());
// 金额转换:分转元
BigDecimal amountYuan = new BigDecimal(reqDTO.getPrice()).divide(new BigDecimal(100), 2, BigDecimal.ROUND_HALF_UP);
data.set("order_amt", amountYuan.toString());
// 订单信息
data.set("mer_order_id", reqDTO.getOutTradeNo());
data.set("notifyurl", reqDTO.getNotifyUrl());
// 可选字段
if (StrUtil.isNotBlank(reqDTO.getReturnUrl())) {
data.set("return_url", reqDTO.getReturnUrl());
}
// 设置默认关闭时间30分钟
data.set("close_time", "30");
// 设置订单标题
String subject = reqDTO.getSubject();
if (StrUtil.isBlank(subject)) {
subject = "商品支付";
}
data.set("order_title", subject);
return data;
}
}
\ No newline at end of file
package com.luhu.computility.module.pay.framework.pay.core.client.impl.wpgj;
import com.luhu.computility.framework.common.util.validation.ValidationUtils;
import com.luhu.computility.module.pay.framework.pay.core.client.PayClientConfig;
import lombok.Data;
import javax.validation.ConstraintValidator;
import javax.validation.constraints.NotBlank;
/**
* 旺铺聚合支付的 PayClientConfig 实现类
*
* @author generated
*/
@Data
public class WpgjPayClientConfig implements PayClientConfig {
/**
* 合作机构渠道号
* 测试环境:105549
*/
@NotBlank(message = "合作机构渠道号不能为空")
private String organizNo;
/**
* 旺铺内部商户号,进件入网后返回
* 测试环境:99911325651RE1R
*/
@NotBlank(message = "旺铺内部商户号不能为空")
private String merNo;
/**
* 商户号,进件入网后返回
* 测试环境:K20241200111267
*/
@NotBlank(message = "商户号不能为空")
private String merCode;
/**
* 终端号,进件入网后返回
* 测试环境:1011215692596
*/
@NotBlank(message = "终端号不能为空")
private String termCode;
/**
* 旺铺公钥,用于加密AES密钥
*/
@NotBlank(message = "旺铺公钥不能为空")
private String publicKey;
/**
* 机构私钥,用于解密回调数据中的AES密钥
*/
@NotBlank(message = "机构私钥不能为空")
private String privateKey;
/**
* API地址
* 测试环境:https://stg5-qr.wpgjcs.com
*/
@NotBlank(message = "API地址不能为空")
private String apiUrl;
@Override
public void validate(ConstraintValidator validator) {
ValidationUtils.validate(validator, this);
}
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment