Files
affiliate_dash/frontend/src/pages/OpenApiDocs.tsx
T

764 lines
28 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useCallback, useEffect, useMemo, useState } from 'react'
import type { ReactNode } from 'react'
import { Alert, Card, Descriptions, Table, Tag, Typography } from 'antd'
import { BookOutlined } from '@ant-design/icons'
import EndpointDoc, { CommonResponseDoc } from '../openapi/EndpointDoc'
import {
commonResponseFields,
endpoints,
errorCodes,
orderStatusTable,
} from '../openapi/endpoints'
import { endpointSections, type EndpointSpec } from '../openapi/types'
const { Paragraph, Text } = Typography
const baseUrl =
typeof window !== 'undefined'
? window.location.origin.replace(':5173', ':8080')
: 'http://localhost:8080'
const deliveryModes = [
{
mode: '发货链接模式',
scene: '实现最简:下单后拿一个签名链接,直接跳转平台发货页,由买家在页面上绑定账号并提交',
interfaces: '/api/client/v1/orders/{order_no}/delivery-link',
steps: '下单 → 获取发货链接 → 链接给买家 → 平台页完成发货',
recommend: '商户无自建发货页、想最快上线时使用',
},
{
mode: '自建发货页模式',
scene: '商户自己有前端页面,把发货流程嵌在自己的系统里(可预填下单时透传的账号/区服)',
interfaces: '/api/client/v1/orders/{order_no}/delivery\n/api/client/v1/orders/{order_no}/delivery/bind\n/api/client/v1/orders/{order_no}/delivery/submit',
steps: '下单 → 查询发货数据 → 发起绑定 → 提交发货',
recommend: '商户需要自定义发货交互(展示区服、预填账号)时使用',
},
]
// ---------- TOC 目录构建 ----------
interface TocLeaf {
id: string
label: string
level: 3 // 接口子小节(接口地址/请求方式/...)
}
interface TocItem {
id: string
label: string
children?: TocLeaf[]
}
interface TocGroup {
title: string
items: TocItem[]
}
function buildToc(): TocGroup[] {
const tocItems = (eps: typeof endpoints) =>
eps.map((ep) => ({
id: `ep-${ep.key}`,
label: ep.title,
children: endpointSections.map((s) => ({
id: `ep-${ep.key}-${s.id}`,
label: s.label,
level: 3 as const,
})),
}))
return [
{
title: '开始',
items: [
{ id: 'overview', label: '概述' },
{ id: 'auth', label: '鉴权与签名' },
],
},
{
title: '基础接口',
items: tocItems(endpoints.filter((ep) => ep.group === 'basic')),
},
{
title: '发货链接模式',
items: tocItems(endpoints.filter((ep) => ep.group === 'delivery-link')),
},
{
title: '自建发货页模式',
items: tocItems(endpoints.filter((ep) => ep.group === 'delivery-self')),
},
{
title: '回调',
items: [
{ id: 'callback', label: '回调通知' },
{ id: 'callback-sign', label: '签名校验' },
{ id: 'callback-retry', label: '推送与重试' },
],
},
{
title: '附录',
items: [{ id: 'status', label: '状态说明' }],
},
]
}
const toc = buildToc()
const pageItems = toc.flatMap((group) => group.items)
const pageIds = new Set(pageItems.map((item) => item.id))
function findPageId(id: string): string {
if (pageIds.has(id)) return id
for (const item of pageItems) {
if (item.children?.some((child) => child.id === id)) return item.id
}
return 'overview'
}
function getInitialHash() {
if (typeof window === 'undefined') return 'overview'
return decodeURIComponent(window.location.hash.replace(/^#/, '')) || 'overview'
}
// ---------- 锚点跳转 ----------
function scrollToId(id: string) {
const el = document.getElementById(id)
if (!el) return
const root = document.getElementById('docScroll')
if (!root) return
const top = root.scrollTop + el.getBoundingClientRect().top - root.getBoundingClientRect().top - 22
root.scrollTo({ top, behavior: 'smooth' })
}
export default function OpenApiDocs() {
const initialHash = useMemo(() => getInitialHash(), [])
const [activePageId, setActivePageId] = useState(() => findPageId(initialHash))
const [activeAnchorId, setActiveAnchorId] = useState(initialHash)
const activeEndpoint = useMemo(
() => endpoints.find((spec) => `ep-${spec.key}` === activePageId),
[activePageId],
)
useEffect(() => {
const syncHash = () => {
const nextHash = getInitialHash()
setActivePageId(findPageId(nextHash))
setActiveAnchorId(nextHash)
}
window.addEventListener('hashchange', syncHash)
return () => window.removeEventListener('hashchange', syncHash)
}, [])
useEffect(() => {
const root = document.getElementById('docScroll')
if (!root) return
if (activeAnchorId !== activePageId) {
requestAnimationFrame(() => scrollToId(activeAnchorId))
return
}
root.scrollTo({ top: 0 })
}, [activeAnchorId, activePageId])
const activeIndex = useMemo(
() => pageItems.findIndex((item) => item.id === activePageId),
[activePageId],
)
const handleNav = useCallback((id: string) => {
const pageId = findPageId(id)
setActivePageId(pageId)
setActiveAnchorId(id)
if (typeof window !== 'undefined') {
window.history.replaceState(null, '', `${window.location.pathname}${window.location.search}#${id}`)
}
}, [])
const handleStep = useCallback(
(direction: -1 | 1) => {
const next = pageItems[activeIndex + direction]
if (next) handleNav(next.id)
},
[activeIndex, handleNav],
)
return (
<div className="api-docs">
{/* 左侧目录 */}
<aside className="api-docs__toc">
<div className="api-docs__toc-header">
<BookOutlined style={{ color: '#2563eb', fontSize: 16 }} />
<span>文档章节目录</span>
</div>
<nav className="api-docs__nav">
{toc.map((group) => (
<div className="api-docs__nav-group" key={group.title}>
<div className="api-docs__nav-heading">{group.title}</div>
{group.items.map((item) => {
const isActive = activePageId === item.id
return (
<div className="api-docs__nav-row" key={item.id}>
<a
className={`api-docs__nav-item${isActive ? ' api-docs__nav-item--active' : ''}`}
onClick={() => handleNav(item.id)}
>
{item.label}
</a>
{item.children?.length && isActive ? (
<div className="api-docs__nav-sub">
{item.children.map((c) => (
<a
key={c.id}
className={`api-docs__nav-sub-item${activeAnchorId === c.id ? ' api-docs__nav-sub-item--active' : ''}`}
onClick={() => handleNav(c.id)}
>
{c.label}
</a>
))}
</div>
) : null}
</div>
)
})}
</div>
))}
</nav>
</aside>
{/* 右侧文档 */}
<div id="docScroll" className="api-docs__content">
<div className="api-docs__inner">
{activePageId === 'overview' ? <OverviewSection /> : null}
{activePageId === 'auth' ? <AuthSection /> : null}
{activePageId === 'callback' ? <CallbackSection /> : null}
{activePageId === 'callback-sign' ? <CallbackSignSection /> : null}
{activePageId === 'callback-retry' ? <CallbackRetrySection /> : null}
{activeEndpoint ? <EndpointSection spec={activeEndpoint} /> : null}
{activePageId === 'status' ? <StatusSection /> : null}
<PageStepper
currentIndex={activeIndex}
items={pageItems}
onStep={handleStep}
/>
</div>
</div>
</div>
)
}
function PageStepper({
currentIndex,
items,
onStep,
}: {
currentIndex: number
items: TocItem[]
onStep: (direction: -1 | 1) => void
}) {
const prev = items[currentIndex - 1]
const next = items[currentIndex + 1]
return (
<div className="api-docs__pager">
<button
className="api-docs__pager-btn"
type="button"
disabled={!prev}
onClick={() => onStep(-1)}
>
<span>上一篇</span>
<strong>{prev?.label ?? '无'}</strong>
</button>
<button
className="api-docs__pager-btn api-docs__pager-btn--next"
type="button"
disabled={!next}
onClick={() => onStep(1)}
>
<span>下一篇</span>
<strong>{next?.label ?? '无'}</strong>
</button>
</div>
)
}
// ---------- 各文档小节 ----------
function SectionWrap({
id,
title,
desc,
children,
}: {
id: string
title: string
desc?: ReactNode
children?: ReactNode
}) {
return (
<div className="api-docs__section">
<h2 id={id} className="api-docs__h2">
{title}
</h2>
{desc ? <Paragraph className="api-docs__summary" type="secondary">{desc}</Paragraph> : null}
{children}
</div>
)
}
function OverviewSection() {
return (
<SectionWrap
id="overview"
title="概述"
desc={
<>
面向商户系统调用的开放 API。凭证在「开放 API / API 密钥」创建,
采用 <Text code>X-App-Key</Text> + HMAC-SHA256 签名鉴权。
发货链路同时支持「返回签名链接」和「返回结构化数据」两种模式。
</>
}
>
<Alert
className="api-docs__alert"
type="info"
showIcon
message="一句话上手"
description={
<ol style={{ margin: 0, paddingLeft: 20 }}>
<li>在「开放 API / API 密钥」创建凭证,获得 <Text code>AppKey</Text> <Text code>AppSecret</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>order_no</Text> 后,按「发货对接方式」二选一:发货链接模式(返回链接即可)或自建发货页模式(结构化接口)。</li>
<li>在「回调」页配置回调地址并订阅事件,订单状态变化会实时推送,见「回调通知」。</li>
</ol>
}
/>
<Card className="api-docs__card" size="small" title="发货对接方式(二选一,先选好再看对应接口)">
<Table
className="api-docs__table"
size="small"
pagination={false}
rowKey="mode"
dataSource={deliveryModes}
scroll={{ x: 1080 }}
columns={[
{
title: '方式',
dataIndex: 'mode',
width: 140,
render: (v: string) => <span style={{ fontWeight: 700, whiteSpace: 'nowrap', color: '#0f172a' }}>{v}</span>,
},
{
title: '适用场景',
dataIndex: 'scene',
width: 280,
render: (v: string) => <span style={{ fontSize: 13, lineHeight: 1.6, display: 'block', color: '#334155' }}>{v}</span>,
},
{
title: '接口顺序',
dataIndex: 'steps',
width: 240,
render: (v: string) => <pre className="api-docs__pre" style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-all' }}>{v}</pre>,
},
{
title: '相关接口',
dataIndex: 'interfaces',
width: 260,
render: (v: string) => <pre className="api-docs__pre" style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-all' }}>{v}</pre>,
},
{
title: '建议',
dataIndex: 'recommend',
width: 180,
render: (v: string) => <span style={{ fontSize: 13, lineHeight: 1.5, display: 'block', color: '#475569' }}>{v}</span>,
},
]}
/>
</Card>
<Card className="api-docs__card" size="small" title="环境信息">
<Descriptions size="small" column={1} bordered>
<Descriptions.Item label="Base URL">
<Text code>{baseUrl}</Text>
</Descriptions.Item>
<Descriptions.Item label="鉴权头">
<Text code>X-App-Key</Text><Text code>X-Timestamp</Text><Text code>X-Nonce</Text><Text code>X-Sign</Text>
</Descriptions.Item>
<Descriptions.Item label="时间偏差">允许 ±300 </Descriptions.Item>
<Descriptions.Item label="时间格式">
所有时间字段统一返回 RFC3339 秒级北京时间,例如 <Text code>2026-07-30T17:53:58+08:00</Text>
</Descriptions.Item>
<Descriptions.Item label="请求体">POST 请求使用 <Text code>application/json</Text></Descriptions.Item>
</Descriptions>
</Card>
<div className="api-docs__common">
<CommonResponseDoc fields={commonResponseFields} errors={errorCodes} />
</div>
</SectionWrap>
)
}
function AuthSection() {
return (
<SectionWrap id="auth" title="鉴权与签名">
<Card className="api-docs__card" size="small" title="鉴权头(每次请求必带)">
<Table
className="api-docs__table"
size="small"
pagination={false}
rowKey="header"
dataSource={[
{ header: 'X-App-Key', required: true, desc: '商户分配的 AppKey' },
{ header: 'X-Timestamp', required: true, desc: '当前 Unix 秒时间戳' },
{ header: 'X-Nonce', required: true, desc: '随机串,8~96 字符,同一客户端有效期内不可重复' },
{ header: 'X-Sign', required: true, desc: 'HMAC-SHA256 签名,见下方算法' },
]}
columns={[
{ title: 'Header', dataIndex: 'header', width: 160, render: (v) => <Text code>{v}</Text> },
{
title: '必填',
dataIndex: 'required',
width: 80,
render: (v) => v ? <span className="api-docs__required"></span> : <Text type="secondary"></Text>,
},
{ title: '说明', dataIndex: 'desc' },
]}
/>
</Card>
<Card className="api-docs__card" size="small" title="签名算法">
<Descriptions size="small" column={1} bordered>
<Descriptions.Item label="参与参数">
<Text code>app_key</Text><Text code>body_sha256</Text><Text code>method</Text><Text code>nonce</Text><Text code>path</Text><Text code>timestamp</Text>
</Descriptions.Item>
<Descriptions.Item label="拼接方式">参数按 ASCII 字典序,用 <Text code>&</Text> 拼成 <Text code>k1=v1&k2=v2&...</Text></Descriptions.Item>
<Descriptions.Item label="排序后顺序">
<Text code>app_key, body_sha256, method, nonce, path, timestamp</Text>
</Descriptions.Item>
<Descriptions.Item label="method">大写,如 GET / POST</Descriptions.Item>
<Descriptions.Item label="path"> URL.Path,不含域名和 query</Descriptions.Item>
<Descriptions.Item label="body_sha256">
<Text code>SHA256(原始 body 字节)</Text> 的十六进制小写;GET 用空 body 的摘要
</Descriptions.Item>
<Descriptions.Item label="value">原样拼接,不做 URL encode</Descriptions.Item>
<Descriptions.Item label="X-Sign">
<Text code>hex( HMAC-SHA256( app_secret, 签名串 ) )</Text>,小写十六进制
</Descriptions.Item>
</Descriptions>
</Card>
<Card className="api-docs__card" size="small" title="签名示例(GETbody 为空)">
<pre className="api-docs__pre">{`app_key=ak_xxx&body_sha256=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855&method=GET&nonce=a1b2c3d4e5f67890&path=/api/client/v1/products&timestamp=1721450000`}</pre>
</Card>
<Card className="api-docs__card" size="small" title="签名示例(POSTbody 非空)">
<pre className="api-docs__pre">{`# body = {"client_order_no":"shop-10001","sku":"suit_pink_sheep"}
app_key=ak_xxx&body_sha256=<实际请求 body 字节的 SHA256 十六进制>&method=POST&nonce=a1b2c3d4e5f67890&path=/api/client/v1/orders&timestamp=1721450000`}</pre>
</Card>
<Alert
className="api-docs__alert"
type="warning"
showIcon
message="签名注意事项"
style={{ marginTop: 16 }}
description={
<ul style={{ margin: 0, paddingLeft: 20 }}>
<li>POST 签名用的 <Text code>body</Text> 必须与实际发送的 body <b>字节级一致</b>,不要签名后再改空格或字段顺序。</li>
<li>value <b>不要</b> URL encode,原样参与拼接。</li>
<li>签名失败常见原因:secret 错、path 多了 querybody 与签名不一致、时间戳过期、nonce 重复、参数未按字典序拼接。</li>
<li>下单幂等直接使用 <Text code>client_order_no</Text>,无需额外 Header</li>
</ul>
}
/>
</SectionWrap>
)
}
function EndpointSection({ spec }: { spec: EndpointSpec }) {
return (
<div className="api-docs__section">
<div className="api-docs__endpoint-head">
<h2 id={`ep-${spec.key}`} className="api-docs__h2">
{spec.title}
</h2>
<div className="api-docs__endpoint-meta">
<Tag color={spec.method === 'GET' ? 'blue' : 'geekblue'}>{spec.method}</Tag>
<Text code>{spec.scope}</Text>
</div>
</div>
<EndpointDoc spec={spec} />
</div>
)
}
// ---------- 回调通知文档 ----------
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",
"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",
"data": { "game_account": "4808146277", "game_channel": "ios-wechat", "role_name": "玩家名" }
}
}`
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 (
<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>body_sha256=&lt;hash&gt;&amp;timestamp=&lt;unix秒&gt;</Text>,按 ASCII 字典序 <Text code>&amp;</Text> 拼接(与开放 API 鉴权同一风格)
</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 = "body_sha256=" + bodyHash + "&timestamp=" + timestamp
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="状态说明">
<Card className="api-docs__card" size="small" title="订单状态 order_status 与 can_ship">
<Table
className="api-docs__table"
size="small"
pagination={false}
rowKey="name"
dataSource={orderStatusTable}
columns={[
{ title: '状态', dataIndex: 'name', width: 140, render: (v) => <Text code>{v}</Text> },
{
title: 'can_ship',
dataIndex: 'example',
width: 160,
render: (v: string) =>
v === 'can_ship=true' ? <Tag color="green">true</Tag> : <Tag>false</Tag>,
},
{ title: '说明', dataIndex: 'desc' },
]}
/>
</Card>
<Alert
className="api-docs__alert"
type="info"
showIcon
message="推荐调用流程"
style={{ marginTop: 16 }}
description={
<pre className="api-docs__pre api-docs__pre--plain">{`拿到 sku(商品列表)
POST /orders 下单(client_order_no 幂等)
拿响应里的 order_no 进入发货链接
买家在发货页绑定账号并提交发货
平台完成发货处理并更新 order_status
GET /orders/{order_no} 轮询,或接收 order.shipping.updated 回调`}</pre>
}
/>
</SectionWrap>
)
}