diff --git a/frontend/src/openapi/EndpointDoc.tsx b/frontend/src/openapi/EndpointDoc.tsx new file mode 100644 index 0000000..b43d962 --- /dev/null +++ b/frontend/src/openapi/EndpointDoc.tsx @@ -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 = { + 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 = [ + { + title: '字段', + dataIndex: 'name', + width: 200, + render: (v: string) => {v}, + }, + { + title: '必填', + dataIndex: 'required', + width: 70, + render: (v?: boolean) => + v ? : , + }, + { + title: '类型', + dataIndex: 'type', + width: 180, + render: (v: string) => {v}, + }, + { title: '说明', dataIndex: 'desc' }, + { + title: '示例', + dataIndex: 'example', + width: 160, + render: (v?: string) => + v ? {v} : null, + }, +] + +function ParamTable({ title, params }: { title: string; params: ParamSpec[] }) { + return ( + + + + ) +} + +function CodeBlock({ title, code }: { title: string; code: string }) { + return ( + +
{code}
+
+ ) +} + +/** 通用接口渲染:方法+路径+权限 → 参数表 → 请求/响应示例 → 字段说明 → 注意事项。 */ +export default function EndpointDoc({ spec }: { spec: EndpointSpec }) { + return ( + + + + + {spec.method} + + + {spec.path} + + {spec.scope} + + + {spec.summary} + + + + {spec.pathParams?.length ? : null} + {spec.queryParams?.length ? : null} + {spec.bodyParams?.length ? : null} + + {spec.requestExample ? : null} + + + {spec.responseFields?.length ? ( + + ) : null} + + {spec.notes?.length ? ( + + {spec.notes.map((n, i) => ( +
  • {n}
  • + ))} + + } + /> + ) : null} +
    + ) +} + +/** 通用响应说明卡片,供页面顶部展示统一响应格式与错误码。 */ +export function CommonResponseDoc({ + fields, + errors, +}: { + fields: ParamSpec[] + errors: ParamSpec[] +}) { + return ( + + + + + {`{ "code": 0, "message": "ok", "data": {} }`} + + {fields.map((f) => ( + {f.name}}> + {f.desc} + + ))} + + + +
    {v} }, + { title: '含义', dataIndex: 'desc' }, + ]} + /> + + + ) +} diff --git a/frontend/src/openapi/endpoints.ts b/frontend/src/openapi/endpoints.ts new file mode 100644 index 0000000..bfb7bd9 --- /dev/null +++ b/frontend/src/openapi/endpoints.ts @@ -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: '冻结余额' }, + ], + }, +] diff --git a/frontend/src/openapi/types.ts b/frontend/src/openapi/types.ts new file mode 100644 index 0000000..73cfd10 --- /dev/null +++ b/frontend/src/openapi/types.ts @@ -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 +} diff --git a/frontend/src/pages/OpenApiDocs.tsx b/frontend/src/pages/OpenApiDocs.tsx index 1a0ed9e..061ee04 100644 --- a/frontend/src/pages/OpenApiDocs.tsx +++ b/frontend/src/pages/OpenApiDocs.tsx @@ -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() { 开放接口 - 面向商户系统、履约器和外部平台调用,凭证在「商户中心 / API 客户端」创建。原 - /api/open/v1 保留给上游发货对接。 + 面向商户系统、履约器和外部平台调用的通用开放 API。凭证在「商户中心 / API 客户端」创建, + 采用 X-App-Key + HMAC-SHA256 签名鉴权。 + 原 /api/open/v1 保留给上游发货对接,不在本页展示。 - - - 签名内容为 timestamp\nnonce\nMETHOD\npath\nsha256(body)。 - - - 旧 X-Api-Key 签名只给兼容客户端使用,新对接统一使用{' '} - X-App-Key。 - - - } - /> - , + }, { key: 'auth', - label: '签名', - children: ( - - - - - timestamp、nonce、METHOD、path、sha256(body) - - 按固定顺序用换行符拼接 - 仅 URL.Path,不含域名和 query - GET 为空字节;POST 必须与实际发送 body 完全一致 - hex(HMAC-SHA256(app_secret, raw)) 小写 - 8~96 字符,同一客户端有效期内不可重复 - -
    {`timestamp
    -nonce
    -POST
    -/api/open/v1/orders
    -sha256(body)`}
    -
    - -
    {signPython}
    -
    -
    - ), + label: '鉴权与签名', + children: , }, { key: 'endpoints', - label: '接口', - children: ( - -
    {v} }, - { title: '路径', dataIndex: 'path', render: (v) => {v} }, - { title: '权限', dataIndex: 'scope', width: 150, render: (v) => {v} }, - { title: '说明', dataIndex: 'desc' }, - ]} - /> - - ), - }, - { - key: 'order', - label: '下单', - children: ( - - - - POST - /api/client/v1/orders - -
    {`{
    -  "client_order_no": "shop-10001",
    -  "sku": "sku-basic",
    -  "quantity": 1,
    -  "buyer_reference": "buyer-or-account",
    -  "data": {
    -    "server": "ios-wechat",
    -    "uid": "player-id"
    -  }
    -}`}
    -
    - -
    {`{
    -  "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
    -    }
    -  }
    -}`}
    -
    -
    - ), + label: '接口列表', + children: , }, { key: 'status', - label: '状态', - children: ( - -
    {v} }, - { title: 'can_fulfill', dataIndex: 'can', width: 120, render: (v) => v === 'true' ? true : false }, - { title: '说明', dataIndex: 'desc' }, - ]} - /> - - ), - }, - { - key: 'callback', - label: '回调', - children: ( - - - order.created / order.fulfillment.updated / order.cancelled - X-Event-ID、X-Timestamp、X-Sign - hex(HMAC-SHA256(callback_secret, timestamp + "\n" + sha256(body))) - 持久化 outbox,失败指数退避重试 - - - ), + label: '状态说明', + children: , }, ]} /> @@ -207,13 +68,181 @@ sha256(body)`} ) } -const preStyle: CSSProperties = { - margin: 0, - padding: 12, - background: '#f5f5f5', - borderRadius: 6, - fontSize: 12, - overflow: 'auto', - whiteSpace: 'pre-wrap', - wordBreak: 'break-all', +function OverviewTab() { + return ( + + +
  • 在「商户中心 / API 客户端」创建凭证,获得 AppKeyAppSecret
  • +
  • 每次请求携带 X-App-Key / X-Timestamp / X-Nonce / X-Sign 四个鉴权头。
  • +
  • 调用「商品列表」拿到可售 sku,调用「下单」创建订单并扣款。
  • +
  • 履约器通过「发货回传」回写 processing/success/failed,状态可由「查询订单」轮询。
  • + + } + /> + + + + {baseUrl} + + + X-App-KeyX-TimestampX-NonceX-Sign + + 允许 ±300 秒 + POST 请求使用 application/json + + + +
    + ) +} + +function AuthTab() { + return ( + + +
    {v} }, + { title: '必填', dataIndex: 'required', width: 80, render: (v) => v ? : }, + { title: '说明', dataIndex: 'desc' }, + ]} + /> + + + + + + {'timestamp\nnonce\nMETHOD\npath\nsha256(body)'} + + 按固定顺序,用换行符 \n 拼接 + 大写,如 GET / POST + 仅 URL.Path,不含域名和 query + GET 为空字节;POST 必须与实际发送 body 完全一致 + + hex( HMAC-SHA256( app_secret, 签名内容 ) ),小写十六进制 + + + + + +
    {`timestamp
    +nonce
    +GET
    +/api/client/v1/products
    +sha256("") = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`}
    +
    + + +
    {`timestamp
    +nonce
    +POST
    +/api/client/v1/orders
    +sha256(body) = <实际请求 body 字节的 SHA256 十六进制>`}
    +
    + + +
  • POST 签名用的 body 必须与实际发送的 body 字节级一致,不要签名后再改空格或字段顺序。
  • +
  • 签名失败常见原因:secret 错、path 多了 query、body 不一致、时间戳过期、nonce 重复。
  • +
  • 下单接口还需携带 Idempotency-Key,且必须与 client_order_no 一致。
  • + + } + /> + + ) +} + +function EndpointsTab() { + return ( + ({ + key: spec.key, + label: ( + + + {spec.method} + + {spec.path} + {spec.summary} + + ), + children: , + }))} + /> + ) +} + +function StatusTab() { + return ( + + +
    {v} }, + { + title: 'can_fulfill', + dataIndex: 'example', + width: 160, + render: (v: string) => + v === 'can_fulfill=true' ? true : false, + }, + { title: '说明', dataIndex: 'desc' }, + ]} + /> + + +
    {v} }, + { title: '说明', dataIndex: 'desc' }, + ]} + /> + + {`拿到 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`} + } + /> + + ) }