优化api文档

This commit is contained in:
yml2213
2026-07-30 15:44:08 +08:00
parent beb171b751
commit 4165fc4d01
4 changed files with 672 additions and 182 deletions
+211 -182
View File
@@ -1,5 +1,13 @@
import type { CSSProperties } from 'react'
import { Alert, Card, Descriptions, Space, Table, Tabs, Tag, Typography } from 'antd'
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
@@ -8,33 +16,17 @@ const baseUrl =
? window.location.origin.replace(':5173', ':8080')
: 'http://localhost:8080'
const signPython = `import hashlib, hmac, json, time, uuid, requests
APP_KEY = "ak_xxx"
APP_SECRET = "sk_xxx"
BASE = "${baseUrl}"
def sign_headers(method: str, path: str, body: bytes = b"") -> dict:
ts = str(int(time.time()))
nonce = uuid.uuid4().hex
body_hash = hashlib.sha256(body).hexdigest()
raw = "\\n".join([ts, nonce, method.upper(), path, body_hash])
sign = hmac.new(APP_SECRET.encode(), raw.encode(), hashlib.sha256).hexdigest()
return {
"X-App-Key": APP_KEY,
"X-Timestamp": ts,
"X-Nonce": nonce,
"X-Sign": sign,
}
path = "/api/client/v1/products"
print(requests.get(BASE + path, headers=sign_headers("GET", path)).json())
path = "/api/client/v1/orders"
payload = {"client_order_no": "shop-10001", "sku": "sku-basic", "quantity": 1}
body = json.dumps(payload, separators=(",", ":"), ensure_ascii=False).encode()
headers = {"Content-Type": "application/json", "Idempotency-Key": payload["client_order_no"], **sign_headers("POST", path, body)}
print(requests.post(BASE + path, headers=headers, data=body).json())`
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 (
@@ -43,163 +35,32 @@ export default function OpenApiDocs() {
</Title>
<Paragraph type="secondary">
/ API
<Text code>/api/open/v1</Text>
API / API
<Text code>X-App-Key</Text> + HMAC-SHA256
<Text code>/api/open/v1</Text>
</Paragraph>
<Alert
type="info"
showIcon
style={{ marginBottom: 16 }}
message="v1 鉴权头:X-App-Key / X-Timestamp / X-Nonce / X-Sign"
description={
<Space direction="vertical" size={4}>
<Text>
<Text code>timestamp\nnonce\nMETHOD\npath\nsha256(body)</Text>
</Text>
<Text>
<Text code>X-Api-Key</Text> 使使{' '}
<Text code>X-App-Key</Text>
</Text>
</Space>
}
/>
<Tabs
items={[
{
key: 'overview',
label: '概览',
children: <OverviewTab />,
},
{
key: 'auth',
label: '签名',
children: (
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
<Card size="small" title="签名规则">
<Descriptions size="small" column={1} bordered>
<Descriptions.Item label="参与字段">
timestampnonceMETHODpathsha256(body)
</Descriptions.Item>
<Descriptions.Item label="拼接方式"></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">hex(HMAC-SHA256(app_secret, raw)) </Descriptions.Item>
<Descriptions.Item label="Nonce">8~96 </Descriptions.Item>
</Descriptions>
<pre style={{ ...preStyle, marginTop: 12 }}>{`timestamp
nonce
POST
/api/open/v1/orders
sha256(body)`}</pre>
</Card>
<Card size="small" title="Python 示例">
<pre style={preStyle}>{signPython}</pre>
</Card>
</Space>
),
label: '鉴权与签名',
children: <AuthTab />,
},
{
key: 'endpoints',
label: '接口',
children: (
<Card size="small">
<Table
size="small"
pagination={false}
rowKey="path"
dataSource={[
{ method: 'GET', path: '/api/client/v1/products', scope: 'products:read', desc: '查询已授权商品' },
{ method: 'POST', path: '/api/client/v1/orders', scope: 'orders:write', desc: '幂等创建订单并扣款' },
{ method: 'GET', path: '/api/client/v1/orders/{order_no}', scope: 'orders:read', desc: '查询订单状态' },
{ method: 'POST', path: '/api/client/v1/orders/{order_no}/cancel', scope: 'orders:write', desc: '取消未履约订单并退款' },
{ method: 'POST', path: '/api/client/v1/orders/{order_no}/ship-notify', scope: 'fulfillment:write', desc: '履约器回传 processing/success/failed' },
{ method: 'GET', path: '/api/client/v1/wallet', scope: 'wallet:read', desc: '查询商户钱包' },
]}
columns={[
{ title: '方法', dataIndex: 'method', width: 90, render: (v) => <Tag color={v === 'GET' ? 'blue' : 'green'}>{v}</Tag> },
{ title: '路径', dataIndex: 'path', render: (v) => <Text code>{v}</Text> },
{ title: '权限', dataIndex: 'scope', width: 150, render: (v) => <Text code>{v}</Text> },
{ title: '说明', dataIndex: 'desc' },
]}
/>
</Card>
),
},
{
key: 'order',
label: '下单',
children: (
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
<Card size="small" title="请求">
<Paragraph>
<Tag color="green">POST</Tag>
<Text code>/api/client/v1/orders</Text>
</Paragraph>
<pre style={preStyle}>{`{
"client_order_no": "shop-10001",
"sku": "sku-basic",
"quantity": 1,
"buyer_reference": "buyer-or-account",
"data": {
"server": "ios-wechat",
"uid": "player-id"
}
}`}</pre>
</Card>
<Card size="small" title="响应">
<pre style={preStyle}>{`{
"code": 0,
"message": "ok",
"data": {
"idempotent": false,
"order": {
"order_no": "FO202607300001...",
"client_order_no": "shop-10001",
"payment_status": "paid",
"fulfillment_status": "pending",
"can_fulfill": true
}
}
}`}</pre>
</Card>
</Space>
),
label: '接口列表',
children: <EndpointsTab />,
},
{
key: 'status',
label: '状态',
children: (
<Card size="small">
<Table
size="small"
pagination={false}
rowKey="status"
dataSource={[
{ status: 'pending', can: 'true', desc: '待履约,可被履约器接单' },
{ status: 'processing', can: 'false', desc: '履约中' },
{ status: 'succeeded', can: 'false', desc: '履约成功' },
{ status: 'failed', can: 'true', desc: '履约失败,可重试' },
{ status: 'cancelled', can: 'false', desc: '已取消' },
]}
columns={[
{ title: 'fulfillment_status', dataIndex: 'status', render: (v) => <Text code>{v}</Text> },
{ title: 'can_fulfill', dataIndex: 'can', width: 120, render: (v) => v === 'true' ? <Tag color="green">true</Tag> : <Tag>false</Tag> },
{ title: '说明', dataIndex: 'desc' },
]}
/>
</Card>
),
},
{
key: 'callback',
label: '回调',
children: (
<Card size="small">
<Descriptions size="small" column={1} bordered>
<Descriptions.Item label="事件">order.created / order.fulfillment.updated / order.cancelled</Descriptions.Item>
<Descriptions.Item label="Header">X-Event-IDX-TimestampX-Sign</Descriptions.Item>
<Descriptions.Item label="签名">hex(HMAC-SHA256(callback_secret, timestamp + "\n" + sha256(body)))</Descriptions.Item>
<Descriptions.Item label="投递"> outbox退</Descriptions.Item>
</Descriptions>
</Card>
),
label: '状态说明',
children: <StatusTab />,
},
]}
/>
@@ -207,13 +68,181 @@ sha256(body)`}</pre>
)
}
const preStyle: CSSProperties = {
margin: 0,
padding: 12,
background: '#f5f5f5',
borderRadius: 6,
fontSize: 12,
overflow: 'auto',
whiteSpace: 'pre-wrap',
wordBreak: 'break-all',
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="签名示例(GETbody 为空)">
<pre style={preStyle}>{`timestamp
nonce
GET
/api/client/v1/products
sha256("") = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`}</pre>
</Card>
<Card size="small" title="签名示例(POSTbody 非空)">
<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 querybody 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>
)
}