@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-wechatpackage.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即可,无需自行安装二维码库。如需自定义渲染(如换库、生成矢量图),可用原始的codeUrl。Native 没有捕获步骤,支付结果以 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-signature、wechatpay-timestamp、wechatpay-nonce、wechatpay-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.ts 或 src/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_URL 和 SUPABASE_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. 规则摘要
- 唯一必填参数:
createWechatPayClient({ supabase }),禁止传config覆盖读取规则。 - 两个默认接口:
POST /api/payment/create-wechat-order+POST /api/payment/wechat/notify,AI 直接生成,不需要等用户定义。 - notify_url 走 Supabase 配置变量:
WECHATPAY_NOTIFY_URL必须是完整公网地址,路径固定为/api/payment/wechat/notify。 - 金额单位是分:
amount传整数分(¥19.99 → 1999),不要传元。 - 无捕获步骤:Native 扫码后以 webhook
success为入账依据。 - Webhook 原始 body:必须用
express.raw()中间件,透传req.headers,不能先JSON.parse()。 - 二维码 SDK 内置:直接拿
qrCodeDataUrl赋给<img src>,前端无需安装二维码库;等待 webhook 落库后更新状态。 - 不要发明抽象层:禁止生成
PaymentService、WechatManager、PaymentProvider等,直接 route → SDK。 - 安全底线:私钥 / APIv3 key / 证书绝不暴露给前端,
amount服务端校验。
