Appearance
通用接口说明
通用请求是商户侧调用平台各业务接口都需要的参数,当前版本采用HTTPS请求头部信息的方式。
回调IP地址
- 15.206.41.122
金额单位
Rupee(元)
请求正式域名
参数 | 是否必须 | 内容 |
---|---|---|
API endpoint | 是 | https://api-gateway.wulavip.com/api |
请求头信息
参数 | 是否必须 | 类型 | 说明 | 示例 |
---|---|---|---|---|
Authorization | 是 | String | 调用生成公共访问 令牌接口,获取 token,拼接成 Bearer {token} | Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUz I1NiJ9.eyJtZXJjaGFudE5hbWUiOi J0ZXN0IiwiZXhwIjoxNzE2ODg4M TMxfQ.ec7kJArUSySYeB982LQU W3m90rIcyYvA9UnoWyRL2k8 |
API信息说明
参数 | 说明 |
---|---|
ApiKey | 生成token必须的参数 |
ApiSecret | 生成token必须的参数 |
MerchantKey | 用于验证我方回调数据签名 |
生成公共访问令牌(JWT TOKEN) ✅
INFO
用途:身份验证
请求URL:/auth/token
请求方式:POST
请求参数
参数 | 必选 | 类型 | 说明 | 示例 |
---|---|---|---|---|
clientId | 是 | String | 用于授权生成公共令牌的 key,取值同'API信息说明'中的ApiKey | e0ee2d16b479 |
clientSecret | 是 | String | 授权生成公共令牌的密码,取值同'API信息说明'中的ApiSecret | 42bde8fb74c9 |
请求示例
json
{
"clientId": "e0ee2d16b479",
"clientSecret": "42bde8fb74c9"
}
响应参数
参数 | 类型 | 说明 | 示例 |
---|---|---|---|
token | String | 客户端唯一令牌,将此 令牌用于其他 API | eyJ0eXAiOiJKV1QiLCJhbGci OiJIUzI1NiJ9.eyJtZXJjaGFud E5hb923KSJI30IiwiZXhwIjox NzE2NjI2NTQ3fQ.6Katm1V YrnzMFrb0g7ZsjF3FeUW8X eEgK4_nX0oW23 |
expireAt | String | 令牌有效期 | 2025-01-01 10:10:00 |
响应信息
json
{
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJtZXJjaGFudE5hb923KSJI30IiwiZXhwIjoxNzE2NjI 2NTQ3fQ.6Katm1VYrnzMFrb0g7ZsjF3FeUW8XeEgK4_nX0oW23",
"expireAt": "2025-01-01 10:10:00"
}
生成代付请求签名
INFO
生成代付签名前,请自主生成一份公私密钥,私钥由贵方自主保存,切勿泄漏。然后提供对应的公钥给我们的运营保存用于服务端处理代付提单请求时校验代付参数信息。
签名规则如下:
- 先对所有业务参数(去除 signature)扁平化排序并拼接为 key1=value1&key2=value2... 字符串;
- 然后将 请求头中的XTimestamp 字段的值直接拼接在该字符串后面(无分隔符);
- 再用 RSA 私钥签名
具体可参考下方提供的工具类方法
1. 生成公私钥:
使用下方代码方法:
java
public static void generate() throws NoSuchAlgorithmException {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
keyGen.initialize(2048); // 建议使用 2048 位
KeyPair keyPair = keyGen.generateKeyPair();
PrivateKey privateKey = keyPair.getPrivate(); // PKCS#8 格式
PublicKey publicKey = keyPair.getPublic(); // X.509 格式
System.out.println("-----BEGIN PRIVATE KEY-----");
System.out.println(Base64.getEncoder().encodeToString(privateKey.getEncoded())); // 保存私钥
System.out.println("-----END PRIVATE KEY-----\n");
System.out.println("-----BEGIN PUBLIC KEY-----");
System.out.println(Base64.getEncoder().encodeToString(publicKey.getEncoded())); // 提交平台
System.out.println("-----END PUBLIC KEY-----");
}
2. 拼接加密串:
按照下方提供的示例,生成signature字符串,最终将signature字符串作为参数,提交代付接口。请求头中添加X-Timestamp
参数,值为签名使用的时间戳。
⚠️⚠️⚠️注意,签名中使用的时间戳需要和请求头中的 X-Timestamp值
保持一致
java
import com.alibaba.fastjson2.JSONObject;
import java.security.PrivateKey;
import java.security.PublicKey;
import static net.coolpe.common.util.SignUtils.flatBody;
import static net.coolpe.common.util.SignUtils.getPrivateKeyFromString;
import static net.coolpe.common.util.SignUtils.getPublicKeyFromString;
import static net.coolpe.common.util.SignUtils.sign;
import static net.coolpe.common.util.SignUtils.verify;
public class Test {
private static String privateKey = "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC78tIeF4sl4w7sdksElTZ1cS4D4RNZAUoP2/j851h2tMBzVj7vrJXG7YBq+cgtx8hkCpt7MczBW7neNS8QB7O7Bur2QV0ahphGoD8oIlCJYVgdnN44H6J4K5EZhyj6aOE4BtfPaA9KuqJhBK1fdx4+SV2nIxhN+xH6RLPpq6fLwSQG9++4XAx4vTFl1f9iIQl3KAitPpUssLMJPCvWg/NyP+Vnp12IffIrKQUetA4oyemAQbWZZrVhDo5nw3psNPq1QqIW5/S0cIYaojGpT8sga6sCUEWyYjl6s/1CitgZgMddZn5FZrBign0RREj2mpLWtK6zLCi9zcIKoFtuEW0HAgMBAAECggEACwjQNz4QnafSr3qpIDAWeMICqUL58ibLj1ojKHijTqIsDllzlnoJInBn3V/5gF6TWvRDI/IiNalJ+fbLYuM2NSEh2FVcFFDvTcOIvGAktFFybUKmyqATgin5+d1f6tu6JUzu4gQXqb6CsV7R7aHpCk/KX3lbAEZg9tLjxoXycwQzi6HEd9wo+eDRXn5oL8DKqizhbjp5+q2Tm5pqiOT1Mqls2wemZH7viHKl5mdULW4xMqtZyJYoN9oyLOZ1Ko5SLuIPqassrfeVHVImorqDdZ8dRzC3j/LMwBsBfYwLnoSExnWQZd+MXiP1Yt1/ORNzbS1Dl17SiAjSzddWydGTIQKBgQC/McLB+xpsLQAMiCwfTsAFAwhMszpuORQ5GQjnD7r8OxRh4VZP4UIu7z5FMbdY6S3mY8rkNXhgpwsz5NSZBYFweO7/VD88v4Couz5XxzUDPHWNWTMdQoe5BjlAUmONyjVG2/QNUC+okP+4z26rb/0GENtyp+kY7sOKyS0dH9ibcQKBgQD7p2lg/sAYf21afluQZuFgsaGoHr/DhIHSU93JItAZ0PRpWLq/WvxrpmdHJCaQP9+GPITkmcLDqPmdl+S9oJNVMdH910udVXXKujaWvIZ24zxWgSv3lDkzD25HW7P54JLOh3T09D4iNWxD7NWK/cbsNQZnfiNrnN85OsmKTIMj9wKBgQCFL4/A+z/DvXE8SZuaz4vZsewVKgD5CU/6GmNmOkICNNGVAZjTmlI1SblyEBjtmbm8tSV/5XOOuK6IHs6uLfSmOcgbGz/V/H0OjSj3krBuKa6loU6HAnJzRE+bbAknm4WTb+NJZuNcJG3O+sjYKfHzSMjlzOwGz0RuKIgBss58MQKBgQD4LsWtg+/8+QbmvUEeK2hQTT7Jp/GlKDREMrPDHpMvMrUog/pAp8HThNvL/GoPzv5py5ugO8gp4Ka0dk1/ejJzTdv0RPTsqJCvq3AUvr651yb1hRTQaRz0L16p/1WCtKj0CAEfZxUz9Y3de1+qYNdisnSrcmoKQj6fmUuUGhsnRwKBgBFTYBnRW5mKl/K4f2BVIUwI2CjMaHSaIO9y0SfL/jR+J6ylfb72hZ+vGAdqJAH2SudMIhMJiQeEfn7w+1JOVzpmAT3i1+dOCRxozv05W8rY1j6zT7h07jtQfSCYHdWN11bdUA6mu22N4s8hN0ey8BPfRSzk2iB//4n2xTt0zG+o";
private static String publicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu/LSHheLJeMO7HZLBJU2dXEuA+ETWQFKD9v4/OdYdrTAc1Y+76yVxu2AavnILcfIZAqbezHMwVu53jUvEAezuwbq9kFdGoaYRqA/KCJQiWFYHZzeOB+ieCuRGYco+mjhOAbXz2gPSrqiYQStX3cePkldpyMYTfsR+kSz6auny8EkBvfvuFwMeL0xZdX/YiEJdygIrT6VLLCzCTwr1oPzcj/lZ6ddiH3yKykFHrQOKMnpgEG1mWa1YQ6OZ8N6bDT6tUKiFuf0tHCGGqIxqU/LIGurAlBFsmI5erP9QorYGYDHXWZ+RWawYoJ9EURI9pqS1rSusywovc3CCqBbbhFtBwIDAQAB";
public static void main(String[] args) throws Exception {
// 注意金额有两位小数点
String req = "{\n"
+ " \"currency\": \"IDR\",\n"
+ " \"clientId\": \"aerqw123123121111553\",\n"
+ " \"payMethod\": \"204001\",\n"
+ " \"name\": \"reily\",\n"
+ " \"phone\": 14832304535,\n"
+ " \"email\": \"ktzmzi_rnz98@126.com\",\n"
+ " \"amount\": \"10000.00\",\n"
+ " \"callbackUrl\": \"https://reckless-molasses.info/\",\n"
+ " \"indiaBank\": {\n"
+ " \"accountNo\": \"78464191261\",\n"
+ " \"ifsc\": \"KKBK0004583\"\n"
+ " },\n"
+ " \"signature\": \"GLpx1woCPTToHhIoLQ+PSvv+uBf94Li3nuhcqaqLnKDUL8lT2X+NbdtFrmsCUB6s2HDejZxp3irYYONitkj26oAPWM7Cm4+KUD8N3eC/pylmblVVLWG8AU7NSX/xps6vnnLPPEvS9BQjnCFjIpEXbrGu6PoRHVg7iqD/vPRpcjTkdIjFd8BkYXL7Tj5aQodWrlAt5dNC02fQBCVSF4n+FvlMB4zIu2VhCaPpP0X9WVRnb0MLmKvj8n0IFDGQLHdC1YcboeLWENezPZXmwXYVtnvedr2jd6DlcuZUTDDPz6OY+wgRlh8Juudw2Ap4+8qY5cdrfcotso7cH0REZhnwEQ==\"\n"
+ "}";
JSONObject orderReq = JSONObject.parseObject(req);
PrivateKey privateKey1 = getPrivateKeyFromString(privateKey);
long timestamp = System.currentTimeMillis(); // 时间戳传入请求头 X-Timestamp
System.out.println("timestamp: " + timestamp);
String body = flatBody(orderReq) + timestamp;
System.out.println("beforeSign: " + body);
String sign = sign(body, privateKey1);
System.out.println("sign: " + sign); // signature加入请求体
PublicKey publicKeyFromString = getPublicKeyFromString(publicKey); // 服务端使用公钥校验参数
boolean verify = verify(body, sign, publicKeyFromString);
System.out.println("verify: "+ verify);
}
}
3. SignUtils:
java
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import lombok.experimental.UtilityClass;
import org.apache.commons.lang3.StringUtils;
import javax.crypto.Cipher;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
@UtilityClass
public class SignUtils {
/**
* 使用 fastjson2 实现的签名逻辑
*/
public static String flatBody(Object requestBody) throws Exception {
JSONObject json = JSON.parseObject(JSON.toJSONString(requestBody)); // 转为 JSONObject
json.remove("signature");
json.remove("md5");
// 扁平化处理
Map<String, String> flatMap = new TreeMap<>();
flattenFastjson("", json, flatMap);
return flatMap.entrySet().stream()
.filter(e -> StringUtils.isNotBlank(e.getValue()))
.map(e -> e.getKey() + "=" + e.getValue())
.collect(Collectors.joining("&"));
}
/**
* 递归扁平化 JSONObject 对象
*/
private static void flattenFastjson(String prefix, JSONObject json, Map<String, String> result) {
for (Map.Entry<String, Object> entry : json.entrySet()) {
String key = prefix.isEmpty() ? entry.getKey() : prefix + "." + entry.getKey();
Object value = entry.getValue();
if (value == null) {
continue;
}
if (value instanceof JSONObject) {
flattenFastjson(key, (JSONObject) value, result);
} else {
result.put(key, value.toString());
}
}
}
/**
* 从 Base64 字符串加载公钥(PEM 格式)
*/
public static PublicKey getPublicKeyFromString(String base64PublicKey) throws Exception {
String key = base64PublicKey
.replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PUBLIC KEY-----", "")
.replaceAll("\\n", "")
.replaceAll("\\s", "");
byte[] decoded = Base64.getDecoder().decode(key);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(decoded);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePublic(keySpec);
}
/**
* 从 Base64 字符串加载私钥(PEM 格式)
*/
public static PrivateKey getPrivateKeyFromString(String base64PrivateKey) throws Exception {
String key = base64PrivateKey
.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----BEGIN OPENSSH PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replace("-----END OPENSSH PRIVATE KEY-----", "")
.replaceAll("\\n", "")
.replaceAll("\\s", "");
byte[] decoded = Base64.getDecoder().decode(key);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(decoded);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePrivate(keySpec);
}
/**
* 使用私钥签名(SHA256withRSA)
*/
public static String sign(String data, PrivateKey privateKey) throws Exception {
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initSign(privateKey);
signature.update(data.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(signature.sign());
}
/**
* 使用公钥验签
*/
public static boolean verify(String data, String base64Sign, PublicKey publicKey) throws Exception {
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initVerify(publicKey);
signature.update(data.getBytes(StandardCharsets.UTF_8));
return signature.verify(Base64.getDecoder().decode(base64Sign));
}
/**
* 使用公钥加密
*/
public static String encrypt(String plainText, PublicKey publicKey) throws Exception {
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
return Base64.getEncoder().encodeToString(cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8)));
}
/**
* 使用私钥解密
*/
public static String decrypt(String base64CipherText, PrivateKey privateKey) throws Exception {
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
return new String(cipher.doFinal(Base64.getDecoder().decode(base64CipherText)), StandardCharsets.UTF_8);
}
}
回调签名方法
INFO
用途:回调验签
签名规则:
将待签名字符串转换为TreeMap,按 key=value 形式拼接,并按ASCII升序排序,最后再加上 merchantKey ,进行MD5加密。将得到的签名与回调数据中的sign进行比对,一致则可以通过
注意: 待签名字符串不包括sign, 空字符串参与签名, 参数之间无任何符号拼接
仅最后的merchantKey字段前需要拼接&
示例:amount=100.00clientId=xxxorderId=xxxpaidAmount=100.00payMethod=101001serviceFee=0.50status=SUCCESStransactionId=CI12345678999999&merchantKey=123456
签名方法
java
import cn.hutool.crypto.digest.DigestUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.alibaba.fastjson2.TypeReference;
import java.util.TreeMap;
public class SignUtil {
private static String getMerchantKey() {
return "123456";
}
public static boolean verifySign(Object object, String sign) {
String encode = encode(object, getMerchantKey());
return encode.equalsIgnoreCase(sign);
}
public static String encode(Object o, String merchantKey) {
TreeMap<String, String> map = JSON.parseObject(JSON.toJSONString(o), new TypeReference<TreeMap<String, String>>() {
});
map.remove("sign");
StringBuilder sb = new StringBuilder();
map.forEach((k, v) -> sb.append(k).append("=").append(v));
sb.append("&merchantKey=").append(merchantKey);
String encode = sb.toString();
return DigestUtil.md5Hex(encode);
}
public static void main(String[] args) {
String body = "{\"amount\":100.00,\"clientId\":\"xxx\",\"orderId\":\"xxx\",\"paidAmount\":100.00,\"payMethod\":\"101001\",\"serviceFee\":0.50,\"sign\":\"aa7ae61151de42d239386ea9892aee7e\",\"status\":\"SUCCESS\",\"transactionId\":\"CI12345678999999\"}\n";
JSONObject object = JSON.parseObject(body);
String sign = object.getString("sign");
System.out.println(verifySign(object, sign));
}
}
通用返回
当接口请求处理被支付平台接收后,HTTPS状态码为200表示请求成功;非200表示请求失败。
通用错误码表
具体错误原因,请查看message返回的信息
响应参数 - errorType
错误编码 | 可能的情况 |
---|---|
AUTH | 必填参数未传,参数错误或不合法 |
PARAM_ERROR | 支付方式错误, 金额/限额错误 |
ORDER | 订单错误 |
BALANCE | 余额不足 |
RISK | 风控错误 |
MERCHANT | 商户错误 |
CLIENT_ID_EXIST | 商户订单号存在 |
RATE_LIMIT_EXCEEDED | 请求过于频繁 |
CHANNEL_NOT_AVAILABLE | 通道不可用 |
响应示例
json
{
"errorType": "CLIENT_ID_EXIST",
"message": "clientId: 2025103651364100 already exist"
}