Files
affiliate_dash/docs/开放接口-皮肤源头对接.md
T
yml2213 89cdd32181 对接皮肤源头开放接口:查询、发货推送与 HMAC 签名鉴权
- 新增订单查询与发货结果推送开放接口,支持 can_ship 与幂等
- 鉴权采用 X-Api-Key + Timestamp + Nonce + HMAC-SHA256 签名
- 订单扩展发货字段与 ship_logs,管理端增加发货记录与开放文档页
2026-07-20 16:06:44 +08:00

324 lines
8.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 皮肤源头开放接口对接文档
版本:`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 | 否 | 发货时间,RFC3339success 时缺省用服务端时间 |
| 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`