From 3e8526d5d7e7d26781d2edddf5a24a9c6dc53c31 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Mon, 3 Aug 2026 11:33:36 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AF=B9=E6=8E=A5=E6=96=87=E6=A1=A3=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E5=9B=9E=E8=B0=83=E9=80=9A=E7=9F=A5=E7=AB=A0=E8=8A=82?= =?UTF-8?q?=EF=BC=88=E4=BA=8B=E4=BB=B6/=E7=AD=BE=E5=90=8D/=E6=8E=A8?= =?UTF-8?q?=E9=80=81=E9=87=8D=E8=AF=95=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 回调文档三个区块:回调通知(事件列表、推送示例、接收方要求)、签名校验(算法与伪代码)、推送与重试(微信式 16 次退避序列) - 概述一句话上手补充回调配置步骤 --- frontend/src/pages/OpenApiDocs.tsx | 242 ++++++++++++++++++++++++++++- 1 file changed, 241 insertions(+), 1 deletion(-) diff --git a/frontend/src/pages/OpenApiDocs.tsx b/frontend/src/pages/OpenApiDocs.tsx index 94e2e43..e3bd85c 100644 --- a/frontend/src/pages/OpenApiDocs.tsx +++ b/frontend/src/pages/OpenApiDocs.tsx @@ -68,6 +68,14 @@ function buildToc(): TocGroup[] { })), })), }, + { + title: '回调', + items: [ + { id: 'callback', label: '回调通知' }, + { id: 'callback-sign', label: '签名校验' }, + { id: 'callback-retry', label: '推送与重试' }, + ], + }, { title: '附录', items: [{ id: 'status', label: '状态说明' }], @@ -215,6 +223,9 @@ export default function OpenApiDocs() {
{activePageId === 'overview' ? : null} {activePageId === 'auth' ? : null} + {activePageId === 'callback' ? : null} + {activePageId === 'callback-sign' ? : null} + {activePageId === 'callback-retry' ? : null} {activeEndpoint ? : null} {activePageId === 'status' ? : null} 每次请求携带 X-App-Key / X-Timestamp / X-Nonce / X-Sign 四个鉴权头。
  • 调用「商品列表」拿到可售 sku,调用「下单」创建订单并扣款。
  • 拿到 order_no 后,可选择返回发货链接,或者用结构化发货接口完成自建页面流程。
  • +
  • 在「回调」页配置回调地址并订阅事件,订单状态变化会实时推送,见「回调通知」。
  • } /> @@ -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 ( + + 平台通过 POST 方式把订单事件推送到商户配置的回调地址。 + 商户在后台「回调」页配置一个 URL 并订阅事件,配置保存在 outbox 表中, + 订单事务提交后才写入待推送记录,进程重启不会丢失。 + + } + > + +
  • 在商户后台「回调」页填写回调 URL 并订阅事件,保存后获得 CallbackSecret(仅展示一次,可在后台重置)。
  • +
  • 接收平台推送:请求头带 X-Event-ID / X-Timestamp / X-Sign,按「签名校验」验证后处理。
  • +
  • 返回 HTTP 2xx 即视为投递成功;非 2xx 或超时按「推送与重试」策略重试。
  • + + } + /> + + {v} }, + { title: '触发时机', dataIndex: 'trigger', width: 220 }, + { title: '说明', dataIndex: 'desc' }, + ]} + /> + + + + + POST https://你的服务器/callback + + + application/json + + +
    {`POST /callback HTTP/1.1
    +Content-Type: application/json
    +X-Event-ID: 4751626d-d608-42c1-a453-60ffcafc01fc
    +X-Timestamp: 1785723081
    +X-Sign: 191892922db64bf41159f58e...`}
    +
    {callbackPayloadExample}
    +
    + +
      +
    • 返回 2xx(任意状态码)即视为投递成功;返回其他状态码或超时视为失败并进入重试。
    • +
    • 同一事件的 X-Event-ID 全局唯一,请按它做幂等处理,避免重复入账。
    • +
    • 事件里的 occurred_at 是事件发生时间,与请求头 X-Timestamp 不同。
    • +
    +
    + + ) +} + +function CallbackSignSection() { + return ( + + +
    {v} }, + { title: '说明', dataIndex: 'desc' }, + ]} + /> + + + + + CallbackSecret(保存回调配置时展示一次,可在后台「重置密钥」) + + + sha256hex(body) —— 原始请求体字节的 SHA256 十六进制(小写) + + + timestamp + "\n" + sha256hex(body),换行符拼接 + + + hex( HMAC-SHA256( secret, 签名串 ) ),小写十六进制 + + + + +
    {`bodyHash = sha256Hex(rawBody)
    +content = timestamp + "\\n" + bodyHash
    +expected = hmacSHA256Hex(secret, content)
    +ok = (X-Sign == expected)`}
    +
    + +
  • 校验用 原始 body 字节,不要重新序列化后再算摘要。
  • +
  • 建议校验 X-Timestamp 与当前时间偏差(如 ±5 分钟)后验签。
  • +
  • 若在后台重置密钥,旧密钥立即失效,新密钥签名推送。
  • + + } + /> + + ) +} + +function CallbackRetrySection() { + return ( + + 回调基于数据库 outbox 持久化推送(每 3 秒轮询一批),失败后按固定间隔退避重试, + 参考微信/支付宝通知机制。重试次数与间隔可通过环境变量调整: + CALLBACK_MAX_ATTEMPTS(默认 16)、 + CALLBACK_RETRY_SCHEDULE(逗号分隔秒数)。 + + } + > + +
    {v} }, + { title: '距上次失败间隔', dataIndex: 'after' }, + ]} + /> + + +
  • 共尝试 16 次(首次 + 15 次重试),全部失败后标记 failed,不再推送。
  • +
  • 单次推送超时 15 秒(可配 CALLBACK_PUSH_TIMEOUT_SECONDS)按失败处理。
  • +
  • 接收方返回任意 2xx 即停止重试。
  • +
  • 未配置回调地址或订阅未匹配事件时不会产生推送。
  • + + } + /> + + ) +} + +function StatusSection() { return (