API开发文档
API开发文档简介
本文阅读对象:使用支付平台商户自服务系统的技术架构师、研发工程师、系统运维工程师。通过本文档,商户可了解支付平台接入的技术、接入的产品业务、接入的流程、接入规范等信息,以便于商户顺利完成接入工作。
重要参数说明:
- API密钥(API密钥):用于API接口签名验证的密钥,请妥善保管,不要泄露给第三方。
- 支付回调地址(支付回调地址):订单完成后平台回调此地址,用于接收订单完成通知。必须是公网可访问的HTTPS地址。
- IP白名单(IP白名单):允许访问API接口的IP地址列表,格式为JSON数组。例如:
192.168.1.1,10.0.0.1
平台接口说明:
- 创建订单接口:https://www.leenopay.com/create/order
- POST请求,用于创建支付订单,商户调用此接口创建订单后,平台会返回支付跳转URL
- 订单查询接口:https://www.leenopay.com/search/order
- POST请求,用于查询订单状态、金额、支付时间等信息
接入网关
请登录商户中心,API管理》API开发文档,查看代理ID(pay_agentID)、商户密钥(APISecret)等参数。
接口基础地址: https://www.leenopay.com
CallbackURL:订单完成回调URL(订单完成后平台回调此地址,也可在创建订单时通过pay_notifyUrl参数指定)
接口约定
- 接口提交方法: POST
- 接口请求体格式: JSON
- 接口响应格式: JSON
- 字符编码: UTF-8
- 签名算法: MD5
签名算法
1. 筛选
获取所有发送或者接收到中根据参数说明需要参与签名(部分参数不参与签名,表格中有说明)的参数。
sign 字段不参与签名计算。
2. 排序
将筛选的参数按照第一个字符的键值ASCII码递增排序(字母升序排序),如果遇到相同字符则按照第二个字符的键值ASCII码递增排序,以此类推。
示例:
- Python: 用
sorted(dict.items())按键升序生成新字典 - Golang: 用
sort.Strings(keys)对提取的键升序排序 - Java: 用
new TreeMap<>()(默认按键自然升序) - PHP: 用
ksort()
3. 拼接
将排序后的参数与其对应值,组合成"参数=参数值"的格式,并且把这些参数用&字符连接起来,此时生成的字符串为待签名字符串。再将"&key=商户密钥"拼接在待签名字符串最后面。
4. 生成
将拼接后的待签名字符串用MD5加密,然后把加密后的MD5字符串转换为大写,得到签名字符串。
签名示例
假设有以下参数:
pay_agentID:AGENT001pay_externalOrderNo:ORDER123456pay_amount:100.00- 商户密钥:
SECRET_KEY_12345
stringSignTemp = "pay_agentID=AGENT001&pay_amount=100.00&pay_externalOrderNo=ORDER123456
&key=SECRET_KEY_12345"
sign = MD5(stringSignTemp).toUpperCase()
最终签名: A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6
创建订单接口
接口地址
POST https://www.leenopay.com/create/order
说明:接口基础地址为 https://www.leenopay.com,完整接口路径为 /create/order
请求参数
| 参数 | 类型 | 必填 | 参与签名 | 描述 |
|---|---|---|---|---|
| pay_agentID | string | 是 | 是 | 平台分配的代理ID |
| pay_externalOrderNo | string | 是 | 是 | 商户订单号,需保证唯一性 |
| pay_userID | string | 是 | 是 | 用户ID |
| pay_userName | string | 是 | 是 | 用户名称 |
| pay_userAvatar | string | 是 | 是 | 用户头像 |
| pay_amount | float64 | 是 | 是 | 订单金额,单位:元。必须是整数金额(如1元、100元),不支持小数金额(如1.5元、2.3元)。在JSON中使用浮点数格式表示,如1元应表示为1.00 |
| pay_notifyUrl | string | 是 | 是 | 订单成功通知地址(请求方式:POST JSON) |
| pay_orderTitle | string | 否 | 否 | 订单标题 |
| pay_orderDesc | string | 否 | 否 | 订单描述 |
| pay_currency | string | 否 | 否 | 货币类型,默认:CNY |
| pay_productName | string | 否 | 否 | 产品名称 |
| pay_phone | string | 否 | 否 | 客户手机号 |
| pay_remark | string | 否 | 是(不为空时) | 备注(不为空时参与签名) |
| pay_extraData | string | 否 | 是(不为空时) | 扩展数据(JSON格式,不为空时参与签名) |
| sign | string | 是 | 否 | 签名字符串,请查看签名算法 |
签名参数说明
参与签名的参数:
- 必填字段:
pay_agentIDpay_externalOrderNopay_userIDpay_userNamepay_userAvatarpay_amountpay_notifyurl(注意:JSON字段名为pay_notifyUrl,但签名时使用pay_notifyurl)
- 可选字段(不为空时参与签名):
pay_remarkpay_extraData
拼接签名字符串样例
pay_agentID=AGENT001&pay_amount=100.00&pay_externalOrderNo=ORDER20240101001&pay_notifyurl=https://example.com/notify&pay_userAvatar=avatar_url_123&pay_userID=USER123&pay_userName=张三&key=SECRET_KEY_12345
请求示例
{
"pay_agentID": "AGENT001",
"pay_externalOrderNo": "ORDER20240101001",
"pay_userID": "USER123",
"pay_userName": "张三",
"pay_userAvatar": "avatar_url_123",
"pay_amount": 100.00,
"pay_notifyUrl": "https://example.com/notify",
"pay_orderTitle": "测试订单",
"pay_currency": "CNY",
"sign": "A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6"
}
返回值
成功响应
| 参数 | 类型 | 必填 | 描述 |
|---|---|---|---|
| code | string | 是 | 状态,取值范围: success-成功, error-失败 |
| msg | string | 是 | 状态描述 |
| data | string | 是 | 数据内容,为JSON字符串(需要二次解析) |
data字段解析后的结构:
| 参数 | 类型 | 必填 | 描述 |
|---|---|---|---|
| pay_orderNo | string | 是 | 平台订单号 |
| pay_externalOrderNo | string | 是 | 商户订单号 |
| pay_url | string | 是 | 支付跳转URL |
| sign | string | 是 | 签名字符串 |
返回值示例
{
"code": "success",
"msg": "通过订单号创建订单成功",
"data": "{\"pay_orderNo\":\"PLATFORM_ORDER_001\",\"pay_externalOrderNo\":\"ORDER20240101001\",
\"pay_url\":\"https://pay.example.com/pay?orderNo=PLATFORM_ORDER_001\",
\"sign\":\"B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6Q7\"}"
}
订单查询接口
接口地址
POST https://www.leenopay.com/search/order
说明:接口基础地址为 https://www.leenopay.com,完整接口路径为 /search/order
请求参数
| 参数 | 类型 | 必填 | 参与签名 | 描述 |
|---|---|---|---|---|
| pay_agentID | string | 是 | 是 | 平台分配的代理ID |
| pay_orderNo | string | 否 | 是(不为空时) | 平台订单号(不为空时参与签名) |
| pay_externalOrderNo | string | 是 | 是 | 商户订单号(必填,参与签名) |
| sign | string | 是 | 否 | 签名字符串,请查看签名算法 |
签名参数说明
参与签名的参数:
- 必填字段:
pay_agentIDpay_externalOrderNo
- 可选字段(不为空时参与签名):
pay_orderNo
拼接签名字符串样例
pay_agentID=AGENT001&pay_externalOrderNo=ORDER20240101001&key=SECRET_KEY_12345
请求示例
{
"pay_agentID": "AGENT001",
"pay_externalOrderNo": "ORDER20240101001",
"sign": "A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6"
}
返回值
成功响应
| 参数 | 类型 | 必填 | 参与签名 | 描述 |
|---|---|---|---|---|
| code | string | 是 | 否 | 状态,取值范围: success-成功, error-失败 |
| msg | string | 是 | 否 | 状态描述 |
| data | string | 是 | 否 | 数据内容,为JSON字符串(需要二次解析) |
data字段解析后的结构:
| 参数 | 类型 | 必填 | 参与签名 | 描述 |
|---|---|---|---|---|
| pay_orderNo | string | 是 | 是 | 平台订单号 |
| pay_externalOrderNo | string | 是 | 是 | 商户订单号 |
| pay_userID | string | 是 | 是 | 用户ID |
| pay_serviceID | string | 否 | 否 | 客服ID(不参与签名) |
| pay_agentID | string | 是 | 是 | 代理ID |
| pay_amount | float64 | 是 | 是 | 实际结算金额,单位:元(扣除费率后) |
| pay_originalAmount | float64 | 是 | 是 | 原始订单金额,单位:元 |
| pay_rate | float64 | 是 | 是 | 费率(百分比,如2.5表示2.5%) |
| pay_rateAmount | float64 | 是 | 是 | 费率金额,单位:元 |
| pay_status | uint64 | 是 | 是 | 订单状态,0-待处理,1-处理中,2-已支付,3-已完成,4-已取消,5-已过期 |
| pay_serviceStatus | uint64 | 是 | 是 | 客服处理状态,0-待客服处理,1-客服已接单,2-客服处理中,3-客服处理完成,4-客服拒绝 |
| pay_remark | string | 否 | 是(不为空时) | 备注(不为空时参与签名) |
| pay_extraData | string | 否 | 是(不为空时) | 扩展数据(JSON格式,不为空时参与签名) |
| pay_payAt | string | 是 | 是 | 支付时间,格式:YYYY-MM-DD HH:mm:ss |
| sign | string | 是 | 否 | 签名字符串,请查看签名算法 |
返回值示例
{
"code": "success",
"msg": "查询订单成功",
"data": "{"pay_orderNo":"PLATFORM_ORDER_001","pay_externalOrderNo":"ORDER20240101001",
"pay_userID":"USER123","pay_serviceID":"","pay_agentID":"AGENT001","pay_amount":97.50,
"pay_originalAmount":100.00,"pay_rate":2.5,"pay_rateAmount":2.50,"pay_status":3,
"pay_serviceStatus":3,"pay_remark":"订单备注","pay_extraData":"","pay_payAt":"2024-01-01 12:00:00","sign":"C3D4E5F6G7H8I9J0K1L2M3N4O5P6Q7R8"}"
}
响应数据签名说明
查询订单接口返回的data字段(JSON字符串)中包含的以下字段参与签名验证:
- 必填字段:
pay_orderNopay_externalOrderNopay_userIDpay_agentIDpay_amount(实际结算金额)pay_originalAmount(原始订单金额)pay_rate(费率)pay_rateAmount(费率金额)pay_status(订单状态)pay_serviceStatus(客服处理状态)pay_payAt(支付时间)
- 可选字段(不为空时参与签名):
pay_remarkpay_extraData
- 不参与签名:
pay_serviceID(客服ID)sign(签名字段本身)
订单完成回调
回调说明
当客服点击下发订单,订单成功完成后,系统会自动向商户配置的回调地址发送订单完成通知。
请求方法: POST
请求体编码格式: JSON
接收到服务器点对点通讯时,在页面输出 OK(没有双引号,OK 两个字母大写),系统采用指数退避重试机制:
- 第1次重试:10秒后
- 第2次重试:30秒后
- 第3次重试:60秒后
如果所有重试均失败,任务将被存入Redis队列,等待后续处理。
回调地址
回调地址在创建订单时通过 pay_notifyUrl 参数指定,也可以使用订单信息中的 callbackURL 字段。
回调参数
| 参数 | 类型 | 必填 | 参与签名 | 描述 |
|---|---|---|---|---|
| pay_orderNo | string | 是 | 是 | 平台订单号 |
| pay_externalOrderNo | string | 是 | 是 | 商户订单号 |
| pay_status | string | 是 | 是 | 订单状态,字符串格式:0-待处理,1-处理中,2-已支付,3-已完成,4-已取消,5-已过期 |
| pay_amount | float64 | 是 | 是 | 实际结算金额(扣除费率后),单位:元 |
| pay_originalAmount | float64 | 是 | 是 | 原始订单金额,单位:元 |
| pay_rate | float64 | 是 | 是 | 费率(百分比,如2.5表示2.5%) |
| pay_rateAmount | float64 | 是 | 是 | 费率金额,单位:元 |
| pay_payAt | string | 是 | 是 | 支付时间,格式:YYYY-MM-DD HH:mm:ss |
| pay_remark | string | 否 | 是(不为空时) | 备注(不为空时参与签名) |
| sign | string | 是 | 否 | 签名字符串,请查看签名算法 |
签名参数说明
参与签名的参数:
- 必填字段:
pay_orderNopay_externalOrderNopay_statuspay_amount(实际结算金额)pay_originalAmount(原始订单金额)pay_rate(费率)pay_rateAmount(费率金额)pay_payAt
- 可选字段(不为空时参与签名):
pay_remark
拼接签名字符串样例
pay_amount=97.50&pay_externalOrderNo=ORDER20240101001&pay_orderNo=PLATFORM_ORDER_001&pay_originalAmount=100.00&pay_payAt=2024-01-01 12:00:00&pay_rate=2.5&pay_rateAmount=2.50&pay_remark=订单已完成&pay_status=3&key=SECRET_KEY_12345
注意:如果 pay_remark 为空,则不参与签名,签名字符串中不包含该参数。
回调示例
{
"pay_orderNo": "PLATFORM_ORDER_001",
"pay_externalOrderNo": "ORDER20240101001",
"pay_status": "3",
"pay_amount": 97.50,
"pay_originalAmount": 100.00,
"pay_rate": 2.5,
"pay_rateAmount": 2.50,
"pay_payAt": "2024-01-01 12:00:00",
"pay_remark": "订单已完成",
"sign": "D4E5F6G7H8I9J0K1L2M3N4O5P6Q7R8S9"
}
回调响应
商户收到回调后,应返回HTTP状态码 200 并在响应体中输出 OK(没有双引号,OK 两个字母大写),表示成功接收。如果返回非200状态码或响应内容不是OK,系统将进行重试。
重要提示:
客服在与客户沟通后,可能会根据客户的实际需求调整订单金额。因此,最终订单金额以回调通知中的金额为准,包括 pay_amount(实际结算金额)和 pay_originalAmount(原始订单金额)。请商户务必以回调数据中的金额进行结算,而非创建订单时的初始金额。
状态说明
订单状态(pay_status)
| 值 | 说明 |
|---|---|
| 0 | 待处理 |
| 1 | 处理中 |
| 2 | 已支付 |
| 3 | 已完成 |
| 4 | 已取消 |
| 5 | 已过期 |
客服处理状态(pay_serviceStatus)
| 值 | 说明 |
|---|---|
| 0 | 待客服处理 |
| 1 | 客服已接单 |
| 2 | 客服处理中 |
| 3 | 客服处理完成 |
| 4 | 客服拒绝 |
错误码说明
通用错误
| code | msg | 说明 |
|---|---|---|
| error | 读取请求体失败 | 请求体读取错误 |
| error | 解析请求数据失败 | JSON解析错误 |
| error | 签名不能为空 | 未提供签名 |
| error | 签名校验失败 | 签名验证不通过 |
| error | 无效的代理ID或代理未配置密钥 | 代理ID不存在或未配置密钥 |
创建订单错误
| code | msg | 说明 |
|---|---|---|
| error | 代理ID(pay_agentID)不能为空 | 缺少必填参数 |
| error | 产品方订单号(pay_externalOrderNo)不能为空 | 缺少必填参数 |
| error | 用户ID(pay_userID)不能为空 | 缺少必填参数 |
| error | 用户名称(pay_userName)不能为空 | 缺少必填参数 |
| error | 用户头像(pay_userAvatar)不能为空 | 缺少必填参数 |
| error | 订单金额(pay_amount)必须大于0 | 金额无效(必须大于0) |
| error | 订单成功通知地址(pay_notifyurl)不能为空 | 缺少回调地址 |
查询订单错误
| code | msg | 说明 |
|---|---|---|
| error | 代理ID(pay_agentID)不能为空 | 缺少必填参数 |
| error | 外部订单号(pay_externalOrderNo)不能为空 | 缺少必填参数 |
| error | 订单不存在 | 订单未找到 |
| error | 订单不属于该代理 | 订单与代理ID不匹配 |
示例代码
Python 签名生成示例
import hashlib
def generate_sign(params, merchant_key):
"""
生成MD5签名
:param params: 参数字典
:param merchant_key: 商户密钥
:return: 签名字符串(大写)
"""
# 1. 移除空值和sign字段
filtered_params = {
k: v for k, v in params.items()
if k != 'sign' and v and str(v).strip()
}
# 2. 排序
sorted_params = sorted(filtered_params.items())
# 3. 拼接参数
sign_str = '&'.join([f"{k}={v}" for k, v in sorted_params])
# 4. 拼接密钥
sign_str += f"&key={merchant_key}"
# 5. MD5加密并转大写
md5_hash = hashlib.md5(sign_str.encode('utf-8')).hexdigest()
return md5_hash.upper()
Java 签名生成示例
import java.security.MessageDigest;
import java.util.*;
public class SignUtil {
public static String generateSign(Map<String, String> params, String merchantKey) {
// 1. 移除空值和sign字段
Map<String, String> filteredParams = new TreeMap<>();
for (Map.Entry<String, String> entry : params.entrySet()) {
if (!entry.getKey().equals("sign") &&
entry.getValue() != null &&
!entry.getValue().trim().isEmpty()) {
filteredParams.put(entry.getKey(), entry.getValue());
}
}
// 2. 排序(TreeMap自动按ASCII排序)
// 3. 拼接参数
StringBuilder sb = new StringBuilder();
int index = 0;
for (Map.Entry<String, String> entry : filteredParams.entrySet()) {
if (index > 0) {
sb.append("&");
}
sb.append(entry.getKey()).append("=").append(entry.getValue());
index++;
}
// 4. 拼接密钥
sb.append("&key=").append(merchantKey);
// 5. MD5加密并转大写
return md5(sb.toString()).toUpperCase();
}
private static String md5(String input) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] digest = md.digest(input.getBytes("UTF-8"));
StringBuilder sb = new StringBuilder();
for (byte b : digest) {
sb.append(String.format("%02x", b));
}
return sb.toString();
} catch (Exception e) {
throw new RuntimeException("MD5加密失败", e);
}
}
}
Go 签名生成示例
package main
import (
"crypto/md5"
"encoding/hex"
"sort"
"strings"
)
func GenerateSign(params map[string]string, merchantKey string) string {
// 1. 移除空值和sign字段
keys := make([]string, 0, len(params))
for k, v := range params {
if k != "sign" && strings.TrimSpace(v) != "" {
keys = append(keys, k)
}
}
// 2. 排序
sort.Strings(keys)
// 3. 拼接参数
var builder strings.Builder
for i, k := range keys {
if i > 0 {
builder.WriteString("&")
}
builder.WriteString(k)
builder.WriteString("=")
builder.WriteString(params[k])
}
// 4. 拼接密钥
builder.WriteString("&key=")
builder.WriteString(merchantKey)
signPlain := builder.String()
// 5. MD5加密并转大写
sum := md5.Sum([]byte(signPlain))
return strings.ToUpper(hex.EncodeToString(sum[:]))
}
注意事项
- 签名安全: 商户密钥请妥善保管,不要泄露给第三方
- 回调地址: 回调地址必须是公网可访问的HTTPS地址
- 幂等性: 创建订单时,
pay_externalOrderNo必须唯一,重复提交可能导致订单创建失败 - 时间格式: 时间字段格式为
YYYY-MM-DD HH:mm:ss - 金额单位: 所有金额单位均为"元"(例如:100.00元),使用浮点数表示,保留两位小数
- 字符编码: 所有请求和响应均使用UTF-8编码
- 响应解析: 注意
data字段是JSON字符串,需要二次解析 - 回调重试: 请确保回调接口能够正确处理重试请求,避免重复处理订单
- 回调响应: 回调接口必须返回HTTP状态码200,并在响应体中输出
OK(没有双引号,OK 两个字母大写)
技术支持
如有问题,请联系技术支持团队。
文档版本: v1.0
最后更新: 2024-01-01