npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@wecode-team/payment-wechat

v0.0.1

Published

We0 微信支付 SDK —— Native 扫码下单与 Webhook 验签

Readme

@wecode-team/payment-wechat 集成指南

当用户选择「微信支付」功能时,在当前 Razzle / Express 项目中集成 @wecode-team/payment-wechat SDK。

SDK 自动从 Supabase 配置表读取微信支付凭证和回调地址,业务侧只需传 supabase 实例。

MVP 范围:Native 扫码下单 + Webhook 验签。订单查询、关单、退款、JSAPI / H5 / APP 等暂不在本包范围内。微信 Native 没有"捕获"步骤,支付结果以 webhook 为准。

1. 安装依赖

npm install @wecode-team/payment-wechat

package.json 的 dependencies 中确保:

"@wecode-team/payment-wechat": "latest"

运行环境:Node.js 18+

2. 核心参数

2.1 createWechatPayClient — 创建客户端

唯一必填参数supabase 实例。

const client = await createWechatPayClient({ supabase });

强约束(必须遵守)

  • 只传 supabase,禁止传 config 参数覆盖读取规则。配置读取策略由 SDK 内部管理(设置 PROJECT_ID 时自动按 CMS 配置表约定读取,否则用通用 config 表)。
  • supabase 必须使用 service role key 创建,不能用 anon key(anon key 无法读取配置表)。
  • 禁止手动拼接微信商户号 / 密钥 / 证书,全部由 SDK 自动管理。
  • WECHATPAY_NOTIFY_URL 必须写入 Supabase 配置中心,值是完整公网回调地址,路径固定为 /api/payment/wechat/notify

正确写法

// ✅ 正确:只传 supabase,配置由 SDK 内部读取
const supabase = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!,
);
const client = await createWechatPayClient({ supabase });

错误写法

// ❌ 错误:传 config 覆盖读取规则
const client = await createWechatPayClient({
  supabase,
  config: { apiKeyField: "MY_WECHAT_KEY" }, // 禁止手动覆盖
});

// ❌ 错误:用 anon key
const supabase = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_ANON_KEY!, // anon key 无法读取配置表
);

配置中心字段:SDK 从 Supabase 配置表读取以下字段(采用「公钥模式」验签):

| CMS 字段名 | 对应 SDK | 必填 | 说明 | |-----------|---------|------|------| | WECHATPAY_APP_ID | appId | 是 | 公众号 / 小程序 / 应用 AppID | | WECHATPAY_MCH_ID | mchId | 是 | 微信支付商户号 | | WECHATPAY_API_V3_KEY | apiKey | 是 | APIv3 密钥 | | WECHATPAY_PRIVATE_KEY_PEM | privateKey | 是 | 商户 API 私钥 apiclient_key.pem(PEM 多行字符串) | | WECHATPAY_PLATFORM_CERT_PEM | publicKey | 是 | 商户 API 证书 apiclient_cert.pem(取证书序列号给请求签名;名字带 PLATFORM 但装的是商户证书) | | WECHATPAY_PAY_PUBLIC_KEY_PEM | paymentPublicKey | 是 | 微信支付公钥 wechat_pay_pub.pem(本地验微信回调签名) | | WECHATPAY_NOTIFY_URL | notifyUrl | 是 | 微信支付回调地址,必须是完整公网 URL,路径固定为 /api/payment/wechat/notify |

PEM 类字段(私钥 / 商户证书 / 微信支付公钥)以多行字符串形式存入 Supabase JSON 字段,保留 -----BEGIN ...----- 头尾与换行。

回调地址 notify_url(Supabase 配置变量):SDK 直接读取 WECHATPAY_NOTIFY_URL,业务代码不传、不拼、不推导:

WECHATPAY_NOTIFY_URL=https://your-domain.com/api/payment/wechat/notify
  • AI 必须在后端注册同一路径的接口:POST /api/payment/wechat/notify(见 3.2)。
  • 微信 Native 下单的 notify_url 只能来自 Supabase 配置变量(SDK 忽略下单参数里的 notify_url),未配置会抛 PROVIDER_NOT_CONFIGURED

2.2 createNativeOrder — 创建 Native 扫码订单

type WechatCreateNativeOrderInput = {
  amount: number;        // 金额,单位为**最小货币单位**(分 = 1/100 CNY)
  orderId: string;       // 业务订单号 out_trade_no,必须唯一
  description: string;   // 商品描述,用户可在微信支付页看到
  attach?: string;       // 附加数据,原样在 webhook 回传(可选)
  timeExpire?: string;   // 订单失效时间,RFC 3339(可选)
  qrCode?: {             // 二维码图片选项(可选)
    width?: number;      // 图片宽度(px),默认 256
    margin?: number;     // 边距,默认 4
  };
};

type WechatCreateNativeOrderResult = {
  codeUrl: string;       // weixin://... 原始链接
  qrCodeDataUrl: string; // 二维码图片 data URL(PNG base64),可直接用作 <img src>
  orderId: string;       // 业务订单号(回传)
};

强约束

  • amount 单位是最小货币单位:CNY 传分(¥19.99 → 1999)。不要传元(如 19.99)
  • orderId 必须全局唯一,建议格式:order_${userId}_${timestamp}
  • notify_url 由 Supabase 配置变量 WECHATPAY_NOTIFY_URL 提供(见 2.1),不在下单参数里传——SDK 的 Native 下单固定取该地址,未配置会抛 PROVIDER_NOT_CONFIGURED
  • 二维码已由 SDK 内置生成qrCodeDataUrl 是一张 PNG 图片的 data URL,前端直接 img.src = qrCodeDataUrl 即可,无需自行安装二维码库。如需自定义渲染(如换库、生成矢量图),可用原始的 codeUrlNative 没有捕获步骤,支付结果以 webhook 为准。

2.3 verifyWebhook — 验签

type WechatWebhookEvent = {
  eventType: string;      // TRANSACTION.SUCCESS / REFUND.SUCCESS 等
  transactionId?: string; // 微信支付交易单号
  orderId: string;        // 业务订单号 out_trade_no
  status: "success" | "failed" | "pending";
  amount: number;         // 金额,单位「分」整数
  raw: unknown;           // 解密后的原始数据
};

强约束(必须遵守)

  • body 必须是原始请求体字符串,不能先 JSON.parse() 再传入。
  • Webhook 路由必须使用 express.raw({ type: "application/json" }) 中间件。
  • headers 必须保留微信原始请求头(wechatpay-signaturewechatpay-timestampwechatpay-noncewechatpay-serial),直接透传 req.headers
  • 验签依赖微信支付公钥,需在配置中心配置 WECHATPAY_PAY_PUBLIC_KEY_PEM。本包使用本地公钥验签,不要求配置 PUB_KEY_ID_...

事件映射:trade_state === "SUCCESS"(或 TRANSACTION.SUCCESS)→ "success"CLOSED / REVOKED / PAYERROR / REFUND.ABNORMAL"failed",其余 → "pending"

正确写法

// ✅ 正确:express.raw 中间件 + 原始 body + 透传 headers
router.post(
  "/api/payment/wechat/notify",
  express.raw({ type: "application/json" }),
  async (req, res) => {
    const event = await client.verifyWebhook(
      req.headers,
      req.body.toString("utf8"), // 原始 buffer 转 string
    );
  },
);

错误写法

// ❌ 错误:先 JSON.parse 再传入
router.post("/api/payment/wechat/notify", async (req, res) => {
  const body = JSON.parse(req.body); // 已经丢失原始格式,验签必然失败
  const event = await client.verifyWebhook(req.headers, body);
});

// ❌ 错误:使用 express.json() 中间件
router.post(
  "/api/payment/wechat/notify",
  express.json(), // json() 会自动 parse,body 已不是原始格式
  async (req, res) => { ... },
);

2.4 其他 API

| 方法 | 说明 | |------|------| | initWechatPay(options) / wechatPay | 初始化默认单例并复用 | | verifyWechatWebhook(headers, body, options) | 低级验签函数,适用于已有自己凭证来源的场景,从 @wecode-team/payment-wechat/webhook 导入 | | normalizeWechatWebhookEvent(eventType, decryptedData) | 仅做事件归一化,适用于已自行验签的场景 |

3. 默认落地路径

本项目使用 Razzle / Express,不要按 Next.js App Router 理解

AI 必须生成以下两个接口,不需要等用户定义(微信 Native 没有捕获步骤,因此只有"下单 + webhook"):

3.1 POST /api/payment/create-wechat-order

职责:接收前端请求,创建 Native 订单,返回二维码链接。

放置位置src/routes/payment.tssrc/api/payment.ts(根据项目现有路由结构调整)。

import express from "express";
import { createClient } from "@supabase/supabase-js";
import { createWechatPayClient } from "@wecode-team/payment-wechat";

const router = express.Router();

router.post("/api/payment/create-wechat-order", async (req, res) => {
  try {
    const supabase = createClient(
      process.env.SUPABASE_URL!,
      process.env.SUPABASE_SERVICE_ROLE_KEY!,
    );
    const client = await createWechatPayClient({ supabase });

    const order = await client.createNativeOrder({
      amount: req.body.amount,           // 单位:分
      orderId: req.body.orderId,
      description: req.body.description,
      attach: req.body.attach,
    });

    res.json({ code: 200, message: "success", data: order });
  } catch (error) {
    const message = (error as Error).message;
    console.error("[payment] create-wechat-order failed:", message);
    res.status(500).json({ code: 500, message });
  }
});

3.2 POST /api/payment/wechat/notify

职责:接收微信支付回调,验签,处理支付结果。

放置位置:同上,与下单路由放在同一文件。

router.post(
  "/api/payment/wechat/notify",
  express.raw({ type: "application/json" }),
  async (req, res) => {
    try {
      const supabase = createClient(
        process.env.SUPABASE_URL!,
        process.env.SUPABASE_SERVICE_ROLE_KEY!,
      );
      const client = await createWechatPayClient({ supabase });

      const event = await client.verifyWebhook(
        req.headers,
        req.body.toString("utf8"),
      );

      console.log("[payment] webhook received:", event.status, event.orderId);

      if (event.status === "success") {
        // TODO: 标记订单支付成功,更新数据库
        // await db.update(orders).set({ status: "paid" }).where(eq(orders.id, event.orderId));
      } else if (event.status === "failed") {
        // TODO: 标记订单支付失败
        // await db.update(orders).set({ status: "failed" }).where(eq(orders.id, event.orderId));
      }

      // 微信要求返回指定结构,否则会重试回调
      res.json({ code: "SUCCESS", message: "成功" });
    } catch (error) {
      const message = (error as Error).message;
      console.error("[payment] webhook verification failed:", message);
      res.status(400).json({ code: "FAIL", message });
    }
  },
);

3.3 前端渲染二维码

const res = await fetch("/api/payment/create-wechat-order", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    amount: 1999, // ¥19.99,单位:分
    orderId: `order_${userId}_${Date.now()}`,
    description: "Pro plan",
  }),
});

if (!res.ok) {
  const error = await res.json().catch(() => ({ message: "网络错误" }));
  toast.error(error.message || "下单失败,请稍后重试");
  return;
}

const result = await res.json();

// SDK 已内置生成二维码图片,前端直接用作 <img src>,无需自行安装二维码库
imgEl.src = result.data.qrCodeDataUrl;

// 之后前端轮询自己的订单状态接口(由 webhook 落库后更新),或用 SSE/WebSocket 等待支付成功

4. 安全边界

强约束(必须遵守)

| # | 约束 | 违反后果 | |---|------|----------| | 1 | 禁止将商户私钥 / APIv3 key / 证书暴露给前端 | 凭证泄露,资金风险 | | 2 | Webhook 路由必须保留原始 body,不能先 JSON.parse | 验签必然失败 | | 3 | amount(分)必须在服务端校验范围(如最低 1 元 / 业务下限) | 防止前端篡改金额 | | 4 | 以 webhook 的 status === "success" 为入账依据 | 防止仅凭"扫码"就误判已支付 | | 5 | 禁止在前端硬编码微信支付凭证 | 凭证泄露 |

5. 错误处理

5.1 SDK 层错误

| 场景 | 错误信息 | 处理方式 | |------|----------|----------| | Supabase 连接失败 | Failed to load config | 检查 SUPABASE_URLSUPABASE_SERVICE_ROLE_KEY 环境变量 | | 配置表不存在 / 无记录 | No config row found | 确认 Supabase 配置表已创建且包含微信支付凭证 | | 凭证为空 | "app_id" is empty 等 | 在配置表中补全对应字段 | | 缺少回调地址 | notify_url is required | 配置 notify_url 或在下单时传 notifyUrl | | 金额非法 | amount must be a positive integer in cents | 传整数(如 1999),不要传元 | | Webhook 验签失败 | WeChat webhook ... verification failed | 确认 body 是原始格式,确认平台公钥配置正确 |

5.2 前端错误处理

前端必须处理以下场景:

// 1. 网络错误
if (!res.ok) {
  const error = await res.json().catch(() => ({ message: "网络错误" }));
  toast.error(error.message || "下单失败,请稍后重试");
  return;
}

// 2. 展示二维码(SDK 已内置生成,直接赋值即可)
imgEl.src = result.data.qrCodeDataUrl;

// 3. 支付结果页
// success:等待 webhook 落库后展示支付成功 + 订单详情
// timeout:二维码过期/未支付,提供重新下单按钮

6. 平台生成场景职责边界

| 角色 | 职责 | |------|------| | 平台层 | 在配置中心写入微信支付凭证和 WECHATPAY_NOTIFY_URL | | AI | 注册支付相关 route,调用 SDK | | SDK | 读取凭证、Native 下单、webhook 验签与归一化 | | 用户 | 不需要在代码里手填任何凭证 |

默认路径是"服务端 Native 下单 + 前端渲染二维码 + webhook 兜底入账",而不是让前端直接对接微信支付凭证。

7. 规则摘要

  1. 唯一必填参数createWechatPayClient({ supabase }),禁止传 config 覆盖读取规则。
  2. 两个默认接口POST /api/payment/create-wechat-order + POST /api/payment/wechat/notify,AI 直接生成,不需要等用户定义。
  3. notify_url 走 Supabase 配置变量WECHATPAY_NOTIFY_URL 必须是完整公网地址,路径固定为 /api/payment/wechat/notify
  4. 金额单位是分amount 传整数分(¥19.99 → 1999),不要传元。
  5. 无捕获步骤:Native 扫码后以 webhook success 为入账依据。
  6. Webhook 原始 body:必须用 express.raw() 中间件,透传 req.headers,不能先 JSON.parse()
  7. 二维码 SDK 内置:直接拿 qrCodeDataUrl 赋给 <img src>,前端无需安装二维码库;等待 webhook 落库后更新状态。
  8. 不要发明抽象层:禁止生成 PaymentServiceWechatManagerPaymentProvider 等,直接 route → SDK。
  9. 安全底线:私钥 / APIv3 key / 证书绝不暴露给前端,amount 服务端校验。