Skip to content

通用接口说明

通用请求是商户侧调用平台各业务接口都需要的参数,当前版本采用HTTPS请求头部信息的方式。

回调IP地址

  • 15.206.41.122

金额单位

Rupee(元)

请求正式域名

参数是否必须内容
API endpointhttps://api-gateway.wulavip.com/api

请求头信息

参数是否必须类型说明示例
AuthorizationString调用生成公共访问 令牌接口,获取 token,拼接成 Bearer {token}Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUz I1NiJ9.eyJtZXJjaGFudE5hbWUiOi J0ZXN0IiwiZXhwIjoxNzE2ODg4M TMxfQ.ec7kJArUSySYeB982LQU W3m90rIcyYvA9UnoWyRL2k8

API信息说明

参数说明
ApiKey生成token必须的参数
ApiSecret生成token必须的参数
MerchantKey用于验证我方回调数据签名

生成公共访问令牌(JWT TOKEN) ✅

INFO

用途:身份验证

请求URL:/auth/token

请求方式:POST

请求参数

参数必选类型说明示例
clientIdString用于授权生成公共令牌的 key,取值同'API信息说明'中的ApiKeye0ee2d16b479
clientSecretString授权生成公共令牌的密码,取值同'API信息说明'中的ApiSecret42bde8fb74c9

请求示例

json
{
  "clientId": "e0ee2d16b479",
  "clientSecret": "42bde8fb74c9"
}

响应参数

参数类型说明示例
tokenString客户端唯一令牌,将此 令牌用于其他 APIeyJ0eXAiOiJKV1QiLCJhbGci OiJIUzI1NiJ9.eyJtZXJjaGFud E5hb923KSJI30IiwiZXhwIjox NzE2NjI2NTQ3fQ.6Katm1V YrnzMFrb0g7ZsjF3FeUW8X eEgK4_nX0oW23
expireAtString令牌有效期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"
}