支持 .env 配置,并完善源头对接文档与测试订单

- 增加 .env.example 与 godotenv 加载,start.sh 自动读环境变量
- 重写发给源头的开放接口对接文档
- 订单页支持创建测试订单(可直接已支付并复制店铺订单号)
This commit is contained in:
yml2213
2026-07-20 16:19:57 +08:00
parent 89cdd32181
commit e4d0a71963
13 changed files with 504 additions and 215 deletions
+139 -178
View File
@@ -1,8 +1,31 @@
# 皮肤源头开放接口对接文档
# 店铺订单对接 API 说明(发给皮肤源头)
版本:`v1`
Base URL`https://{你们的域名}`(开发环境示例:`http://localhost:8080`
统一响应:
> 文档对象:皮肤源头 / 发货系统技术人员
> 版本:v1
> 更新说明:鉴权为 **ApiKey + HMAC 签名**,请按本文实现,勿只传 Key。
---
## 0. 你们要做什么(一句话)
1. 用**我们店铺的订单号**`order_no`)调【订单查询】,确认商品 `sku`、是否可发货 `can_ship`
2. 可发则在你们系统发货
3. 发货结果调【发货推送】告诉我们,我们同步订单状态
发货记录、积分消耗在你们后台给店铺看;我们这边以订单状态为准。
---
## 1. 环境信息(由店铺方填写后发给你们)
| 项 | 值(示例 / 请替换) |
|----|---------------------|
| **API 根地址 Base URL** | `https://api.example.com`(开发:`http://主机:8080` |
| **X-Api-Key** | 由店铺方分配,如 `sk_xxxx` |
| **api_secret** | 由店铺方单独发送,**只用于本地算签名,不要写在 URL/Header** |
| **时间偏差** | 默认允许 ±300 秒 |
统一响应格式:
```json
{
@@ -12,32 +35,21 @@ Base URL`https://{你们的域名}`(开发环境示例:`http://localhost:
}
```
- `code = 0` 表示成功
- `code != 0` 表示失败, `message` 为准
- `code = 0`:成功
- `code != 0`失败, `message`
---
## 1. 鉴权(ApiKey + 签名
开放接口采用 **双因子**
| 凭证 | 说明 |
|------|------|
| `api_key` | 标识对接方,放在请求头 `X-Api-Key` |
| `api_secret` | **仅用于本地算签名,禁止放在 Header / 前端 / 日志明文传输** |
每次请求必须带齐:
## 2. 鉴权(每次请求必带
| Header | 必填 | 说明 |
|--------|------|------|
| `X-Api-Key` | 是 | 对接方 Key |
| `X-Timestamp` | 是 | Unix **秒**时间戳 |
| `X-Nonce` | 是 | 随机串,长度 8~64同一 Key 有效期内不可重复 |
| `X-Sign` | 是 | HMAC-SHA256 签名,小写十六进制 |
| `X-Api-Key` | 是 | 店铺分配的 Key |
| `X-Timestamp` | 是 | 当前 Unix **秒**时间戳 |
| `X-Nonce` | 是 | 随机串,长度 864同一 Key 有效期内不可重复 |
| `X-Sign` | 是 | 见下方算法 |
### 1.1 签名算法
待签名字符串(UTF-8,行之间用 `\n`,共 6 行):
### 2.1 签名字符串(6 行,用 `\n` 连接)
```text
{api_key}
@@ -48,105 +60,63 @@ Base URL`https://{你们的域名}`(开发环境示例:`http://localhost:
{body}
```
| | 规则 |
|----|------|
| METHOD | 大写,如 `GET` / `POST` |
| path | `URL.Path`**不含** query,如 `/api/open/v1/orders/O123` |
| body | 原始请求体字符串;GET 无 body 时用 **空字符串**(仍保留最后一行空内容,即末尾仍有换行结构中的 body 段为空) |
| 字段 | 规则 |
|------|------|
| METHOD | 大写`GET` / `POST` |
| path | 仅路径,**不要**域名、**不要** query。例:`/api/open/v1/orders/O123` |
| body | 原始 HTTP body 字符串;GET 用**空字符串** |
| X-Sign | `hex( HMAC-SHA256( api_secret, 签名字符串 ) )`**小写**十六进制 |
计算:
```text
X-Sign = hex( HMAC-SHA256( api_secret, string_to_sign ) )
```
- 使用 **小写** hex
- 时间戳与服务器偏差超过 **300 秒**(可配置)则拒绝
- 同一 `api_key + nonce` 在有效期内重复使用 → 拒绝(防重放)
### 1.2 Python 示例
### 2.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"
API_KEY = "请替换为店铺下发的 key"
API_SECRET = "请替换为店铺下发的 secret"
BASE = "https://api.example.com" # 请替换
def sign(method: str, path: str, body: str = "") -> dict:
def sign_headers(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()
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_hex,
"X-Sign": sign,
}
# 查询订单
path = "/api/open/v1/orders/O202607201550038000"
r = requests.get(BASE + path, headers=sign("GET", path))
print(r.json())
# —— 查询订单 ——
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("POST", path, body)}
r = requests.post(BASE + path, headers=headers, data=body.encode())
print(r.json())
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 **字节级一致**(不要先 `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 幂等成功
```
- POST 时 `json.dumps` 后的字符串要原样发送,不要一边签名一边再改空格/字段顺序。
- 签名失败常见原因:secret 错、path 多了 query、body 不一致、时间戳过期、nonce 重复。
---
## 3. 订单查询(发货前置)
## 3. 接口一:订单查询(发货前置)
### 请求
### 3.1 请求
```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}
```
### 成功响应
`order_no`:店铺订单号(由买家/店铺提供,格式类似 `O202607201550038000`)。
### 3.2 成功示例
```json
{
@@ -172,60 +142,51 @@ X-Sign: {hmac_sha256_hex}
}
```
### 字段说明
### 3.3 字段说明
| 字段 | 类型 | 说明 |
|------|------|------|
| 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 | 最近一次发货失败原因 |
| 字段 | 说明 |
|------|------|
| `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` | 最近失败原因 |
### 订单状态 status
### 3.4 订单状态与 can_ship
| | 含义 | can_ship |
|----|------|----------|
| pending | 待支付 | false |
| paid | 已支付 | **true** |
| delivering | 发货中 | false |
| delivered | 已交付 | false |
| ship_failed | 发货失败(可重试) | **true** |
| cancelled | 已取消 | false |
| status | 含义 | can_ship |
|--------|------|----------|
| `pending` | 待支付 | **false** |
| `paid` | 已支付 | **true** ← 正常可发 |
| `delivering` | 发货中 | false |
| `delivered` | 已交付 | false |
| `ship_failed` | 发货失败 | **true** ← 可重试 |
| `cancelled` | 已取消 | false |
### 错误
### 3.5 错误
| HTTP | message |
|------|---------|
| 401 | 鉴权失败(Key / 签名 / 时间 / Nonce |
| 404 | 订单不存在 |
| 400 | 订单号不能为空 |
### 调用说明
请使用上一节 Python 示例生成签名后请求;仅带 `X-Api-Key` 而不带签名会被拒绝。
| HTTP / code | 含义 |
|-------------|------|
| 401 | 鉴权失败(Key/签名/时间/Nonce |
| 404 | 订单不存在 |
| 400 | 参数错误 |
---
## 4. 发货结果推送
## 4. 接口二:发货结果推送
### 请求
### 4.1 请求
```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
@@ -238,31 +199,29 @@ X-Sign: {hmac_sha256_hex}
}
```
### 请求字段
| 字段 | 必填 | 说明 |
|------|------|------|
| order_no | 是 | 店铺订单号 |
| ship_status | 是 | `success` / `failed` / `processing` |
| provider_order_no | 否 | 上游发货单号 |
| shipped_at | 否 | 发货时间,RFC3339success 时缺省用服务端时间 |
| fail_reason | 否 | 失败原因(failed 时建议填 |
| `order_no` | 是 | 店铺订单号 |
| `ship_status` | 是 | `success` / `failed` / `processing` |
| `provider_order_no` | 否 | 你们系统的发货单号 |
| `shipped_at` | 否 | RFC3339success 未传则用服务端时间 |
| `fail_reason` | 否 | 失败原因(failed 时建议填) |
### ship_status 订单状态映射
### 4.2 ship_status → 我们订单状态
| ship_status | 订单变为 | 说明 |
|-------------|---------|------|
| processing | delivering | 已接单/发货中 |
| success | delivered | 发货成功 |
| failed | ship_failed | 发货失败,允许再次查询重试 |
| `processing` | `delivering` | 已接单 / 发货中 |
| `success` | `delivered` | 发货成功 |
| `failed` | `ship_failed` | 失败,允许之后再次查询重试 |
### 幂等
### 4.3 幂等与限制
- 订单已是 `delivered`,再次推送 `success`**返回成功**,不重复处理
- 订单已是 `delivered`,推 `failed` → 拒绝
- 订单 `cancelled` → 拒绝更新
- 订单已是 `delivered`,再 `success`**返回成功**,不重复处理
- 订单已是 `delivered`,推 `failed` → 拒绝
- 订单 `cancelled` → 拒绝更新
### 成功响应
### 4.4 成功响应示例
```json
{
@@ -276,48 +235,50 @@ X-Sign: {hmac_sha256_hex}
}
```
### 调用说明
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
拿到店铺订单号 order_no
GET 订单查询
can_ship == false → 停止,展示 cannot_ship_reason
↓ true
(可选)POST ship_status=processing
按 product.sku 发货
POST ship_status=success 或 failed
```
发货务必使用查询结果中的 **`product.sku`**,不要依赖中文名。
**发货请以 `product.sku` 为准**,不要依赖中文名。
---
## 6. 联调检查清单
- [ ] 缺少 Sign / 错误 Secret 返回 401
- [ ] 错误 ApiKey 返回 401
- [ ] 过期 Timestamp / 重复 Nonce 返回 401
- [ ] 不存在的订单号返回 404
- [ ] `pending` 订单 `can_ship=false`
- [ ] `paid` 订单 `can_ship=true` 且 sku 正确
- [ ] success 推送后状态为 delivered
- [ ] 只带 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
- [ ] failed 后可再 query 且 can_ship=true
---
## 7. 联系方式
## 7. 商品 sku 说明
接口问题、密钥申请、联调环境请联系店铺方技术对接人
商品英文 sku 对照见:`docs/商品英文名对照.md`
商品英文码与中文名对照由店铺维护(如:套装-糯粉咩咩 → `suit_pink_sheep`
查询接口返回的 `product.sku` 即当前订单应发商品。
---
## 8. 联系方式
接口问题、密钥申请、联调订单号请联系**店铺方技术对接人**。
本文档由店铺系统生成/维护,以店铺最新版本为准。