Files
affiliate_dash/frontend/src/pages/OpenApiDocs.tsx
T
2026-07-30 15:44:08 +08:00

249 lines
8.7 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 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="签名示例(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>
)
}