这个功能用来做什么
把商城里的卡密、直充商品作为虚拟货源,供闲管家商户采购。商城按真实订单扣款和履约,返回卡密或充值结果。本功能与闲管家的进销存、闲鱼店铺销售管理 API 相互独立。
每笔采购扣的是店主绑定的商城会员账户余额。 店主需先给自己的商城账户充值。外部买家付款不等于商城采购已付款;不会直接扣外部平台钱包,也不会自动从店铺销售收入划款。
卡密成功后返回实际分配的卡密;直充必须由真实适配器或人工确认实际履约,不能仅凭请求受理就当作充值成功。
开始前准备什么
| 谁准备 | 内容 |
|---|---|
| 平台对接人员 | 向闲管家申请虚拟货源应用,取得 app_id、app_secret,确认测试商户、货源地址和每单官方回调地址 |
| 商城管理员 | 可用 HTTPS 域名、有效商城运行授权、正确的履约加密配置 |
| 商城管理员 | 已发布且可售的商品、实际商品/SKU ID、卡池或直充履约方式 |
| 店主 | 正常商城会员、渠道供货权限、足够商城余额,明确所属主站或分站 |
先分清两套凭据:app_id/app_secret 是应用凭据,由平台提供并配置在商城网关;mch_id/mch_secret 是会员确认余额采购后由商城生成的商户凭据。mch_id 不是会员 ID,也不能随意编造。
渠道供货权限与客户开放 API 权限独立。无需先生成开放 API key/secret,开放其中一类权限也不会自动开放另一类。
在哪里配置
管理员进入商城后台「用户管理 → 电商渠道供货」:
- 在「客户权限」找到店主会员,设为「手动允许」并保存;也可设置默认开放,未单独设置的客户跟随默认。客户单独允许/禁止优先,会员停用仍会拒绝采购。
- 进入「供货网关」,在「闲管家虚拟货源」一行点击「配置」。初始网关和「发送订单回调」开关均关闭。
- 填写 App ID、App Secret、授权有效天数和单笔上限。App ID 使用平台提供的正整数字符串;有效期 1–90 天、默认 30 天;上限单位是整数分,范围 1–100,000,000 分。
- 添加「供货编码 → 商城商品 ID → 商城 SKU ID」映射并启用,最多 200 个映射。供货编码不可重复,使用 1–80 位英文字母、数字、
_、-。 - 核对商品条件及测试账户余额,受控联调时启用网关,之后由店主确认授权。需要异步通知时再开启「发送订单回调」。
商品条件:卡密必须有真实可用卡池,且不能要求额外下单资料;直充须采用有限库存,商品下单模板包含 account,模板字段只支持 text、textarea。商品还须已发布、位于该会员和分站可售范围,且有实际可用报价。
App Secret 留空保存保留原值,后台不回显明文。配置、商户密钥、通知地址、卡密按记录作用域加密保存。
看不到页面或按钮时
| 操作 | 所需后台权限 |
|---|---|
| 读取配置、进入页面 | site.settings.read |
| 保存配置、修改默认开放规则 | site.settings.write |
| 查看客户权限和商户绑定 | members.read |
| 修改客户单独权限 | members.write |
| 查看订单、查询原订单 | members.read、orders.read、payments.read、fulfillment.read |
| 重试回调 | 上述查单权限,加 fulfillment.write |
| 撤销他人绑定 | members.read、members.write |
finance.read 不能代替支付读取权限。开通客户权限、启用网关和会员同意采购是三步,任何一步都不能代替另外两步。
店主怎样绑定、更新和撤销
- 登录采购所属主站或分站的商城会员,打开「开放 API / 接口管理 → 电商渠道授权 → 闲管家供货授权」。直接地址:
https://商城域名/developer/channel-supplier.html?provider=xianguanjia- 未登录时点击「登录商城账户」,登录后回到授权页重新读取账户。
- 核对会员、当前站点和单笔采购上限。勾选允许使用本商城余额采购的同意项,再点击「确认授权」。仅访问页面或登录不会自动授权。
- 页面显示
mch_id和仅本次展示的mch_secret,将它们填入闲管家的货源商户配置。刷新页面不会再次显示明文密钥;遗失后应重新授权并更新平台凭据。
mch_id 绑定租户、站点、渠道和真实会员。重新授权替换旧密钥;授权到期或撤销后旧凭据失效。会员在同页「撤销授权」,管理员在「商户绑定」页签撤销。授权、撤销要求本站 Origin,拒绝跨站浏览器提交。
撤销不撤回已经完成的扣款或交付。需要善后时使用商城订单与工单,不要假设撤销后外部查单、回调仍可继续。
第一次怎样确认成功
- 网关启用后,先读取
open/info,核对 App ID 和能力声明。 - 绑定商户后读取
user/info,确认返回余额与店主商城账户一致。balance=10000表示 100.00 元。 - 查询列表和详情,确认供货编码、类型、售价、库存;直充商品还要核对
template的账号字段。 - 发起一笔已确认的测试采购,记录
order_no与返回的out_order_no。请求将可能真实扣商城余额。 - 查看
order_status:10要继续查原单;20才表示交付成功;30表示失败。不能仅凭 HTTP 200 或code=0判断成功。 - 在商城「我的订单」「资金中心」核对一笔订单和一次扣款。卡密核对实际内容和条数;直充核对实际充值结果。开启回调后还需确认后台通知状态为「已确认」。
未开启回调时仍可查询订单,采购也仍可能扣款;新采购依然必须提供合法的 notify_url 和 biz_order_no,不能因为回调开关关闭而省略。
接口地址与约定
向平台登记:
baseUrl = https://商城域名/supplier/xianguanjia平台资料中的 /baseUrl 是占位符,不是商城真实 URL 的一部分。全部接口为 POST:
| 完整路径 | 用途 |
|---|---|
/supplier/xianguanjia/goofish/open/info | 平台信息,不要求商户签名 |
/supplier/xianguanjia/goofish/user/info | 当前绑定会员真实余额 |
/supplier/xianguanjia/goofish/goods/list | 可售商品、售价与库存 |
/supplier/xianguanjia/goofish/goods/detail | 商品详情及直充模板 |
/supplier/xianguanjia/goofish/order/purchase/create | 卡密采购 |
/supplier/xianguanjia/goofish/order/recharge/create | 直充采购 |
/supplier/xianguanjia/goofish/order/detail | 原采购订单查询 |
除 open/info 外,Query 必须且只能包含 mch_id、timestamp、sign 各一次。业务参数放 UTF-8 JSON 正文,正文上限 64 KiB(65,536 字节),拒绝未知字段、重复字段及多个 JSON 对象。建议无业务参数时明确发送 {} 并按这两个字节签名。
每商户授权每服务端分钟最多接受 120 次请求。网关关闭时全部协议接口拒绝,包括 open/info。金额均为整数分,时间戳为 Unix 秒。
签名
bodyMd5 = hexLower(MD5(actualUTF8Body))
sign = hexLower(MD5(UTF8(app_id + "," + app_secret + "," + bodyMd5 + "," + timestamp + "," + mch_id + "," + mch_secret)))timestamp 使用当前十进制 Unix 秒,与商城服务器前后相差不超过 300 秒,不使用毫秒、前导零或 + 前缀。
对实际发送的原始 JSON 字节求 MD5。空白、换行、字段顺序改变都会改变签名,不能签名后重新序列化。六段之间用英文逗号连接,不是阿奇索的前后拼 secret,也不是商城开放 API 的 HMAC。
平台信息
POST /supplier/xianguanjia/goofish/open/info
Content-Type: application/json
{}{"code":0,"msg":"OK","data":{"app_id":677859093659717,"features":{"is_support_order_refund":false,"is_support_goods_notify":false,"is_support_loss_purchase":false}}}app_id 在响应中是数字,示例值不代表您的应用。当前不声明自动退款、商品变更通知、亏损采购能力;未实现可售券码及券码撤销。平台信息可读不代表商户签名和采购已经验收。
余额与商品
以下固定时间戳和全零签名只展示字段,实际请求必须动态签名。后续接口采用相同 Query 格式,各自按正文重算签名。
余额
POST /supplier/xianguanjia/goofish/user/info?mch_id=MERCHANT_ID×tamp=1893456000&sign=00000000000000000000000000000000
Content-Type: application/json
{}{"code":0,"msg":"OK","data":{"balance":10000}}balance 是绑定会员的商城余额,单位分。
商品列表
向 goods/list 发送:
{"page_no":1,"page_size":20,"keyword":"月卡","goods_type":2}| 字段 | 规则 |
|---|---|
page_no | 必填,1–10000 |
page_size | 必填,1–100 |
keyword | 可选,名称关键词或完整供货编码,最多 200 UTF-8 字节 |
goods_type | 1 直充、2 卡密;省略或 0 返回全部支持类型。值 3 可解析,但没有可售券码实现 |
{"code":0,"msg":"OK","data":{"list":[{"goods_no":"sku-example","goods_type":2,"goods_name":"月卡 / 标准版","price":2000,"stock":3,"status":1,"update_time":1893456000}],"count":1}}goods_no 是映射的供货编码;price 为单价分,stock 为实际可用库存,status=1 为返回的可售商品。count 用于分页;不要把第一页当全部。当前 update_time 使用商城生成本次商品视图的时间,不能当作可靠的商品变更游标。
商品详情与直充模板
向 goods/detail 发送:
{"goods_type":1,"goods_no":"recharge-example"}{"code":0,"msg":"OK","data":{"goods_no":"recharge-example","goods_type":1,"goods_name":"账户充值","price":2000,"stock":10,"status":1,"update_time":1893456000,"template":[{"code":"account","name":"充值账号","check":0}]}}类型必须与商品匹配。template[].code 是直充提交键,name 是显示名称;当前映射的 check=0 不意味着商城不校验必填、长度或其他购买条件。按真实商品要求提供资料。卡密通常不返回 template。
商品列表和详情不会预留库存;下单时仍检查实际报价、库存、实名、购买策略和销售时段。
卡密与直充采购
卡密采购
POST /supplier/xianguanjia/goofish/order/purchase/create?mch_id=MERCHANT_ID×tamp=1893456000&sign=00000000000000000000000000000000
Content-Type: application/json
{"order_no":"XGJ_ORDER_001","goods_no":"sku-example","buy_quantity":1,"max_amount":2000,"biz_order_no":"MARKET_ORDER_001","notify_url":"https://open.goofish.pro/api/open/callback/virtual/order/notify/PROVIDER_TOKEN"}| 字段 | 必填 | 规则 |
|---|---|---|
order_no | 是 | 采购业务号,1–80 位英文字母、数字、_、-;同一商户作用域的幂等号 |
goods_no | 是 | 已启用的供货编码,字符规则同 order_no |
buy_quantity | 是 | 整数 1–999,还须满足实际商品规则 |
max_amount | 否 | 本次整单安全价,正整数分;省略时仍受网关与会员授权单笔上限限制 |
biz_order_no | 是 | 外部业务订单号,字符规则同 order_no,不代替采购业务号去重 |
notify_url | 是 | 平台提供的每单官方完整 HTTPS 回调地址;即使关闭发送回调仍必填 |
product_id、product_sku、item_id | 否 | 整数关联字段,参与请求一致性校验,不改变商城 SKU 或扣款账户 |
biz_content | 仅直充必填 | 字符串键值对象,卡密采购不要发送充值资料 |
实际整单金额不能超过网关上限、会员授权上限和已传 max_amount 的最小值。max_amount 不是指定成交金额。notify_url 的 token 由平台逐单提供,商城不会猜测或全局复用它。
直充采购
使用 /supplier/xianguanjia/goofish/order/recharge/create,其余 Query 签名规则相同,正文增加 biz_content:
{"order_no":"XGJ_RECHARGE_001","goods_no":"recharge-example","buy_quantity":1,"max_amount":2000,"biz_order_no":"MARKET_ORDER_002","notify_url":"https://open.goofish.pro/api/open/callback/virtual/order/notify/PROVIDER_TOKEN","biz_content":{"account":"TARGET_ACCOUNT"}}biz_content.account 必须非空,biz_content 最多 20 个字段,值为字符串;其余键按商品真实模板提供。先取详情,不要把其他商品模板直接复用。直充会经过真实履约流程,请求受理不等于充值成功。
响应与成功判定
卡密实际交付成功:
{"code":0,"msg":"OK","data":{"order_type":2,"order_no":"XGJ_ORDER_001","out_order_no":"mall-supplier-order-id","order_status":20,"goods_no":"sku-example","goods_name":"月卡 / 标准版","buy_quantity":1,"order_amount":2000,"order_time":1893456000,"end_time":1893456001,"remark":"","card_items":[{"card_pwd":"ACTUAL_ALLOCATED_CARD"}]}}| 字段 | 含义 |
|---|---|
order_type | 1 直充,2 卡密 |
order_no | 您提交的采购业务号 |
out_order_no | 商城供货订单 ID,也是该笔商城订单 ID |
order_status | 10 处理中,20 交付成功,30 失败 |
order_amount | 记录中的整单金额,分;失败记录也可能保留该值 |
order_time | 创建时间,Unix 秒 |
end_time | 终态时间,处理中不返回 |
remark | 处理说明或失败原因 |
card_items | 仅卡密成功时返回,card_pwd 为实际卡密,条数应等于购买数量;可选 card_no |
处理中为 order_status=10,没有 end_time、card_items。直充 order_type=1 真实完成后才是 20,不返回卡密字段。明确失败示例:
{"code":0,"msg":"OK","data":{"order_type":2,"order_no":"XGJ_ORDER_001","out_order_no":"mall-supplier-order-id","order_status":30,"goods_no":"sku-example","goods_name":"月卡 / 标准版","buy_quantity":1,"order_amount":2000,"order_time":1893456000,"end_time":1893456001,"remark":"商城余额不足;本采购号已失败"}}code=0 只表示接口返回了结果,必须继续看 order_status。HTTP 200、order_amount 有值,都不能证明已经扣款或交付。
错误、幂等与查原单
主要协议错误:
{"code":1202,"msg":"采购金额超过授权上限或安全价"}{"code":1203,"msg":"记录已变更或同一采购号的参数不一致"}{"code":1209,"msg":"采购结果待确认,请查询原订单,勿重复采购"}其他参数、时间、签名、授权、限流、记录不存在或网关开关错误使用 code=1 和具体 msg。失败也可能在 code=0 的订单数据中表现为状态 30,不能只处理非零 code。
同租户、渠道、会员与分站下,order_no 是持久化幂等号。修改商品、数量、充值账号、回调、安全价、关联字段或采购类型后沿用旧号会拒绝。同号重试只读取原订单,不再扣余额、不重新充值、不再取卡。
超时、1209、响应损坏或状态 10 时调用 order/detail:
POST /supplier/xianguanjia/goofish/order/detail?mch_id=MERCHANT_ID×tamp=1893456000&sign=00000000000000000000000000000000
Content-Type: application/json
{"order_type":2,"order_no":"XGJ_ORDER_001"}order_type 必须与原单一致;order_no、out_order_no 至少传一个。二者同传时以 order_no 为准,即使该号不存在也不会自动改用 out_order_no。返回 data 与采购响应订单结构一致。
状态 30 后即使再充值,同号也不会重启付款。先核对商城订单和钱包流水,再决定是否使用新的业务号发起一笔新采购。余额支付前失败可能遗留商城待付款订单,由原超时流程关闭;不要手工补付已经失败的渠道采购单。
完整 Node.js 查询示例
xianguanjia-client.mjs 适用于 Node.js 18+,无需第三方依赖。默认只查询余额,供应用对接服务端使用;App Secret 和商户密钥均不得放入公开客户端或日志。导出函数可按上文路径发送业务 JSON,不自动重试采购。
import { createHash } from 'node:crypto';
import { pathToFileURL } from 'node:url';
const md5 = value => createHash('md5').update(value, 'utf8').digest('hex');
export function xgjSign(appId, appSecret, mchId, mchSecret, timestamp, raw) {
return md5([appId, appSecret, md5(raw), timestamp, mchId, mchSecret].join(','));
}
export async function callXGJ(action, body = {}) {
const { KST_MALL_BASE, XGJ_APP_ID, XGJ_APP_SECRET, XGJ_MCH_ID, XGJ_MCH_SECRET } = process.env;
if (![KST_MALL_BASE, XGJ_APP_ID, XGJ_APP_SECRET, XGJ_MCH_ID, XGJ_MCH_SECRET].every(Boolean)) {
throw new Error('缺少商城地址、应用凭据或商户凭据');
}
const actions = ['open/info', 'user/info', 'goods/list', 'goods/detail',
'order/purchase/create', 'order/recharge/create', 'order/detail'];
if (!actions.includes(action)) throw new Error('接口方法无效');
const base = new URL(KST_MALL_BASE);
if (base.protocol !== 'https:' || base.username || base.password) {
throw new Error('商城地址必须使用 HTTPS');
}
if (!body || typeof body !== 'object' || Array.isArray(body)) {
throw new Error('正文必须是 JSON 对象');
}
// 只序列化一次,签名与发送复用完全相同的字符串。
const raw = JSON.stringify(body);
if (Buffer.byteLength(raw) > 65536) throw new Error('请求体超过 64 KiB');
const target = new URL('/supplier/xianguanjia/goofish/' + action, base);
if (action !== 'open/info') {
const timestamp = String(Math.floor(Date.now() / 1000));
target.search = new URLSearchParams({
mch_id: XGJ_MCH_ID,
timestamp,
sign: xgjSign(XGJ_APP_ID, XGJ_APP_SECRET, XGJ_MCH_ID, XGJ_MCH_SECRET, timestamp, raw)
}).toString();
}
const response = await fetch(target, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: raw,
signal: AbortSignal.timeout(20000),
redirect: 'error'
});
let result;
try { result = await response.json(); }
catch { throw new Error(`HTTP ${response.status}:响应不是合法 JSON,请查原单`); }
if (!response.ok || result?.code !== 0) {
const error = new Error(result?.msg || `HTTP ${response.status}:请求失败`);
error.code = result?.code;
throw error;
}
// 返回订单数据后,调用方仍须判断 order_status 是否为 20。
return result.data;
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
callXGJ('user/info')
.then(data => console.log(JSON.stringify(data, null, 2)))
.catch(error => {
console.error(error.code ?? '', error.message);
process.exitCode = 1;
});
}Windows PowerShell:
$env:KST_MALL_BASE = 'https://您的商城域名'
$env:XGJ_APP_ID = '平台提供的AppID'
$env:XGJ_APP_SECRET = '平台提供的AppSecret'
$env:XGJ_MCH_ID = '商城授权页生成的mch_id'
$env:XGJ_MCH_SECRET = '商城授权页本次显示的mch_secret'
node xianguanjia-client.mjs以下是公开协议测试值,仅用于离线签名校验,不可用于真实请求:
const sign = xgjSign(
'677859093659717', 'wK63PxlOBaY9NoqMksLeZySzGIW25ifA',
'100', 'o9wl81dncmvby3ijpq7eur456zhgtaxs', '1724414553',
'{"goods_type":1,"goods_no":"12344532"}'
);
// 应为 f8677927fd8421a75718e8cfaaf92221订单回调与平台确认
商城只接受以下官方地址格式作为采购 notify_url:
https://open.goofish.pro/api/open/callback/virtual/order/notify/{token}token 为 1–256 位英文字母、数字、_、-。不接受其他主机、显式端口(包括 :443)、查询串、片段或转义路径;发送时拒绝内网解析地址和重定向。不要填自己的业务回调地址替代官方地址。
商城向该地址发送已持久化的终态结果,并在发送时附加 mch_id、timestamp、sign:
POST /api/open/callback/virtual/order/notify/PROVIDER_TOKEN?mch_id=MERCHANT_ID×tamp=1893456000&sign=00000000000000000000000000000000
Host: open.goofish.pro
Content-Type: application/json
{"order_type":2,"order_no":"XGJ_ORDER_001","out_order_no":"mall-supplier-order-id","order_status":20,"end_time":1893456001,"remark":"","card_items":[{"card_pwd":"ACTUAL_ALLOCATED_CARD"}]}回调签名使用同样的六段 MD5 和实际正文。直充以及失败结果不带 card_items。回调三个 Query 字段与平台接收端的具体校验要求,仍须商户真实联调确认,不能把本文当成平台验收证明。
平台确认(ACK)必须同时满足 HTTP 200、合法 JSON、数字 code=0,例如:
{"code":0,"msg":"OK"}当前只接受 code、msg,msg 可省略;拒绝未知或重复字段,响应上限 4096 字节。非 200、超时、空响应、字符串 "0"、其他 code 或非法 JSON 都不算确认。
失败后依次等待 2、4、8、16、32、64、128 分钟重试,第 8 次失败转人工处理。后台「采购订单 → 重试回调」只重发通知,不创建订单、扣款或重新履约。相同结果的重复查单不会重置回调次数、退避或已确认状态;实际持久化结果改变时会按新结果重新安排通知。
已确认的同一结果不自动重发。网关关闭、通知开关关闭,或商户授权、会员/渠道权限、站点失效时,通知无法正常发送;恢复条件后核对原单和待处理通知。关闭「发送订单回调」不会自动关闭采购网关。
对账、售后与安全条件
- 后台「电商渠道供货 → 采购订单」可按采购号、供货订单 ID、会员搜索,查看状态、原因、回调次数,点击「查询原订单」。每次最多读取 200 笔,不代表无限历史检索。
out_order_no关联供货记录和商城订单 ID;商城展示订单号为CS加该 ID。会员「我的订单」核对支付和交付,「资金中心」核对余额流水。- 工单提交
order_no、out_order_no、biz_order_no和问题,不提交密钥、完整卡密或回调 token。
/pc/#orders
/pc/#funds
/support/当前未实现闲管家虚拟货源退款申请、退款通知或券码撤销接口;is_support_order_refund=false。售后走商城工单与真实订单退款流程,不能自行拼出一个退款 URL 发起资金操作。
申请退款不等于退款成功。渠道查单在商城订单退款处理中暂停返回卡密;已实际交付的卡密、直充不能通过当前确认流程退款;已退款订单不能再次分配卡密。符合条件的余额订单确认退款后,资金退回原商城会员余额,不会通过此接口自动退给外部平台买家。
会员停用、渠道供货权限关闭、站点不匹配、商品实名或购买策略不满足都会拒绝采购。order 场景要求逐单交易密码时,本协议没有密码字段,授权和自动采购拒绝执行。商城运行授权失效或版本停用会停止新采购;原已付款订单保留查单和回调善后能力,但网关、商户授权、会员/渠道权限、站点等检查仍然有效。
状态未知或履约失败不能伪装为充值成功。对账以商城支付、退款、钱包流水及实际履约为准,不能仅凭平台“已收款”或接口“已受理”认定完成。
真实联调边界
需要平台实际提供应用凭据、测试商户、每单官方回调 token,核验 mch_id 接受规则、回调签名与 ACK、直充模板、状态转换、有效期和平台错误重试行为。协议测试、本地 handler 测试或隔离数据库验收都不等于平台审核通过或真实交易已验收。
参考:闲管家虚拟货源接口资料。平台当前接入要求仍以实际确认结果为准。