快速开始

10 分钟跑通第一笔支付订单

前置条件

  • 已联系商务,拿到 apiKey / apiSecret / baseUrl
  • 已在微盈云后台为本平台开通 payments service
  • 已完成 platform 自身的子商户进件,sub_mchid 状态为 finished

1. 安装 / 准备签名工具

SMP 不绑定 SDK,按规则手动签名即可。下面是 Node.js 的最小工具:

ts
// smp-client.ts
import crypto from "crypto";

const SMP_BASE_URL = process.env.SMP_BASE_URL!;
const SMP_API_KEY = process.env.SMP_API_KEY!;
const SMP_API_SECRET = process.env.SMP_API_SECRET!;

const sign = (serviceCode: string, body: unknown) => {
  const signingKey = crypto
    .createHash("sha256")
    .update(SMP_API_SECRET)
    .digest("hex");
  const ts = Date.now().toString();
  const payload = ts + serviceCode.toLowerCase() + JSON.stringify(body ?? {});
  const sig = crypto.createHmac("sha256", signingKey).update(payload).digest("hex");
  return { ts, sig };
};

export async function smp<T>(
  method: "GET" | "POST" | "PUT" | "PATCH",
  path: string,
  serviceCode: "payments" | "activation" | "miniprogram",
  body?: unknown,
): Promise<T> {
  const { ts, sig } = sign(serviceCode, body);
  const res = await fetch(`${SMP_BASE_URL}${path}`, {
    method,
    headers: {
      "Content-Type": "application/json",
      "X-Api-Key": SMP_API_KEY,
      "X-Timestamp": ts,
      "X-Signature": sig,
      "X-Service-Code": serviceCode,
    },
    body: body == null ? undefined : JSON.stringify(body),
  });
  const data = await res.json().catch(() => ({}));
  if (!res.ok) {
    throw Object.assign(new Error(data.error || res.statusText), {
      statusCode: res.status,
      ...data,
    });
  }
  return data as T;
}

2. 第一笔订单(微信 Native 收款)

ts
const order = await smp("POST", "/v1/payments/orders", "payments", {
  outTradeNo: `hello_${Date.now()}`,
  amount: 0.01,                                 // 1 分
  description: "测试订单",
  channel: "wechat",
  notifyUrl: "https://your.domain/notify",
});

console.log("二维码:", order.payParams.codeUrl);

codeUrl 用任意二维码组件渲染,扫码即可付款。

3. 处理支付成功通知

SMP 收到微信回调后会向 notifyUrl POST:

ts
// 接收端示例(Express)
app.post("/notify", async (req, res) => {
  const { headers, body } = req;
  const ts = String(headers["x-timestamp"]);
  const sig = String(headers["x-signature"]);
  const expected = sign("payments", body).sig;  // 用上面同一个 sign() 工具

  if (sig !== expected) {
    return res.status(401).end("invalid sig");
  }
  if (Math.abs(Date.now() - Number(ts)) > 5 * 60 * 1000) {
    return res.status(401).end("expired");
  }

  if (body.status === "paid") {
    await fulfill(body.outTradeNo, body.transactionId);
  }
  res.status(204).end();
});

⚠️ 收到通知务必做幂等(按 outTradeNo + status 去重)。SMP 转发失败时只 log 不重试。

4. 主动查询(兜底)

如果 webhook 长时间未到,可以主动查:

ts
const status = await smp("GET", `/v1/payments/orders/${order.id}`, "payments");
// pending 时 SMP 会顺带向上游 query 一次最新状态再返回

下一步