import { useCallback, useEffect, useMemo, useState } from 'react' import type { ReactNode } from 'react' import { Alert, Card, Descriptions, Table, Tag, Typography } from 'antd' import EndpointDoc, { CommonResponseDoc } from '../openapi/EndpointDoc' import { commonResponseFields, endpoints, errorCodes, orderStatusTable, paymentStatusTable, } 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: 'Web 发货页', scene: '商户想要“只返回一个链接”,让用户直接打开平台页面填写 UID', interfaces: '/api/client/v1/orders/{order_no}/delivery-link', }, { 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', }, ] // ---------- 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[] { return [ { title: '开始', items: [ { id: 'overview', label: '概述' }, { id: 'auth', label: '鉴权与签名' }, ], }, { title: '接口', items: endpoints.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, })), })), }, { 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 (
{/* 左侧目录 */} {/* 右侧文档 */}
{activePageId === 'overview' ? : null} {activePageId === 'auth' ? : null} {activeEndpoint ? : null} {activePageId === 'status' ? : null}
) } 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 (
) } // ---------- 各文档小节 ---------- function SectionWrap({ id, title, desc, children, }: { id: string title: string desc?: ReactNode children?: ReactNode }) { return (

{title}

{desc ? {desc} : null} {children}
) } function OverviewSection() { return ( 面向商户系统调用的开放 API。凭证在「开放 API / API 密钥」创建, 采用 X-App-Key + HMAC-SHA256 签名鉴权。 发货链路同时支持「返回签名链接」和「返回结构化数据」两种模式。 /api/open/v1 仍是上游回调链路,和本页的商户 API 分开。 } >
  • 在「开放 API / API 密钥」创建凭证,获得 AppKeyAppSecret
  • 每次请求携带 X-App-Key / X-Timestamp / X-Nonce / X-Sign 四个鉴权头。
  • 调用「商品列表」拿到可售 sku,调用「下单」创建订单并扣款。
  • 拿到 order_no 后,可选择返回发货链接,或者用结构化发货接口完成自建页面流程。
  • } />
    {v}
    , }, ]} /> {baseUrl} X-App-KeyX-TimestampX-NonceX-Sign 允许 ±300 秒 所有时间字段统一返回 RFC3339 秒级北京时间,例如 2026-07-30T17:53:58+08:00 POST 请求使用 application/json
    ) } function AuthSection() { return (
    {v} }, { title: '必填', dataIndex: 'required', width: 80, render: (v) => v ? : , }, { title: '说明', dataIndex: 'desc' }, ]} /> app_keybody_sha256methodnoncepathtimestamp 参数按 ASCII 字典序,用 & 拼成 k1=v1&k2=v2&... app_key, body_sha256, method, nonce, path, timestamp 大写,如 GET / POST 仅 URL.Path,不含域名和 query SHA256(原始 body 字节) 的十六进制小写;GET 用空 body 的摘要 原样拼接,不做 URL encode hex( HMAC-SHA256( app_secret, 签名串 ) ),小写十六进制
    {`app_key=ak_xxx&body_sha256=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855&method=GET&nonce=a1b2c3d4e5f67890&path=/api/client/v1/products×tamp=1721450000`}
    {`# 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×tamp=1721450000`}
  • POST 签名用的 body 必须与实际发送的 body 字节级一致,不要签名后再改空格或字段顺序。
  • value 不要 URL encode,原样参与拼接。
  • 签名失败常见原因:secret 错、path 多了 query、body 与签名不一致、时间戳过期、nonce 重复、参数未按字典序拼接。
  • 下单接口还需携带 Idempotency-Key,且必须与 client_order_no 一致。
  • } /> ) } function EndpointSection({ spec }: { spec: EndpointSpec }) { return (

    {spec.title}

    {spec.method} {spec.scope}
    ) } function StatusSection() { return (
    {v} }, { title: 'can_fulfill', dataIndex: 'example', width: 160, render: (v: string) => v === 'can_fulfill=true' ? true : false, }, { title: '说明', dataIndex: 'desc' }, ]} />
    {v} }, { title: '说明', dataIndex: 'desc' }, ]} /> {`拿到 sku(商品列表) ↓ POST /orders 下单(幂等,Idempotency-Key = client_order_no) ↓ 拿响应里的 order_no 进入发货平台链接 ↓ 发货平台查询订单并执行发货 ↓ 发货平台通过上游回调更新 fulfillment_status ↓ GET /orders/{order_no} 轮询,或接收 order.fulfillment.updated 回调`} } /> ) }