优化api文档
This commit is contained in:
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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=processing;success → succeeded;failed → 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: '冻结余额' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
@@ -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
@@ -1,5 +1,13 @@
|
|||||||
import type { CSSProperties } from 'react'
|
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
|
const { Title, Paragraph, Text } = Typography
|
||||||
|
|
||||||
@@ -8,33 +16,17 @@ const baseUrl =
|
|||||||
? window.location.origin.replace(':5173', ':8080')
|
? window.location.origin.replace(':5173', ':8080')
|
||||||
: 'http://localhost:8080'
|
: 'http://localhost:8080'
|
||||||
|
|
||||||
const signPython = `import hashlib, hmac, json, time, uuid, requests
|
const preStyle: CSSProperties = {
|
||||||
|
margin: 0,
|
||||||
APP_KEY = "ak_xxx"
|
padding: 12,
|
||||||
APP_SECRET = "sk_xxx"
|
background: '#f6f8fa',
|
||||||
BASE = "${baseUrl}"
|
borderRadius: 6,
|
||||||
|
fontSize: 12.5,
|
||||||
def sign_headers(method: str, path: str, body: bytes = b"") -> dict:
|
lineHeight: 1.6,
|
||||||
ts = str(int(time.time()))
|
overflow: 'auto',
|
||||||
nonce = uuid.uuid4().hex
|
whiteSpace: 'pre-wrap',
|
||||||
body_hash = hashlib.sha256(body).hexdigest()
|
wordBreak: 'break-all',
|
||||||
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())`
|
|
||||||
|
|
||||||
export default function OpenApiDocs() {
|
export default function OpenApiDocs() {
|
||||||
return (
|
return (
|
||||||
@@ -43,163 +35,32 @@ export default function OpenApiDocs() {
|
|||||||
开放接口
|
开放接口
|
||||||
</Title>
|
</Title>
|
||||||
<Paragraph type="secondary">
|
<Paragraph type="secondary">
|
||||||
面向商户系统、履约器和外部平台调用,凭证在「商户中心 / API 客户端」创建。原
|
面向商户系统、履约器和外部平台调用的通用开放 API。凭证在「商户中心 / API 客户端」创建,
|
||||||
<Text code>/api/open/v1</Text> 保留给上游发货对接。
|
采用 <Text code>X-App-Key</Text> + HMAC-SHA256 签名鉴权。
|
||||||
|
原 <Text code>/api/open/v1</Text> 保留给上游发货对接,不在本页展示。
|
||||||
</Paragraph>
|
</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
|
<Tabs
|
||||||
items={[
|
items={[
|
||||||
|
{
|
||||||
|
key: 'overview',
|
||||||
|
label: '概览',
|
||||||
|
children: <OverviewTab />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'auth',
|
key: 'auth',
|
||||||
label: '签名',
|
label: '鉴权与签名',
|
||||||
children: (
|
children: <AuthTab />,
|
||||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
|
||||||
<Card size="small" title="签名规则">
|
|
||||||
<Descriptions size="small" column={1} bordered>
|
|
||||||
<Descriptions.Item label="参与字段">
|
|
||||||
timestamp、nonce、METHOD、path、sha256(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>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'endpoints',
|
key: 'endpoints',
|
||||||
label: '接口',
|
label: '接口列表',
|
||||||
children: (
|
children: <EndpointsTab />,
|
||||||
<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>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'status',
|
key: 'status',
|
||||||
label: '状态',
|
label: '状态说明',
|
||||||
children: (
|
children: <StatusTab />,
|
||||||
<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-ID、X-Timestamp、X-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>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
@@ -207,13 +68,181 @@ sha256(body)`}</pre>
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const preStyle: CSSProperties = {
|
function OverviewTab() {
|
||||||
margin: 0,
|
return (
|
||||||
padding: 12,
|
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||||||
background: '#f5f5f5',
|
<Alert
|
||||||
borderRadius: 6,
|
type="info"
|
||||||
fontSize: 12,
|
showIcon
|
||||||
overflow: 'auto',
|
message="一句话上手"
|
||||||
whiteSpace: 'pre-wrap',
|
description={
|
||||||
wordBreak: 'break-all',
|
<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>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user