249 lines
8.7 KiB
TypeScript
249 lines
8.7 KiB
TypeScript
import type { CSSProperties } from 'react'
|
||
import { Alert, Card, Collapse, Descriptions, Space, Table, Tabs, Tag, Typography } from 'antd'
|
||
import EndpointDoc, { CommonResponseDoc } from '../openapi/EndpointDoc'
|
||
import {
|
||
commonResponseFields,
|
||
endpoints,
|
||
errorCodes,
|
||
orderStatusTable,
|
||
paymentStatusTable,
|
||
} from '../openapi/endpoints'
|
||
|
||
const { Title, Paragraph, Text } = Typography
|
||
|
||
const baseUrl =
|
||
typeof window !== 'undefined'
|
||
? window.location.origin.replace(':5173', ':8080')
|
||
: 'http://localhost:8080'
|
||
|
||
const preStyle: CSSProperties = {
|
||
margin: 0,
|
||
padding: 12,
|
||
background: '#f6f8fa',
|
||
borderRadius: 6,
|
||
fontSize: 12.5,
|
||
lineHeight: 1.6,
|
||
overflow: 'auto',
|
||
whiteSpace: 'pre-wrap',
|
||
wordBreak: 'break-all',
|
||
}
|
||
|
||
export default function OpenApiDocs() {
|
||
return (
|
||
<div>
|
||
<Title level={4} style={{ marginTop: 0 }}>
|
||
开放接口
|
||
</Title>
|
||
<Paragraph type="secondary">
|
||
面向商户系统、履约器和外部平台调用的通用开放 API。凭证在「商户中心 / API 客户端」创建,
|
||
采用 <Text code>X-App-Key</Text> + HMAC-SHA256 签名鉴权。
|
||
原 <Text code>/api/open/v1</Text> 保留给上游发货对接,不在本页展示。
|
||
</Paragraph>
|
||
|
||
<Tabs
|
||
items={[
|
||
{
|
||
key: 'overview',
|
||
label: '概览',
|
||
children: <OverviewTab />,
|
||
},
|
||
{
|
||
key: 'auth',
|
||
label: '鉴权与签名',
|
||
children: <AuthTab />,
|
||
},
|
||
{
|
||
key: 'endpoints',
|
||
label: '接口列表',
|
||
children: <EndpointsTab />,
|
||
},
|
||
{
|
||
key: 'status',
|
||
label: '状态说明',
|
||
children: <StatusTab />,
|
||
},
|
||
]}
|
||
/>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
function OverviewTab() {
|
||
return (
|
||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||
<Alert
|
||
type="info"
|
||
showIcon
|
||
message="一句话上手"
|
||
description={
|
||
<ol style={{ margin: 0, paddingLeft: 20 }}>
|
||
<li>在「商户中心 / 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>履约器通过「发货回传」回写 processing/success/failed,状态可由「查询订单」轮询。</li>
|
||
</ol>
|
||
}
|
||
/>
|
||
<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="请求体">POST 请求使用 <Text code>application/json</Text></Descriptions.Item>
|
||
</Descriptions>
|
||
</Card>
|
||
<CommonResponseDoc fields={commonResponseFields} errors={errorCodes} />
|
||
</Space>
|
||
)
|
||
}
|
||
|
||
function AuthTab() {
|
||
return (
|
||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||
<Card size="small" title="鉴权头(每次请求必带)">
|
||
<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 ? <Tag color="red">是</Tag> : <Tag>否</Tag> },
|
||
{ title: '说明', dataIndex: 'desc' },
|
||
]}
|
||
/>
|
||
</Card>
|
||
|
||
<Card size="small" title="签名算法">
|
||
<Descriptions size="small" column={1} bordered>
|
||
<Descriptions.Item label="签名内容">
|
||
<Text code>{'timestamp\nnonce\nMETHOD\npath\nsha256(body)'}</Text>
|
||
</Descriptions.Item>
|
||
<Descriptions.Item label="拼接方式">按固定顺序,用换行符 <Text code>\n</Text> 拼接</Descriptions.Item>
|
||
<Descriptions.Item label="METHOD">大写,如 GET / POST</Descriptions.Item>
|
||
<Descriptions.Item label="path">仅 URL.Path,不含域名和 query</Descriptions.Item>
|
||
<Descriptions.Item label="body">GET 为空字节;POST 必须与实际发送 body 完全一致</Descriptions.Item>
|
||
<Descriptions.Item label="X-Sign">
|
||
<Text code>hex( HMAC-SHA256( app_secret, 签名内容 ) )</Text>,小写十六进制
|
||
</Descriptions.Item>
|
||
</Descriptions>
|
||
</Card>
|
||
|
||
<Card size="small" title="签名示例(GET,body 为空)">
|
||
<pre style={preStyle}>{`timestamp
|
||
nonce
|
||
GET
|
||
/api/client/v1/products
|
||
sha256("") = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`}</pre>
|
||
</Card>
|
||
|
||
<Card size="small" title="签名示例(POST,body 非空)">
|
||
<pre style={preStyle}>{`timestamp
|
||
nonce
|
||
POST
|
||
/api/client/v1/orders
|
||
sha256(body) = <实际请求 body 字节的 SHA256 十六进制>`}</pre>
|
||
</Card>
|
||
|
||
<Alert
|
||
type="warning"
|
||
showIcon
|
||
message="签名注意事项"
|
||
description={
|
||
<ul style={{ margin: 0, paddingLeft: 20 }}>
|
||
<li>POST 签名用的 <Text code>body</Text> 必须与实际发送的 body <b>字节级一致</b>,不要签名后再改空格或字段顺序。</li>
|
||
<li>签名失败常见原因:secret 错、path 多了 query、body 不一致、时间戳过期、nonce 重复。</li>
|
||
<li>下单接口还需携带 <Text code>Idempotency-Key</Text>,且必须与 <Text code>client_order_no</Text> 一致。</li>
|
||
</ul>
|
||
}
|
||
/>
|
||
</Space>
|
||
)
|
||
}
|
||
|
||
function EndpointsTab() {
|
||
return (
|
||
<Collapse
|
||
accordion
|
||
items={endpoints.map((spec) => ({
|
||
key: spec.key,
|
||
label: (
|
||
<Space size="small">
|
||
<Tag color={spec.method === 'GET' ? 'blue' : 'green'} style={{ margin: 0, fontWeight: 700 }}>
|
||
{spec.method}
|
||
</Tag>
|
||
<Text code style={{ fontSize: 13 }}>{spec.path}</Text>
|
||
<Text type="secondary" style={{ fontSize: 12 }}>{spec.summary}</Text>
|
||
</Space>
|
||
),
|
||
children: <EndpointDoc spec={spec} />,
|
||
}))}
|
||
/>
|
||
)
|
||
}
|
||
|
||
function StatusTab() {
|
||
return (
|
||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||
<Card size="small" title="履约状态 fulfillment_status 与 can_fulfill">
|
||
<Table
|
||
size="small"
|
||
pagination={false}
|
||
rowKey="name"
|
||
dataSource={orderStatusTable}
|
||
columns={[
|
||
{ title: '状态', dataIndex: 'name', width: 140, render: (v) => <Text code>{v}</Text> },
|
||
{
|
||
title: 'can_fulfill',
|
||
dataIndex: 'example',
|
||
width: 160,
|
||
render: (v: string) =>
|
||
v === 'can_fulfill=true' ? <Tag color="green">true</Tag> : <Tag>false</Tag>,
|
||
},
|
||
{ title: '说明', dataIndex: 'desc' },
|
||
]}
|
||
/>
|
||
</Card>
|
||
<Card size="small" title="支付状态 payment_status">
|
||
<Table
|
||
size="small"
|
||
pagination={false}
|
||
rowKey="name"
|
||
dataSource={paymentStatusTable}
|
||
columns={[
|
||
{ title: '状态', dataIndex: 'name', width: 140, render: (v) => <Text code>{v}</Text> },
|
||
{ title: '说明', dataIndex: 'desc' },
|
||
]}
|
||
/>
|
||
</Card>
|
||
<Alert
|
||
type="info"
|
||
showIcon
|
||
message="推荐调用流程"
|
||
description={
|
||
<pre style={{ ...preStyle, background: 'transparent', padding: 0 }}>{`拿到 sku(商品列表)
|
||
↓
|
||
POST /orders 下单(幂等,Idempotency-Key = client_order_no)
|
||
↓
|
||
(履约器)POST /orders/{order_no}/ship-notify ship_status=processing
|
||
↓
|
||
按 product.sku 发货
|
||
↓
|
||
POST /orders/{order_no}/ship-notify ship_status=success 或 failed
|
||
↓
|
||
(可选)GET /orders/{order_no} 轮询确认 fulfillment_status=succeeded`}</pre>
|
||
}
|
||
/>
|
||
</Space>
|
||
)
|
||
}
|