对接皮肤源头开放接口:查询、发货推送与 HMAC 签名鉴权

- 新增订单查询与发货结果推送开放接口,支持 can_ship 与幂等
- 鉴权采用 X-Api-Key + Timestamp + Nonce + HMAC-SHA256 签名
- 订单扩展发货字段与 ship_logs,管理端增加发货记录与开放文档页
This commit is contained in:
yml2213
2026-07-20 16:06:44 +08:00
parent 829bea309d
commit 89cdd32181
16 changed files with 1584 additions and 39 deletions
+322
View File
@@ -0,0 +1,322 @@
import type { CSSProperties } from 'react'
import { Alert, Card, Descriptions, Space, Table, Tabs, Tag, Typography } from 'antd'
const { Title, Paragraph, Text, Link } = Typography
const baseUrl =
typeof window !== 'undefined' ? window.location.origin.replace(':5173', ':8080') : 'http://localhost:8080'
const signPython = `import hmac, hashlib, time, uuid, requests
API_KEY = "sk_source_dev_key_change_me"
API_SECRET = "sk_source_dev_secret_change_me"
BASE = "${baseUrl}"
def sign_headers(method: str, path: str, body: str = "") -> dict:
ts = str(int(time.time()))
nonce = uuid.uuid4().hex
raw = "\\n".join([API_KEY, ts, nonce, method.upper(), path, body])
sign = hmac.new(API_SECRET.encode(), raw.encode(), hashlib.sha256).hexdigest()
return {
"X-Api-Key": API_KEY,
"X-Timestamp": ts,
"X-Nonce": nonce,
"X-Sign": sign,
}
# 查询
path = "/api/open/v1/orders/O你的订单号"
print(requests.get(BASE + path, headers=sign_headers("GET", path)).json())
# 推送(body 必须与签名一致)
path = "/api/open/v1/orders/ship-notify"
body = '{"order_no":"O你的订单号","ship_status":"success","provider_order_no":"SRC001"}'
headers = {"Content-Type": "application/json", **sign_headers("POST", path, body)}
print(requests.post(BASE + path, headers=headers, data=body.encode()).json())`
export default function OpenApiDocs() {
return (
<div>
<Title level={4} style={{ marginTop: 0 }}>
</Title>
<Paragraph type="secondary">
使 Markdown {' '}
<Text code>docs/-.md</Text>
</Paragraph>
<Alert
type="warning"
showIcon
style={{ marginBottom: 16 }}
message="鉴权:X-Api-Key + HMAC 签名(必填)"
description={
<div>
<Text code>X-Api-Key</Text><Text code>X-Timestamp</Text>
<Text code>X-Nonce</Text><Text code>X-Sign</Text>
<br />
Key
<Text code copyable>
sk_source_dev_key_change_me
</Text>
Secret
<Text code copyable>
sk_source_dev_secret_change_me
</Text>
<br />
<Text code>OPEN_API_KEY</Text> / <Text code>OPEN_API_SECRET</Text> /{' '}
<Text code>OPEN_SIGN_SKEW</Text>
</div>
}
/>
<Tabs
items={[
{
key: 'auth',
label: '签名规则',
children: (
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
<Card size="small" title="待签名字符串(6 行,\\n 分隔)">
<pre style={preStyle}>{`{api_key}
{timestamp}
{nonce}
{METHOD}
{path}
{body}`}</pre>
<Descriptions size="small" column={1} bordered style={{ marginTop: 12 }}>
<Descriptions.Item label="METHOD"> GET / POST</Descriptions.Item>
<Descriptions.Item label="path">
URL.Path query /api/open/v1/orders/O123
</Descriptions.Item>
<Descriptions.Item label="body">
GET POST body
</Descriptions.Item>
<Descriptions.Item label="X-Sign">
hex(HMAC-SHA256(api_secret, string_to_sign))
</Descriptions.Item>
<Descriptions.Item label="时间窗"> ±300 </Descriptions.Item>
<Descriptions.Item label="Nonce">8~64 Key </Descriptions.Item>
</Descriptions>
</Card>
<Card size="small" title="Python 完整示例">
<pre style={preStyle}>{signPython}</pre>
</Card>
</Space>
),
},
{
key: 'flow',
label: '对接流程',
children: (
<Card size="small">
<Paragraph>
<ol>
<li> status = paid</li>
<li> <Text code>order_no</Text></li>
<li>
<Text code>product.sku</Text> {' '}
<Text code>can_ship</Text>
</li>
<li>
<Text code>can_ship=true</Text> sku
</li>
<li> success / failed / processing</li>
<li> success </li>
</ol>
</Paragraph>
<Paragraph type="secondary" style={{ marginBottom: 0 }}>
Base URL <Text code>{baseUrl}</Text>
</Paragraph>
</Card>
),
},
{
key: 'query',
label: '订单查询',
children: (
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
<Card size="small" title="请求">
<Paragraph>
<Tag color="blue">GET</Tag>
<Text code>/api/open/v1/orders/&#123;order_no&#125;</Text>
</Paragraph>
<Paragraph type="secondary" style={{ marginBottom: 0 }}>
HeaderX-Api-Key / X-Timestamp / X-Nonce / X-Sign
</Paragraph>
</Card>
<Card size="small" title="响应字段">
<Table
size="small"
pagination={false}
rowKey="field"
dataSource={[
{ field: 'order_no', desc: '店铺订单号' },
{ field: 'status', desc: '订单状态' },
{ field: 'can_ship', desc: '是否可发货(发货前置请以此为准)' },
{ field: 'cannot_ship_reason', desc: '不可发货原因' },
{ field: 'product.sku', desc: '商品英文固定标识(发货用)' },
{ field: 'product.name', desc: '商品中文名' },
{ field: 'product.game', desc: '游戏,如和平精英' },
{ field: 'buyer_name', desc: '买家名' },
{ field: 'amount', desc: '金额' },
{ field: 'shipped_at', desc: '发货成功时间' },
]}
columns={[
{
title: '字段',
dataIndex: 'field',
width: 200,
render: (v) => <Text code>{v}</Text>,
},
{ title: '说明', dataIndex: 'desc' },
]}
/>
</Card>
<Card size="small" title="can_ship 规则">
<Table
size="small"
pagination={false}
rowKey="status"
dataSource={[
{ status: 'pending', ship: 'false', note: '未支付' },
{ status: 'paid', ship: 'true', note: '可发' },
{ status: 'delivering', ship: 'false', note: '发货中' },
{ status: 'delivered', ship: 'false', note: '已完成' },
{ status: 'ship_failed', ship: 'true', note: '可重试' },
{ status: 'cancelled', ship: 'false', note: '已取消' },
]}
columns={[
{
title: 'status',
dataIndex: 'status',
render: (v) => <Text code>{v}</Text>,
},
{
title: 'can_ship',
dataIndex: 'ship',
render: (v) =>
v === 'true' ? <Tag color="green">true</Tag> : <Tag>false</Tag>,
},
{ title: '说明', dataIndex: 'note' },
]}
/>
</Card>
</Space>
),
},
{
key: 'notify',
label: '发货推送',
children: (
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
<Card size="small" title="请求">
<Paragraph>
<Tag color="green">POST</Tag>
<Text code>/api/open/v1/orders/ship-notify</Text>
</Paragraph>
<Paragraph type="secondary">
Body Body
</Paragraph>
<pre style={preStyle}>{`{
"order_no": "O202607201550038000",
"ship_status": "success",
"provider_order_no": "SRC20260720001",
"shipped_at": "2026-07-20T16:00:00+08:00",
"fail_reason": ""
}`}</pre>
</Card>
<Card size="small" title="请求参数">
<Descriptions size="small" column={1} bordered>
<Descriptions.Item label="order_no"></Descriptions.Item>
<Descriptions.Item label="ship_status">
success / failed / processing
</Descriptions.Item>
<Descriptions.Item label="provider_order_no"></Descriptions.Item>
<Descriptions.Item label="shipped_at">
RFC3339success
</Descriptions.Item>
<Descriptions.Item label="fail_reason"></Descriptions.Item>
</Descriptions>
</Card>
<Card size="small" title="状态映射与幂等">
<Table
size="small"
pagination={false}
rowKey="ship"
style={{ marginBottom: 12 }}
dataSource={[
{ ship: 'processing', order: 'delivering', note: '已接单/发货中' },
{ ship: 'success', order: 'delivered', note: '发货成功' },
{ ship: 'failed', order: 'ship_failed', note: '失败可重试' },
]}
columns={[
{
title: 'ship_status',
dataIndex: 'ship',
render: (v) => <Text code>{v}</Text>,
},
{
title: '订单状态',
dataIndex: 'order',
render: (v) => <Text code>{v}</Text>,
},
{ title: '说明', dataIndex: 'note' },
]}
/>
<Alert
type="warning"
showIcon
message="幂等:订单已 delivered 时再次推送 success 仍返回成功,不会重复处理。"
/>
</Card>
</Space>
),
},
{
key: 'errors',
label: '错误码',
children: (
<Card size="small">
<Table
size="small"
pagination={false}
rowKey="code"
dataSource={[
{ code: 0, http: 200, msg: '成功' },
{ code: 401, http: 401, msg: '鉴权失败:Key/签名/时间/Nonce' },
{ code: 404, http: 404, msg: '订单不存在' },
{ code: 400, http: 400, msg: '参数错误 / 状态不允许' },
]}
columns={[
{ title: 'code', dataIndex: 'code', width: 80 },
{ title: 'HTTP', dataIndex: 'http', width: 80 },
{ title: '说明', dataIndex: 'msg' },
]}
/>
<Paragraph type="secondary" style={{ marginTop: 12, marginBottom: 0 }}>
sku
<Link href="/skins"> </Link>
docs/.md
</Paragraph>
</Card>
),
},
]}
/>
</div>
)
}
const preStyle: CSSProperties = {
margin: 0,
padding: 12,
background: '#f5f5f5',
borderRadius: 6,
fontSize: 12,
overflow: 'auto',
whiteSpace: 'pre-wrap',
wordBreak: 'break-all',
}