对接文档新增回调通知章节(事件/签名/推送重试)
- 回调文档三个区块:回调通知(事件列表、推送示例、接收方要求)、签名校验(算法与伪代码)、推送与重试(微信式 16 次退避序列) - 概述一句话上手补充回调配置步骤
This commit is contained in:
@@ -68,6 +68,14 @@ function buildToc(): TocGroup[] {
|
|||||||
})),
|
})),
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: '回调',
|
||||||
|
items: [
|
||||||
|
{ id: 'callback', label: '回调通知' },
|
||||||
|
{ id: 'callback-sign', label: '签名校验' },
|
||||||
|
{ id: 'callback-retry', label: '推送与重试' },
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: '附录',
|
title: '附录',
|
||||||
items: [{ id: 'status', label: '状态说明' }],
|
items: [{ id: 'status', label: '状态说明' }],
|
||||||
@@ -215,6 +223,9 @@ export default function OpenApiDocs() {
|
|||||||
<div className="api-docs__inner">
|
<div className="api-docs__inner">
|
||||||
{activePageId === 'overview' ? <OverviewSection /> : null}
|
{activePageId === 'overview' ? <OverviewSection /> : null}
|
||||||
{activePageId === 'auth' ? <AuthSection /> : null}
|
{activePageId === 'auth' ? <AuthSection /> : null}
|
||||||
|
{activePageId === 'callback' ? <CallbackSection /> : null}
|
||||||
|
{activePageId === 'callback-sign' ? <CallbackSignSection /> : null}
|
||||||
|
{activePageId === 'callback-retry' ? <CallbackRetrySection /> : null}
|
||||||
{activeEndpoint ? <EndpointSection spec={activeEndpoint} /> : null}
|
{activeEndpoint ? <EndpointSection spec={activeEndpoint} /> : null}
|
||||||
{activePageId === 'status' ? <StatusSection /> : null}
|
{activePageId === 'status' ? <StatusSection /> : null}
|
||||||
<PageStepper
|
<PageStepper
|
||||||
@@ -312,6 +323,7 @@ function OverviewSection() {
|
|||||||
<li>每次请求携带 <Text code>X-App-Key / X-Timestamp / X-Nonce / X-Sign</Text> 四个鉴权头。</li>
|
<li>每次请求携带 <Text code>X-App-Key / X-Timestamp / X-Nonce / X-Sign</Text> 四个鉴权头。</li>
|
||||||
<li>调用「商品列表」拿到可售 <Text code>sku</Text>,调用「下单」创建订单并扣款。</li>
|
<li>调用「商品列表」拿到可售 <Text code>sku</Text>,调用「下单」创建订单并扣款。</li>
|
||||||
<li>拿到 <Text code>order_no</Text> 后,可选择返回发货链接,或者用结构化发货接口完成自建页面流程。</li>
|
<li>拿到 <Text code>order_no</Text> 后,可选择返回发货链接,或者用结构化发货接口完成自建页面流程。</li>
|
||||||
|
<li>在「回调」页配置回调地址并订阅事件,订单状态变化会实时推送,见「回调通知」。</li>
|
||||||
</ol>
|
</ol>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -449,8 +461,236 @@ function EndpointSection({ spec }: { spec: EndpointSpec }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function StatusSection() {
|
// ---------- 回调通知文档 ----------
|
||||||
|
|
||||||
|
const callbackEvents = [
|
||||||
|
{
|
||||||
|
event: 'order.created',
|
||||||
|
desc: '订单创建成功(已扣款,order_status=paid)',
|
||||||
|
trigger: '创建订单',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
event: 'order.shipping.updated',
|
||||||
|
desc: '发货状态变化:提交发货(delivering)、发货成功(delivered)、发货失败(ship_failed)',
|
||||||
|
trigger: '提交发货 / 上游回传发货结果',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
event: 'order.cancelled',
|
||||||
|
desc: '订单取消并退款(order_status=cancelled)',
|
||||||
|
trigger: '取消订单',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const callbackPayloadExample = `{
|
||||||
|
"event_id": "4751626d-d608-42c1-a453-60ffcafc01fc",
|
||||||
|
"event": "order.shipping.updated",
|
||||||
|
"occurred_at": "2026-08-03T10:11:20+08:00",
|
||||||
|
"data": {
|
||||||
|
"order_no": "FO20260803000123",
|
||||||
|
"client_order_no": "shop-10001",
|
||||||
|
"order_status": "delivered",
|
||||||
|
"can_ship": false,
|
||||||
|
"product": { "sku": "suit_pink_sheep", "name": "套装-糯粉咩咩" },
|
||||||
|
"quantity": 1,
|
||||||
|
"amount": 105,
|
||||||
|
"currency": "POINT",
|
||||||
|
"provider_order_no": "PROV-xxxx",
|
||||||
|
"failure_reason": "",
|
||||||
|
"created_at": "2026-08-03T10:00:00+08:00",
|
||||||
|
"delivered_at": "2026-08-03T10:11:20+08:00"
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
const retrySchedule = [
|
||||||
|
{ attempt: 2, after: '15 秒' },
|
||||||
|
{ attempt: 3, after: '15 秒' },
|
||||||
|
{ attempt: 4, after: '30 秒' },
|
||||||
|
{ attempt: 5, after: '3 分钟' },
|
||||||
|
{ attempt: 6, after: '10 分钟' },
|
||||||
|
{ attempt: 7, after: '20 分钟' },
|
||||||
|
{ attempt: 8, after: '30 分钟' },
|
||||||
|
{ attempt: 9, after: '30 分钟' },
|
||||||
|
{ attempt: 10, after: '30 分钟' },
|
||||||
|
{ attempt: 11, after: '1 小时' },
|
||||||
|
{ attempt: 12, after: '3 小时' },
|
||||||
|
{ attempt: 13, after: '3 小时' },
|
||||||
|
{ attempt: 14, after: '3 小时' },
|
||||||
|
{ attempt: 15, after: '6 小时' },
|
||||||
|
{ attempt: 16, after: '6 小时' },
|
||||||
|
]
|
||||||
|
|
||||||
|
function CallbackSection() {
|
||||||
return (
|
return (
|
||||||
|
<SectionWrap
|
||||||
|
id="callback"
|
||||||
|
title="回调通知"
|
||||||
|
desc={
|
||||||
|
<>
|
||||||
|
平台通过 <Text code>POST</Text> 方式把订单事件推送到商户配置的回调地址。
|
||||||
|
商户在后台「回调」页配置一个 URL 并订阅事件,配置保存在 outbox 表中,
|
||||||
|
订单事务提交后才写入待推送记录,进程重启不会丢失。
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Alert
|
||||||
|
className="api-docs__alert"
|
||||||
|
type="info"
|
||||||
|
showIcon
|
||||||
|
message="快速接入"
|
||||||
|
description={
|
||||||
|
<ol style={{ margin: 0, paddingLeft: 20 }}>
|
||||||
|
<li>在商户后台「回调」页填写回调 URL 并订阅事件,保存后获得 <Text code>CallbackSecret</Text>(仅展示一次,可在后台重置)。</li>
|
||||||
|
<li>接收平台推送:请求头带 <Text code>X-Event-ID / X-Timestamp / X-Sign</Text>,按「签名校验」验证后处理。</li>
|
||||||
|
<li>返回 HTTP 2xx 即视为投递成功;非 2xx 或超时按「推送与重试」策略重试。</li>
|
||||||
|
</ol>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Card className="api-docs__card" size="small" title="回调事件">
|
||||||
|
<Table
|
||||||
|
className="api-docs__table"
|
||||||
|
size="small"
|
||||||
|
pagination={false}
|
||||||
|
rowKey="event"
|
||||||
|
dataSource={callbackEvents}
|
||||||
|
columns={[
|
||||||
|
{ title: '事件', dataIndex: 'event', width: 220, render: (v) => <Text code>{v}</Text> },
|
||||||
|
{ title: '触发时机', dataIndex: 'trigger', width: 220 },
|
||||||
|
{ title: '说明', dataIndex: 'desc' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
<Card className="api-docs__card" size="small" title="推送请求示例">
|
||||||
|
<Descriptions size="small" column={1} bordered style={{ marginBottom: 12 }}>
|
||||||
|
<Descriptions.Item label="URL">
|
||||||
|
<Text code>POST https://你的服务器/callback</Text>
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="Content-Type">
|
||||||
|
<Text code>application/json</Text>
|
||||||
|
</Descriptions.Item>
|
||||||
|
</Descriptions>
|
||||||
|
<pre className="api-docs__pre">{`POST /callback HTTP/1.1
|
||||||
|
Content-Type: application/json
|
||||||
|
X-Event-ID: 4751626d-d608-42c1-a453-60ffcafc01fc
|
||||||
|
X-Timestamp: 1785723081
|
||||||
|
X-Sign: 191892922db64bf41159f58e...`}</pre>
|
||||||
|
<pre className="api-docs__pre">{callbackPayloadExample}</pre>
|
||||||
|
</Card>
|
||||||
|
<Card className="api-docs__card" size="small" title="接收方要求">
|
||||||
|
<ul style={{ margin: 0, paddingLeft: 20 }}>
|
||||||
|
<li>返回 <Text code>2xx</Text>(任意状态码)即视为投递成功;返回其他状态码或超时视为失败并进入重试。</li>
|
||||||
|
<li>同一事件的 <Text code>X-Event-ID</Text> 全局唯一,请按它做幂等处理,避免重复入账。</li>
|
||||||
|
<li>事件里的 <Text code>occurred_at</Text> 是事件发生时间,与请求头 <Text code>X-Timestamp</Text> 不同。</li>
|
||||||
|
</ul>
|
||||||
|
</Card>
|
||||||
|
</SectionWrap>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CallbackSignSection() {
|
||||||
|
return (
|
||||||
|
<SectionWrap id="callback-sign" title="签名校验">
|
||||||
|
<Card className="api-docs__card" size="small" title="请求头">
|
||||||
|
<Table
|
||||||
|
className="api-docs__table"
|
||||||
|
size="small"
|
||||||
|
pagination={false}
|
||||||
|
rowKey="header"
|
||||||
|
dataSource={[
|
||||||
|
{ header: 'X-Event-ID', desc: '事件唯一 ID,接收方用于幂等' },
|
||||||
|
{ header: 'X-Timestamp', desc: '推送时的 Unix 秒时间戳' },
|
||||||
|
{ header: 'X-Sign', desc: 'HMAC-SHA256 签名,见下方算法' },
|
||||||
|
]}
|
||||||
|
columns={[
|
||||||
|
{ title: 'Header', dataIndex: 'header', width: 160, render: (v) => <Text code>{v}</Text> },
|
||||||
|
{ title: '说明', dataIndex: 'desc' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
<Card className="api-docs__card" size="small" title="签名算法">
|
||||||
|
<Descriptions size="small" column={1} bordered>
|
||||||
|
<Descriptions.Item label="密钥">
|
||||||
|
<Text code>CallbackSecret</Text>(保存回调配置时展示一次,可在后台「重置密钥」)
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="body_hash">
|
||||||
|
<Text code>sha256hex(body)</Text> —— 原始请求体字节的 SHA256 十六进制(小写)
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="签名串">
|
||||||
|
<Text code>timestamp + "\n" + sha256hex(body)</Text>,换行符拼接
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="X-Sign">
|
||||||
|
<Text code>hex( HMAC-SHA256( secret, 签名串 ) )</Text>,小写十六进制
|
||||||
|
</Descriptions.Item>
|
||||||
|
</Descriptions>
|
||||||
|
</Card>
|
||||||
|
<Card className="api-docs__card" size="small" title="校验伪代码">
|
||||||
|
<pre className="api-docs__pre">{`bodyHash = sha256Hex(rawBody)
|
||||||
|
content = timestamp + "\\n" + bodyHash
|
||||||
|
expected = hmacSHA256Hex(secret, content)
|
||||||
|
ok = (X-Sign == expected)`}</pre>
|
||||||
|
</Card>
|
||||||
|
<Alert
|
||||||
|
className="api-docs__alert"
|
||||||
|
type="warning"
|
||||||
|
showIcon
|
||||||
|
message="注意"
|
||||||
|
description={
|
||||||
|
<ul style={{ margin: 0, paddingLeft: 20 }}>
|
||||||
|
<li>校验用 <Text code>原始 body 字节</Text>,不要重新序列化后再算摘要。</li>
|
||||||
|
<li>建议校验 <Text code>X-Timestamp</Text> 与当前时间偏差(如 ±5 分钟)后验签。</li>
|
||||||
|
<li>若在后台重置密钥,旧密钥立即失效,新密钥签名推送。</li>
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</SectionWrap>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CallbackRetrySection() {
|
||||||
|
return (
|
||||||
|
<SectionWrap
|
||||||
|
id="callback-retry"
|
||||||
|
title="推送与重试"
|
||||||
|
desc={
|
||||||
|
<>
|
||||||
|
回调基于数据库 outbox 持久化推送(每 3 秒轮询一批),失败后按固定间隔退避重试,
|
||||||
|
参考微信/支付宝通知机制。重试次数与间隔可通过环境变量调整:
|
||||||
|
<Text code>CALLBACK_MAX_ATTEMPTS</Text>(默认 16)、
|
||||||
|
<Text code>CALLBACK_RETRY_SCHEDULE</Text>(逗号分隔秒数)。
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Card className="api-docs__card" size="small" title="重试策略(默认)">
|
||||||
|
<Table
|
||||||
|
className="api-docs__table"
|
||||||
|
size="small"
|
||||||
|
pagination={false}
|
||||||
|
rowKey="attempt"
|
||||||
|
dataSource={retrySchedule}
|
||||||
|
columns={[
|
||||||
|
{ title: '第 N 次推送', dataIndex: 'attempt', width: 140, render: (v) => <Text code>{v}</Text> },
|
||||||
|
{ title: '距上次失败间隔', dataIndex: 'after' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
<Alert
|
||||||
|
className="api-docs__alert"
|
||||||
|
type="info"
|
||||||
|
showIcon
|
||||||
|
message="规则"
|
||||||
|
description={
|
||||||
|
<ul style={{ margin: 0, paddingLeft: 20 }}>
|
||||||
|
<li>共尝试 <Text code>16</Text> 次(首次 + 15 次重试),全部失败后标记 <Text code>failed</Text>,不再推送。</li>
|
||||||
|
<li>单次推送超时 <Text code>15 秒</Text>(可配 <Text code>CALLBACK_PUSH_TIMEOUT_SECONDS</Text>)按失败处理。</li>
|
||||||
|
<li>接收方返回任意 <Text code>2xx</Text> 即停止重试。</li>
|
||||||
|
<li>未配置回调地址或订阅未匹配事件时不会产生推送。</li>
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</SectionWrap>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function StatusSection() { return (
|
||||||
<SectionWrap id="status" title="状态说明">
|
<SectionWrap id="status" title="状态说明">
|
||||||
<Card className="api-docs__card" size="small" title="订单状态 order_status 与 can_ship">
|
<Card className="api-docs__card" size="small" title="订单状态 order_status 与 can_ship">
|
||||||
<Table
|
<Table
|
||||||
|
|||||||
Reference in New Issue
Block a user