付款服务接口文档

接口域名: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/payout/create

接口功能:接收商户付款请求,验签后创建付款订单并调用渠道代付

2.1 请求入参(JSON Body)

参数名 类型 是否必填 参数说明
merchantId String 商户编号,平台分配唯一标识
merchantOrderNo String 商户唯一订单号,不可重复
amount BigDecimal 付款金额(单位:元),必须大于0
currency String 币种编码
ipAddress String 用户IP
notifyUrl String 付款结果异步通知URL
bankCode String 银行IFSC编码
accountNo String 收款账户号码
accountName String 收款账户持有者姓名
email String 收款人邮箱
phone String 收款人电话号码
bankName String 银行名称
subBranch String 支行名称
region String 地区
province String 省份
city String 城市
postalCode String 邮编
address String 地址
bankType String 银行类型枚举:IMPS / UPI
remark String 付款备注
sign String 请求参数签名,生成规则参考签名模块

2.2 响应返回参数

参数名 子参数 类型 场景 说明
code - String 全部返回 响应码,0000=下单成功
msg - String 全部返回 响应描述
data - Object 成功返回 付款订单业务子对象
payoutOrderNo String 成功返回 平台生成的付款订单号
status String 成功返回 付款状态,参考6.1 付款状态枚举

2.3 示例

2.3.1 请求示例

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

{
  "merchantId": "M100001",
  "merchantOrderNo": "PO20260719001",
  "amount": 500.00,
  "currency": "INR",
  "ipAddress": "192.168.1.100",
  "notifyUrl": "https://merchant.com/notify",
  "bankCode": "HDFC0001234",
  "accountNo": "1234567890",
  "accountName": "John Doe",
  "email": "john@example.com",
  "phone": "+91-9876543210",
  "bankName": "HDFC Bank",
  "subBranch": "Mumbai Main",
  "province": "Maharashtra",
  "city": "Mumbai",
  "bankType": "IMPS",
  "remark": "Salary payment",
  "sign": "xxxxxxxxxxxxxxxxxxxx"
}

2.3.2 成功响应示例

{
  "code": "0000",
  "msg": "下单成功",
  "data": {
    "payoutOrderNo": "PW20260719123456",
    "status": "PAYOUT_ING"
  }
}

三、付款结果查询接口

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

接口功能:主动查询付款订单实时状态,若订单未到终态会主动向渠道同步最新状态

3.1 请求入参(二选一)

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

3.2 响应返回参数

参数名 子参数 类型 场景 说明
code - String 全部返回 0000=查询成功
msg - String 全部返回 响应描述
data - Object 成功返回 付款订单详情子对象
respCode String 成功返回 业务响应码
respDesc String 成功返回 业务响应描述
payoutOrderNo String 成功返回 平台付款订单号
merchantOrderNo String 成功返回 商户订单号
status String 成功返回 付款状态,参考6.1 付款状态枚举
payoutSuccessTime String 付款成功订单 付款完成时间 yyyy-MM-dd HH:mm:ss

3.3 示例

3.3.1 请求示例

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

{
  "merchantId": "M100001",
  "payoutOrderNo": "PW20260719123456",
  "sign": "xxxxxxxxxxxxxxxxxxxx"
}

3.3.2 成功响应示例

{
  "code": "0000",
  "msg": "查询成功",
  "data": {
    "respCode": "0000",
    "respDesc": "查询成功",
    "payoutOrderNo": "PW20260719123456",
    "merchantOrderNo": "PO20260719001",
    "status": "PAYOUT_SUCCESS",
    "payoutSuccessTime": "2026-07-19 14:30:00"
  }
}

四、商户余额查询接口

请求地址:POST https://pay.yilise.com/api/v1/payout/balance

接口功能:查询商户可用余额,可用余额 = 收入结算金额汇总 - 支出结算金额汇总

4.1 请求入参(JSON Body)

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

4.2 响应返回参数

参数名 子参数 类型 场景 说明
code - String 全部返回 0000=查询成功
msg - String 全部返回 响应描述
data - Object 成功返回 余额详情子对象
respCode String 成功返回 业务响应码
respDesc String 成功返回 业务响应描述
availableBalance BigDecimal 成功返回 可用余额

4.3 示例

4.3.1 请求示例

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

{
  "merchantId": "M100001",
  "sign": "xxxxxxxxxxxxxxxxxxxx"
}

4.3.2 成功响应示例

{
  "code": "0000",
  "msg": "查询成功",
  "data": {
    "respCode": "0000",
    "respDesc": "查询成功",
    "availableBalance": 125680.50
  }
}

五、付款结果异步通知

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

5.1 回调报文示例

{
  "respCode": "0000",
  "respDesc": "付款成功",
  "merchantOrderNo": "PO20260719001",
  "payoutOrderNo": "PW20260719123456",
  "status": "PAYOUT_SUCCESS",
  "payoutSuccessTime": "2026-07-19 14:30:00",
  "sign": "xxxxxxxxxxxxxxxxxxxx"
}

5.2 回调字段说明

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

六、系统枚举定义

6.1 付款状态枚举

状态码 中文名称 业务说明
PAYOUT_ING 付款中 已提交渠道,等待渠道返回结果
PAYOUT_SUCCESS 付款成功 渠道返回付款成功,资金已转出
PAYOUT_FAILED 付款失败 渠道返回付款失败,资金未转出

6.2 银行类型枚举

枚举值 说明
IMPS 即时支付结算(Immediate Payment Service)
UPI 统一支付接口(Unified Payments Interface)

七、对接开发注意事项

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

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

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

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

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

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

7. 异步通知需做好幂等处理,避免多次回调导致重复业务操作。

八、签名规则与工具类

8.1 签名规则说明

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

8.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<String, String> 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<String, String> 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);
    }
}

8.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<String, Object> signContentMap;
        if (signContent instanceof Map) {
            signContentMap = (Map<String, Object>) signContent;
        } else {
            signContentMap = BeanUtil.beanToMap(signContent, false, true);
        }
        return buildSignData(signContentMap);
    }

    private static String buildSignData(Map<String, Object> signContentMap) {
        signContentMap.remove(SIGN_KEY);
        List<String> 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;
        }
    }
}
付款服务接口文档

付款服务接口文档

PayoutController — 对外付款 API 接口说明

Base URL: /api/v1/payout
POST /api/v1/payout/create

接收商户付款请求,验签后创建付款订单并调用渠道代付。

请求参数(Request Body — JSON)
字段名类型必填说明
merchantIdString商户ID
signString签名
merchantOrderNoString商户订单号
amountBigDecimal付款金额(单位:元),必须大于 0
currencyString币种
ipAddressString用户 IP
notifyUrlString付款结果异步通知 URL
bankCodeString银行 IFSC 编码
accountNoString收款账户号码
accountNameString收款账户持有者姓名
emailString收款人邮箱
phoneString收款人电话号码
bankNameString银行名称
subBranchString支行名称
regionString地区
provinceString省份
cityString城市
postalCodeString邮编
addressString地址
bankTypeString银行类型(IMPS / UPI)
remarkString付款备注
响应参数
字段名类型说明
codeString响应码,"0000" 表示成功
messageString响应描述
isSuccessBoolean是否成功
data.payoutOrderNoString平台付款订单号
data.statusString付款状态
示例
请求示例
{ "merchantId": "M100001", "sign": "a1b2c3d4e5f6...", "merchantOrderNo": "PO20260719001", "amount": 500.00, "currency": "INR", "ipAddress": "192.168.1.100", "notifyUrl": "https://merchant.com/notify", "bankCode": "HDFC0001234", "accountNo": "1234567890", "accountName": "John Doe", "email": "john@example.com", "phone": "+91-9876543210", "bankName": "HDFC Bank", "subBranch": "Mumbai Main", "province": "Maharashtra", "city": "Mumbai", "bankType": "IMPS", "remark": "Salary payment" }
成功响应
{ "code": "0000", "message": "操作成功", "isSuccess": true, "data": { "payoutOrderNo": "PW20260719123456", "status": "PAYOUT_ING" } }
失败响应
{ "code": "1", "message": "商户订单号不能为空", "isSuccess": false, "data": null }
POST /api/v1/payout/query

接收商户查询请求,验签后返回付款订单最新状态。若订单未到终态,会主动向渠道同步最新状态。

请求参数(Request Body — JSON)
字段名类型必填说明
merchantIdString商户ID
signString签名
merchantOrderNoString条件必填商户订单号(与 payoutOrderNo 二选一)
payoutOrderNoString条件必填平台付款订单号(与 merchantOrderNo 二选一)
响应参数
字段名类型说明
codeString响应码,"0000" 表示成功
messageString响应描述
isSuccessBoolean是否成功
data.respCodeString业务响应码
data.respDescString业务响应描述
data.payoutOrderNoString平台付款订单号
data.merchantOrderNoString商户订单号
data.statusString付款状态
data.payoutSuccessTimeString付款成功时间(ISO 格式)
示例
请求示例
{ "merchantId": "M100001", "sign": "a1b2c3d4e5f6...", "payoutOrderNo": "PW20260719123456" }
成功响应
{ "code": "0000", "message": "操作成功", "isSuccess": true, "data": { "respCode": "0000", "respDesc": "查询成功", "payoutOrderNo": "PW20260719123456", "merchantOrderNo": "PO20260719001", "status": "PAYOUT_SUCCESS", "payoutSuccessTime": "2026-07-19T14:30:00" } }
失败响应
{ "code": "1", "message": "订单不存在", "isSuccess": false, "data": null }
POST /api/v1/payout/balance

接收商户查询请求,验签后返回可用余额。可用余额 = 收入结算金额汇总 - 支出结算金额汇总。

请求参数(Request Body — JSON)
字段名类型必填说明
merchantIdString商户ID
signString签名
响应参数
字段名类型说明
codeString响应码,"0000" 表示成功
messageString响应描述
isSuccessBoolean是否成功
data.respCodeString业务响应码
data.respDescString业务响应描述
data.availableBalanceBigDecimal可用余额
示例
请求示例
{ "merchantId": "M100001", "sign": "a1b2c3d4e5f6..." }
成功响应
{ "code": "0000", "message": "操作成功", "isSuccess": true, "data": { "respCode": "0000", "respDesc": "查询成功", "availableBalance": 125680.50 } }
失败响应
{ "code": "1", "message": "签名验证失败", "isSuccess": false, "data": null }