318 lines
9.1 KiB
Markdown
318 lines
9.1 KiB
Markdown
# 店铺订单对接 API 文档
|
||
|
||
> 文档对象:发货系统技术人员
|
||
> 版本:v1
|
||
> 更新说明:鉴权为 **ApiKey + HMAC 签名**,请按本文实现,勿只传 Key。
|
||
|
||
---
|
||
|
||
## 0. 你们要做什么(一句话)
|
||
|
||
1. 用**我们店铺的订单号**(`order_no`)调【订单查询】,确认商品 `sku`、是否可发货 `can_ship`
|
||
2. 可发则在你们系统发货
|
||
3. 发货结果调【发货推送】告诉我们,我们同步订单状态
|
||
|
||
发货记录、积分消耗在你们后台给店铺看;我们这边以订单状态为准。
|
||
|
||
---
|
||
|
||
## 1. 环境信息
|
||
|
||
| 项 | 值(示例 / 请替换) |
|
||
|----|---------------------|
|
||
| **API 根地址 Base URL** | `https://221329.cc.cd` 开发临时地址 |
|
||
| **X-Api-Key** | 测试使用,如 `sk_source_dev_key_3ad3d1bbacda0e54` |
|
||
| **api_secret** | 测试使用,**只用于本地算签名,不要写在 URL/Header**, `sk_source_dev_secret_5a43ec55cd020d83` |
|
||
| **时间偏差** | 默认允许 ±300 秒 |
|
||
|
||
统一响应格式:
|
||
|
||
```json
|
||
{
|
||
"code": 0,
|
||
"message": "ok",
|
||
"data": {}
|
||
}
|
||
```
|
||
|
||
- `code = 0`:成功
|
||
- `code != 0`:失败,看 `message`
|
||
- 时间字段统一使用 RFC3339 秒级北京时间,例如 `2026-07-30T17:53:58+08:00`;请求侧仍接受合法 RFC3339 时间。
|
||
|
||
---
|
||
|
||
## 2. 鉴权(每次请求必带)
|
||
|
||
| Header | 必填 | 说明 |
|
||
|--------|------|------|
|
||
| `X-Api-Key` | 是 | 店铺分配的 Key |
|
||
| `X-Timestamp` | 是 | 当前 Unix **秒**时间戳 |
|
||
| `X-Nonce` | 是 | 随机串,长度 8~64;同一 Key 在有效期内不可重复 |
|
||
| `X-Sign` | 是 | 见下方算法 |
|
||
|
||
### 2.1 签名字符串(参数字典序 + `&` 拼接)
|
||
|
||
参与签名的参数:
|
||
|
||
| 参数名 | 说明 |
|
||
|--------|------|
|
||
| `api_key` | 与 Header `X-Api-Key` 相同 |
|
||
| `timestamp` | 与 Header `X-Timestamp` 相同 |
|
||
| `nonce` | 与 Header `X-Nonce` 相同 |
|
||
| `method` | 大写:`GET` / `POST` |
|
||
| `path` | 仅路径,**不要**域名、**不要** query。例:`/api/open/v1/orders/O123` |
|
||
| `body` | 原始 HTTP body;GET 用**空字符串** |
|
||
|
||
规则:
|
||
|
||
1. value **原样拼接,不做 URL encode**
|
||
2. 按参数名 **ASCII 字典序** 排序
|
||
3. 拼成:`k1=v1&k2=v2&k3=v3...`
|
||
4. `X-Sign = hex( HMAC-SHA256( api_secret, 签名字符串 ) )`,**小写**十六进制
|
||
|
||
排序后参数名顺序固定为:
|
||
|
||
```text
|
||
api_key, body, method, nonce, path, timestamp
|
||
```
|
||
|
||
**GET 示例**(body 为空):
|
||
|
||
```text
|
||
api_key=sk_xxx&body=&method=GET&nonce=a1b2c3d4e5f67890&path=/api/open/v1/orders/O202607201550038000×tamp=1721450000
|
||
```
|
||
|
||
**POST 示例**:
|
||
|
||
```text
|
||
api_key=sk_xxx&body={"order_no":"O202607201550038000","ship_status":"success"}&method=POST&nonce=a1b2c3d4e5f67890&path=/api/open/v1/orders/ship-notify×tamp=1721450000
|
||
```
|
||
|
||
### 2.2 Python 参考实现
|
||
|
||
```python
|
||
import hmac, hashlib, time, uuid, requests
|
||
|
||
API_KEY = "请替换为店铺下发的 key"
|
||
API_SECRET = "请替换为店铺下发的 secret"
|
||
BASE = "https://api.example.com" # 请替换
|
||
|
||
def build_sign_string(api_key: str, timestamp: str, nonce: str, method: str, path: str, body: str = "") -> str:
|
||
params = {
|
||
"api_key": api_key,
|
||
"body": body,
|
||
"method": method.upper(),
|
||
"nonce": nonce,
|
||
"path": path,
|
||
"timestamp": timestamp,
|
||
}
|
||
# 字典序 + & 拼接,value 不 encode
|
||
return "&".join(f"{k}={params[k]}" for k in sorted(params.keys()))
|
||
|
||
def sign_headers(method: str, path: str, body: str = "") -> dict:
|
||
ts = str(int(time.time()))
|
||
nonce = uuid.uuid4().hex
|
||
raw = build_sign_string(API_KEY, ts, nonce, method, 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/O202607201550038000" # 换成真实店铺订单号
|
||
print(requests.get(BASE + path, headers=sign_headers("GET", path)).json())
|
||
|
||
# —— 推送发货成功(body 必须与签名用的 body 完全一致)——
|
||
path = "/api/open/v1/orders/ship-notify"
|
||
body = '{"order_no":"O202607201550038000","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("utf-8")).json())
|
||
```
|
||
|
||
**注意:**
|
||
|
||
- POST 时签名用的 `body` 必须与实际发送的 body **字节级一致**(不要一边签名一边再改空格/字段顺序)。
|
||
- value **不要** URL encode。
|
||
- 签名失败常见原因:secret 错、path 多了 query、body 不一致、时间戳过期、nonce 重复、参数未按字典序拼接。
|
||
|
||
---
|
||
|
||
## 3. 接口一:订单查询(发货前置)
|
||
|
||
### 3.1 请求
|
||
|
||
```http
|
||
GET /api/open/v1/orders/{order_no}
|
||
```
|
||
|
||
`order_no`:店铺订单号(由买家/店铺提供,格式类似 `O202607201550038000`)。
|
||
|
||
### 3.2 成功示例
|
||
|
||
```json
|
||
{
|
||
"code": 0,
|
||
"message": "ok",
|
||
"data": {
|
||
"order_no": "O202607201550038000",
|
||
"status": "paid",
|
||
"can_ship": true,
|
||
"cannot_ship_reason": "",
|
||
"product": {
|
||
"name": "套装-糯粉咩咩",
|
||
"sku": "suit_pink_sheep",
|
||
"game": "和平精英"
|
||
},
|
||
"buyer_name": "测试买家",
|
||
"amount": 0,
|
||
"provider_order_no": "",
|
||
"created_at": "2026-07-20T15:50:03+08:00",
|
||
"shipped_at": null,
|
||
"ship_fail_reason": ""
|
||
}
|
||
}
|
||
```
|
||
|
||
### 3.3 字段说明
|
||
|
||
| 字段 | 说明 |
|
||
|------|------|
|
||
| `order_no` | 店铺订单号 |
|
||
| `status` | 订单状态,见下表 |
|
||
| **`can_ship`** | **是否允许发货,发货前必须为 true** |
|
||
| `cannot_ship_reason` | 不可发原因(can_ship=false 时) |
|
||
| **`product.sku`** | **发货商品标识(英文固定码),请按此发货** |
|
||
| `product.name` | 中文名(展示用) |
|
||
| `product.game` | 游戏,如「和平精英」 |
|
||
| `buyer_name` | 买家备注 |
|
||
| `amount` | 金额 |
|
||
| `provider_order_no` | 若已回传过你们的单号 |
|
||
| `shipped_at` | 发货成功时间 |
|
||
| `ship_fail_reason` | 最近失败原因 |
|
||
|
||
### 3.4 订单状态与 can_ship
|
||
|
||
| status | 含义 | can_ship |
|
||
|--------|------|----------|
|
||
| `pending` | 待支付 | **false** |
|
||
| `paid` | 已支付 | **true** ← 正常可发 |
|
||
| `delivering` | 发货中 | false |
|
||
| `delivered` | 已交付 | false |
|
||
| `ship_failed` | 发货失败 | **true** ← 可重试 |
|
||
| `cancelled` | 已取消 | false |
|
||
|
||
### 3.5 错误
|
||
|
||
| HTTP / code | 含义 |
|
||
|-------------|------|
|
||
| 401 | 鉴权失败(Key/签名/时间/Nonce) |
|
||
| 404 | 订单号不存在 |
|
||
| 400 | 参数错误 |
|
||
|
||
---
|
||
|
||
## 4. 接口二:发货结果推送
|
||
|
||
> 当前接口只接受 `success` / `failed` 两种最终结果。`processing` 是旧口径里曾出现过的中间状态,当前 `ship_notify` 不再接受;当 `ship_status=failed` 时,`fail_reason` 必须填写详细失败原因。
|
||
|
||
### 4.1 请求
|
||
|
||
```http
|
||
POST /api/open/v1/orders/ship-notify
|
||
Content-Type: application/json
|
||
```
|
||
|
||
```json
|
||
{
|
||
"order_no": "O202607201550038000",
|
||
"ship_status": "success",
|
||
"provider_order_no": "SRC20260720001",
|
||
"shipped_at": "2026-07-20T16:00:00+08:00",
|
||
"fail_reason": ""
|
||
}
|
||
```
|
||
|
||
| 字段 | 必填 | 说明 |
|
||
|------|------|------|
|
||
| `order_no` | 是 | 店铺订单号 |
|
||
| `ship_status` | 是 | 仅 `success` / `failed` |
|
||
| `provider_order_no` | 否 | 你们系统的发货单号 |
|
||
| `shipped_at` | 否 | RFC3339 时间,建议秒级北京时间;success 未传则用服务端时间 |
|
||
| `fail_reason` | 失败时是 | `failed` 时必填,填写详细失败原因 |
|
||
|
||
### 4.2 ship_status → 我们订单状态
|
||
|
||
| ship_status | 订单变为 | 说明 |
|
||
|-------------|---------|------|
|
||
| `success` | `delivered` | 发货成功 |
|
||
| `failed` | `ship_failed` | 失败,允许之后再次查询并重试 |
|
||
|
||
### 4.3 幂等与限制
|
||
|
||
- 订单已是 `delivered`,再推 `success` → **仍返回成功**,不重复处理
|
||
- 订单已是 `delivered`,推 `failed` → 拒绝
|
||
- 订单 `cancelled` → 拒绝更新
|
||
|
||
### 4.4 成功响应示例
|
||
|
||
```json
|
||
{
|
||
"code": 0,
|
||
"message": "ok",
|
||
"data": {
|
||
"order_no": "O202607201550038000",
|
||
"status": "delivered",
|
||
"message": "发货成功,订单已交付"
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 5. 推荐调用顺序
|
||
|
||
```text
|
||
拿到店铺订单号 order_no
|
||
↓
|
||
GET 订单查询
|
||
↓
|
||
can_ship == false ? → 停止,展示 cannot_ship_reason
|
||
↓ true
|
||
按 product.sku 发货
|
||
↓
|
||
POST ship_status=success 或 failed
|
||
```
|
||
|
||
**发货请以 `product.sku` 为准**,不要只依赖中文名。
|
||
|
||
---
|
||
|
||
## 6. 联调检查清单
|
||
|
||
- [ ] 只带 Key 不带签名 → 401
|
||
- [ ] Secret 错误 → 401「签名校验失败」
|
||
- [ ] 错误/过期 Timestamp、重复 Nonce → 401
|
||
- [ ] 不存在的 order_no → 404
|
||
- [ ] 未支付订单 can_ship=false
|
||
- [ ] 已支付订单 can_ship=true,且 sku 正确
|
||
- [ ] success 后订单 delivered
|
||
- [ ] 重复 success 幂等成功
|
||
- [ ] failed 后可再 query 且 can_ship=true
|
||
|
||
---
|
||
|
||
## 7. 商品 sku 说明
|
||
|
||
商品英文码与中文名对照由店铺维护(如:套装-糯粉咩咩 → `suit_pink_sheep`)。
|
||
查询接口返回的 `product.sku` 即当前订单应发商品。
|
||
|
||
---
|
||
|
||
## 8. 联系方式
|
||
|
||
接口问题、密钥申请、联调订单号请联系**店铺方技术对接人**。
|
||
本文档由店铺系统生成/维护,以店铺最新版本为准。
|