feat(pay): 新增收货地址DTO和签名服务实现

- 新增ShippingDTO用于收货地址信息传输
- 新增SignatureService接口定义签名生成与验证方法
- 实现SignatureServiceImpl支持PingPong支付签名逻辑
- 支持MD5和SHA256两种签名算法
- 添加签名参数过滤和排序功能
- 集成PingPong配置属性进行签名处理
This commit is contained in:
2025-12-22 17:10:35 +08:00
parent 502e181db8
commit 47bd1c5525
3 changed files with 226 additions and 0 deletions

View File

@@ -0,0 +1,35 @@
package com.mtkj.mtpay.dto.risk;
import lombok.Data;
import java.io.Serializable;
/**
* 收货地址DTO
*/
@Data
public class ShippingDTO implements Serializable {
private String firstName;
private String lastName;
private String phone;
private String email;
private String street;
private String postcode;
private String city;
private String state;
private String country;
private String lastModifierStreetTime;
private String lastModifierPhoneTime;
}

View File

@@ -0,0 +1,36 @@
package com.mtkj.mtpay.service;
import java.util.Map;
/**
* 签名服务接口
* 负责生成和验证PingPong支付接口的签名
*/
public interface SignatureService {
/**
* 生成签名
*
* @param params 待签名参数Map
* @return 签名大写MD5
*/
String generateSign(Map<String, Object> params);
/**
* 生成签名
*
* @param params 待签名参数Map
* @param secret 签名密钥
* @param signType 签名类型MD5或SHA256
* @return 签名(大写)
*/
String generateSign(Map<String, Object> params, String secret, String signType);
/**
* 验证签名
*
* @param params 包含sign和signType的参数Map
* @return 验证结果
*/
boolean verifySign(Map<String, Object> params);
}

View File

@@ -0,0 +1,155 @@
package com.mtkj.mtpay.service.impl;
import com.mtkj.mtpay.config.PingPongProperties;
import com.mtkj.mtpay.service.SignatureService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.*;
/**
* 签名服务实现类
* 负责生成和验证PingPong支付接口的签名
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class SignatureServiceImpl implements SignatureService {
private final PingPongProperties pingPongProperties;
/**
* 加签参数列表(根据文档定义)
*/
private static final Set<String> SIGN_SCOPE = Set.of(
"clientId",
"accId",
"amount",
"currency",
"cardNum",
"transactionId",
"merchantTransactionId",
"requestId",
"signType",
"notificationUrl",
"shopperResultUrl"
);
@Override
public String generateSign(Map<String, Object> params) {
return generateSign(params, pingPongProperties.getSecret(), pingPongProperties.getSignType());
}
@Override
public String generateSign(Map<String, Object> params, String secret, String signType) {
// 1. 筛选出需要参与签名的参数
Map<String, Object> signParams = filterSignParams(params);
// 2. 按照字典序排序
Map<String, Object> sortedParams = new TreeMap<>(signParams);
// 3. 构建签名串:{secret}key1=val1&key2=val2...
StringBuilder signStr = new StringBuilder(secret);
for (Map.Entry<String, Object> entry : sortedParams.entrySet()) {
String value = entry.getValue() != null ? entry.getValue().toString().trim() : "";
signStr.append(entry.getKey()).append("=").append(value).append("&");
}
// 移除最后一个&
if (signStr.length() > 0 && signStr.charAt(signStr.length() - 1) == '&') {
signStr.setLength(signStr.length() - 1);
}
String signContent = signStr.toString();
log.debug("签名串: {}", signContent);
// 4. 进行MD5或SHA256运算并转大写
String sign = hash(signContent, signType).toUpperCase();
log.debug("生成的签名: {}", sign);
return sign;
}
@Override
public boolean verifySign(Map<String, Object> params) {
String receivedSign = (String) params.get("sign");
if (receivedSign == null || receivedSign.isEmpty()) {
log.warn("签名参数为空");
return false;
}
String signType = (String) params.getOrDefault("signType", pingPongProperties.getSignType());
// 移除sign参数后计算签名
Map<String, Object> paramsWithoutSign = new HashMap<>(params);
paramsWithoutSign.remove("sign");
String calculatedSign = generateSign(paramsWithoutSign, pingPongProperties.getSecret(), signType);
boolean isValid = receivedSign.equalsIgnoreCase(calculatedSign);
if (!isValid) {
log.warn("签名验证失败,接收到的签名: {}, 计算出的签名: {}", receivedSign, calculatedSign);
}
return isValid;
}
/**
* 筛选出需要参与签名的参数
*
* @param params 所有参数
* @return 需要签名的参数
*/
private Map<String, Object> filterSignParams(Map<String, Object> params) {
Map<String, Object> signParams = new HashMap<>();
for (Map.Entry<String, Object> entry : params.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
// 跳过sign参数本身
if ("sign".equals(key)) {
continue;
}
// 只保留在加签参数列表中的参数,且值不为空
if (SIGN_SCOPE.contains(key) && value != null && !value.toString().trim().isEmpty()) {
signParams.put(key, value);
}
}
return signParams;
}
/**
* 哈希运算
*
* @param content 待哈希内容
* @param algorithm 算法类型MD5或SHA256
* @return 哈希值(小写)
*/
private String hash(String content, String algorithm) {
try {
MessageDigest digest = MessageDigest.getInstance(algorithm);
byte[] hashBytes = digest.digest(content.getBytes(StandardCharsets.UTF_8));
StringBuilder hexString = new StringBuilder();
for (byte b : hashBytes) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
return hexString.toString();
} catch (Exception e) {
log.error("哈希运算失败", e);
throw new RuntimeException("哈希运算失败: " + e.getMessage(), e);
}
}
}