一、接入总览
VmShell Pay 采用支付中台模式。商户在前台提交应用并通过后台审核后,可获得 AppId 与 AppSecret。商户系统只需要调用本平台 API,本平台会按已启用通道进入 VmShellPAY 支付网络,并在收到支付或退款结果后更新商户钱包余额、订单状态与争议数据。
| 环境 | 网关地址 | 说明 |
|---|---|---|
| 正式支付 | https://vmshell.win/api/v1/pay.php | 创建中台交易并进入 VmShellPAY 支付网络。 |
| 正式查单 | https://vmshell.win/api/v1/query.php | 查询平台订单状态,可同步支付通道状态。 |
| 沙箱测试 | https://vmshell.win/api/sandbox/test_pay.php | 仅创建测试订单,不触发真实扣款。 |
正式环境前置条件:商户主体资料通过审核、应用状态为已启用、应用环境为正式、服务器请求 IP 命中应用白名单、回调地址公网可访问。未满足条件时,正式支付接口会拒绝创建订单。
二、支付通道能力
商户侧无需直接保管通道商户号、通道 AppId 或通道签名密钥。相关支付网络配置由平台后台统一托管,商户只需要使用自己的 AppId、AppSecret 与回调地址完成对接。
| 配置项 | 当前值 |
|---|---|
| 可用支付方式 | 支付宝.香港、支付宝.大陆、WeChat Pay |
| 结算币种 | HKD |
| 签名方式 | MD5 |
| 商户侧密钥 | 以“我的应用”页面展示的 AppSecret 为准 |
| 支付网关 | https://vmshell.win/api/v1/pay.php |
如商户需要透传支付扩展字段,可在下单请求中传入 customer_email、customer_name、customer_phone、client_ip、subject、body、remark、ext_param 等字段;平台会保存到交易详情,并在查单和 Webhook 回调中返回给商户。
三、通用请求格式
正式 API 支持 application/x-www-form-urlencoded 的 POST 参数和 GET 查询参数;生产环境建议统一使用 POST。所有金额使用 HKD 主币单位并保留两位小数,例如 10.00。支付宝.香港、支付宝.大陆、微信支付三个正式网关默认单笔范围为 5.00 - 500000.00 HKD,后台可在支付网关中心调整。所有字符串建议使用 UTF-8 编码。
| 规则 | 要求 | 说明 |
|---|---|---|
| 币种 | HKD | 正式环境只支持 HKD。不要传 CNY、USD 或其他币种。 |
| 金额格式 | ^[0-9]+(\.[0-9]{1,2})?$ | 金额必须位于当前网关配置范围内,默认 5.00 - 500000.00 HKD,建议固定两位小数,例如 10.00。 |
| 订单号 | 商户应用内唯一 | order_id 同一 AppId 下不可重复,建议只使用字母、数字、下划线、短横线。 |
| 邮箱 | 合法 Email | customer_email 必填,用于买家账单查询、争议中心和商户回调对账。 |
| 时间戳 | Unix 秒级时间戳 | timestamp 建议必传,商户可自行做 5 分钟有效期校验。 |
| 扩展参数 | 字符串或 JSON 字符串 | ext_param 不要传数组对象,建议先 JSON 编码成字符串后参与签名。 |
| 签名字段名 | signature 或 sign | 两者均可;平台计算签名时会忽略这两个字段。 |
| 返回格式 | application/json; charset=utf-8 | 成功和失败均返回 JSON,商户系统必须按 HTTP 状态码和业务字段共同判断。 |
四、签名认证
每个商户应用拥有独立 AppSecret。新应用默认使用 HMAC-SHA256、timestamp 和 nonce 防重放;旧应用可由后台切换为 MD5 兼容。请求参数按键名升序排序,过滤空值、数组、sign 与 signature 字段后拼接为 key=value 串,然后用 AppSecret 签名。平台会验证签名、时间窗口、nonce、应用审核状态、关停状态、IP 白名单、回调地址与可用通道。
原始参数:
app_id=vmp_xxx
order_id=ORDER_10001
amount=10.00
currency=HKD
customer_email=buyer@example.com
subject=Demo Order
timestamp=1781160000
排序拼接:
amount=10.00&app_id=vmp_xxx¤cy=HKD&customer_email=buyer@example.com&order_id=ORDER_10001&subject=Demo Order×tamp=1781160000&key=YOUR_APP_SECRET
签名结果:
signature=hash_hmac('sha256', 排序拼接字符串, YOUR_APP_SECRET)| 认证字段 | 是否必填 | 说明 |
|---|---|---|
app_id | 是 | 商户应用 ID,来自“我的应用”。 |
sign_type | 是 | 新应用默认 HMAC-SHA256;MD5 仅用于旧商户兼容。 |
timestamp | 是 | Unix 时间戳,默认 300 秒有效期。 |
nonce | 是 | 随机字符串,同一 AppId + endpoint + nonce 在有效期内只能使用一次。 |
encrypted_data | 可选 | 启用 AES-256-GCM 时,敏感字段放入加密载荷;notify_url 保持明文。 |
signature / sign | 是 | 按应用签名方式生成,建议统一使用 signature。 |
注意:空字符串、NULL、数组不参与签名;JSON 扩展参数必须作为字符串参与签名。签名原文不要 URL 编码后再计算,直接使用业务参数原值拼接。
五、支付下单 API
| 参数 | 类型/格式 | 必填 | 参与签名 | 说明 |
|---|---|---|---|---|
app_id | string,1-80 | 是 | 是 | 商户应用 ID。 |
method / payment_method | enum | 否 | 是 | alipay_hk 支付宝.香港,alipay_cn 支付宝.大陆,wechat_pay WeChat Pay。建议显式传入。 |
order_id | string,1-120 | 是 | 是 | 商户唯一订单号,同一 AppId 下不可重复。 |
amount | decimal,例 12.50 | 是 | 是 | 订单金额,单位 HKD 主币;默认单笔范围 5.00 - 500000.00 HKD,超出时返回 AMOUNT_OUT_OF_RANGE。 |
currency | HKD | 否 | 是 | 默认 HKD,正式环境只支持 HKD。 |
customer_email | email,最长 255 | 是 | 是 | 付款用户邮箱,用于账单查询、争议中心和商户回调对账。 |
customer_name | string,建议 1-80 | 否 | 是 | 付款用户姓名,平台保存并在查单与回调中返回。 |
customer_phone | string,建议 E.164 | 否 | 是 | 付款用户电话,例如 +852 5000 0000。 |
subject | string,建议 1-120 | 建议 | 是 | 商品标题;不传时平台会按订单号生成默认标题。 |
body / description | string,建议 1-255 | 否 | 是 | 商品描述;不传时默认等于 subject。 |
remark / note / memo | string,建议 1-255 | 否 | 是 | 付款备注或商户内部备注,平台原样保存并返回。 |
ext_param | JSON 字符串 | 否 | 是 | 商户扩展参数,例如 {"cart_id":"CART_10001"}。请作为字符串传入,不要传数组。 |
notify_url | https/http URL | 是 | 是 | 支付、退款、争议处理后通知商户的地址。若应用后台已配置,可不传。 |
return_url | https/http URL | 否 | 是 | 用户支付完成后跳转地址。 |
client_ip / clientIp | IPv4/IPv6 | 否 | 是 | 买家 IP,用于风控和支付路由;不传时平台使用请求来源 IP。 |
direct | 1/true/yes | 否 | 是 | 直接按指定支付方式下单,返回二维码/支付链接,不再让买家进入收银台选择通道。 |
return_qr / qrcode | 1 | 否 | 是 | 同 direct=1。 |
sign_type | HMAC-SHA256 | 建议 | 是 | 新应用默认 HMAC-SHA256;旧应用可由后台切换为 MD5 兼容。 |
timestamp | Unix 秒 | HMAC 必填 | 是 | 请求时间,默认 300 秒有效。 |
nonce | string | HMAC 必填 | 是 | 随机字符串,同一 AppId + endpoint + nonce 只能使用一次。 |
encrypted_data / iv / tag | AES-256-GCM | 按应用配置 | 是 | 敏感字段密文;notify_url 保持明文,不放入密文。 |
signature / sign | HMAC/MD5 | 是 | 否 | 请求签名。 |
普通收银台模式响应
{
"status": "success",
"message": "Payment session created. The platform will process the payment and notify merchant callback after payment result is confirmed.",
"pay_url": "https://vmshell.win/pages/checkout.php?session_id=vmp_sess_xxx",
"checkout_url": "https://vmshell.win/pages/checkout.php?session_id=vmp_sess_xxx",
"mobile_h5_url": "https://vmshell.win/pages/mobile_h5_pay.php?session_id=vmp_sess_xxx",
"transaction_id": "VMP20260611180000ABCD1234",
"order_id": "ORDER_10001",
"customer_email": "buyer@example.com",
"customer_name": "Test Buyer",
"customer_phone": "+852 5000 0000",
"subject": "Demo Order",
"body": "Demo product description",
"remark": "merchant-note-001",
"ext_param": "{\"cart_id\":\"CART_10001\"}",
"client_ip": "203.0.113.10",
"payment_method": "alipay_hk",
"amount": "10.00",
"currency": "HKD",
"expires_at": "2026-06-11 19:00:00",
"sign_type": "HMAC-SHA256",
"timestamp": "1781185200",
"nonce": "b7d4f4b4a2d0e1c6c8a9f001",
"signature": "hmac_sha256_signature"
}Direct QR 模式响应
{
"status": "success",
"message": "Direct payment QR created. The platform will notify merchant callback after payment result is confirmed.",
"checkout_url": "https://vmshell.win/pages/checkout.php?session_id=vmp_sess_xxx",
"mobile_h5_url": "https://vmshell.win/pages/mobile_h5_pay.php?session_id=vmp_sess_xxx",
"pay_url": "weixin://wxpay/bizpayurl?pr=xxx 或 https://...",
"payment_url": "weixin://wxpay/bizpayurl?pr=xxx 或 https://...",
"qr_content": "二维码原始内容,商户可自行生成二维码",
"qr_code_url": "https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=...",
"transaction_id": "VMP20260611180000ABCD1234",
"gateway_order_id": "P2064945733190164482",
"order_id": "ORDER_10001",
"payment_method": "alipay_hk",
"amount": "10.00",
"currency": "HKD",
"expires_at": "2026-06-11 19:00:00",
"sign_type": "HMAC-SHA256",
"timestamp": "1781185200",
"nonce": "b7d4f4b4a2d0e1c6c8a9f001",
"signature": "hmac_sha256_signature"
}六、订单查询 API
商户可使用平台交易号或商户订单号查询订单。若请求中带有 sync_upstream=1,平台会同步支付通道状态,并写入查询日志。
| 参数 | 类型/格式 | 必填 | 参与签名 | 说明 |
|---|---|---|---|---|
app_id | string | 是 | 是 | 商户应用 ID。 |
order_id | string | 二选一 | 是 | 商户订单号。 |
transaction_id | string | 二选一 | 是 | 平台交易号。 |
sync / sync_upstream | 0/1 | 否 | 是 | 是否同步支付网络状态。生产环境建议不要高频同步。 |
sign_type | HMAC-SHA256 | 建议 | 是 | 按应用配置生成签名。 |
timestamp | Unix 秒 | HMAC 必填 | 是 | 请求时间。 |
nonce | string | HMAC 必填 | 是 | 防重放随机串。 |
signature / sign | HMAC/MD5 | 是 | 否 | 签名。 |
查单成功后会返回下单时透传的 customer_email、customer_name、customer_phone、subject、body、remark、ext_param、client_ip 等字段,便于商户系统对账和售后。
{
"success": true,
"message": "查询成功",
"data": {
"app_id": "vmp_xxx",
"order_id": "ORDER_10001",
"transaction_id": "VMP20260611180000ABCD1234",
"checkout_session_id": "vmp_sess_xxx",
"customer_email": "buyer@example.com",
"customer_name": "Test Buyer",
"customer_phone": "+852 5000 0000",
"gateway_transaction_id": "P2064945733190164482",
"gateway_code": "alipay_hk",
"payment_method": "alipay_hk",
"subject": "Demo Order",
"body": "Demo product description",
"remark": "merchant-note-001",
"ext_param": "{\"cart_id\":\"CART_10001\"}",
"client_ip": "203.0.113.10",
"amount": "10.00",
"currency": "HKD",
"status": "paid",
"refund_status": "none",
"merchant_notify_status": "success",
"channel": {
"payOrderId": "P2064945733190164482",
"channelOrderNo": "",
"state": "SUCCESS",
"successTime": "2026-06-11 18:05:00",
"channelUser": ""
},
"signature": "平台返回签名"
}
}七、商户 Webhook 回调
支付结果先进入平台统一处理流程。平台完成验签、入账、服务费记录、退款同步或争议裁决后,再调用商户应用配置的 notify_url。回调请求使用 POST 表单格式,商户必须验证签名并做幂等处理。浏览器同步回跳只用于展示,不作为最终入账依据。
| 字段 | 类型/格式 | 说明 |
|---|---|---|
app_id | string | 商户应用 ID。 |
order_id | string | 商户订单号。 |
transaction_id | string | 平台交易号。 |
checkout_session_id | string | 平台收银台会话号。 |
gateway_transaction_id | string | 支付网络订单号,如已返回。 |
payment_method | enum | alipay_hk、alipay_cn、wechat_pay 或平台内部兼容值。 |
customer_email | 付款用户邮箱。 | |
customer_name | string | 付款用户姓名。 |
customer_phone | string | 付款用户电话。 |
subject | string | 商品标题。 |
body | string | 商品描述。 |
remark | string | 付款备注或商户内部备注。 |
ext_param | string | 商户扩展参数,按下单请求原样返回。 |
client_ip | string | 下单时记录的客户端 IP。 |
amount | decimal | 订单金额。 |
currency | HKD | 订单币种。 |
status | enum | 当前订单或退款状态。 |
refund_status | enum | 退款状态,未退款时为空或 none。 |
refund_id | string | 退款回调时返回退款单号。 |
refund_amount | decimal | 退款回调时返回退款金额。 |
merchant_net_amount | decimal | 商户净入账金额。 |
timestamp | Unix 秒 | 平台发起回调的时间。 |
nonce | string | 平台回调随机串,商户侧可用于幂等/防重放。 |
sign_type | string | 平台回调签名方式,默认 HMAC-SHA256。 |
encrypted_data / iv / tag | AES-256-GCM | 应用启用字段加密时,敏感字段会进入加密载荷。 |
signature | HMAC/MD5 | 平台使用商户 AppSecret 生成的回调签名。 |
回调验签与响应
商户收到回调后,按应用签名方式用 AppSecret 验证 signature;如存在 encrypted_data,先验签外层参数,再用 AES-256-GCM 解密敏感字段。验签成功并完成订单处理后,建议返回 HTTP 200,响应内容可为 success 或 JSON。商户必须以 order_id 或 transaction_id 做幂等,重复回调不能重复发货或重复入账。
{
"app_id": "vmp_xxx",
"order_id": "ORDER_10001",
"transaction_id": "VMP20260611180000ABCD1234",
"gateway_transaction_id": "P2064945733190164482",
"payment_method": "alipay_hk",
"customer_email": "buyer@example.com",
"subject": "Demo Order",
"remark": "merchant-note-001",
"ext_param": "{\"cart_id\":\"CART_10001\"}",
"amount": "10.00",
"currency": "HKD",
"status": "paid",
"merchant_net_amount": "9.50",
"timestamp": "1781160600",
"signature": "平台回调签名"
}八、状态枚举
| 类型 | 状态值 | 说明 |
|---|---|---|
| 订单 | pending | 订单已创建,等待买家支付。 |
| 订单 | processing | 已进入支付网络处理,尚未确认最终结果。 |
| 订单 | paid / success / completed | 支付成功,可以发货或开通服务。 |
| 订单 | failed | 支付失败或支付网络未返回有效支付资料。 |
| 订单 | refunded | 订单已全额退款。 |
| 退款 | none | 未发生退款。 |
| 退款 | partial_refund | 订单已部分退款。 |
| 退款 | refunded | 订单已全额退款。 |
| 商户回调 | pending | 尚未通知或等待重试。 |
| 商户回调 | success | 商户回调地址返回 HTTP 2xx。 |
| 商户回调 | failed | 商户回调地址不可达或返回非 2xx。 |
九、多语言示例代码
PHP
<?php
$params = [
'app_id' => 'YOUR_APP_ID',
'order_id' => 'ORDER_' . time(),
'amount' => '10.00',
'currency' => 'HKD',
'customer_email' => 'buyer@example.com',
'customer_name' => 'Test Buyer',
'customer_phone' => '+852 5000 0000',
'subject' => 'Demo Order',
'body' => 'Demo product description',
'remark' => 'merchant-note-001',
'ext_param' => '{"cart_id":"CART_10001"}',
'notify_url' => 'https://merchant.example.com/pay/notify',
'sign_type' => 'HMAC-SHA256',
'timestamp' => time(),
'nonce' => bin2hex(random_bytes(12))
];
ksort($params);
$pairs = [];
foreach ($params as $k => $v) { if ($v !== '' && $v !== null) $pairs[] = $k . '=' . $v; }
$params['signature'] = strtolower(hash_hmac('sha256', implode('&', $pairs), 'YOUR_APP_SECRET'));
$ch = curl_init('https://vmshell.win/api/v1/pay.php');
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => http_build_query($params), CURLOPT_RETURNTRANSFER => true]);
echo curl_exec($ch);Node.js
const crypto = require('crypto');
const axios = require('axios');
const params = { app_id:'YOUR_APP_ID', method:'alipay_hk', order_id:'ORDER_' + Date.now(), amount:'10.00', currency:'HKD', customer_email:'buyer@example.com', subject:'Demo Order', body:'Demo product description', remark:'merchant-note-001', ext_param:'{"cart_id":"CART_10001"}', notify_url:'https://merchant.example.com/pay/notify', sign_type:'HMAC-SHA256', timestamp:Math.floor(Date.now()/1000), nonce:crypto.randomBytes(12).toString('hex') };
const base = Object.keys(params).sort().map(k => `${k}=${params[k]}`).join('&');
params.signature = crypto.createHmac('sha256', 'YOUR_APP_SECRET').update(base).digest('hex').toLowerCase();
axios.post('https://vmshell.win/api/v1/pay.php', params).then(r => console.log(r.data));Python
import hashlib, hmac, secrets, time, requests
params = {'app_id':'YOUR_APP_ID','method':'alipay_hk','order_id':f'ORDER_{int(time.time())}','amount':'10.00','currency':'HKD','customer_email':'buyer@example.com','subject':'Demo Order','body':'Demo product description','remark':'merchant-note-001','ext_param':'{"cart_id":"CART_10001"}','notify_url':'https://merchant.example.com/pay/notify','sign_type':'HMAC-SHA256','timestamp':str(int(time.time())),'nonce':secrets.token_hex(12)}
base = '&'.join([f'{k}={params[k]}' for k in sorted(params.keys()) if params[k]])
params['signature'] = hmac.new(b'YOUR_APP_SECRET', base.encode(), hashlib.sha256).hexdigest().lower()
print(requests.post('https://vmshell.win/api/v1/pay.php', data=params).json())Java
Map<String, String> p = new TreeMap<>();
p.put("app_id", "YOUR_APP_ID"); p.put("method", "alipay_hk"); p.put("order_id", "ORDER_10001"); p.put("amount", "10.00"); p.put("currency", "HKD"); p.put("customer_email", "buyer@example.com"); p.put("subject", "Demo Order"); p.put("sign_type", "HMAC-SHA256"); p.put("timestamp", String.valueOf(System.currentTimeMillis() / 1000)); p.put("nonce", UUID.randomUUID().toString().replace("-", ""));
String base = p.entrySet().stream().map(e -> e.getKey() + "=" + e.getValue()).collect(Collectors.joining("&"));
String signature = new HmacUtils(HmacAlgorithms.HMAC_SHA_256, "YOUR_APP_SECRET").hmacHex(base).toLowerCase();
// 使用 HttpClient POST 至 https://vmshell.win/api/v1/pay.phpGo
params := map[string]string{"app_id":"YOUR_APP_ID","method":"alipay_hk","order_id":"ORDER_10001","amount":"10.00","currency":"HKD","customer_email":"buyer@example.com","subject":"Demo Order","sign_type":"HMAC-SHA256","timestamp":strconv.FormatInt(time.Now().Unix(),10),"nonce":randomHex(12)}
keys := make([]string, 0, len(params)); for k := range params { keys = append(keys, k) }; sort.Strings(keys)
parts := []string{}; for _, k := range keys { parts = append(parts, k+"="+params[k]) }
h := hmac.New(sha256.New, []byte("YOUR_APP_SECRET")); h.Write([]byte(strings.Join(parts, "&")))
params["signature"] = hex.EncodeToString(h.Sum(nil))
// http.PostForm("https://vmshell.win/api/v1/pay.php", url.Values{...})十、下游商户接入教程
本章节按真实上线顺序说明商户系统如何接入 VmShell PAY。商户只需要维护自己的 AppId、AppSecret、服务器出口 IP、notify_url 和 return_url;支付网络路由、手续费计算、钱包入账、退款扣款和回调重试由平台统一处理。
1. 在商户后台创建应用
| 步骤 | 操作 | 说明 |
|---|---|---|
| 提交主体资料 | 进入“商户入驻资料”页面 | 主体资料审核通过后,才能创建应用或申请正式环境。 |
| 提交应用申请 | 进入“我的应用”页面 | 填写应用名称、商户网站、服务器 IP 白名单、联系人邮箱、notify_url、return_url。 |
| 完成沙箱联调 | 进入“沙箱联调”页面 | 使用真实 AppId 和 AppSecret 创建沙箱订单、查询订单、测试 Webhook。 |
| 申请正式环境 | 沙箱通过后提交正式申请 | 后台审核通过后,应用才能调用正式 /api/v1/pay.php。 |
| 复制密钥 | 在“我的应用”查看 AppId/AppSecret | AppSecret 只保存在商户服务器端,不要写入浏览器、前端 JS、App 客户端。 |
2. 准备商户服务器文件
下面示例假设商户网站路径为 https://merchant.example.com/pay/,商户需要准备四个 PHP 文件:
| 文件 | 用途 | 公网地址示例 |
|---|---|---|
create_order.php | 商户服务端创建订单并调用 VmShell PAY 下单接口 | https://merchant.example.com/pay/create_order.php |
notify.php | 接收 VmShell PAY 支付/退款异步通知 | https://merchant.example.com/pay/notify.php |
return.php | 买家支付后浏览器跳回页面 | https://merchant.example.com/pay/return.php |
query.php | 商户后台或定时任务主动查单 | https://merchant.example.com/pay/query.php |
重要:return.php 只是浏览器跳转,不代表支付成功;发货、开通服务、充值到账必须以 notify.php 验签成功或 query.php 查单成功为准。手机端如收到 mobile_h5_url,应优先打开它,不要直接把 checkout_url 当作支付宝/微信 App 唤起地址。
3. 商户配置文件 config.php
<?php
// config.php
return [
'api_base' => 'https://vmshell.win',
'app_id' => 'YOUR_APP_ID',
'app_secret' => 'YOUR_APP_SECRET',
'notify_url' => 'https://merchant.example.com/pay/notify.php',
'return_url' => 'https://merchant.example.com/pay/return.php',
];4. 公共签名函数 common.php
<?php
// common.php
function vmshell_sign(array $params, string $secret): string {
unset($params['sign'], $params['signature']);
ksort($params);
$pairs = [];
foreach ($params as $key => $value) {
if ($value === '' || $value === null || is_array($value)) continue;
$pairs[] = $key . '=' . $value;
}
$canonical = implode('&', $pairs);
$signType = strtoupper($params['sign_type'] ?? 'HMAC-SHA256');
return $signType === 'MD5'
? strtolower(md5($canonical . '&key=' . $secret))
: strtolower(hash_hmac('sha256', $canonical, $secret));
}
function vmshell_post(string $url, array $params): array {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($params),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 30,
]);
$raw = curl_exec($ch);
$error = curl_error($ch);
$http = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return ['http_code' => $http, 'error' => $error, 'raw' => $raw, 'json' => json_decode((string)$raw, true)];
}5. 创建支付订单 create_order.php
<?php
require __DIR__ . '/common.php';
$cfg = require __DIR__ . '/config.php';
// 实际项目中请从数据库读取订单,并确保 order_id 唯一。
$orderId = 'ORDER_' . date('YmdHis');
$amount = '10.00';
$params = [
'app_id' => $cfg['app_id'],
'method' => 'alipay_hk', // alipay_hk / alipay_cn / wechat_pay
'order_id' => $orderId,
'amount' => $amount,
'currency' => 'HKD',
'customer_email' => 'buyer@example.com',
'customer_name' => 'Test Buyer',
'customer_phone' => '+852 5000 0000',
'subject' => 'Demo Product',
'body' => 'Demo product description',
'remark' => 'merchant internal note',
'ext_param' => json_encode(['cart_id' => 'CART_10001'], JSON_UNESCAPED_UNICODE),
'notify_url' => $cfg['notify_url'],
'return_url' => $cfg['return_url'],
'sign_type' => 'HMAC-SHA256',
'timestamp' => time(),
'nonce' => bin2hex(random_bytes(12)),
];
$params['signature'] = vmshell_sign($params, $cfg['app_secret']);
$result = vmshell_post($cfg['api_base'] . '/api/v1/pay.php', $params);
if (($result['json']['status'] ?? '') === 'success') {
// 普通收银台模式跳转 pay_url;direct=1 时可读取 qr_content/qr_code_url 自行展示二维码。
header('Location: ' . $result['json']['pay_url']);
exit;
}
header('Content-Type: application/json; charset=utf-8');
echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);6. 直接返回二维码模式
如果商户希望调用接口后直接得到二维码或支付链接,可在下单参数中加入 direct=1,并指定 method。商户页面可使用返回的 qr_code_url 图片,或用 qr_content 自行生成二维码。
$params['method'] = 'alipay_cn';
$params['direct'] = '1';
$params['timestamp'] = time();
$params['nonce'] = bin2hex(random_bytes(12));
$params['signature'] = vmshell_sign($params, $cfg['app_secret']);
$result = vmshell_post($cfg['api_base'] . '/api/v1/pay.php', $params);
if (($result['json']['status'] ?? '') === 'success') {
echo '<img src="' . htmlspecialchars($result['json']['qr_code_url'] ?? '') . '" alt="Pay QR">';
echo '<p>请使用对应钱包扫码支付</p>';
}7. 接收异步通知 notify.php
<?php
require __DIR__ . '/common.php';
$cfg = require __DIR__ . '/config.php';
$payload = $_POST ?: $_GET;
$receivedSign = $payload['signature'] ?? ($payload['sign'] ?? '');
$calculated = vmshell_sign($payload, $cfg['app_secret']);
if (!$receivedSign || !hash_equals($calculated, strtolower($receivedSign))) {
http_response_code(403);
echo 'invalid signature';
exit;
}
$orderId = $payload['order_id'] ?? '';
$transactionId = $payload['transaction_id'] ?? '';
$status = $payload['status'] ?? '';
$amount = $payload['amount'] ?? '0.00';
$currency = $payload['currency'] ?? 'HKD';
$refundId = $payload['refund_id'] ?? '';
$refundAmount = $payload['refund_amount'] ?? '';
// 1. 从数据库查询本地订单。
// 2. 校验 order_id、amount、currency 是否和本地订单一致。
// 3. 以 order_id 或 transaction_id 做幂等,重复通知直接返回 success。
// 4. paid/success/completed 才能发货或开通服务。
// 5. partial_refund/refunded 需要更新退款状态和售后记录。
if (in_array($status, ['paid', 'success', 'completed'], true)) {
// markOrderPaid($orderId, $transactionId, $payload);
} elseif (in_array($status, ['partial_refund', 'refunded'], true)) {
// markOrderRefunded($orderId, $refundId, $refundAmount, $payload);
}
echo 'success';8. 浏览器跳转 return.php
<?php
// return.php
// 这里仅展示支付结果页。不要在这里发货,必须等待 notify.php 或 query.php 确认。
$orderId = $_GET['order_id'] ?? '';
?>
<h1>支付处理中</h1>
<p>订单号:<?php echo htmlspecialchars($orderId); ?></p>
<p>请稍候,系统会在支付确认后自动更新订单状态。</p>9. 主动查单 query.php
<?php
require __DIR__ . '/common.php';
$cfg = require __DIR__ . '/config.php';
$params = [
'app_id' => $cfg['app_id'],
'order_id' => $_GET['order_id'] ?? 'ORDER_10001',
'sign_type' => 'HMAC-SHA256',
'timestamp' => time(),
'nonce' => bin2hex(random_bytes(12)),
];
$params['signature'] = vmshell_sign($params, $cfg['app_secret']);
$result = vmshell_post($cfg['api_base'] . '/api/v1/query.php', $params);
header('Content-Type: application/json; charset=utf-8');
echo json_encode($result['json'], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);10. 上线前必测清单
| 测试项 | 验收标准 |
|---|---|
| 签名测试 | 错误密钥会被拒绝,正确密钥可创建订单。 |
| IP 白名单 | 商户服务器出口 IP 不在白名单时,正式接口返回 403。 |
| 支付成功 | 商户收到 notify_url,验签成功,订单只处理一次。 |
| 浏览器跳转 | return_url 只展示结果,不做发货依据。 |
| 主动查单 | 商户可用 order_id 查询到订单和付款邮箱。 |
| 退款通知 | 退款后商户收到 partial_refund 或 refunded,并记录退款单号。 |
| 金额核对 | 商户后台订单金额、平台服务手续费、商户净入账、退款手续费和钱包流水一致。 |
| 重复通知 | 同一 order_id 重复回调不会重复发货、重复充值或重复退款。 |
十一、错误码与排查
| HTTP/字段 | 常见信息 | 说明 | 建议处理 |
|---|---|---|---|
200 | status=success 或 success=true | 请求成功。 | 读取返回字段并继续业务处理。 |
400 | AMOUNT_OUT_OF_RANGE / Missing required parameters | 缺少必要参数、金额格式错误或金额不在网关限额内。 | 检查 app_id、order_id、amount、currency、customer_email、notify_url 与 signature;默认限额为 5.00 - 500000.00 HKD。 |
400 | Only HKD is supported | 正式环境只允许 HKD。 | 把订单币种固定为 HKD。 |
403 | Signature verification failed | 签名验证失败。 | 确认参数排序、空值过滤、JSON 字符串、AppSecret 与 sign/signature 字段处理。 |
403 | Unauthorized IP address | 请求 IP 不在应用白名单内。 | 到应用资料中填写服务器出口 IP,或联系平台后台审核更新。 |
403 | Application is still in sandbox environment | 应用仍为沙箱或未正式启用。 | 完成沙箱测试并提交正式环境审批。 |
400 | Duplicate order_id | 订单号重复。 | 每次新订单使用唯一 order_id;重试时先查单,不要直接换金额复用。 |
502 | Payment channel order failed | Direct QR 请求已创建平台订单,但支付网络未返回二维码或支付链接。 | 查看返回的 diagnosis 与 upstream.response,必要时换支付方式或联系平台技术。 |
404 | 订单不存在 | 查单没有找到订单。 | 确认 app_id、order_id/transaction_id 是否属于同一个应用。 |
上线检查清单
| 检查项 | 要求 |
|---|---|
| 应用状态 | 已审核通过并切换为正式环境。 |
| IP 白名单 | 填写商户服务器真实出口 IP。浏览器用户 IP 不等于服务器出口 IP。 |
| 回调地址 | notify_url 必须公网可访问,支持 POST,处理成功返回 HTTP 2xx。 |
| 幂等处理 | 商户系统以 order_id 或 transaction_id 防重复处理。 |
| 发货依据 | 只以平台回调验签成功且 status=paid/success/completed,或查单返回成功状态为准。 |
| 退款同步 | 退款可在商户交易详情页发起,平台会同步支付网络并回调商户。 |