优化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
|
||||
}
|
||||
Reference in New Issue
Block a user