增加发货平台 2

This commit is contained in:
yml2213
2026-07-07 08:51:23 +08:00
parent fb0ec5085f
commit c101b5f359
10 changed files with 640 additions and 1 deletions
+39
View File
@@ -0,0 +1,39 @@
概述
面向未绑定电商店铺的商户:由您的业务系统调用 API 发单,平台扣减钱包积分后履约;终端玩家通过返回的 H5 链接填写游戏账号并完成后续步骤。
对接文档可直接阅读,无需验证。App Key / App Secret 请在 开放 API → API 密钥 或 调用调试 中查看(仅主账号):未开动态口令时输入登录密码,已开启时输入 Google Authenticator 6 位验证码。
#通用约定
项目 说明
基础路径 http://skin-exchange.yiquyou.icu
请求方式 本文档接口均为 POST
Content-Type application/json(请求体为 JSON,签名使用原始 body 字符串)
鉴权 Header X-App-Key、X-Timestamp、X-Sign,见 鉴权与签名
#接口一览
接口 路径
商品列表 POST /api/open/v1/products/index
创建订单 POST /api/open/v1/orders/store
查询订单 POST /api/open/v1/orders/show
余额查询 POST /api/open/v1/wallet/balance
#统一响应结构
字段 类型 说明
code number 0 表示成功,非 0 为业务失败
message string 提示文案
data object 业务数据,失败时可能为空对象
成功示例:
json
{
"code": 0,
"message": "success",
"data": {}
}
失败示例:
json
{
"code": 1,
"message": "钱包积分不足",
"data": {}
}
@@ -0,0 +1,17 @@
名词
名称 说明
platform_order_no 您的业务单号(幂等键,请勿重复用于不同订单)
order_no 平台生成的内部单号,查询详情或排查时使用
product_code 商品编码,来自「商品列表」接口,创建订单时必填
platform 开放 API 订单固定为 api
notify_url 下单时传入的异步通知地址,订单状态变更时 POST 推送
balance_points 商户钱包积分余额
#对接流程
code
1. 拉取商品列表 → 记录 product_code
2. 创建订单(扣积分)→ 获得 h5.recharge_url
3. 将 H5 链接发给买家填写 UID / 完成绑定
4. 轮询「查询订单」获取状态与玩家信息
下单前请保证钱包积分充足;余额不足时不会创建订单。
+231
View File
@@ -0,0 +1,231 @@
鉴权
每个请求在 Header 携带:
Header 说明
X-App-Key App Key
X-Timestamp Unix 秒级时间戳(字符串),与服务器相差不超过 5 分钟
X-Sign 签名(见下)
#签名算法
code
待签名字符串 = app_key + timestamp + raw_json_body
sign = HMAC_SHA256(待签名字符串, app_secret) // 小写十六进制
要点 说明
raw_json_body 与 HTTP Body 完全一致 的原始字符串;签名后再原样发送
是否排序 不需要对 JSON 字段排序;勿解析后再 json_encode 一次
Header app_key、timestamp 只放在 Header,不参与 Body JSON
中文 PHP 使用 JSON_UNESCAPED_UNICODEPython 使用 ensure_ascii=False
商户后台 开放 API → 调用调试 需先通过密码或动态口令验证,方可加载密钥并查看待签名字符串、签名结果与最终发包参数。
#PHP
php
<?php
$appKey = 'your_app_key';
$appSecret = 'your_app_secret';
$apiBase = 'http://skin-exchange.yiquyou.icu';
$payload = [
'platform_order_no' => 'YOUR-ORDER-001',
'product_code' => '10000001',
'platform_buy_num' => 1,
];
// 生成 body 后不要再改,签名与 curl 必须用同一字符串
$body = json_encode($payload, JSON_UNESCAPED_UNICODE);
$timestamp = (string) time();
$preSign = $appKey . $timestamp . $body;
$sign = hash_hmac('sha256', $preSign, $appSecret);
$ch = curl_init($apiBase . '/api/open/v1/orders/store');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $body,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'X-App-Key: ' . $appKey,
'X-Timestamp: ' . $timestamp,
'X-Sign: ' . $sign,
],
CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($ch);
curl_close($ch);
#Java
依赖 JDK 标准库(javax.crypto),无需第三方包。
java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
public class OpenApiSignExample {
private static String hmacSha256Hex(String secret, String message) throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
byte[] hash = mac.doFinal(message.getBytes(StandardCharsets.UTF_8));
StringBuilder hex = new StringBuilder(hash.length * 2);
for (byte b : hash) {
hex.append(String.format("%02x", b));
}
return hex.toString();
}
public static void main(String[] args) throws Exception {
String appKey = "your_app_key";
String appSecret = "your_app_secret";
String apiBase = "http://skin-exchange.yiquyou.icu";
// 与最终请求体一致;可用 Gson/Jackson 生成,但不要签名后再格式化
String body = "{\"platform_order_no\":\"YOUR-ORDER-001\",\"product_code\":\"10000001\",\"platform_buy_num\":1}";
String timestamp = String.valueOf(Instant.now().getEpochSecond());
String preSign = appKey + timestamp + body;
String sign = hmacSha256Hex(appSecret, preSign);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(apiBase + "/api/open/v1/orders/store"))
.header("Content-Type", "application/json")
.header("X-App-Key", appKey)
.header("X-Timestamp", timestamp)
.header("X-Sign", sign)
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode());
System.out.println(response.body());
}
}
#Python
Python 3hmac + hashlib 标准库。
python
import hashlib
import hmac
import json
import time
import urllib.request
APP_KEY = "your_app_key"
APP_SECRET = "your_app_secret"
API_BASE = "http://skin-exchange.yiquyou.icu"
payload = {
"platform_order_no": "YOUR-ORDER-001",
"product_code": "10000001",
"platform_buy_num": 1,
}
# separators 生成紧凑 JSON,与常见 PHP json_encode 一致;签名后勿再 dumps
body = json.dumps(payload, ensure_ascii=False, separators=(",", ":"))
timestamp = str(int(time.time()))
pre_sign = APP_KEY + timestamp + body
sign = hmac.new(
APP_SECRET.encode("utf-8"),
pre_sign.encode("utf-8"),
hashlib.sha256,
).hexdigest()
req = urllib.request.Request(
API_BASE + "/api/open/v1/orders/store",
data=body.encode("utf-8"),
method="POST",
headers={
"Content-Type": "application/json",
"X-App-Key": APP_KEY,
"X-Timestamp": timestamp,
"X-Sign": sign,
},
)
with urllib.request.urlopen(req) as resp:
print(resp.status)
print(resp.read().decode())
使用 requests 时:
python
import requests
resp = requests.post(
API_BASE + "/api/open/v1/orders/store",
data=body.encode("utf-8"), # 传 bytes,避免库再次序列化
headers={
"Content-Type": "application/json",
"X-App-Key": APP_KEY,
"X-Timestamp": timestamp,
"X-Sign": sign,
},
timeout=30,
)
print(resp.status_code, resp.text)
#Go
go
package main
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
func sign(appKey, appSecret, body string) (timestamp, sign string) {
timestamp = fmt.Sprintf("%d", time.Now().Unix())
mac := hmac.New(sha256.New, []byte(appSecret))
mac.Write([]byte(appKey + timestamp + body))
sign = hex.EncodeToString(mac.Sum(nil))
return
}
func main() {
appKey := "your_app_key"
appSecret := "your_app_secret"
apiBase := "http://skin-exchange.yiquyou.icu"
// 建议用 struct + json.Marshal;保证签名用的 body 与 Post 发送的字节一致
type orderReq struct {
PlatformOrderNo string `json:"platform_order_no"`
ProductCode string `json:"product_code"`
PlatformBuyNum int `json:"platform_buy_num"`
}
bodyBytes, _ := json.Marshal(orderReq{
PlatformOrderNo: "YOUR-ORDER-001",
ProductCode: "10000001",
PlatformBuyNum: 1,
})
body := string(bodyBytes)
timestamp, sig := sign(appKey, appSecret, body)
req, _ := http.NewRequest(http.MethodPost, apiBase+"/api/open/v1/orders/store", bytes.NewReader(bodyBytes))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-App-Key", appKey)
req.Header.Set("X-Timestamp", timestamp)
req.Header.Set("X-Sign", sig)
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
out, _ := io.ReadAll(resp.Body)
fmt.Println(resp.Status, string(out))
}
#常见错误
现象 原因
签名校验失败 Body 与签名时不一致(多空格、字段顺序、Unicode 转义不同)
timestamp 无效或已过期 服务器时间差超过 5 分钟
缺少 app_key、timestamp 或 sign Header 名称或大小写错误(应为 X-App-Key 等)
排查:用商户后台 调用调试 对比「待签名字符串」与己方代码输出是否一致。
@@ -0,0 +1,68 @@
商品列表
拉取当前商户已授权且可售商品,默认仅 on_sale。下单时使用 data.list[].product_code(供货商品编码)。
#接口地址
code
POST /api/open/v1/products/index
#请求方式
POST
#请求参数
Body 为 JSON 对象:
参数名 类型 必填 说明
page number 否 页码,默认 1
per_page number 否 每页条数,默认 20,最大 100
filters object 否 筛选条件
filters.status string 否 on_sale(默认)/ off_sale / draft / all
filters.supply_product_name string 否 供货商品名称,模糊搜索
#响应参数
data 字段说明:
参数名 类型 说明
data.list array 商品列表
data.list[].product_code string 商品编码(下单必填,见创建订单)
data.list[].name string 商品名称
data.list[].channel string 供货渠道
data.list[].external_product_id string | null 外部商品 ID
data.list[].image_url string 商品图 URL,无图时为空字符串
data.list[].status string 商户商品在售状态:on_sale / off_sale / draft
data.list[].supply_status string 平台供货商品状态:on_sale / off_sale / draft
data.list[].supply_price_points number 供货价(积分)
data.list[].fee_points number 手续费(积分)
data.list[].unit_cost_points number 单笔扣费积分 = 供货价 + 手续费
data.list[].max_order_quantity number 单笔最大购买数量(platform_buy_num 上限)
data.list[].sale_price_points number | null 商户自定售价(积分),可空
data.total number 总条数
data.page number 当前页码
data.per_page number 每页条数
下单扣费:unit_cost_points × platform_buy_numplatform_buy_num 见创建订单接口,且不得超过 max_order_quantity)。
#响应示例
json
{
"code": 0,
"message": "success",
"data": {
"list": [
{
"product_code": "10000300",
"name": "和平精英点券",
"channel": "lewan",
"external_product_id": "EXT-001",
"image_url": "https://example.com/p.png",
"status": "on_sale",
"supply_status": "on_sale",
"supply_price_points": 3000,
"fee_points": 800,
"unit_cost_points": 3800,
"max_order_quantity": 99,
"sale_price_points": null
}
],
"total": 1,
"page": 1,
"per_page": 20
}
}
@@ -0,0 +1,104 @@
创建订单
按 product_code(商品编码)下单并扣减钱包积分。相同 platform_order_no 重复请求返回原订单(幂等)。
#接口地址
code
POST /api/open/v1/orders/store
#请求方式
POST
#请求参数
Body 为 JSON 对象:
参数名 类型 必填 说明
platform_order_no string 是 您的业务单号(幂等键,最长 64 字符)
product_code string 是 商品列表返回的 product_code
platform_buy_num number 否 购买数量,默认 1;不得超过商品列表返回的 max_order_quantity
platform_amount number 否 平台侧记录用金额,默认 0
player_account string 否 玩家 UID;不传则由买家在 H5 填写
player_game_region string 否 区服-系统(如 安卓 / 苹果)
player_game_srv string 否 区服-渠道(如 微信 / QQ)
player_game_role string 否 角色名
submit_player boolean 否 true 且已传 player_account 时,直接提交玩家信息(状态 17);否则待买家在 H5 操作(状态 15)
notify_url string 否 订单状态变更时的异步通知地址(http/https,最长 500 字符),见 订单异步通知
#响应参数
data.order 字段说明:
参数名 类型 说明
data.order.order_no string 平台内部单号
data.order.platform_order_no string 您的业务单号
data.order.platform string 固定为 api
data.order.product_name string 商品名称
data.order.recharge_status number 充值状态码(见下表)
data.order.recharge_status_label string 充值状态中文
data.order.status number 同 recharge_status
data.order.status_label string 同 recharge_status_label
data.order.points_charged number 本单扣除积分
data.order.player_account string 玩家 UID
data.order.created_at string 创建时间 YYYY-MM-DD HH:mm:ss
data.order.platform_buy_num number 购买数量
data.order.player_game_region string 区服-系统
data.order.player_game_srv string 区服-渠道
data.order.player_game_role string 角色名
data.order.recharge_bind_required number 是否需绑定 0 / 1
data.order.recharge_bind_url string | null 绑定链接(发货中且需绑定时可能有值)
data.order.recharge_bind_at string | null 绑定链接生成时间
data.order.recharge_submit_at string | null 提交充值时间
data.order.recharge_finish_at string | null 完成时间
data.order.recharge_result_message string | null 充值结果说明
data.order.updated_at string 更新时间
data.order.h5.entry_url string H5 兑换首页(买家手动输入单号,一般少用)
data.order.h5.recharge_url string 推荐:发给买家的充值/绑定链接
#状态码
值 含义
15 待买家在 H5 提交信息
17 买家已提交,处理中
20 发货中
30 成功
40 失败
50 需人工处理
60 已取消
开放 API 订单不含店铺券码/核销字段(platform_coupon_*、is_verifi 等)。
#响应示例
待买家在 H5 填写 UIDrecharge_status = 15):
json
{
"code": 0,
"message": "下单成功",
"data": {
"order": {
"order_no": "202601011200001234",
"platform_order_no": "YOUR-ORDER-001",
"platform": "api",
"product_name": "和平精英点券",
"recharge_status": 15,
"recharge_status_label": "待绑定",
"status": 15,
"status_label": "待绑定",
"points_charged": 3800,
"player_account": "",
"created_at": "2026-01-01 12:00:00",
"product_code": "10000001",
"platform_buy_num": 1,
"player_game_region": "",
"player_game_srv": "",
"player_game_role": "",
"recharge_bind_required": 0,
"recharge_bind_url": null,
"recharge_bind_at": null,
"recharge_submit_at": null,
"recharge_finish_at": null,
"recharge_result_message": null,
"updated_at": "2026-01-01 12:00:00",
"h5": {
"entry_url": "http://skin-exchange.yiquyou.icu/h5/exchange",
"recharge_url": "http://skin-exchange.yiquyou.icu/h5/bind?code=..."
}
}
}
}
已传 UID 且 submit_player: truerecharge_status = 17)时,recharge_url 可能指向绑定页或处理页,字段结构相同。
@@ -0,0 +1,85 @@
查询订单
按平台内部单号或您的业务单号查询订单详情,响应结构与「创建订单」的 data.order 一致。
#接口地址
code
POST /api/open/v1/orders/show
#请求方式
POST
#请求参数
Body 为 JSON 对象,二选一(不可同时为空):
参数名 类型 必填 说明
platform_order_no string 条件 您的业务单号(开放 API 订单)
order_no string 条件 平台内部单号
#响应参数
与 创建订单 的 data.order 字段相同,主要包括:
参数名 类型 说明
data.order.order_no string 平台内部单号
data.order.platform_order_no string 您的业务单号
data.order.platform string 固定为 api
data.order.product_name string 商品名称
data.order.recharge_status number 充值状态码
data.order.recharge_status_label string 充值状态中文
data.order.points_charged number 扣除积分
data.order.player_account string 玩家 UID
data.order.recharge_submit_at string | null 提交时间
data.order.recharge_finish_at string | null 完成时间
data.order.recharge_result_message string | null 结果说明
data.order.h5.entry_url string H5 兑换首页
data.order.h5.recharge_url string 充值/绑定 H5 链接
#状态码
值 含义
15 待买家在 H5 提交信息
17 买家已提交,处理中
20 发货中
30 成功
40 失败
50 需人工处理
60 已取消
#响应示例
json
{
"code": 0,
"message": "success",
"data": {
"order": {
"order_no": "202601011200001234",
"platform_order_no": "YOUR-ORDER-001",
"platform": "api",
"product_name": "和平精英点券",
"recharge_status": 30,
"recharge_status_label": "充值成功",
"status": 30,
"status_label": "充值成功",
"points_charged": 3800,
"player_account": "1234567890",
"created_at": "2026-01-01 12:00:00",
"product_code": "10000001",
"platform_buy_num": 1,
"player_game_region": "安卓",
"player_game_srv": "微信",
"player_game_role": "测试角色",
"recharge_bind_at": null,
"recharge_submit_at": "2026-01-01 12:01:00",
"recharge_finish_at": "2026-01-01 12:05:00",
"recharge_result_message": null,
"updated_at": "2026-01-01 12:05:00",
"h5": {
"entry_url": "http://skin-exchange.yiquyou.icu/h5/exchange",
"recharge_url": "http://skin-exchange.yiquyou.icu/h5/bind?code=..."
}
}
}
}
订单不存在时:
json
{
"code": 1,
"message": "订单不存在",
"data": {}
}
@@ -0,0 +1,24 @@
余额查询
查询当前商户钱包可用积分余额。
#接口地址
code
POST /api/open/v1/wallet/balance
#请求方式
POST
#请求参数
Body 可为空对象 {},仍需按 鉴权与签名 规则携带 Header 并签名。
#响应参数
参数名 类型 说明
data.balance_points number 当前可用积分余额
#响应示例
json
{
"code": 0,
"message": "success",
"data": {
"balance_points": 12345
}
}
@@ -0,0 +1,66 @@
订单异步通知
创建订单时可传入 notify_url。当该订单充值状态发生变化时,平台将向该地址 POST JSON 通知(队列异步发送,失败自动重试)。
#触发时机
下单成功创建订单后(首次状态,如 15 待绑定)
之后每次 recharge_status 变更(如 17 已提交、30 成功、40 失败等)
仅 开放 API 订单(platform=api)且下单时传了有效 notify_url 才会推送。
#商户接收要求
项目 说明
方法 POST
Content-Type application/json
成功响应 HTTP 2xx(建议返回 {"code":0} 或纯文本 OK
超时 平台默认 10 秒,超时或 5xx 将按队列重试(默认最多 5 次)
#签名
Header
Header 说明
X-App-Key 您的 App Key
X-Timestamp Unix 秒级时间戳
X-Sign HMAC_SHA256(app_key + timestamp + raw_json_body, app_secret) 小写 hex
验签时使用与请求开放 API 完全相同的原始 Body 字符串,勿重新格式化 JSON。
#通知 Body 结构
json
{
"event": "order.status_changed",
"order": {
"order_no": "202601011200001234",
"platform_order_no": "YOUR-ORDER-001",
"platform": "api",
"product_code": "10000001",
"product_name": "和平精英点券",
"recharge_status": 30,
"recharge_status_label": "充值成功",
"status": 30,
"status_label": "充值成功",
"points_charged": 3800,
"player_account": "123456789",
"notify_url": "https://your-server.com/open-api/order-notify",
"recharge_finish_at": "2026-01-01 12:30:00",
"updated_at": "2026-01-01 12:30:00"
}
}
order 字段与「查询订单」接口结构基本一致(通知中不含 h5 链接;含 product_code)。
#PHP 验签示例
php
$rawBody = file_get_contents('php://input');
$appKey = $_SERVER['HTTP_X_APP_KEY'] ?? '';
$timestamp = $_SERVER['HTTP_X_TIMESTAMP'] ?? '';
$sign = $_SERVER['HTTP_X_SIGN'] ?? '';
$appSecret = 'your_app_secret';
$expected = hash_hmac('sha256', $appKey . $timestamp . $rawBody, $appSecret);
if (!hash_equals($expected, $sign)) {
http_response_code(401);
exit('invalid sign');
}
$data = json_decode($rawBody, true);
// 处理 $data['order'] ...
http_response_code(200);
echo json_encode(['code' => 0]);
+5
View File
@@ -0,0 +1,5 @@
常见错误
情况 说明
code 非 0 业务失败(签名错误、商品不可售、余额不足等)
余额不足 不写入订单,请充值积分后使用新单号重试
重复 platform_order_no 返回已存在订单及当前状态,不会重复扣款
+1 -1
View File
@@ -253,7 +253,7 @@ npm run test:industry:http -- --baseUrl=https://ks.khhao.com --load --endpoint=q
```bash
# .env 生产配置
KUASHOU_INDUSTRY_APP_KEY=ks660621772091030245
KUASHOU_INDUSTRY_APP_KEY=ks66xxx
KUASHOU_INDUSTRY_SIGN_SECRET=OZR3AUW2...
KUASHOU_INDUSTRY_ACCESS_TOKEN=ChFvYXV0aC5hY2Nlc3NUb2tlbh... # OAuth 获取
KUASHOU_INDUSTRY_SEND_CALLBACK_ENABLED=true