支付接口文档

接口域名:pay.yilise.com | 版本:v1 | 编码:UTF-8 | 请求格式:JSON

一、全局通用规范

1.1 同步接口统一响应格式

所有POST业务接口同步返回固定结构,code=0000 代表请求成功,其余code均为业务失败

{
  "code": "0000",
  "msg": "操作成功",
  "data": {}
}

1.2 公共说明

  • 所有接口请求Header必须携带 Content-Type: application/json
  • 所有业务请求必须携带 sign 签名字段,签名校验规则请参考签名模块
  • 异步通知回调报文不使用上述统一响应结构,为独立报文格式

二、创建支付订单接口

请求地址:POST https://pay.yilise.com/api/v1/order/create

接口功能:创建支付订单,返回H5收银台跳转链接

2.1 请求入参(JSON Body)

参数名 类型 是否必填 参数说明
merchantId String 商户编号,平台分配唯一标识
merchantOrderNo String 商户唯一订单号,不可重复
amount BigDecimal 订单金额
currency String 币种编码
description String 订单描述
clientIp String 客户端IP
deviceType String 终端设备类型枚举:IOS / ANDROID / WINDOWS / LINUX / MACOS
systemVersion String 终端系统版本
notifyUrl String 支付结果异步回调地址
returnUrl String 支付成功页面跳转地址
expireMin Integer 订单支付超时时间,单位分钟
payerFirstName String 付款人姓
payerLastName String 付款人名
payerPhone String 付款人手机号
payerEmail String 付款人邮箱
sign String 请求参数签名,生成规则参考签名模块

2.2 响应返回参数

参数名 子参数 类型 场景 说明
code - String 全部返回 响应码,0000=下单成功
msg - String 全部返回 响应描述
data - Object 成功返回 订单业务子对象
orderNo String 成功返回 平台生成唯一订单号
cashierForwardUrl String 成功返回 H5收银台跳转链接

2.3 示例

2.3.1 请求示例

POST https://pay.yilise.com/api/v1/order/create
Content-Type: application/json

{
  "merchantId": "MCH202607030001",
  "merchantOrderNo": "M202607030001",
  "amount": 9900,
  "currency": "CNY",
  "description": "月度会员充值套餐",
  "clientIp": "113.20.12.34",
  "deviceType": "ANDROID",
  "systemVersion": "14",
  "notifyUrl": "https://pay.yilise.com/notify",
  "returnUrl": "https://pay.yilise.com/return",
  "expireMin": 30,
  "payerFirstName": "james",
  "payerLastName": "lbj",
  "payerPhone": "XXXXXXXXXXXX",
  "payerEmail": "11@outlook.com",
  "sign": "xxxxxxxxxxxxxxxxxxxx"
}

2.3.2 成功响应示例

{
  "code": "0000",
  "msg": "下单成功",
  "data": {
    "orderNo": "20260703123023000001",
    "cashierForwardUrl": "https://pay.yilise.com/api/v1/order/pay/20260703123023000001"
  }
}

三、订单查询接口

请求地址:POST https://pay.yilise.com/api/v1/order/query

接口功能:主动查询订单实时支付状态

3.1 请求入参(二选一)

参数名 类型 是否必填 参数说明
merchantId String 商户编号,平台分配唯一标识
orderNo String 支付平台订单号和商户订单号二选一
merchantOrderNo String 支付平台订单号和商户订单号二选一
sign String 请求参数签名,生成规则参考签名模块

3.2 响应返回参数

参数名 子参数 类型 场景 说明
code - String 全部返回 0000=查询成功
msg - String 全部返回 响应描述
data - Object 成功返回 订单详情子对象
merchantId String 成功返回 商户编号
orderNo String 成功返回 平台订单号
merchantOrderNo String 成功返回 商户订单号
status String 成功返回 订单状态,参考5.1 订单状态枚举
paidTime String 已支付订单 支付完成时间 yyyy-MM-dd HH:mm:ss

3.3 示例

3.3.1 请求示例

POST https://pay.yilise.com/api/v1/query
Content-Type: application/json

{
  "merchantId": "MCH202607030001",
  "merchantOrderNo": "M202607030001",
  "sign": "xxxxxxxxxxxxxxxxxxxx"
}

3.3.2 成功响应示例

{
  "code": "0000",
  "msg": "查询成功",
  "data": {
    "respCode": "0000",
    "respDesc": "支付成功",
    "merchantId": "MCH202607030001",
    "orderNo": "20260703123023000001",
    "merchantOrderNo": "M202607030001",
    "status": "PAID",
    "paidTime": "2026-07-03 12:30:30"
  }
}

四、支付结果异步通知

重要说明: 1、异步通知为平台主动POST推送,商户收到通知校验签名后,返回纯文本 success 表示接收成功,否则平台会重试推送。 2、只在支付成功时推送

4.1 回调报文示例

{
  "respCode": "0000",
  "respDesc": "支付成功",
  "merchantOrderNo": "2026070310001",
  "orderNo": "20260703123023000001",
  "status": "PAID",
  "paidTime": "2026-07-03 12:30:30",
  "sign": "xxxxxxxxxxxxxxxxxxxx"
}

4.2 回调字段说明

字段名 说明
respCode 响应码,不用作业务判断的依据
respDesc 响应描述
merchantOrderNo 商户订单号
orderNo 平台订单号
status 订单状态,仅PAID为支付成功
paidTime 支付完成时间
sign 回调参数签名,校验规则参考签名模块

五、系统枚举定义

5.1 订单状态枚举

状态码 中文名称 业务说明
PENDING 待支付 订单创建,用户未发起付款
PAYING 支付中 用户已提交付款,等待渠道返回结果
PAID 已支付 支付成功,可执行发货/充值等业务
FAILED 支付失败 付款失败,资金未扣款
CLOSED 已关闭 订单超时/手动关闭,无法再支付

5.2 设备类型枚举

枚举值 说明
IOS 苹果手机/平板设备
ANDROID 安卓手机设备
WINDOWS Windows电脑客户端
LINUX Linux客户端
MACOS Mac电脑客户端

六、对接开发注意事项

1. 业务判断标准:同步接口仅 code=0000 视为调用成功;订单仅 status=PAID 视为支付完成。

2. 商户订单号全局唯一,重复提交会返回错误码,需做好幂等控制。

3. clientIp 必须传入用户真实外网IP,内网IP会校验失败。

4. notifyUrl 必须为公网可访问地址,不能携带临时鉴权Token。

5. 所有请求、异步回调必须校验签名,未校验签名直接拒绝业务处理,防止恶意伪造请求(签名规则参考签名模块)。

6. 业务逻辑优先以异步通知结果为准;未收到回调时,使用订单查询接口轮询补单。

7. 异步通知需做好幂等处理,避免多次回调导致重复发放商品/充值。

七、签名规则与工具类

7.1 签名规则说明

  • 签名算法:RSA(SHA1withRSA)/ RSA2(SHA256withRSA),默认使用RSA
  • 签名流程:
    1. 排除参数中的sign字段
    2. 将所有参与签名的参数按key字母升序排序
    3. 拼接为key1=value1&key2=value2格式的字符串
    4. 使用商户私钥对拼接字符串进行RSA签名
    5. 将签名结果进行Base64编码,作为sign参数值
  • 验签流程:使用平台公钥对签名串反向校验,验证参数是否被篡改

7.2 RSA工具类(RSAUtil.java)

package com.xpay.common.util;

import lombok.extern.slf4j.Slf4j;

import javax.crypto.Cipher;
import java.io.ByteArrayOutputStream;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

@Slf4j
public class RSAUtil {
    private static final String RSA_KEY_ALGORITHM = "RSA";
    private static final String RSA_SIGNATURE_ALGORITHM = "SHA1withRSA";
    private static final String RSA2_SIGNATURE_ALGORITHM = "SHA256withRSA";
    private static final int KEY_SIZE = 1024;

    public static Map generateKey() {
        KeyPairGenerator keygen;
        try {
            keygen = KeyPairGenerator.getInstance(RSA_KEY_ALGORITHM);
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("RSA初始化密钥出现错误,算法异常");
        }
        SecureRandom secrand = new SecureRandom();
        secrand.setSeed(String.valueOf(System.currentTimeMillis()).getBytes());
        keygen.initialize(KEY_SIZE, secrand);
        KeyPair keyPair = keygen.genKeyPair();
        byte[] pub_key = keyPair.getPublic().getEncoded();
        String publicKeyStr = Base64.getEncoder().encodeToString(pub_key);
        byte[] pri_key = keyPair.getPrivate().getEncoded();
        String privateKeyStr = Base64.getEncoder().encodeToString(pri_key);
        Map keyPairMap = new HashMap<>();
        keyPairMap.put("publicKeyStr", publicKeyStr);
        keyPairMap.put("privateKeyStr", privateKeyStr);
        return keyPairMap;
    }

    public static String encryptByPublicKey(String data, String publicKeyStr) {
        try {
            byte[] decodePublicKeyByte = Base64.getDecoder().decode(publicKeyStr);
            X509EncodedKeySpec keySpec = new X509EncodedKeySpec(decodePublicKeyByte);
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            PublicKey publicKey = keyFactory.generatePublic(keySpec);
            Cipher cipher = Cipher.getInstance("RSA");
            cipher.init(Cipher.ENCRYPT_MODE, publicKey);
            byte[] bytesContent = data.getBytes();
            int inputLen = bytesContent.length;
            int offLen = 0;
            int i = 0;
            ByteArrayOutputStream bops = new ByteArrayOutputStream();
            while (inputLen - offLen > 0) {
                byte[] cache;
                if (inputLen - offLen > 117) {
                    cache = cipher.doFinal(bytesContent, offLen, 117);
                } else {
                    cache = cipher.doFinal(bytesContent, offLen, inputLen - offLen);
                }
                bops.write(cache);
                i++;
                offLen = 117 * i;
            }
            bops.close();
            byte[] encryptedData = bops.toByteArray();
            String encode = Base64.getEncoder().encodeToString(encryptedData);
            return encode;
        } catch (Exception e) {
            log.error("加密失败:", e);
            return "";
        }
    }

    public static String decryptByPrivateKey(String data, String privateKeyStr) throws Exception {
        try {
            byte[] priKey = Base64.getDecoder().decode(privateKeyStr);
            PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(priKey);
            KeyFactory keyFactory = KeyFactory.getInstance(RSA_KEY_ALGORITHM);
            PrivateKey privateKey = keyFactory.generatePrivate(pkcs8KeySpec);
            Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
            cipher.init(Cipher.DECRYPT_MODE, privateKey);
            byte[] bytesContent = Base64.getDecoder().decode(data.getBytes());
            int inputLen = bytesContent.length;
            int offLen = 0;
            int i = 0;
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            while (inputLen - offLen > 0) {
                byte[] cache;
                if (inputLen - offLen > 128) {
                    cache = cipher.doFinal(bytesContent, offLen, 128);
                } else {
                    cache = cipher.doFinal(bytesContent, offLen, inputLen - offLen);
                }
                byteArrayOutputStream.write(cache);
                i++;
                offLen = 128 * i;
            }
            byteArrayOutputStream.close();
            byte[] byteArray = byteArrayOutputStream.toByteArray();
            return new String(byteArray);
        } catch (Exception e) {
            log.error("解密失败,待加密内容:{}", data, e);
            return "";
        }
    }

    public static String sign(byte[] data, byte[] priKey, String signType) throws Exception {
        PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(priKey);
        KeyFactory keyFactory = KeyFactory.getInstance(RSA_KEY_ALGORITHM);
        PrivateKey privateKey = keyFactory.generatePrivate(pkcs8KeySpec);
        String algorithm = RSA_KEY_ALGORITHM.equals(signType) ? RSA_SIGNATURE_ALGORITHM : RSA2_SIGNATURE_ALGORITHM;
        Signature signature = Signature.getInstance(algorithm);
        signature.initSign(privateKey);
        signature.update(data);
        byte[] sign = signature.sign();
        return Base64.getEncoder().encodeToString(sign);
    }

    public static boolean verify(byte[] data, byte[] sign, byte[] pubKey, String signType) throws Exception {
        KeyFactory keyFactory = KeyFactory.getInstance(RSA_KEY_ALGORITHM);
        X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(pubKey);
        PublicKey publicKey = keyFactory.generatePublic(x509KeySpec);
        String algorithm = RSA_KEY_ALGORITHM.equals(signType) ? RSA_SIGNATURE_ALGORITHM : RSA2_SIGNATURE_ALGORITHM;
        Signature signature = Signature.getInstance(algorithm);
        signature.initVerify(publicKey);
        signature.update(data);
        return signature.verify(sign);
    }
}

7.3 签名工具类(SignUtil.java)

package com.xpay.common.util;

import cn.hutool.core.bean.BeanUtil;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import java.util.*;

@Slf4j
public class SignUtil {

    private static final String SIGN_KEY = "sign";
    private static final String CONNECT = "&";
    private static final String CONNECT_EQUAL = "=";

    public static String sign(Object signContent, String privateKey) {
        try {
            String signMsg = wrapSignData(signContent);
            String sign = RSAUtil.sign(signMsg.getBytes(), Base64.getDecoder().decode(privateKey), "RSA");
            log.info("待签名字符串:{},计算所得签名:{}", signMsg, sign);
            return sign;
        } catch (Exception e) {
            log.error("请求信息:{},签名处理失败:", JSON.toJSONString(signContent), e);
            return "";
        }
    }

    private static String wrapSignData(Object signContent) {
        Map signContentMap;
        if (signContent instanceof Map) {
            signContentMap = (Map) signContent;
        } else {
            signContentMap = BeanUtil.beanToMap(signContent, false, true);
        }
        return buildSignData(signContentMap);
    }

    private static String buildSignData(Map signContentMap) {
        signContentMap.remove(SIGN_KEY);
        List keys = new ArrayList<>(signContentMap.keySet());
        Collections.sort(keys);
        StringBuffer content = new StringBuffer();
        for (int i = 0; i < keys.size(); i++) {
            String key = keys.get(i);
            Object value = signContentMap.get(key);
            if (Objects.isNull(value)) {
                continue;
            }
            if (value instanceof List) {
                value = JSON.toJSONString(value);
            }
            if (i != keys.size() - 1) {
                content.append(key).append(CONNECT_EQUAL).append(value).append(CONNECT);
            } else {
                content.append(key).append(CONNECT_EQUAL).append(value);
            }
        }
        return content.toString();
    }

    public static Boolean verifySign(Object signContent, String sign, String publicKey) {
        try {
            String signData = wrapSignData(signContent);
            log.info("待签名字符串:{},签名:{}", signData, sign);
            return RSAUtil.verify(signData.getBytes(), Base64.getDecoder().decode(sign), Base64.getDecoder().decode(publicKey), "RSA");
        } catch (Exception e) {
            log.error("请求信息:{},验证处理异常:", JSON.toJSONString(signContent), e);
            return false;
        }
    }
}