Files
affiliate_dash/frontend/src/pages/OpenApiDocs.tsx
T
2026-07-31 12:29:43 +08:00

316 lines
12 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',
}
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',
},
]
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> 仍是上游回调链路,和本页的商户 API 分开。
</Paragraph>
<Tabs
tabPosition="left"
style={{ minHeight: 680 }}
items={[
{
key: 'overview',
label: '概览',
children: <OverviewTab />,
},
{
key: 'auth',
label: '鉴权与签名',
children: <AuthTab />,
},
{
key: 'endpoints',
label: '接口列表',
children: <EndpointsTab />,
},
{
key: 'delivery',
label: '发货模式',
children: <DeliveryModeTab />,
},
{
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>拿到 <Text code>order_no</Text> 后,可选择返回发货链接,或者用结构化发货接口完成自建页面流程。</li>
</ol>
}
/>
<Card size="small" title="发货链路概览">
<Table
size="small"
pagination={false}
rowKey="mode"
dataSource={deliveryModes}
columns={[
{ title: '模式', dataIndex: 'mode', width: 140 },
{ title: '适用场景', dataIndex: 'scene' },
{
title: '相关接口',
dataIndex: 'interfaces',
render: (v: string) => (
<pre style={preStyle}>{v}</pre>
),
},
]}
/>
</Card>
<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 DeliveryModeTab() {
return (
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
<Card size="small" title="推荐用法">
<Descriptions size="small" column={1} bordered>
<Descriptions.Item label="只想给用户一个链接">
用 <Text code>delivery-link</Text>,后端直接返回签名后的 Web 发货页地址,前端只负责跳转或复制。
</Descriptions.Item>
<Descriptions.Item label="想自建发货页面">
用 <Text code>delivery</Text> 获取订单数据,再按需调用 <Text code>bind</Text> 和 <Text code>submit</Text>。
</Descriptions.Item>
<Descriptions.Item label="取消作废">
取消作废属于商户后台操作,恢复后会重新签发新有效期和新签名。
</Descriptions.Item>
</Descriptions>
</Card>
<Alert
type="info"
showIcon
message="两种模式共用同一套底层履约逻辑"
description="不管是 Web 发货页还是商户自建页面,最终都会走同一套订单校验、绑定和提交逻辑,只是前端形态不同。"
/>
</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>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 size="small" title="签名示例(GET,body 为空)">
<pre style={preStyle}>{`app_key=ak_xxx&body_sha256=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855&method=GET&nonce=a1b2c3d4e5f67890&path=/api/client/v1/products&timestamp=1721450000`}</pre>
</Card>
<Card size="small" title="签名示例(POST,body 非空)">
<pre style={preStyle}>{`# 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
type="warning"
showIcon
message="签名注意事项"
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 多了 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)
↓
拿响应里的 order_no 进入发货平台链接
↓
发货平台查询订单并执行发货
↓
发货平台通过上游回调更新 fulfillment_status
↓
GET /orders/{order_no} 轮询,或接收 order.fulfillment.updated 回调`}</pre>
}
/>
</Space>
)
}