对接皮肤源头开放接口:查询、发货推送与 HMAC 签名鉴权
- 新增订单查询与发货结果推送开放接口,支持 can_ship 与幂等 - 鉴权采用 X-Api-Key + Timestamp + Nonce + HMAC-SHA256 签名 - 订单扩展发货字段与 ship_logs,管理端增加发货记录与开放文档页
This commit is contained in:
@@ -0,0 +1,323 @@
|
||||
# 皮肤源头开放接口对接文档
|
||||
|
||||
版本:`v1`
|
||||
Base URL:`https://{你们的域名}`(开发环境示例:`http://localhost:8080`)
|
||||
统一响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "ok",
|
||||
"data": {}
|
||||
}
|
||||
```
|
||||
|
||||
- `code = 0` 表示成功
|
||||
- `code != 0` 表示失败,以 `message` 为准
|
||||
|
||||
---
|
||||
|
||||
## 1. 鉴权(ApiKey + 签名)
|
||||
|
||||
开放接口采用 **双因子**:
|
||||
|
||||
| 凭证 | 说明 |
|
||||
|------|------|
|
||||
| `api_key` | 标识对接方,放在请求头 `X-Api-Key` |
|
||||
| `api_secret` | **仅用于本地算签名,禁止放在 Header / 前端 / 日志明文传输** |
|
||||
|
||||
每次请求必须带齐:
|
||||
|
||||
| Header | 必填 | 说明 |
|
||||
|--------|------|------|
|
||||
| `X-Api-Key` | 是 | 对接方 Key |
|
||||
| `X-Timestamp` | 是 | Unix **秒**级时间戳 |
|
||||
| `X-Nonce` | 是 | 随机串,长度 8~64,同一 Key 下有效期内不可重复 |
|
||||
| `X-Sign` | 是 | HMAC-SHA256 签名,小写十六进制 |
|
||||
|
||||
### 1.1 签名算法
|
||||
|
||||
待签名字符串(UTF-8,行之间用 `\n`,共 6 行):
|
||||
|
||||
```text
|
||||
{api_key}
|
||||
{timestamp}
|
||||
{nonce}
|
||||
{METHOD}
|
||||
{path}
|
||||
{body}
|
||||
```
|
||||
|
||||
| 项 | 规则 |
|
||||
|----|------|
|
||||
| METHOD | 大写,如 `GET` / `POST` |
|
||||
| path | `URL.Path`,**不含** query,如 `/api/open/v1/orders/O123` |
|
||||
| body | 原始请求体字符串;GET 无 body 时用 **空字符串**(仍保留最后一行空内容,即末尾仍有换行结构中的 body 段为空) |
|
||||
|
||||
计算:
|
||||
|
||||
```text
|
||||
X-Sign = hex( HMAC-SHA256( api_secret, string_to_sign ) )
|
||||
```
|
||||
|
||||
- 使用 **小写** hex
|
||||
- 时间戳与服务器偏差超过 **300 秒**(可配置)则拒绝
|
||||
- 同一 `api_key + nonce` 在有效期内重复使用 → 拒绝(防重放)
|
||||
|
||||
### 1.2 Python 示例
|
||||
|
||||
```python
|
||||
import hmac, hashlib, time, uuid, requests
|
||||
|
||||
API_KEY = "sk_source_dev_key_change_me"
|
||||
API_SECRET = "sk_source_dev_secret_change_me"
|
||||
BASE = "http://localhost:8080"
|
||||
|
||||
def sign(method: str, path: str, body: str = "") -> dict:
|
||||
ts = str(int(time.time()))
|
||||
nonce = uuid.uuid4().hex
|
||||
string_to_sign = "\n".join([API_KEY, ts, nonce, method.upper(), path, body])
|
||||
sign_hex = hmac.new(
|
||||
API_SECRET.encode(), string_to_sign.encode(), hashlib.sha256
|
||||
).hexdigest()
|
||||
return {
|
||||
"X-Api-Key": API_KEY,
|
||||
"X-Timestamp": ts,
|
||||
"X-Nonce": nonce,
|
||||
"X-Sign": sign_hex,
|
||||
}
|
||||
|
||||
# 查询订单
|
||||
path = "/api/open/v1/orders/O202607201550038000"
|
||||
r = requests.get(BASE + path, headers=sign("GET", path))
|
||||
print(r.json())
|
||||
|
||||
# 发货推送
|
||||
path = "/api/open/v1/orders/ship-notify"
|
||||
body = '{"order_no":"O202607201550038000","ship_status":"success","provider_order_no":"SRC001"}'
|
||||
headers = {"Content-Type": "application/json", **sign("POST", path, body)}
|
||||
r = requests.post(BASE + path, headers=headers, data=body.encode())
|
||||
print(r.json())
|
||||
```
|
||||
|
||||
> **注意**:POST 签名用的 `body` 必须与实际发送的 body **字节级一致**(不要先 `json.dumps` 又改空格)。
|
||||
|
||||
### 1.3 鉴权失败示例
|
||||
|
||||
```json
|
||||
{ "code": 401, "message": "签名校验失败" }
|
||||
```
|
||||
|
||||
常见原因:secret 错误、body 不一致、timestamp 过期、nonce 重复、path 写错(多了 query / 域名)。
|
||||
|
||||
### 1.4 环境变量(我方)
|
||||
|
||||
| 变量 | 说明 | 开发默认 |
|
||||
|------|------|----------|
|
||||
| `OPEN_API_KEY` | Api Key | `sk_source_dev_key_change_me` |
|
||||
| `OPEN_API_SECRET` | 签名密钥 | `sk_source_dev_secret_change_me` |
|
||||
| `OPEN_SIGN_SKEW` | 时间偏差秒数 | `300` |
|
||||
|
||||
生产务必更换 Key / Secret。
|
||||
---
|
||||
|
||||
## 2. 对接流程
|
||||
|
||||
```text
|
||||
1. 买家在店铺下单并完成支付
|
||||
2. 上游拿到「店铺订单号 order_no」
|
||||
3. 调用【订单查询】确认商品 sku、can_ship
|
||||
4. can_ship=true 时执行发货
|
||||
5. 发货结果调用【发货推送】回传
|
||||
6. 我方同步订单状态;重复推送 success 幂等成功
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 订单查询(发货前置)
|
||||
|
||||
### 请求
|
||||
|
||||
```http
|
||||
GET /api/open/v1/orders/{order_no}
|
||||
X-Api-Key: {api_key}
|
||||
X-Timestamp: {unix_seconds}
|
||||
X-Nonce: {random_8_to_64}
|
||||
X-Sign: {hmac_sha256_hex}
|
||||
```
|
||||
|
||||
### 成功响应
|
||||
|
||||
```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": ""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 字段说明
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| order_no | string | 店铺订单号 |
|
||||
| status | string | 订单状态,见下表 |
|
||||
| can_ship | bool | **是否允许发货**,请以此为准 |
|
||||
| cannot_ship_reason | string | 不可发货原因(can_ship=false 时) |
|
||||
| product.name | string | 商品中文名 |
|
||||
| product.sku | string | **商品英文固定标识,发货请用此字段** |
|
||||
| product.game | string | 游戏,如「和平精英」 |
|
||||
| buyer_name | string | 买家备注名 |
|
||||
| amount | number | 订单金额 |
|
||||
| provider_order_no | string | 上游单号(若已回传) |
|
||||
| created_at | string | 下单时间 |
|
||||
| shipped_at | string\|null | 发货成功时间 |
|
||||
| ship_fail_reason | string | 最近一次发货失败原因 |
|
||||
|
||||
### 订单状态 status
|
||||
|
||||
| 值 | 含义 | can_ship |
|
||||
|----|------|----------|
|
||||
| pending | 待支付 | false |
|
||||
| paid | 已支付 | **true** |
|
||||
| delivering | 发货中 | false |
|
||||
| delivered | 已交付 | false |
|
||||
| ship_failed | 发货失败(可重试) | **true** |
|
||||
| cancelled | 已取消 | false |
|
||||
|
||||
### 错误
|
||||
|
||||
| HTTP | message |
|
||||
|------|---------|
|
||||
| 401 | 鉴权失败(Key / 签名 / 时间 / Nonce) |
|
||||
| 404 | 订单不存在 |
|
||||
| 400 | 订单号不能为空 |
|
||||
|
||||
### 调用说明
|
||||
|
||||
请使用上一节 Python 示例生成签名后请求;仅带 `X-Api-Key` 而不带签名会被拒绝。
|
||||
|
||||
---
|
||||
|
||||
## 4. 发货结果推送
|
||||
|
||||
### 请求
|
||||
|
||||
```http
|
||||
POST /api/open/v1/orders/ship-notify
|
||||
Content-Type: application/json
|
||||
X-Api-Key: {api_key}
|
||||
X-Timestamp: {unix_seconds}
|
||||
X-Nonce: {random_8_to_64}
|
||||
X-Sign: {hmac_sha256_hex}
|
||||
```
|
||||
|
||||
```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` / `processing` |
|
||||
| provider_order_no | 否 | 上游发货单号 |
|
||||
| shipped_at | 否 | 发货时间,RFC3339;success 时缺省用服务端时间 |
|
||||
| fail_reason | 否 | 失败原因(failed 时建议填写) |
|
||||
|
||||
### ship_status 与订单状态映射
|
||||
|
||||
| ship_status | 订单变为 | 说明 |
|
||||
|-------------|---------|------|
|
||||
| processing | delivering | 已接单/发货中 |
|
||||
| success | delivered | 发货成功 |
|
||||
| failed | ship_failed | 发货失败,允许再次查询后重试 |
|
||||
|
||||
### 幂等
|
||||
|
||||
- 订单已是 `delivered`,再次推送 `success` → **返回成功**,不重复处理
|
||||
- 订单已是 `delivered`,推送 `failed` → 拒绝
|
||||
- 订单 `cancelled` → 拒绝更新
|
||||
|
||||
### 成功响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "ok",
|
||||
"data": {
|
||||
"order_no": "O202607201550038000",
|
||||
"status": "delivered",
|
||||
"message": "发货成功,订单已交付"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 调用说明
|
||||
|
||||
Body 与签名字符串中的 body **必须完全一致**。完整示例见 **§1.2 Python**。
|
||||
|
||||
| ship_status | body 示例 |
|
||||
|-------------|-----------|
|
||||
| processing | `{"order_no":"O...","ship_status":"processing","provider_order_no":"SRC..."}` |
|
||||
| success | `{"order_no":"O...","ship_status":"success","provider_order_no":"SRC...","shipped_at":"2026-07-20T16:00:00+08:00"}` |
|
||||
| failed | `{"order_no":"O...","ship_status":"failed","fail_reason":"账号不存在"}` |
|
||||
|
||||
---
|
||||
|
||||
## 5. 推荐调用顺序
|
||||
|
||||
```text
|
||||
query → can_ship?
|
||||
├─ false → 停止,展示 cannot_ship_reason
|
||||
└─ true
|
||||
→(可选)push processing
|
||||
→ 执行发货
|
||||
→ push success / failed
|
||||
```
|
||||
|
||||
发货务必使用查询结果中的 **`product.sku`**,不要仅依赖中文名。
|
||||
|
||||
---
|
||||
|
||||
## 6. 联调检查清单
|
||||
|
||||
- [ ] 缺少 Sign / 错误 Secret 返回 401
|
||||
- [ ] 错误 ApiKey 返回 401
|
||||
- [ ] 过期 Timestamp / 重复 Nonce 返回 401
|
||||
- [ ] 不存在的订单号返回 404
|
||||
- [ ] `pending` 订单 `can_ship=false`
|
||||
- [ ] `paid` 订单 `can_ship=true` 且 sku 正确
|
||||
- [ ] success 推送后状态为 delivered
|
||||
- [ ] 重复 success 幂等成功
|
||||
- [ ] failed 后可再次 query 且 can_ship=true
|
||||
|
||||
---
|
||||
|
||||
## 7. 联系方式
|
||||
|
||||
接口问题、密钥申请、联调环境请联系店铺方技术对接人。
|
||||
商品英文 sku 对照见:`docs/商品英文名对照.md`
|
||||
Reference in New Issue
Block a user