接入指南
CNIXPay 商户接口 V2 接入指南:Bearer 认证、REST 订单接口、四种订单状态、回调事件与验签、错误码和 OpenAPI。
概述
CNIXPay 商户接口 V2 用于在你的服务端创建稳定币收款订单。每笔订单分配独立的收款地址;一笔订单可以同时挂多个「网络 × 币种」收款方式,用户在哪条网络付款都会计入订单。订单状态只有 4 个,只看 status 就知道能不能发货、要不要人工处理。
Base URL:https://api.cnixpay.com/api/v2
| 网络 | chain | 标准 | token |
|---|---|---|---|
| TRON | tron | TRC20 | USDT |
| BNB Smart Chain | bsc | BEP20 | USDT / USDC |
| Ethereum | eth | ERC20 | USDT / USDC |
| Polygon | polygon | ERC20 | USDT / USDC |
| Solana | solana | SPL | USDT / USDC |
可用的网络与币种由平台配置,以接口返回为准。
快速开始
- 注册商户账号,在商户后台创建应用并提交上线。
- 在应用详情中获取
API Key与API Secret,配置 https 回调地址,按需配置 IP 白名单。 - 在你的服务端调用 创建订单,把返回的收款地址展示给用户,或让用户跳转到
cashierUrl。 - 实现回调接口:验签 后按
status处理,返回 HTTP 2xx。 - 用一笔小额订单在真实网络上走通全流程,即可上线。
curl -X POST https://api.cnixpay.com/api/v2/orders \
-H "Authorization: Bearer $CNIXPAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"merchantOrderNo": "ORDER_10086",
"amount": "100.00",
"paymentOptions": [
{ "chain": "tron", "token": "USDT" },
{ "chain": "bsc", "token": "USDC" }
],
"callbackUrl": "https://shop.example.com/notify"
}'import os
import requests
resp = requests.post(
"https://api.cnixpay.com/api/v2/orders",
headers={"Authorization": f"Bearer {os.environ['CNIXPAY_API_KEY']}"},
json={
"merchantOrderNo": "ORDER_10086",
"amount": "100.00", # 金额必须是字符串
"paymentOptions": [{"chain": "tron", "token": "USDT"}, {"chain": "bsc", "token": "USDC"}],
"callbackUrl": "https://shop.example.com/notify",
},
timeout=10,
)
body = resp.json()
if body["code"] != 0:
raise RuntimeError(f"{body['code']} {body['message']} (requestId={body['requestId']})")
order = body["data"]const res = await fetch('https://api.cnixpay.com/api/v2/orders', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.CNIXPAY_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
merchantOrderNo: 'ORDER_10086',
amount: '100.00', // 金额必须是字符串
paymentOptions: [{ chain: 'tron', token: 'USDT' }, { chain: 'bsc', token: 'USDC' }],
callbackUrl: 'https://shop.example.com/notify',
}),
})
const body = await res.json()
if (body.code !== 0) throw new Error(`${body.code} ${body.message} (requestId=${body.requestId})`)
const order = body.data$ch = curl_init('https://api.cnixpay.com/api/v2/orders');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('CNIXPAY_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'merchantOrderNo' => 'ORDER_10086',
'amount' => '100.00', // 金额必须是字符串
'paymentOptions' => [['chain' => 'tron', 'token' => 'USDT'], ['chain' => 'bsc', 'token' => 'USDC']],
'callbackUrl' => 'https://shop.example.com/notify',
]),
]);
$body = json_decode(curl_exec($ch), true);
if ($body['code'] !== 0) {
throw new RuntimeException("{$body['code']} {$body['message']} (requestId={$body['requestId']})");
}
$order = $body['data'];payload := strings.NewReader(`{
"merchantOrderNo": "ORDER_10086",
"amount": "100.00",
"paymentOptions": [{"chain": "tron", "token": "USDT"}, {"chain": "bsc", "token": "USDC"}],
"callbackUrl": "https://shop.example.com/notify"
}`)
req, _ := http.NewRequest(http.MethodPost, "https://api.cnixpay.com/api/v2/orders", payload)
req.Header.Set("Authorization", "Bearer "+os.Getenv("CNIXPAY_API_KEY"))
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
var body struct {
Code int `json:"code"`
Message string `json:"message"`
Data json.RawMessage `json:"data"`
RequestID string `json:"requestId"`
}
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
return err
}
if body.Code != 0 {
return fmt.Errorf("%d %s (requestId=%s)", body.Code, body.Message, body.RequestID)
}String json = """
{
"merchantOrderNo": "ORDER_10086",
"amount": "100.00",
"paymentOptions": [{"chain": "tron", "token": "USDT"}, {"chain": "bsc", "token": "USDC"}],
"callbackUrl": "https://shop.example.com/notify"
}""";
HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.cnixpay.com/api/v2/orders"))
.header("Authorization", "Bearer " + System.getenv("CNIXPAY_API_KEY"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(json))
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
// 解析 JSON:code == 0 为成功,data 为订单对象;失败时记录 requestId认证与限流
请求头 Authorization: Bearer <API Key> 传递应用的正式 API Key。API Secret 只用于验证回调签名,不要在请求里传,也不要放进前端代码。
- V2 只接受正式 Key;带
test_前缀的 Key 返回 401。平台不提供模拟沙箱,联调请用小额真实订单。 - 应用配置了 IP 白名单时,只有白名单内的 IP 可以调用。
- 限流按应用计,超限返回 HTTP 429,并带
Retry-After(秒),等待后重试即可。
| 接口 | 限流 |
|---|---|
| 创建订单 | 5 次/秒 |
| 查询订单、订单列表 | 共享 20 次/秒 |
| 追加收款方式 | 10 次/秒 |
| 关闭订单 | 5 次/秒 |
响应格式
响应体固定为 { code, message, data, requestId },code = 0 为成功。HTTP 状态码跟随错误类型(错误码前三位即 HTTP 状态码),失败时 HTTP 状态码和 code 都可以用来判断。
HTTP/1.1 200
X-Request-Id: req_1c9e...
{ "code": 0, "message": "success", "data": { ... }, "requestId": "req_1c9e..." }HTTP/1.1 400
X-Request-Id: req_8f2a...
{
"code": 40001,
"message": "paymentOptions[1].token 不能为空",
"data": { "field": "paymentOptions[1].token" },
"requestId": "req_8f2a..."
}requestId:每个请求唯一,同时在响应头X-Request-Id里。反馈问题时提供它,可直接定位到那一次请求。message:按请求头Accept-Language返回中文或英文,默认中文。- 失败时
data为null,或只包含field(出错的字段名)、orderNo(商户订单号重复时已有的订单)。
金额与币种
- 金额一律是十进制字符串,请求和响应都是,如
"100.50"。请求里传 JSON 数字(100.5)会被拒绝(40002)。做加减或比较请用 Decimal 库,不要转成浮点数。 - 报价币种
currency:USD(默认),或原生币TRX/BNB/ETH/POL/SOL。USD 订单按 USDT / USDC 1:1 收款;原生币订单只收该币。 - USD 金额 2 位小数,原生币 6 位;到账明细
transactions[].amount按链上精度。 - USD 按分记账:入账金额取整到分(不足一分的零头不入账),手续费四舍五入到分,因此
feeAmount + netAmount = paidAmount。 - 同一收款地址上,USD 订单接受 USDT 和 USDC,都按 1:1 计入。付错币(如 USD 订单收到原生币)不计入订单,由平台人工处理。
- 时间一律是 Unix 秒。
订单对象
查询、列表、创建、追加、关闭的返回和回调都是同一个订单对象,字段齐全。
{
"orderNo": "20261017105142a1b2c3d4e5f6",
"merchantOrderNo": "ORDER_10086",
"status": "paid",
"closeReason": "",
"version": 3,
"amount": "100.00",
"currency": "USD",
"paidAmount": "100.00",
"feeAmount": "0.60",
"netAmount": "99.40",
"paymentOptions": [
{ "chain": "tron", "token": "USDT", "address": "TR7NHq...", "amount": "100.00" },
{ "chain": "bsc", "token": "USDC", "address": "0x55d3...", "amount": "100.00" }
],
"transactions": [
{ "chain": "tron", "token": "USDT", "txHash": "abc123...", "fromAddress": "TUser...",
"toAddress": "TR7NHq...", "amount": "100.000000", "status": "confirmed", "blockTime": 1760670190 }
],
"txCount": 1,
"cashierUrl": "https://pay.cnixpay.com/pay/20261017105142a1b2c3d4e5f6",
"description": "VIP",
"extra": "user_id=1001",
"createTime": 1760668600,
"expireTime": 1760670400,
"paidTime": 1760670200,
"updateTime": 1760670200
}| 字段 | 说明 |
|---|---|
status | 订单状态,见 订单状态 |
closeReason | status 为 closed 时:timeout 超时未付款,merchant 商户关闭;其他为空字符串 |
version | 订单版本号,创建时为 1,对外可见的字段每变化一次加 1。回调可能乱序或重试送达,以 version 大的为准 |
amount / currency | 应收金额和币种 |
paidAmount | 累计已到账(只算已确认的交易)。少付或多付多少,用 amount 与 paidAmount 相减 |
feeAmount / netAmount | 平台手续费 / 商户实收(实际进入商户余额的金额)。钱在 paid 或 underpaid 时入账,pending 时都为 0 |
paymentOptions | 全部收款方式,amount 为该方式应付的链上资产数量 |
transactions | 链上到账明细,最多最近 50 条,可能有多笔、跨多条链。付款人实际用哪条链、哪个币(token 是实际到账的币,可能与下单时选的不同)、付到哪个地址,都以这里为准。status 为 pending(确认中)或 confirmed(已确认,计入 paidAmount) |
txCount | 到账明细总笔数 |
cashierUrl | 平台收银台链接 |
extra | 下单时透传的内容,原样返回 |
updateTime | 最近一次变化的时间,与 version 同时更新 |
订单状态
只有 4 个状态,除 pending 外正常情况下都是最终结果(唯一例外见下方「人工补单」):
status | 含义 | 你该做什么 |
|---|---|---|
pending | 还没有结果:待付款、已收到部分款、确认中,以及过期后的宽限期内 | 等待;paidAmount > 0 时可提示用户补足 |
paid | 已足额支付(多付也算) | 发货 |
underpaid | 宽限期结束,只收到部分款,已按实收入账到商户余额 | 人工处理:补发部分商品、联系用户补款或退款 |
closed | 没收到钱,订单已关闭(closeReason 区分超时和商户关闭) | 作废订单 |
pending ──足额到账──▶ paid
│
├──商户关闭(仅未收到付款时)──▶ closed(closeReason = merchant)
│
└──宽限期结束仍未付足
├──收到过付款──▶ underpaid
└──没收到钱────▶ closed(closeReason = timeout)- 宽限期:订单过了
expireTime后还有一段宽限期(默认 60 分钟)。付款人在过期前转账但链上到得晚,或在宽限期内补足,订单仍会变为paid。宽限期内对外仍是pending,所以你只需要记住:pending就等待,其他状态就处理。 - 出结果的时间:宽限期结束后约 10 分钟内给出结果(
underpaid或closed);如果这时还有链上确认中的到账,会等它确认完成或失败后再给出结果,期间订单保持pending。 - 链上确认:确认所需时间由平台按订单金额设定,小额通常几秒到 1 分钟,大额需要更多确认。确认完成前,
transactions里这笔为pending、不计入paidAmount,订单仍为pending。 - 宽限期结束后才到的钱、商户关闭后才到的钱,不会自动入账,由平台人工处理:退回付款人,或核实后补单。
- 人工补单是结果唯一可能变化的情况:补单后按实收重新判定,
closed可能变为paid或underpaid,underpaid可能变为paid,并推送对应事件。所以已作废的订单也可能收到order.paid,请按version处理,不要直接丢弃。 paid之后又到账的钱(如重复付款)照常入账,状态保持paid,金额和version更新,不再推送回调。paid是最终结果:之后即使链上发生回滚,订单对你也完全不变,损失由平台承担。
收到回调(或查询订单):
按请求头 X-Webhook-Id 去重;order.version 小于本地记录的直接忽略
switch order.status:
paid → 发货(paidAmount > amount 时可考虑退还多付部分)
underpaid → 人工处理(补发部分商品、联系用户补款或退款)
closed → 作废订单
pending → 等待(paidAmount > 0 时可提示用户补足 amount − paidAmount)创建订单
POST/orders
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
merchantOrderNo | string ≤64 | 是 | 商户订单号,应用内唯一 |
amount | string | 是 | 应收金额,十进制字符串,大于 0。USD 最多 2 位小数,原生币最多 6 位 |
currency | string | 否 | USD(默认)或 TRX / BNB / ETH / POL / SOL |
paymentOptions | array | 否 | 收款方式,1–10 条。不传时 USD 订单由收银台让付款人选择,原生币订单自动用该币所在的链 |
paymentOptions[].chain | string | 是 | 网络,见 概述 |
paymentOptions[].token | string | 是 | USDT / USDC / 原生币符号 |
expireMinutes | integer | 否 | 有效期(分钟),5–1440,默认 30 |
callbackUrl | string ≤500 | 否 | 回调地址,只允许 https,不能是本地或内网地址;不传则用应用配置(也须为 https) |
returnUrl | string ≤500 | 否 | 收银台支付完成后跳转,只允许 https |
description | string ≤200 | 否 | 订单摘要,收银台展示 |
extra | string ≤500 | 否 | 透传,查询和回调原样返回 |
成功返回完整的订单对象(version = 1)。两种用法:
- 自建收银台:传
paymentOptions,用返回的paymentOptions[].address和amount渲染支付页。 - 平台收银台:不传
paymentOptions,把用户跳转到cashierUrl,由付款人选择网络和币种。
商户订单号重复返回 409 / 40901,并在 data.orderNo 带上已有订单。下单超时、不确定是否成功时,用它查询确认即可,不要换订单号重下。
HTTP/1.1 409
{ "code": 40901, "message": "商户订单号已存在", "data": { "orderNo": "20261017105142a1b2c3d4e5f6" }, "requestId": "req_..." }查询与列表
| 接口 | 说明 |
|---|---|
GET/orders/{orderNo} | 按系统订单号查询,返回订单对象(含 transactions)。不存在返回 404 / 40401 |
GET/orders | 订单列表,返回 { items, total, page, pageSize },列表项是完整的订单对象(含 transactions) |
| 参数 | 说明 |
|---|---|
merchantOrderNo | 按商户订单号查(精确),可重复传多个,最多 100 个:?merchantOrderNo=A&merchantOrderNo=B |
orderNo | 按系统订单号查,同上 |
status | 订单状态,可重复传多个 |
createTimeFrom / createTimeTo | 按创建时间筛选(Unix 秒,左闭右开) |
page / pageSize | 默认 1 / 20,pageSize 最大 100 |
按创建时间倒序返回。回调兜底:每隔几分钟,把本地还没出结果的订单按 merchantOrderNo 批量查一次(每次最多 100 个),状态变了就按 订单状态 处理。这样即使回调接口宕机、重试用尽,也不会漏发货。
GET https://api.cnixpay.com/api/v2/orders?merchantOrderNo=ORDER_10086&merchantOrderNo=ORDER_10087&pageSize=100
Authorization: Bearer $CNIXPAY_API_KEY追加收款方式
POST/orders/{orderNo}/payment-options
请求体 { "paymentOptions": [{ "chain": "polygon", "token": "USDC" }] },返回订单对象。
- 只有
pending、还没收到付款、未过expireTime的订单可以追加,否则 409 / 40902。原生币订单不能追加。 - 已存在的收款方式幂等(不报错、不重复添加);同一网络的不同币种共用一个收款地址。
- 合计不超过 10 条,只能追加,不能删除。
关闭订单
POST/orders/{orderNo}/close
- 无请求体。只能关闭还没收到付款的
pending订单(含已过expireTime、仍在宽限期内的);已收到付款,或有链上确认中的到账(transactions里status为pending),返回 409 / 40902。 - 关闭后
status = closed、closeReason = merchant,之后再到账不会自动入账。 - 已由商户关闭的订单重复关闭视为成功,直接返回当前订单。关闭由你主动触发,不推送回调。
回调事件
平台这边发生、你不知道的变化会推送到 callbackUrl。请求体是 { id, event, createTime, data },data 是事件发生那一刻的订单对象。
event | 推送时机 | 订单状态 | 你该做什么 |
|---|---|---|---|
order.partially_paid | 收到一笔付款但还不够 | pending | 可选:提示用户补足 |
order.paid | 足额到账,立即推送 | paid | 发货 |
order.underpaid | 宽限期结束,只收到部分款 | underpaid | 人工处理 |
order.closed | 宽限期结束,没收到钱 | closed(timeout) | 作废订单 |
POST /notify HTTP/1.1
Content-Type: application/json
X-Webhook-Id: evt_7f3c9a...
X-Webhook-Timestamp: 1760670205
X-Webhook-Signature: v2=5b1f0e...
{
"id": "evt_7f3c9a...",
"event": "order.paid",
"createTime": 1760670200,
"data": { "orderNo": "20261017105142a1b2c3d4e5f6", "status": "paid", "version": 3, "...": "..." }
}- 返回 HTTP 2xx 即视为送达。单次请求超时 5 秒,不跟随重定向(3xx 算失败)。失败后按时间表重试:15 秒、1 分钟、5 分钟、10 分钟、30 分钟、1 小时、2 小时、6 小时、12 小时,共约 22 小时。四种事件都会重试。
- 订单没有回调地址(下单没传
callbackUrl,应用也没配置)时不推送任何事件。 - 回调经平台的转发服务发出,来源 IP 不固定:请不要按 IP 白名单限制回调接口,用签名校验来源。
- 正常情况下每个订单只推送一个结果事件(
paid/underpaid/closed),宽限期内补足的只会收到order.paid;平台人工补单改变结果时会再推送order.paid或order.underpaid。 - 没收到钱的订单要在宽限期结束后约 10 分钟才收到
order.closed(默认即过期后约 70 分钟);需要更早释放库存,可按expireTime自行处理。 - 重试时请求体不变,可能晚于后续事件送达:按
X-Webhook-Id去重,按data.version取最新,或收到后再查询订单。 - 商户后台的订单详情可查看每个事件的推送记录,并可重新推送。
回调验签
签名只在请求头,对原始请求体整体签名,不用解析 JSON 就能验证,以后新增字段也不影响验签:
- 取请求头
X-Webhook-Timestamp,校验与当前时间相差 5 分钟内(防重放;每次重试都会重新生成)。 - 以 API Secret 为密钥,计算
HMAC-SHA256(timestamp + "." + 原始请求体),输出小写十六进制,前面加v2=。 - 与请求头
X-Webhook-Signature做常量时间比较。
import crypto from 'node:crypto'
// rawBody 必须是原始请求体(Buffer),不要先解析成 JSON 再序列化
export function verifyWebhook(rawBody, headers, apiSecret) {
const timestamp = headers['x-webhook-timestamp']
const signature = headers['x-webhook-signature'] ?? ''
if (!/^\d+$/.test(timestamp ?? '') || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false
const expected =
'v2=' + crypto.createHmac('sha256', apiSecret).update(`${timestamp}.`).update(rawBody).digest('hex')
const a = Buffer.from(expected)
const b = Buffer.from(signature)
return a.length === b.length && crypto.timingSafeEqual(a, b)
}
// Express: app.post('/notify', express.raw({ type: 'application/json' }), handler)import hashlib
import hmac
import time
def verify_webhook(raw_body: bytes, headers, api_secret: str) -> bool:
timestamp = headers.get("X-Webhook-Timestamp", "")
signature = headers.get("X-Webhook-Signature", "")
if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > 300:
return False
expected = "v2=" + hmac.new(
api_secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)func VerifyWebhook(r *http.Request, rawBody []byte, apiSecret string) bool {
timestamp := r.Header.Get("X-Webhook-Timestamp")
sec, err := strconv.ParseInt(timestamp, 10, 64)
if err != nil || time.Since(time.Unix(sec, 0)).Abs() > 5*time.Minute {
return false
}
mac := hmac.New(sha256.New, []byte(apiSecret))
mac.Write([]byte(timestamp + "."))
mac.Write(rawBody)
expected := "v2=" + hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(r.Header.Get("X-Webhook-Signature")))
}function verifyWebhook(string $rawBody, string $timestamp, string $signature, string $apiSecret): bool
{
if (!ctype_digit($timestamp) || abs(time() - (int) $timestamp) > 300) {
return false;
}
$expected = 'v2=' . hash_hmac('sha256', $timestamp . '.' . $rawBody, $apiSecret);
return hash_equals($expected, $signature);
}
// $raw = file_get_contents('php://input');
// verifyWebhook($raw, $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'] ?? '', $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '', $secret);static boolean verifyWebhook(byte[] rawBody, String timestamp, String signature, String apiSecret)
throws GeneralSecurityException {
long ts;
try {
ts = Long.parseLong(timestamp);
} catch (NumberFormatException e) {
return false;
}
if (Math.abs(System.currentTimeMillis() / 1000 - ts) > 300) return false;
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(apiSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
mac.update((timestamp + ".").getBytes(StandardCharsets.UTF_8));
String expected = "v2=" + HexFormat.of().formatHex(mac.doFinal(rawBody));
return MessageDigest.isEqual(
expected.getBytes(StandardCharsets.UTF_8), signature.getBytes(StandardCharsets.UTF_8));
}很多框架默认会先把请求体解析成 JSON。验签必须用原始字节,例如 Express 用 express.raw(),PHP 用 file_get_contents('php://input')。
错误码
code | HTTP | 说明 |
|---|---|---|
40001 | 400 | 参数缺失或格式错误,data.field 为字段名 |
40002 | 400 | 金额格式错误:不是字符串、不大于 0 或超出小数位 |
40003 | 400 | 金额超出该链、该币的单笔上限 |
40004 | 400 | 链或币种不支持 / 未开放 |
40005 | 400 | 收款方式数量超限 |
40101 | 401 | API Key 缺失或无效(含测试 Key) |
40301 | 403 | 请求 IP 不在应用白名单 |
40302 | 403 | 应用未上线、已停用,或商户账户已禁用 |
40401 | 404 | 订单不存在 |
40901 | 409 | 商户订单号已存在(data.orderNo 为已有订单) |
40902 | 409 | 订单状态不允许此操作 |
42901 | 429 | 请求过于频繁,按 Retry-After 重试 |
50001 | 500 | 系统繁忙,可稍后重试 |
OpenAPI 与 AI
- OpenAPI 3.1:
https://api.cnixpay.com/api/v2/openapi.json(公开,无需认证)。完整描述接口、字段、枚举、错误码和回调格式,可直接导入 Postman / Apifox,或交给 AI 编程助手生成对接代码。 - llms.txt:
/llms.txt(索引)与/llms-full.txt(本指南全文的纯文本),方便交给 AI 使用。
最佳实践
- 回调必须验签。不要信任任何未通过签名校验的通知。
- 回调接口保持幂等。按
X-Webhook-Id去重,按version丢弃旧数据。 - 快速返回 2xx。耗时逻辑放进异步队列,避免触发重试。
- 关键操作前查询确认。发货前可再调一次查询订单;定时按
merchantOrderNo批量查询未完结的订单,兜底漏收的回调。 - 下单超时不要换单号重下。用同一个
merchantOrderNo重试,收到 40901 就按data.orderNo查询。 - 金额使用 Decimal。所有金额都是字符串,避免浮点误差。
- 记录
requestId。出错时把它连同时间一起记进日志,排查时提供给我们。