店铺共享 API 文档

通过 API 接口对接供货商商品,实现自动查询、库存同步、下单购买。

自动化交易

通过 API 自动完成商品查询、库存检查、下单购买

灵活定价

支持固定加价和百分比加价两种模式

库存同步

实时库存查询,支持缓存机制

安全认证

基于 MD5 签名的认证机制

API 凭证

你的凭证信息

app_id
登录用户后台 → 我的主页 → API 接口凭证
app_key
登录用户后台 → 我的主页 → API 接口凭证
妥善保管你的 app_key,不要泄露给他人。所有 API 请求都需要携带这两个参数。
基础 URL:https://你的域名/shared  |  传输方式:HTTP POST  |  数据格式:application/x-www-form-urlencoded

签名算法

所有 API 请求都需要携带 sign 参数,签名计算步骤如下:

  1. 将所有请求参数(包括 app_id 和 app_key)放入数组
  2. 移除 sign 字段(如果存在)
  3. 按照参数名字典序排序(ksort)
  4. 移除所有值为空字符串的参数
  5. 拼接成 URL 查询字符串,末尾追加 &key=你的app_key
  6. 对整个字符串进行 URL 解码
  7. 计算 MD5 值,即为签名
function generateSignature(array $data, string $appKey): string {
    unset($data['sign']);
    ksort($data);
    foreach ($data as $key => $val) {
        if ($val === '') unset($data[$key]);
    }
    return md5(urldecode(http_build_query($data) . "&key=" . $appKey));
}
import hashlib
from urllib.parse import urlencode, unquote

def generate_signature(data: dict, app_key: str) -> str:
    if 'sign' in data:
        del data['sign']
    sorted_data = {k: v for k, v in sorted(data.items()) if v != ''}
    sign_string = unquote(urlencode(sorted_data) + "&key=" + app_key)
    return hashlib.md5(sign_string.encode('utf-8')).hexdigest()
const crypto = require('crypto');

function generateSignature(data, appKey) {
    delete data.sign;
    const sortedKeys = Object.keys(data).sort();
    const params = [];
    sortedKeys.forEach(key => {
        if (data[key] !== '') {
            params.push(`${key}=${encodeURIComponent(data[key])}`);
        }
    });
    const signString = decodeURIComponent(params.join('&') + '&key=' + appKey);
    return crypto.createHash('md5').update(signString).digest('hex');
}

API 端点

POST /shared/authentication/connect

测试 API 连接是否正常,获取店铺名称和余额。

请求参数

参数类型必填说明
app_idstring必填商户 ID
app_keystring必填商户密钥
signstring必填签名

响应示例

{
    "code": 200,
    "msg": "success",
    "data": {
        "shopName": "示例店铺",
        "balance": 1000.50
    }
}
POST /shared/commodity/items

获取所有可供共享的商品列表(含分类结构)。

请求参数

参数类型必填说明
app_idstring必填商户 ID
app_keystring必填商户密钥
signstring必填签名

响应示例

{
    "code": 200,
    "data": [
        {
            "id": 1,
            "name": "游戏账号",
            "children": [
                {
                    "id": 101,
                    "code": "PROD001",
                    "name": "王者荣耀账号",
                    "description": "包含多个皮肤",
                    "price": 100.00,
                    "user_price": 95.00,
                    "factory_price": 90.00,
                    "cover": "https://your-domain.com/uploads/cover.jpg",
                    "delivery_way": 0,
                    "contact_type": 0,
                    "password_status": 0,
                    "sort": 0,
                    "config": "category[普通]=100.00\ncategory[高级]=200.00",
                    "widget": "[{\"cn\":\"区服\",\"name\":\"server\",\"type\":\"select\"}]",
                    "draft_status": 0,
                    "inventory_hidden": 0,
                    "only_user": 0,
                    "purchase_count": 0,
                    "minimum": 1,
                    "seckill_status": 0
                }
            ]
        }
    ]
}

字段说明

外层数组为分类列表,每个分类包含 id(分类ID)、name(分类名)和 children(商品数组)。

商品字段如下:

字段类型说明
idint商品 ID
codestring商品唯一标识码(购买时使用)
namestring商品名称
descriptionstring商品描述(可能包含 HTML)
pricefloat普通用户价格
user_pricefloat会员价格
factory_pricefloat成本价(你的拿货价)
coverstring商品封面图 URL
delivery_wayint发货方式:0=自动发货,1=手动发货
contact_typeint联系方式类型:0=可选,1=必填邮箱,2=必填手机
password_statusint是否需要查询密码:0=否,1=是
sortint排序权重
configstring商品配置(INI 格式,含种类和价格,详见下方说明)
widgetstring自定义表单字段(JSON 格式,详见下方说明)
draft_statusint是否支持预选:0=否,1=是
inventory_hiddenint是否隐藏库存:0=显示,1=隐藏
only_userint是否仅会员可购买:0=所有人,1=仅会员
purchase_countint限购数量(0=不限购)
minimumint最低购买数量(默认1)
seckill_statusint是否为秒杀商品:0=否,1=是

config 字段详解

config 字段使用 INI 格式描述商品种类及其对应价格:

category[普通]=100.00
category[高级]=200.00
category[豪华]=300.00

解析方法(PHP):

$config = parse_ini_string($item['config']);
// 结果:['category' => ['普通' => '100.00', '高级' => '200.00', '豪华' => '300.00']]
// 每个 key 为种类名称,value 为该种类价格
// 购买时通过 race 参数传递用户选择的种类名称

widget 字段详解

widget 字段是一个 JSON 数组,定义了商品的自定义表单字段:

[
    {"cn": "游戏区服", "name": "server", "type": "select"},
    {"cn": "充值账号", "name": "account", "type": "input"}
]
属性说明
cn字段显示名称(中文标签)
name字段参数名(购买时作为额外参数传递)
type字段类型:input=文本输入,select=下拉选择

购买时将用户填写的值作为额外参数传递:

// 如果 widget 包含 {"name": "server", "cn": "游戏区服"}
// 则购买时额外传递:
$params['server'] = '艾欧尼亚';
POST /shared/commodity/item

根据商品 CODE 查询单个商品详情。

请求参数

参数类型必填说明
app_idstring必填商户 ID
app_keystring必填商户密钥
sharedCodestring必填商品 CODE
signstring必填签名

响应格式同商品列表,仅返回匹配的商品。

POST /shared/commodity/inventory

查询指定商品的库存数量和详细信息。

请求参数

参数类型必填说明
app_idstring必填商户 ID
app_keystring必填商户密钥
sharedCodestring必填商品 CODE
racestring可选商品种类
signstring必填签名

响应示例

{
    "code": 200,
    "data": {
        "count": 150,
        "delivery_way": 0,
        "draft_status": 0,
        "price": 100.00,
        "user_price": 95.00,
        "factory_price": 90.00,
        "config": "category[普通]=100.00\ncategory[高级]=200.00",
        "is_category": true
    }
}

字段说明

字段类型说明
countint当前库存数量
delivery_wayint发货方式:0=自动发货,1=手动发货
draft_statusint是否支持预选:0=否,1=是
pricefloat普通用户价格
user_pricefloat会员价格
factory_pricefloat成本价(你的拿货价)
configstring种类价格配置(INI 格式)
is_categorybool是否有多个种类(true 时需让用户选择种类后传 race 参数)
POST /shared/commodity/inventoryState

检查指定数量的商品是否有足够库存。成功返回 200,库存不足返回 500。

请求参数

参数类型必填说明
app_idstring必填商户 ID
app_keystring必填商户密钥
shared_codestring必填商品 CODE
numint必填购买数量
card_idint可选预选的卡密 ID
racestring可选商品种类
signstring必填签名
POST /shared/commodity/trade

购买商品并获取卡密信息。金额从你的账户余额中扣除。

请求参数

参数类型必填说明
app_idstring必填商户 ID
app_keystring必填商户密钥
shared_codestring必填商品 CODE
numint必填购买数量
request_nostring必填请求唯一标识(防重复下单)
contactstring可选联系方式
card_idint可选预选的卡密 ID
deviceint可选设备类型
passwordstring可选商品密码
racestring可选商品种类
signstring必填签名
request_no 用于防止重复下单,建议使用时间戳 + 随机数生成。如果商品有自定义表单字段(widget),需要作为额外参数传递。

响应示例

{
    "code": 200,
    "data": {
        "trade_no": "20250117123456789",
        "secret": "账号:xxx\n密码:xxx",
        "widget": {
            "server": "艾欧尼亚"
        },
        "status": 2
    }
}

响应字段说明

字段类型说明
trade_nostring订单号(可用于查询订单状态)
secretstring卡密信息(发货内容,自动发货时立即返回)
widgetobject|null自定义表单数据(如有)
statusint订单状态:0=待支付,1=已支付待处理,2=已完成
注意:手动发货的商品(delivery_way=1),status 可能为 1(已支付待处理),secret 为空,需要等供货商手动发货后通过订单查询接口获取卡密。

代码示例

$data = [
    'app_id' => $appId,  'app_key' => $appKey,
    'shared_code' => 'PROD001',  'num' => 1,  'race' => '普通',
    'request_no' => mt_rand(100,999) . date("ymdHis") . mt_rand(100,999),
    'contact' => '',  'card_id' => 0,  'device' => 0,  'password' => ''
];
$data['sign'] = generateSignature($data, $appKey);

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $domain . '/shared/commodity/trade');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
$result = json_decode(curl_exec($ch), true);
curl_close($ch);

if ($result['code'] == 200) {
    echo "订单号:" . $result['data']['trade_no'];
    echo "卡密:" . $result['data']['secret'];
}
POST /shared/commodity/draftCard

获取支持预选的商品的可选卡密列表。

请求参数

参数类型必填说明
app_idstring必填商户 ID
app_keystring必填商户密钥
sharedCodestring必填商品 CODE
pageint必填页码
limitint必填每页数量
racestring可选商品种类
signstring必填签名

响应示例

{
    "code": 200,
    "data": {
        "data": [
            { "id": 1001, "draft": "预览:账号等级100级" },
            { "id": 1002, "draft": "预览:账号等级80级" }
        ],
        "total": 50
    }
}
POST /shared/commodity/query

根据订单号查询订单状态和卡密信息。下单后使用此接口获取购买结果。

请求参数

参数类型必填说明
app_idstring必填商户 ID
app_keystring必填商户密钥
tradeNostring必填订单号(购买接口返回的 trade_no)
signstring必填签名

响应示例

{
    "code": 200,
    "msg": "success",
    "data": {
        "status": 1,
        "secret": "账号:xxx\n密码:xxx",
        "widget": null
    }
}

响应字段说明

字段类型说明
statusint订单状态:0=待支付,1=已完成,2=已关闭
secretstring卡密内容(订单完成后返回)
widgetobject|null自定义表单数据(如有)
提示:建议在调用购买接口后,轮询此接口获取订单结果。手动发货的商品 status 可能不会立即变为 1。

PHP SDK

完整的 PHP 集成类,复制即用。

class SharedAPIClient {
    private $domain, $appId, $appKey;

    public function __construct($domain, $appId, $appKey) {
        $this->domain = rtrim($domain, '/');
        $this->appId = $appId;
        $this->appKey = $appKey;
    }

    private function generateSignature($data) {
        unset($data['sign']);
        ksort($data);
        foreach ($data as $key => $val) {
            if ($val === '') unset($data[$key]);
        }
        return md5(urldecode(http_build_query($data) . "&key=" . $this->appKey));
    }

    private function post($url, $params = []) {
        $data = array_merge($params, ['app_id' => $this->appId, 'app_key' => $this->appKey]);
        $data['sign'] = $this->generateSignature($data);

        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $this->domain . $url);
        curl_setopt($ch, CURLOPT_POST, 1);
        curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_TIMEOUT, 30);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);

        $response = curl_exec($ch);
        $error = curl_error($ch);
        curl_close($ch);

        if ($error) throw new Exception("CURL Error: " . $error);
        $result = json_decode($response, true);
        if (!$result || $result['code'] != 200)
            throw new Exception($result['msg'] ?? 'Unknown error');
        return $result['data'] ?? [];
    }

    public function connect()      { return $this->post('/shared/authentication/connect'); }
    public function getItems()     { return $this->post('/shared/commodity/items'); }
    public function getItem($code) { return $this->post('/shared/commodity/item', ['sharedCode' => $code]); }

    public function getInventory($code, $race = '') {
        return $this->post('/shared/commodity/inventory', ['sharedCode' => $code, 'race' => $race]);
    }

    public function checkInventory($code, $num, $cardId = 0, $race = '') {
        return $this->post('/shared/commodity/inventoryState', [
            'shared_code' => $code, 'num' => $num, 'card_id' => $cardId, 'race' => $race
        ]);
    }

    public function purchase($code, $num, $options = []) {
        return $this->post('/shared/commodity/trade', array_merge([
            'shared_code' => $code, 'num' => $num,
            'request_no' => mt_rand(100,999) . date("ymdHis") . mt_rand(100,999),
            'contact' => '', 'card_id' => 0, 'device' => 0, 'password' => '', 'race' => ''
        ], $options));
    }

    public function getDraftCards($code, $page = 1, $limit = 20, $race = '') {
        return $this->post('/shared/commodity/draftCard', [
            'sharedCode' => $code, 'page' => $page, 'limit' => $limit, 'race' => $race
        ]);
    }
}

// 使用示例
$client = new SharedAPIClient('https://供货商域名', '你的app_id', '你的app_key');

$info = $client->connect();
echo "店铺:" . $info['shopName'] . "  余额:¥" . $info['balance'];

$order = $client->purchase('PROD001', 1, ['race' => '普通']);
echo "卡密:" . $order['secret'];

Python SDK

import requests, hashlib, time, random
from urllib.parse import urlencode, unquote

class SharedAPIClient:
    def __init__(self, domain, app_id, app_key):
        self.domain = domain.rstrip('/')
        self.app_id = app_id
        self.app_key = app_key

    def _sign(self, data):
        if 'sign' in data: del data['sign']
        sorted_data = {k: v for k, v in sorted(data.items()) if v != ''}
        return hashlib.md5(
            unquote(urlencode(sorted_data) + "&key=" + self.app_key).encode()
        ).hexdigest()

    def post(self, url, params=None):
        data = {**(params or {}), 'app_id': self.app_id, 'app_key': self.app_key}
        data['sign'] = self._sign(data)
        r = requests.post(f"{self.domain}{url}", data=data, timeout=30).json()
        if r.get('code') != 200: raise Exception(r.get('msg', 'Unknown error'))
        return r.get('data', {})

    def connect(self):    return self.post('/shared/authentication/connect')
    def get_items(self):  return self.post('/shared/commodity/items')

    def get_inventory(self, code, race=''):
        return self.post('/shared/commodity/inventory', {'sharedCode': code, 'race': race})

    def purchase(self, code, num, **opts):
        return self.post('/shared/commodity/trade', {
            'shared_code': code, 'num': num, 'contact': '', 'card_id': 0,
            'device': 0, 'password': '', 'race': '',
            'request_no': f"{random.randint(100,999)}{int(time.time())}{random.randint(100,999)}",
            **opts
        })

# 使用示例
client = SharedAPIClient('https://供货商域名', '你的app_id', '你的app_key')
info = client.connect()
print(f"店铺:{info['shopName']}  余额:¥{info['balance']}")

order = client.purchase('PROD001', 1, race='普通')
print(f"卡密:{order['secret']}")

常见问题

如何获取 app_id 和 app_key?
登录用户后台 → 我的主页 → API 接口凭证,即可看到你的 app_id 和 app_key。
签名验证总是失败怎么办?

排查步骤:

  1. 确认 app_key 没有多余的空格
  2. 确认参数按字典序排序(ksort)
  3. 确认空值参数已被移除
  4. 确认字符串做了 URL 解码(urldecode)后再 MD5
  5. 确认签名字段本身没有参与签名计算
如何设置加价?

固定金额加价:最终价 = 原价 + 加价金额(例:原价 100 + 加价 10 = 110 元)

百分比加价:最终价 = 原价 × (1 + 百分比)(例:原价 100 × 1.2 = 120 元)

如何处理自定义表单(widget)?

从商品信息的 widget 字段解析出表单配置(JSON),购买时将用户填写的值作为额外参数传递。

// 如果 widget 包含 {"name": "server", "cn": "游戏区服"}
// 则购买时额外传递:
$params['server'] = '艾欧尼亚';
商品有多个种类怎么处理?

商品种类在 config 字段中(INI 格式):

category[普通]=100.00
category[高级]=200.00
category[豪华]=300.00

购买时通过 race 参数传递用户选择的种类名称。

API 有频率限制吗?

没有硬性限制,建议:查询接口每秒不超过 10 次,购买接口使用 request_no 防重复,库存查询建议开启缓存。

如何测试 API 是否正常?

使用 curl 快速测试:

curl -X POST "https://你的域名/shared/authentication/connect" \
  -d "app_id=你的ID" \
  -d "app_key=你的KEY" \
  -d "sign=计算得到的签名"

预期返回:{"code":200,"msg":"success","data":{"shopName":"xxx","balance":xxx}}

商品列表返回为空?

可能原因:

  1. 供货商没有开启 API 共享的商品(需供货商在后台开启商品的 API 共享状态)
  2. 商品已下架(status 不是上架状态)
  3. 商品所属分类被禁用
  4. app_id 或 app_key 不正确,连接的不是目标店铺

建议先调用 /shared/authentication/connect 确认连接正常,再排查商品设置。

购买时提示"库存不足"?

可能原因:

  1. 自动发货商品:供货商的卡密已售完,需要供货商补充库存
  2. 种类选择错误:如果商品有多个种类(config 中有 category),确保 race 参数传递了正确的种类名称
  3. 库存同步延迟:如果开启了库存缓存,可能存在短暂的缓存延迟(默认 300 秒刷新)

建议先调用 /shared/commodity/inventoryState 检查库存状态后再下单。

购买失败但余额被扣除了?

这种情况通常不会发生,系统使用 request_no 防止重复下单。如果遇到:

  1. 先通过 /shared/commodity/query 查询订单状态,确认订单是否实际已完成
  2. 手动发货的商品(delivery_way=1)可能处于"已支付待处理"状态,等待供货商发货即可
  3. 如确认异常,请联系供货商处理退款
如何实现库存同步?

系统提供两种库存同步方式:

方式一 — 实时查询:每次用户访问商品页面时调用 /shared/commodity/inventory 获取最新库存。优点是数据准确,缺点是请求频繁。

方式二 — 定时缓存:开启库存同步后,系统自动每 300 秒刷新一次库存缓存。适合访问量大的场景,减少对供货商的请求压力。

建议:小流量用实时查询,大流量用定时缓存。

错误码

code说明
200请求成功
401认证失败(app_id 或 app_key 错误、签名验证失败)
404资源不存在(商品不存在、订单不存在等)
500业务错误(具体原因见 msg 字段)

常见错误信息

msg说明
sign error签名验证失败,检查签名算法
库存不足商品库存不够
The request ID already existsrequest_no 重复,换一个唯一标识
商品不存在shared_code 不正确
 安全提示:妥善保管你的 app_key,不要在前端代码中暴露。建议使用 HTTPS 传输,所有 API 调用应在服务端完成。