接入指南
PayHub 是多租户支付网关。商户在自己的控制台里绑定支付宝、微信支付或持牌聚合通道,平台负责下单、收银台、 异步通知与退款编排,资金直接进入商户自己的上游账户。
合规提示:本系统不提供代收代付、资金归集与结算能力。在中国境内经营支付业务需要《支付业务许可证》, 请确保你的业务角色(商户 / 服务商)与实际持牌情况一致。
1. 认证与签名
所有 /api/v1 接口使用 HMAC-SHA256 请求签名,请求头必须包含:
| 请求头 | 说明 |
|---|---|
X-Api-Key | 商户 App Key,在控制台「开发者设置」查看 |
X-Timestamp | Unix 时间戳(秒或毫秒),与服务器偏差需 ≤ 5 分钟 |
X-Nonce | 随机串,同一 nonce 只能使用一次(防重放) |
X-Signature | 下方规则算出的 64 位小写十六进制签名 |
签名串由 5 段组成,用换行符连接:
METHOD // 大写,如 POST PATH // 不含域名与查询串,如 /api/v1/orders TIMESTAMP // 与 X-Timestamp 完全一致的原始字符串 NONCE // 与 X-Nonce 一致 SHA256_HEX(BODY) // 原始请求体(GET 为空字符串)的 SHA-256 十六进制
然后 signature = HMAC_SHA256_HEX(secret, 上面拼好的字符串)。
import crypto from "node:crypto";
const APP_KEY = process.env.PAYHUB_APP_KEY;
const APP_SECRET = process.env.PAYHUB_APP_SECRET;
const BASE = "https://pay.example.com";
async function call(method, path, body) {
const rawBody = body ? JSON.stringify(body) : "";
const timestamp = Math.floor(Date.now() / 1000).toString();
const nonce = crypto.randomBytes(12).toString("hex");
const bodyHash = crypto.createHash("sha256").update(rawBody).digest("hex");
const message = [method, path, timestamp, nonce, bodyHash].join("\n");
const signature = crypto
.createHmac("sha256", APP_SECRET)
.update(message)
.digest("hex");
const response = await fetch(BASE + path, {
method,
headers: {
"content-type": "application/json",
"x-api-key": APP_KEY,
"x-timestamp": timestamp,
"x-nonce": nonce,
"x-signature": signature,
},
body: body ? rawBody : undefined,
});
return response.json();
}
// 创建订单
const result = await call("POST", "/api/v1/orders", {
outTradeNo: "ORDER_20261028_0001",
amount: 1990, // 单位:分
subject: "星野咖啡 · 双人套餐",
provider: "wechat",
notifyUrl: "https://shop.example.com/pay/notify",
});
console.log(result.data.payUrl);2. 创建订单
POST /api/v1/orders
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
outTradeNo | string | 是 | 商户订单号,6-64 位,字母数字下划线短横线,需唯一(相同单号重复请求返回原订单) |
amount | int | 是 | 金额,单位:分 |
subject | string | 是 | 商品标题,≤ 128 字 |
provider | string | 是 | alipay / wechat / unionpay / aggregator / mock |
notifyUrl | string | 是 | 支付结果通知地址(PayHub 回调你) |
returnUrl | string | 否 | 支付完成后的跳转地址 |
body | string | 否 | 商品描述 |
expiresIn | int | 否 | 订单有效期(秒),默认 1800,范围 60-86400 |
响应:
{
"success": true,
"data": {
"tradeNo": "PH20261028230001123456",
"outTradeNo": "ORDER_20261028_0001",
"amount": 1990,
"amountText": "19.90",
"provider": "wechat",
"status": "PENDING",
"payType": "qr",
"payUrl": "https://pay.example.com/pay/PH2026...",
"qrCode": "weixin://wxpay/bizpayurl?pr=xxxx",
"expiresAt": "2026-10-28T15:30:01.000Z",
"reused": false
}
}把 payUrl 渲染成二维码或跳转链接即可(本系统的收银台页面已经做了这件事)。
3. 查询订单
GET /api/v1/orders/<tradeNo>,返回订单状态、支付时间与退款记录。
4. 退款
POST /api/v1/refunds
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
tradeNo | string | 是 | 平台订单号 |
amount | int | 否 | 退款金额(分),缺省为剩余可退金额 |
reason | string | 否 | 退款原因 |
5. 异步通知
订单支付成功后,PayHub 会向你的 notifyUrl 发送 POST JSON:
{
"event": "payment.succeeded",
"tradeNo": "PH20261028230001123456",
"outTradeNo": "ORDER_20261028_0001",
"provider": "wechat",
"amount": 1990,
"currency": "CNY",
"subject": "星野咖啡 · 双人套餐",
"status": "PAID",
"paidAt": "2026-10-28T15:22:31.000Z",
"merchantCode": "M00001",
"sign": "...."
}- 签名:除
sign外的字段按字段名排序,拼成k=v&k=v,用 App Secret 做 HMAC-SHA256。 - 应答:返回 HTTP 2xx 即视为成功;否则按 1 分钟、5 分钟、15 分钟、1 小时、6 小时、12 小时重试,共 6 次。
- 幂等:同一订单可能收到多次通知,请按
tradeNo去重。
// 接收 PayHub 的支付结果通知
app.post("/pay/notify", (req, res) => {
const { sign, ...rest } = req.body;
const content = Object.keys(rest)
.filter((key) => rest[key] !== undefined && rest[key] !== null && rest[key] !== "")
.sort()
.map((key) => `${key}=${rest[key]}`)
.join("&");
const expected = crypto
.createHmac("sha256", process.env.PAYHUB_APP_SECRET)
.update(content)
.digest("hex");
if (expected !== sign) return res.status(400).send("bad sign");
if (orderAlreadyProcessed(rest.tradeNo)) return res.send("success"); // 幂等
markOrderPaid(rest.outTradeNo, rest.amount);
res.send("success");
});6. 错误码
| code | HTTP | 含义 |
|---|---|---|
MISSING_SIGNATURE | 401 | 缺少签名请求头 |
TIMESTAMP_EXPIRED | 401 | 时间戳超出允许偏差 |
INVALID_SIGNATURE | 401 | 签名校验失败 |
DUPLICATE_NONCE | 409 | nonce 重复(疑似重放) |
OUT_TRADE_NO_CONFLICT | 409 | 商户订单号已存在但金额不一致 |
CHANNEL_UNAVAILABLE | 400 | 商户未开通该支付通道 |
PROVIDER_ERROR | 502 | 上游支付网关报错,详情见 message |
AMOUNT_MISMATCH | 409 | 上游回调金额与订单不一致(会被拒绝入账) |
RATE_LIMITED | 429 | 请求频率超限 |
7. 部署与运维
- Web 进程:
pnpm build && pnpm start;后台任务:pnpm worker。 - 数据库:PostgreSQL,使用
prisma db push建表,pnpm db:seed初始化管理员。 - 通道凭证以 AES-256-GCM 加密存储,密钥来自
ENCRYPTION_KEY;更换密钥需重新录入凭证。 - 多实例部署时,进程内限流与通知投递可能重复,建议商户侧按
tradeNo幂等处理。