← 返回文档首页
PRODUCT INTEGRATION

梨界支付中心 · 接入指南

本文档公开可访问,可作为 prompt 直接喂给 AI 助手生成接入代码。所有 敏感凭据(hmac_secret、微信商户私钥、支付宝应用私钥)只允许在服务端保存。

1. 接入参数

Base URLhttps://pay.ljmatrix.cn
site_code后台 /admin/sites 创建后获取,例 lxhs
hmac_secret注册 site 时一次性显示,仅服务端保存
支付方式 channelwechat_native / wechat_jsapi / alipay_pc / alipay_wap / alipay_qr
alipay_pcalipay_page 等价(前者业务方惯用,后者历史命名)
金额单位(int),不是元。9.9 元 = 990 分

2. HMAC 签名算法

所有 /api/v1/* 请求和 callback 都用同一套 HMAC 协议。

2.1 请求头

X-Pay-Site:        <site_code>
X-Pay-Timestamp:   <unix秒,10位>      # 必须 ±5 分钟内
X-Pay-Nonce:       <随机串,≥12字符>    # 5 分钟内不可重复
X-Pay-Signature:   sha256=<hmac_hex>
Content-Type:      application/json

2.2 Signing string(6 行用 \n 拼接)

METHOD                              # POST / GET
PATH_WITH_QUERY                     # /api/v1/order/create   或带 query 时按 key 排序后 URL-encode 拼接
TIMESTAMP                           # 同 X-Pay-Timestamp
NONCE                               # 同 X-Pay-Nonce
SITE_CODE                           # 同 X-Pay-Site
SHA256_HEX(RAW_BODY)                # GET 用 sha256("") = e3b0c44...

2.3 计算签名

signature = "sha256=" + HMAC_SHA256_HEX(hmac_secret, signing_string)

⚠️ 验签必须用 timingSafeEqual(防时序攻击),不能直接 ===

3. 创建订单

POST https://pay.ljmatrix.cn/api/v1/order/create
Content-Type: application/json
X-Pay-Site: lxhs
X-Pay-Timestamp: 1778240000
X-Pay-Nonce: 7a3b9c1d4e5f6a8b
X-Pay-Signature: sha256=...
Idempotency-Key: ORDER202605080001        # 推荐:同 biz_order_no 即可

{
  "biz_order_no": "ORDER202605080001",
  "amount_fen": 9900,
  "channel": "wechat_native",
  "subject": "课程报名费",
  "client_ip": "1.2.3.4",
  "user_agent": "Mozilla/5.0 ...",
  "return_url": "https://your-site.com/checkout/ORDER202605080001",
  "metadata": { "user_id": "u_123", "product": "lxhs" }
}

字段说明

字段必填说明
biz_order_no业务侧订单号,同 site 唯一约束(见 §10)
amount_fen金额,单位"分",正整数
channel见 §1 通道列表
subject商品/订单主题,最长 127 字符,会展示给用户
client_ip用户公网 IP(微信支付强制要求)
user_agent仅日志/审计用
return_url条件必填仅浏览器跳转通道(alipay_page / alipay_wap)必须。 用户付完款支付宝把浏览器跳回的目标 URL。不传则用户停留在支付宝"付款成功,3 秒后自动返回商户"页面 → 没跳转目标 → UX 卡死。 建议传业务方的 checkout / 订单详情页,并在该页轮询订单状态。 扫码通道(wechat_native / alipay_qr)不需要。
payer_openidwechat_jsapi 通道用,公众号 H5 内识别用户的 openid
metadata任意 JSON,会在 callback 原样回传

响应

{
  "order_no": "lp_xxx",                    // pay 内部订单号
  "biz_order_no": "ORDER202605080001",
  "channel": "wechat_native",
  "amount_fen": 9900,
  "expires_at": 1778241800,
  "instructions": {
    "type": "QR",                          // QR / REDIRECT / JSAPI
    "qr_data_url": "data:image/png;base64,...",
    "raw_pay_url": "weixin://wxpay/bizpayurl?pr=..."
  }
}

前端拿到后:

3.5 前端付款后跳回的处理(新接入方必读

关键事实:用户在支付宝付款成功 → 浏览器跳回 return_url 时,业务方后端还不一定已经被 lj-pay 通知到了。

实测时间线:

T+0s    用户在支付宝点"确认付款",支付宝标 PAID
T+3s    用户浏览器跳回业务方 return_url
T+3s    支付宝异步发 webhook → lj-pay → 业务方 callback(最快路径,正常 3-30 秒)
↓ 如果 webhook 丢失或被防火墙拦:
T+90s+  lj-pay reconcile worker 主动查支付宝 → 走同样的 callback 路径

所以:用户跳回 return_url 那一刻(T+3s),业务方页面查订单可能还是 PENDING。不轮询,用户会看到"待支付"卡死

解决方案(前后端协议)

第 1 步:业务方在创单时把 return_url 末尾带一个标记参数(如 ?paid=1):

// 业务方后端创单
const returnUrl = `https://your-site.com/checkout/${order.id}?paid=1`
await ljPayCreate({ ..., return_url: returnUrl })

支付宝跳回时会追加自己的参数(out_trade_no / trade_no / sign 等), 最终 URL 变成 ?paid=1&out_trade_no=...&...paid=1 仍能被前端识别,其他参数业务方不需要验证。

第 2 步:checkout 页检测到 paid=1 就启动 1Hz 轮询:

// React + react-query 例子
const justPaid = new URLSearchParams(window.location.search).has("paid")
const { data } = useQuery({
  queryKey: ["order", orderId],
  queryFn: () => fetch(`/api/orders/${orderId}`).then(r => r.json()),
  refetchInterval: justPaid && data?.status !== "PAID" ? 1000 : false,
  refetchIntervalInBackground: false,
})

useEffect(() => {
  if (data?.status === "PAID") {
    const u = new URL(window.location.href)
    u.searchParams.delete("paid")
    window.history.replaceState({}, "", u.toString())
    toast.success("支付成功")
  }
}, [data?.status])

第 3 步:建议 5 分钟超时兜底(reconcile 最迟在创单后 ~3 分钟会标 PAID;超过 5 分钟还没 PAID 就引导用户联系客服)。

不要在前端用支付宝跳回 URL 里的 trade_status=TRADE_SUCCESS 当判断依据 —— 那是支付宝签的,业务方既没公钥也没必要验。只信你后端轮询的结果

4. 查询订单

GET https://pay.ljmatrix.cn/api/v1/order/by-biz-no/{biz_order_no}
GET https://pay.ljmatrix.cn/api/v1/order/by-pay-no/{order_no}

响应

{
  "order_no": "lp_xxx",
  "biz_order_no": "ORDER202605080001",
  "channel": "wechat_native",
  "status": "PAID",                        // PENDING/PAID/FAILED/REFUNDED/PARTIAL_REFUNDED/CANCELED/EXPIRED/PAY_AFTER_CLOSE
  "amount_fen": 9900,
  "paid_amount_fen": 9900,
  "refunded_fen": 0,
  "channel_trade_no": "4200001234",
  "created_at": "2026-05-08T13:00:00Z",
  "paid_at": "2026-05-08T13:01:23Z"
}

5. 取消订单

POST https://pay.ljmatrix.cn/api/v1/order/cancel
{ "biz_order_no": "ORDER202605080001" }

仅 PENDING 订单可取消。已支付的只能走退款。

6. 发起退款

POST https://pay.ljmatrix.cn/api/v1/order/refund
{
  "biz_order_no": "ORDER202605080001",
  "out_refund_no": "REFUND202605080001",
  "refund_fen": 9900,
  "reason": "用户申请退款"
}

响应

{
  "refund_no": "lpr_xxx",
  "out_refund_no": "REFUND202605080001",
  "status": "PROCESSING",                  // PROCESSING / SUCCESS / FAILED
  "refund_fen": 9900,
  "channel_refund_id": "5000xxx"
}

退款是异步的,最终状态通过 refund.success webhook 通知业务方。

7. 业务回调(Pay → 业务)

支付中心在订单状态变化时反向调业务方的 callback_url,使用同一套 HMAC 协议

7.1 Headers

POST {site.callback_url}
Content-Type: application/json
X-Pay-Site:        <site_code>
X-Pay-Timestamp:   <unix秒>
X-Pay-Nonce:       <随机串>
X-Pay-Request-Id:  dlv_xxx                # 投递 ID,用于幂等去重
X-Pay-Delivery-Id: dlv_xxx
X-Pay-Event:       order.paid             # / refund.success / order.pay_after_close
X-Pay-Signature:   sha256=...

7.2 order.paid body

{
  "event": "order.paid",
  "order_no": "lp_xxx",
  "biz_order_no": "ORDER202605080001",
  "channel": "wechat_native",
  "amount_fen": 9900,
  "paid_amount_fen": 9900,
  "channel_trade_no": "4200001234",
  "paid_at": "2026-05-08T13:01:23.000Z",
  "metadata": { "user_id": "u_123" }
}

7.3 refund.success body

{
  "event": "refund.success",
  "order_no": "lp_xxx",
  "biz_order_no": "ORDER202605080001",
  "refund_no": "lpr_yyy",
  "out_refund_no": "REFUND202605080001",
  "refund_fen": 9900,
  "channel_refund_id": "5000xxx",
  "refunded_at": "2026-05-08T13:05:00.000Z"
}

7.4 业务方必须做的 4 件事

  1. raw text body(不是 JSON.parse 后再 stringify)算 HMAC 验签
  2. 校验 amount_fen === yourOrder.amountFen(防 pay 被攻破伪造)
  3. 幂等:根据 biz_order_no + event 去重,已处理直接返回 ok
  4. 必须返回 HTTP 200 + {"ok":true},否则 pay 会重试 8 次

8. 状态机

PENDING ┬→ 支付通知/查单成功 → PAID
        ├→ cancel 接口 → CANCELED
        ├→ 超时未支付 → EXPIRED
        └→ 渠道明确失败 → FAILED

PAID    ┬→ 全额退款成功 → REFUNDED
        └→ 部分退款成功 → PARTIAL_REFUNDED

PARTIAL_REFUNDED → 继续退款直到全额 → REFUNDED

异常状态:
EXPIRED/CANCELED/FAILED → 渠道查单发现已支付 → PAY_AFTER_CLOSE(人工处理)

PAY_AFTER_CLOSE 含义:用户在订单已被本地关闭(超时 / 取消 / 失败)之后才完成支付。这是异常状态:钱已经收了但本地状态不正常。lj-pay 会发 order.pay_after_close 事件给业务方 callback,业务方收到这个事件应当人工介入(要么发货并恢复订单到 PAID,要么发起退款),不要自动当 PAID 处理。常见原因:用户把支付宝/微信付款页放后台太久了再付。

8.5 限制与默认值

字段限制
biz_order_no必填,最长 128 字符(建议 ASCII,不要混中文)
out_refund_no必填,最长 128 字符
subject必填,最长 127 字符(支付宝硬性约束)
reason(退款)可选,最长 255 字符
metadata可选,JSON object(不能是数组)
X-Pay-Nonce最短 12 字符;5 分钟内同 site 不可重复
X-Pay-TimestampUnix 秒,10 位整数;与服务端时差 ±300 秒
订单 TTL30 分钟:超时进入 EXPIRED;之后该 biz_order_no 不能复用(见 §10)
amount_fen正整数,单位"分";上限受上游渠道单笔限额约束
默认 currencyCNY(目前只支持 CNY)

9. 回调重试节奏

业务回调失败后,pay 按以下间隔重试,最多 11 次约 24 小时:

1s, 5s, 30s, 2m, 5m, 15m, 1h, 3h, 6h, 12h, 24h

仍失败 → 进入 DEAD 状态,后台「业务回调」可手动重放。

10. biz_order_no 的关键约束(必读)

同一个 site + biz_order_no 在 lj-pay 中只能存活一笔订单。

规则:

用户重试支付时,业务方必须生成新的 biz_order_no。常见做法:

// 业务方代码示意:用户点「重新支付」时
const newBizOrderNo = originalBizOrderNo + "_retry_" + Date.now()
// 或者:直接生成全新的 UUID / cuid
const newBizOrderNo = crypto.randomUUID()
// 把新订单号映射到原业务订单(DB 加一张 RetryMapping 表,或在 metadata 里存 original_id)

这是为了防止用户付款瞬间同时收到「PENDING 单过期 → 重新支付」和「过期单上其实已支付(PAY_AFTER_CLOSE)」造成账目错乱。

11. 错误码

错误码HTTP说明
INVALID_SIGNATURE401HMAC 验签失败
EXPIRED_TIMESTAMP401timestamp 超 ±5 分钟
REPLAYED_NONCE401nonce 已使用过(10min 缓存)
SITE_NOT_FOUND401site_code 不存在
SITE_DISABLED403站点已禁用
RATE_LIMITED429该 site 调用频率超限(默认 100/min 创单、600/min 查单、30/min 退款)
INVALID_REQUEST400参数 / JSON 格式错
INVALID_AMOUNT400amount_fen 非正整数
DUPLICATE_PENDING_ORDER409同 biz_order_no 已有未过期 PENDING
ORDER_ALREADY_PAID409已支付 / 已退款 / 已过期的订单不能复用 biz_order_no(见上一节)
ORDER_NOT_FOUND404订单不存在
ORDER_NOT_REFUNDABLE400订单非 PAID / PARTIAL_REFUNDED
REFUND_AMOUNT_EXCEEDED400退款金额超过可退金额
CHANNEL_DISABLED400该 channel 在后台未启用
CHANNEL_NOT_IMPLEMENTED501通道暂未实现
CHANNEL_API_ERROR502上游微信 / 支付宝返回错(含 raw 信息)
WEBHOOK_INVALID400渠道回调验签 / 解密失败
CALLBACK_URL_BLOCKED400callback URL 不在 allowlist 或解析到内网

11.5 HMAC 密钥轮换

业务方怀疑 hmac_secret 泄漏,或例行轮换:

  1. 联系 lj-pay 管理员(或自助到后台 /admin/sites/{site_code})触发轮换
  2. 系统生成新 secret,把旧 secret 标记为 previousSecretCiphertext,过期默认 24 小时
  3. 业务方在过期前更新 .envLJ_PAY_HMAC_SECRET 到新值并重启
  4. 过期窗口内 lj-pay 同时接受新旧两把 secret 签名(双签生效),保证不停机
  5. 过期后旧 secret 失效

callback(lj-pay → 业务方)方向用同一把 hmac_secret,业务方按新 secret 验签即可。

11.6 mock 通道警告

种子数据里有 wechat-mock / alipay-mock 两个通道,仅供本地开发与单元测试

12. 上线检查清单

13. 容易踩的坑

14. 自抄 SDK 模板

登录后台 /admin/developer 可看到 Node.js / Python / PHP 三种 SDK 模板,带「复制」按钮。

15. 给 AI 助手用的提示词

把这段提示词喂给 ChatGPT / Claude / Gemini 即可让 AI 生成完整接入代码:

我要给项目 X 接入梨界支付中心。

完整接入文档:https://pay.ljmatrix.cn/docs/integration.md
OpenAPI 规范:https://pay.ljmatrix.cn/openapi.json

我的项目栈:[Next.js / Express / Flask / Django / Laravel ...]
我的 callback URL:[https://业务域名/api/internal/pay-callback]
我已注册 site_code = "[code]",hmac_secret 在业务后端 .env

请生成:
1. lib/lj-pay.{ts|py|php}(HMAC 签名 + create/refund/cancel/query 4 个 API)
2. /api/internal/pay-callback 路由(验签 + 幂等 + 金额校验 + 标已支付)
3. 一个 ¥0.01 端到端测试脚本

要求:
- 金额单位是分
- 验签用 raw body + timingSafeEqual
- callback 幂等(biz_order_no + event 去重)
- 全部代码自包含可直接跑