优化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
+164
View File
@@ -0,0 +1,164 @@
import type { CSSProperties } from 'react'
import { Alert, Card, Descriptions, Space, Table, Tag, Typography } from 'antd'
import type { ColumnsType } from 'antd/es/table'
import type { EndpointSpec, ParamSpec } from './types'
const { Text, Paragraph } = Typography
const methodColor: Record<string, string> = {
GET: 'blue',
POST: 'green',
PUT: 'orange',
PATCH: 'gold',
DELETE: 'red',
}
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 paramColumns: ColumnsType<ParamSpec> = [
{
title: '字段',
dataIndex: 'name',
width: 200,
render: (v: string) => <Text code style={{ whiteSpace: 'nowrap' }}>{v}</Text>,
},
{
title: '必填',
dataIndex: 'required',
width: 70,
render: (v?: boolean) =>
v ? <Tag color="red"></Tag> : <Tag></Tag>,
},
{
title: '类型',
dataIndex: 'type',
width: 180,
render: (v: string) => <Text type="secondary" style={{ fontSize: 12 }}>{v}</Text>,
},
{ title: '说明', dataIndex: 'desc' },
{
title: '示例',
dataIndex: 'example',
width: 160,
render: (v?: string) =>
v ? <Text code style={{ fontSize: 12 }}>{v}</Text> : null,
},
]
function ParamTable({ title, params }: { title: string; params: ParamSpec[] }) {
return (
<Card size="small" title={title} style={{ width: '100%' }}>
<Table
size="small"
pagination={false}
rowKey="name"
dataSource={params}
columns={paramColumns}
/>
</Card>
)
}
function CodeBlock({ title, code }: { title: string; code: string }) {
return (
<Card size="small" title={title} style={{ width: '100%' }}>
<pre style={preStyle}>{code}</pre>
</Card>
)
}
/** 通用接口渲染:方法+路径+权限 → 参数表 → 请求/响应示例 → 字段说明 → 注意事项。 */
export default function EndpointDoc({ spec }: { spec: EndpointSpec }) {
return (
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
<Card size="small">
<Space wrap size="middle" align="center">
<Tag color={methodColor[spec.method]} style={{ margin: 0, fontSize: 13, fontWeight: 700 }}>
{spec.method}
</Tag>
<Text code copyable style={{ fontSize: 14 }}>
{spec.path}
</Text>
<Tag color="purple">{spec.scope}</Tag>
</Space>
<Paragraph type="secondary" style={{ margin: '8px 0 0' }}>
{spec.summary}
</Paragraph>
</Card>
{spec.pathParams?.length ? <ParamTable title="路径参数" params={spec.pathParams} /> : null}
{spec.queryParams?.length ? <ParamTable title="查询参数" params={spec.queryParams} /> : null}
{spec.bodyParams?.length ? <ParamTable title="请求体参数" params={spec.bodyParams} /> : null}
{spec.requestExample ? <CodeBlock title="请求示例" code={spec.requestExample} /> : null}
<CodeBlock title="响应示例" code={spec.responseExample} />
{spec.responseFields?.length ? (
<ParamTable title="响应字段说明" params={spec.responseFields} />
) : null}
{spec.notes?.length ? (
<Alert
type="warning"
showIcon
message="注意事项"
description={
<ul style={{ margin: 0, paddingLeft: 20 }}>
{spec.notes.map((n, i) => (
<li key={i}>{n}</li>
))}
</ul>
}
/>
) : null}
</Space>
)
}
/** 通用响应说明卡片,供页面顶部展示统一响应格式与错误码。 */
export function CommonResponseDoc({
fields,
errors,
}: {
fields: ParamSpec[]
errors: ParamSpec[]
}) {
return (
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
<Card size="small" title="统一响应格式">
<Descriptions size="small" column={1} bordered>
<Descriptions.Item label="结构">
<Text code>{`{ "code": 0, "message": "ok", "data": {} }`}</Text>
</Descriptions.Item>
{fields.map((f) => (
<Descriptions.Item key={f.name} label={<Text code>{f.name}</Text>}>
{f.desc}
</Descriptions.Item>
))}
</Descriptions>
</Card>
<Card size="small" title="错误码">
<Table
size="small"
pagination={false}
rowKey="name"
dataSource={errors}
columns={[
{ title: 'HTTP/code', dataIndex: 'name', width: 120, render: (v) => <Text code>{v}</Text> },
{ title: '含义', dataIndex: 'desc' },
]}
/>
</Card>
</Space>
)
}
+252
View File
@@ -0,0 +1,252 @@
import type { EndpointSpec, ParamSpec } from './types'
// 通用响应体字段说明,所有接口共用。
export const commonResponseFields: ParamSpec[] = [
{ name: 'code', type: 'int', required: true, desc: '0 表示成功,非 0 表示失败' },
{ name: 'message', type: 'string', required: true, desc: '成功为 "ok",失败为错误描述' },
{ name: 'data', type: 'object', desc: '业务数据,失败时省略' },
]
// 通用错误码。
export const errorCodes: ParamSpec[] = [
{ name: '400', type: 'int', desc: '参数错误 / 业务校验失败(余额不足、库存不足、状态非法等)' },
{ name: '401', type: 'int', desc: '鉴权失败(Key 缺失、签名错误、时间戳过期、Nonce 重复)' },
{ name: '403', type: 'int', desc: '权限不足(缺少所需 scope 或商户功能未开通)' },
{ name: '404', type: 'int', desc: '资源不存在(订单号不存在等)' },
{ name: '500', type: 'int', desc: '服务端内部错误' },
]
// 订单状态与可履约说明(payment_status / fulfillment_status)。
export const orderStatusTable: ParamSpec[] = [
{ name: 'pending', type: 'fulfillment_status', desc: '待履约,可被履约器接单', example: 'can_fulfill=true' },
{ name: 'processing', type: 'fulfillment_status', desc: '履约中', example: 'can_fulfill=false' },
{ name: 'succeeded', type: 'fulfillment_status', desc: '履约成功', example: 'can_fulfill=false' },
{ name: 'failed', type: 'fulfillment_status', desc: '履约失败,可重试', example: 'can_fulfill=true' },
{ name: 'cancelled', type: 'fulfillment_status', desc: '已取消', example: 'can_fulfill=false' },
]
export const paymentStatusTable: ParamSpec[] = [
{ name: 'pending', type: 'payment_status', desc: '待支付' },
{ name: 'paid', type: 'payment_status', desc: '已支付(下单成功即为 paid' },
{ name: 'refunded', type: 'payment_status', desc: '已退款(取消订单后)' },
{ name: 'cancelled', type: 'payment_status', desc: '已取消' },
]
// 订单对象公共字段说明,下单/查询/取消/回传响应共用。
const orderFields: ParamSpec[] = [
{ name: 'order_no', type: 'string', required: true, desc: '平台订单号' },
{ name: 'client_order_no', type: 'string', required: true, desc: '调用方传入的幂等单号' },
{ name: 'payment_status', type: 'enum', desc: '支付状态,见「状态说明」' },
{ name: 'fulfillment_status', type: 'enum', desc: '履约状态,见「状态说明」' },
{ name: 'can_fulfill', type: 'bool', desc: '是否允许履约器接单/发货' },
{ name: 'cannot_fulfill_reason', type: 'string', desc: 'can_fulfill=false 时的原因' },
{ name: 'product.sku', type: 'string', desc: '商品标识,履约以此为准' },
{ name: 'product.name', type: 'string', desc: '商品展示名' },
{ name: 'quantity', type: 'int', desc: '数量' },
{ name: 'base_amount', type: 'int', desc: '商品基础金额(最小货币单位)' },
{ name: 'fee_type', type: 'enum(rate|fixed)', desc: '手续费类型' },
{ name: 'service_fee_amount', type: 'int', desc: '手续费金额' },
{ name: 'amount', type: 'int', desc: '实付总额 = base_amount + service_fee_amount' },
{ name: 'currency', type: 'string', desc: '货币,默认 POINT' },
{ name: 'buyer_reference', type: 'string', desc: '买家标识/备注' },
{ name: 'data', type: 'object', desc: '下单时透传的请求数据(原样回显)' },
{ name: 'result', type: 'object', desc: '履约器回传的结果数据(原样回显)' },
{ name: 'provider_order_no', type: 'string', desc: '履约器侧单号' },
{ name: 'failure_reason', type: 'string', desc: '最近一次失败原因' },
{ name: 'created_at', type: 'datetime', desc: '创建时间' },
{ name: 'delivered_at', type: 'datetime', desc: '履约成功时间' },
{ name: 'cancelled_at', type: 'datetime', desc: '取消时间' },
]
const orderResponseExample = `{
"code": 0,
"message": "ok",
"data": {
"order_no": "FO20260730000123",
"client_order_no": "shop-10001",
"payment_status": "paid",
"fulfillment_status": "pending",
"can_fulfill": true,
"cannot_fulfill_reason": "",
"product": { "sku": "suit_pink_sheep", "name": "套装-糯粉咩咩" },
"quantity": 1,
"base_amount": 100,
"fee_type": "rate",
"service_fee_amount": 5,
"amount": 105,
"currency": "POINT",
"buyer_reference": "buyer-001",
"data": { "server": "ios-wechat", "uid": "player-id" },
"provider_order_no": "",
"failure_reason": "",
"created_at": "2026-07-30T10:00:00+08:00",
"delivered_at": null,
"cancelled_at": null
}
}`
export const endpoints: EndpointSpec[] = [
{
key: 'list-products',
method: 'GET',
path: '/api/client/v1/products',
summary: '查询商户已授权且上架的商品列表',
scope: 'products:read',
queryParams: [
{ name: 'page', type: 'int', desc: '页码,默认 1', example: '1' },
{ name: 'size', type: 'int', desc: '每页条数,默认 20', example: '20' },
],
responseExample: `{
"code": 0,
"message": "ok",
"data": {
"list": [
{
"id": 12,
"sku": "suit_pink_sheep",
"display_name": "套装-糯粉咩咩",
"price_amount": 100,
"cost_amount": 80,
"currency": "POINT",
"stock": -1,
"status": "active",
"product": { "code": "suit_pink_sheep", "name": "套装-糯粉咩咩", "category": "和平精英" }
}
],
"total": 1,
"page": 1,
"size": 20
}
}`,
responseFields: [
{ name: 'list[].sku', type: 'string', desc: '商品标识,下单时作为 sku 传入' },
{ name: 'list[].display_name', type: 'string', desc: '商户自定义展示名' },
{ name: 'list[].price_amount', type: 'int', desc: '单价(最小货币单位)' },
{ name: 'list[].stock', type: 'int', desc: '库存,-1 表示不限' },
{ name: 'list[].status', type: 'enum', desc: '商品状态,active 可售' },
{ name: 'list[].product', type: 'object', desc: '平台商品目录快照' },
{ name: 'total', type: 'int', desc: '总条数' },
{ name: 'page', type: 'int', desc: '当前页码' },
{ name: 'size', type: 'int', desc: '每页条数' },
],
},
{
key: 'create-order',
method: 'POST',
path: '/api/client/v1/orders',
summary: '幂等创建订单并扣款(成功返回 201,重复请求返回 200 且 idempotent=true',
scope: 'orders:write',
bodyParams: [
{ name: 'client_order_no', type: 'string', required: true, desc: '调用方幂等单号,需与 Idempotency-Key 一致', example: 'shop-10001' },
{ name: 'sku', type: 'string', required: true, desc: '商品标识,取自商品列表', example: 'suit_pink_sheep' },
{ name: 'quantity', type: 'int', desc: '数量,默认 1', example: '1' },
{ name: 'buyer_reference', type: 'string', desc: '买家标识/备注', example: 'buyer-001' },
{ name: 'data', type: 'object', desc: '透传给履约器的业务数据(区服、账号等),原样存储并回显' },
],
requestExample: `{
"client_order_no": "shop-10001",
"sku": "suit_pink_sheep",
"quantity": 1,
"buyer_reference": "buyer-001",
"data": { "server": "ios-wechat", "uid": "player-id" }
}`,
responseExample: `{
"code": 0,
"message": "ok",
"data": {
"idempotent": false,
"order": {
"order_no": "FO20260730000123",
"client_order_no": "shop-10001",
"payment_status": "paid",
"fulfillment_status": "pending",
"can_fulfill": true
}
}
}`,
responseFields: [
{ name: 'idempotent', type: 'bool', desc: 'true 表示该 client_order_no 已存在,返回既有订单' },
{ name: 'order', type: 'object', desc: '订单对象,字段见「订单字段说明」' },
],
notes: [
'Header 需带 Idempotency-Key,且必须与 client_order_no 一致。',
'下单即扣款(payment_status=paid),余额不足返回 400。',
'同一 client_order_no 重复请求不会重复扣款,返回 idempotent=true。',
],
},
{
key: 'query-order',
method: 'GET',
path: '/api/client/v1/orders/{order_no}',
summary: '查询订单状态与详情',
scope: 'orders:read',
pathParams: [{ name: 'order_no', type: 'string', required: true, desc: '平台订单号', example: 'FO20260730000123' }],
responseExample: orderResponseExample,
responseFields: orderFields,
},
{
key: 'cancel-order',
method: 'POST',
path: '/api/client/v1/orders/{order_no}/cancel',
summary: '取消未履约订单并退款(仅 pending/failed 可取消)',
scope: 'orders:write',
pathParams: [{ name: 'order_no', type: 'string', required: true, desc: '平台订单号', example: 'FO20260730000123' }],
bodyParams: [{ name: 'reason', type: 'string', desc: '取消原因(可选)', example: '买家主动取消' }],
requestExample: `{ "reason": "买家主动取消" }`,
responseExample: orderResponseExample,
responseFields: orderFields,
notes: [
'取消成功后 payment_status 变为 refunded,金额退回商户钱包。',
'已进入 processing/succeeded 的订单不可取消,返回 400。',
],
},
{
key: 'ship-notify',
method: 'POST',
path: '/api/client/v1/orders/{order_no}/ship-notify',
summary: '履约器回传履约结果(processing/success/failed',
scope: 'fulfillment:write',
pathParams: [{ name: 'order_no', type: 'string', required: true, desc: '平台订单号', example: 'FO20260730000123' }],
bodyParams: [
{ name: 'ship_status', type: 'enum(processing|success|failed)', required: true, desc: '履约结果', example: 'success' },
{ name: 'provider_order_no', type: 'string', desc: '履约器侧单号', example: 'SRC20260720001' },
{ name: 'fail_reason', type: 'string', desc: '失败原因(failed 时建议填)' },
{ name: 'result', type: 'object', desc: '履约结果数据,原样存储并回显' },
],
requestExample: `{
"ship_status": "success",
"provider_order_no": "SRC20260720001",
"result": { "delivered_at": "2026-07-30T10:05:00+08:00" }
}`,
responseExample: orderResponseExample,
responseFields: orderFields,
notes: [
'processing → fulfillment_status=processingsuccess → succeededfailed → failed(可重试)。',
'已 succeeded 的订单再推 success 仍返回成功,不重复处理(幂等)。',
'已 succeeded 推 failed、或 cancelled 订单推送 → 返回 400 拒绝。',
],
},
{
key: 'get-wallet',
method: 'GET',
path: '/api/client/v1/wallet',
summary: '查询商户钱包余额',
scope: 'wallet:read',
responseExample: `{
"code": 0,
"message": "ok",
"data": {
"id": 1,
"merchant_id": 2,
"currency": "POINT",
"available_balance": 895,
"frozen_balance": 0
}
}`,
responseFields: [
{ name: 'currency', type: 'string', desc: '货币,默认 POINT' },
{ name: 'available_balance', type: 'int', desc: '可用余额(最小货币单位)' },
{ name: 'frozen_balance', type: 'int', desc: '冻结余额' },
],
},
]
+45
View File
@@ -0,0 +1,45 @@
// 开放接口文档的数据模型。所有接口信息以数据描述,由 EndpointDoc 统一渲染。
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
/** 单个接口的完整描述。 */
export interface EndpointSpec {
/** 唯一 key,用于折叠面板与锚点 */
key: string
/** HTTP 方法 */
method: HttpMethod
/** 接口路径,路径参数用 {name} 表示 */
path: string
/** 一句话说明 */
summary: string
/** 调用所需权限 scope */
scope: string
/** 路径参数说明(可选) */
pathParams?: ParamSpec[]
/** 查询参数说明(可选) */
queryParams?: ParamSpec[]
/** 请求体字段说明(可选,POST/PUT 用) */
bodyParams?: ParamSpec[]
/** 请求体示例 JSON 字符串(可选) */
requestExample?: string
/** 响应体示例 JSON 字符串 */
responseExample: string
/** 响应体字段说明 */
responseFields?: ParamSpec[]
/** 额外说明(幂等、限制等),渲染为提示框 */
notes?: string[]
}
/** 参数/字段说明。 */
export interface ParamSpec {
/** 字段名 */
name: string
/** 是否必填 */
required?: boolean
/** 类型,如 string / int / object / enum(a|b|c) */
type: string
/** 说明 */
desc: string
/** 示例值(可选) */
example?: string
}
+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>
)
}