开放接口 · V1.0

开放接口设计说明

平台开出一家店,就把这家店作为对接租户。外部系统用同一套 AppKey / AppSecret 调用 ERP 仓储、跑腿发单、技师发单。商品库存、抢单池与小程序后台是同一份数据。

1. 接口请求模型

所有开放接口走同一套鉴权:请求头携带 AppKey、时间戳、随机串与签名。业务参数按动作不同,放在 Query(GET)或 JSON Body(POST)。

统一鉴权 每个请求都要带 X-App-Key / X-Timestamp / X-Nonce / X-Sign,服务端校验通过后才进入业务。
按动作路由 URL 形如 /web/index.php?r=api/open/{模块}/{动作},模块为 erp、errand、worker。
租户范围 AppKey 绑定店铺。可选 mch_id 指定该店下的入驻商家;不传则操作平台自营(mch_id=0)。

1.1 接口地址

环境基础地址
当前站点{站点域名}/web/index.php?r=api/open/{模块}/{动作}
示例https://gw.kuaiyibu.net/web/index.php?r=api/open/erp/ping
请把域名换成你们实际部署域名。模块名:erp(仓储)、errand(跑腿)、worker(技师)。

1.2 系统级请求头(每个接口都必须)

请求头类型必须说明
X-App-Key字符串是店铺 AppKey,用于识别租户
X-Timestamp数字字符串是Unix 秒级时间戳,与服务器相差不超过 5 分钟
X-Nonce字符串是8~64 位字母或数字;同一 AppKey 下 5 分钟内不可重复
X-Sign字符串是HMAC-SHA256 十六进制小写签名
Content-Type字符串POST 时固定 application/json; charset=UTF-8

兼容写法:也可用 Query/Form 传 app_key、timestamp、nonce、sign(优先读请求头)。

2. 应用 KEY 与密钥管理

位置做什么
平台店铺列表店铺行「开放接口」:生成或重置 AppKey、AppSecret
商家后台仓储user/goods/stock/index 页「对外开放」:查看同一把店铺密钥
AppKey:可出现在请求里,用来认出是哪家店。
AppSecret:只留在对接方服务器,用来算签名。禁止写入前端、小程序、日志明文或传给第三方。

3. 签名算法

3.1 待签名字符串

三行文本,用换行符 \n 连接(最后一行也要有,GET 无 Body 时第三行为空字符串):

timestamp + "\n" + nonce + "\n" + rawBody

3.2 计算公式

X-Sign = hex_lowercase( HMAC_SHA256( key=AppSecret, message=待签名字符串 ) )

3.3 手算示例(GET)

假设 AppSecret=skdemo,Timestamp=1730000000,Nonce=Ab12Cd34,GET 无 Body:

待签名 =
1730000000
Ab12Cd34
(第三行为空)

X-Sign = HMAC-SHA256("skdemo", "1730000000\nAb12Cd34\n")
       → 小写十六进制

3.4 手算示例(POST)

Body 原文为(注意一字不差):

{"mch_id":0,"bill_type":"in","post_now":"1","lines":[{"goods_id":12,"attr_id_list":[3],"qty":10}]}
待签名 =
1730000000
Ab12Cd34
{"mch_id":0,"bill_type":"in","post_now":"1","lines":[{"goods_id":12,"attr_id_list":[3],"qty":10}]}
常见失败原因:Body 先 JSON.stringify 再改字段、多空格、用 Form 却按 JSON 签名、时间戳用了毫秒、Sign 写成了大写。

4. 公共约定

4.1 商家范围 mch_id

传参方式说明
Query mch_idGET / POST 均可
JSON Body mch_idPOST 时;Query 未传则读 Body
都不传默认 0 = 平台自营商品/单据

传入的 mch_id 必须属于该 AppKey 对应店铺,否则返回「商家不属于该店铺」。

4.2 统一返回结构

{
  "code": 0,
  "msg": "ok",
  "data": { }
}
字段说明
code0 成功;非 0 失败(多数为 1)
msg说明文案;失败时为原因
data业务数据;部分接口成功时另有 bill_id 等顶层字段

5. 接口一览

模块动作方法作用
ERP
api/open/erp/
pingGET核对密钥与 ERP 开关
goodsGET商品列表(最多 50)
skusGET某商品规格与库存
billsGET最近库存单据
stockPOST提交入库 / 出库 / 盘点
跑腿
api/open/errand/
typesGET跑腿场景列表
orderPOST发单进抢单池
queryGET查订单状态
cancelPOST未接单时取消
技师
api/open/worker/
goodsGET服务项目列表
workersGET技师列表
orderPOST发单待接
queryGET查订单状态
cancelPOST未接单时取消

跑腿、技师完整字段说明见:跑腿发单、技师发单。下文详细展开 ERP。

6. ERP 仓储接口

基础路径:/web/index.php?r=api/open/erp/{动作}。须先在对应商家或平台自营商品上开启「ERP 仓储」,库存单据才会写入;过账后小程序可售库存同步变化。

6.1 连通性检测 ping

GET api/open/erp/ping 需签名

应用场景

对接联调时核对 AppKey/签名是否正确,并确认当前 mch 下 ERP 是否已开启。

应用级输入参数

名称位置必须说明
mch_idQuery否商家 ID,默认 0

返回 data

字段说明
store_id店铺 ID
store_name店铺名称
mch_id本次生效的商家 ID
erp_enabled是否已开启 ERP 仓储

返回示例

{
  "code": 0,
  "msg": "ok",
  "data": {
    "store_id": 1,
    "store_name": "示例店铺",
    "mch_id": 0,
    "erp_enabled": true
  }
}

6.2 商品列表 goods

GET api/open/erp/goods 需签名

应用场景

拉取当前 mch 下普通商品,用于对接方对照 goods_id。

应用级输入参数

名称位置必须说明
mch_idQuery否商家 ID,默认 0
qQuery否按商品名称模糊搜索

返回 data(数组,最多 50 条)

字段说明
id商品 ID(后续 skus / stock 用)
name商品名称
cover_pic封面图
price售价
goods_num总库存汇总

6.3 规格库存 skus

GET api/open/erp/skus 需签名

应用场景

查询某商品各规格当前可售数量,得到提交库存单据所需的 attr_id_list。

应用级输入参数

名称位置必须说明
goods_idQuery是商品 ID
mch_idQuery否须与商品所属商家一致

返回 data 元素

字段说明
attr_id_list规格属性 ID 数组(已排序),提交 stock 时原样回传
label规格展示名,如「红色 / L」
num当前库存
pic规格图(可能为空)

返回示例

{
  "code": 0,
  "msg": "ok",
  "data": [
    { "attr_id_list": [3, 8], "label": "红色 / L", "num": 20, "pic": "" }
  ]
}

6.4 单据列表 bills

GET api/open/erp/bills 需签名

应用场景

查看最近库存单据(最多 30 条)。

应用级输入参数

名称位置必须说明
typeQuery否in 入库 / out 出库 / check 盘点;不传返回全部
mch_idQuery否商家 ID

返回 data 元素

字段说明
id单据 ID
bill_no单号
bill_typein / out / check
type_label类型中文
status / status_label状态与文案(草稿 / 已过账等)
remark备注
addtime创建时间 Unix 秒

6.5 提交库存单据 stock

POST api/open/erp/stock 需签名 · JSON Body

应用场景

外部 ERP / WMS 把入库、出库、盘点写入本店,可选立即过账以变更小程序可售库存。

提交方式

POST,Body 为 JSON。签名必须针对原始 Body 字符串计算。

应用级输入参数(JSON)

名称类型必须说明
mch_idint否商家 ID,默认 0
bill_typestring是in 入库;out 出库;check 盘点(qty=实盘数量)
remarkstring否单据备注
post_nowstring否1 或缺省:立即过账;0:只存草稿,不改可售库存
warehouse_idint否仓库 ID,未启用多仓可传 0
linesarray是明细行,至少 1 行;同一商品同一规格只能一行
lines[].goods_idint是商品 ID
lines[].attr_id_listarray是规格属性 ID 列表(与 skus 返回一致)
lines[].qtyint是数量;入库/出库须 >0;盘点为实盘数且 ≥0
lines[].unit_costnumber否成本单价
lines[].remarkstring否行备注

请求体示例

{
  "mch_id": 0,
  "bill_type": "in",
  "remark": "采购到货",
  "post_now": "1",
  "lines": [
    {
      "goods_id": 12,
      "attr_id_list": [3, 8],
      "qty": 10,
      "unit_cost": 0,
      "remark": ""
    }
  ]
}

返回结果

情形示例
立即过账成功{"code":0,"msg":"OK","bill_id":101}
仅存草稿{"code":0,"msg":"草稿已保存","bill_id":102}
未开 ERP{"code":1,"msg":"请先在商品列表开启「ERP 仓储模式」"}
参数错误{"code":1,"msg":"第 1 行:请选择规格"} 等

7. 跑腿发单(摘要)

完整参数与字段说明见 跑腿发单文档。基础路径:api/open/errand/{动作},鉴权同第 3 节。

动作方法要点
typesGET可选 biz_scene(默认 1=跑腿),返回场景 id / name
orderPOST必填 out_order_no、purchasing_type_id、fee、content、pickup、dropoff;同一 out_order_no 幂等
queryGETout_order_no 或 order_no
cancelPOST骑手未接单可取消;Body 传 out_order_no 或 order_no

发单 Body 示例

{
  "out_order_no": "WB20261026001",
  "purchasing_type_id": 3,
  "fee": 12.5,
  "rider_fee": 8,
  "expect_minutes": 90,
  "content": "文件一份,请当面签收",
  "remark": "门禁密码 1208",
  "pickup": {
    "name": "张三", "mobile": "13800000000",
    "address": "深圳市南山区某某路 1 号",
    "lng": "113.93", "lat": "22.54"
  },
  "dropoff": {
    "name": "李四", "mobile": "13900000000",
    "address": "深圳市南山区某某大厦",
    "lng": "113.95", "lat": "22.53"
  }
}

8. 技师发单(摘要)

完整说明见 技师发单文档。基础路径:api/open/worker/{动作}。

动作方法要点
goodsGET已上架服务项目
workersGET可选 goods_id 过滤会做该项目的技师
orderPOST必填 out_order_no、goods_id、fee、service_time、customer;worker_id=0 进抢单
query / cancelGET / POST同跑腿,技师已接单不可取消

发单 Body 示例

{
  "out_order_no": "HS20261026001",
  "goods_id": 18,
  "worker_id": 0,
  "fee": 128,
  "service_time": "2026-10-26 14:00:00",
  "content": "空调清洗一台",
  "customer": {
    "name": "王女士",
    "mobile": "13800000000",
    "address": "深圳市南山区某某路 1 号",
    "lng": "113.93",
    "lat": "22.54"
  }
}

9. 错误码与常见失败

codemsg 示例处理建议
1缺少 AppKey、时间戳、随机串或签名检查四个请求头是否齐全
1时间戳无效或已过期用 Unix 秒,与服务器时差 ≤5 分钟
1随机串格式无效 / 已使用8~64 位字母数字;每次请求换新 nonce
1AppKey 无效 / 尚未发放 AppSecret平台后台重新生成密钥
1签名不正确核对 AppSecret、Body 原文、换行拼接
1商家不属于该店铺mch_id 必须是本店商家
1请使用 POST / 请求体须为 JSONstock、发单、取消须 POST JSON
0ok / OK / 草稿已保存成功

10. 调用示例

10.1 PHP(推荐服务端)

<?php
$appKey = 'akxxxxxxxx';
$appSecret = 'skxxxxxxxxxxxxxxxx';
$timestamp = (string) time();
$nonce = bin2hex(random_bytes(8));
$body = ''; // GET 为空;POST 时填 JSON 原文
$sign = hash_hmac('sha256', $timestamp . "\n" . $nonce . "\n" . $body, $appSecret);

$url = 'https://你的域名/web/index.php?r=api/open/erp/ping';
$ch = curl_init($url);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'X-App-Key: ' . $appKey,
        'X-Timestamp: ' . $timestamp,
        'X-Nonce: ' . $nonce,
        'X-Sign: ' . $sign,
    ],
]);
echo curl_exec($ch);

10.2 Node.js

const crypto = require('crypto');
const appKey = 'akxxxxxxxx';
const appSecret = 'skxxxxxxxxxxxxxxxx';
const timestamp = String(Math.floor(Date.now() / 1000));
const nonce = crypto.randomBytes(8).toString('hex');
const rawBody = ''; // POST 时用发出去的 JSON 字符串
const sign = crypto.createHmac('sha256', appSecret)
  .update(`${timestamp}\n${nonce}\n${rawBody}`).digest('hex');

fetch('https://你的域名/web/index.php?r=api/open/erp/ping', {
  headers: {
    'X-App-Key': appKey,
    'X-Timestamp': timestamp,
    'X-Nonce': nonce,
    'X-Sign': sign,
  }
}).then(r => r.json()).then(console.log);

10.3 curl(POST 库存)

# 先在脚本里算出 SIGN,BODY 必须与签名用的字符串一致
BODY='{"mch_id":0,"bill_type":"in","post_now":"1","lines":[{"goods_id":12,"attr_id_list":[3],"qty":10}]}'
curl -X POST "https://你的域名/web/index.php?r=api/open/erp/stock" \
  -H "Content-Type: application/json; charset=UTF-8" \
  -H "X-App-Key: akxxxxxxxx" \
  -H "X-Timestamp: 1730000000" \
  -H "X-Nonce: Ab12Cd34Ef56" \
  -H "X-Sign: 算好的小写签名" \
  -d "$BODY"

相关文档