payment 支付服务
统一的支付集成包,支持 Stripe 和微信支付
本页面包含支付配置和 API 使用的完整指南。
概述
@mono/payment 提供统一的支付服务接口,支持多个支付提供商。无论使用哪个提供商,都可以通过相同的 API 处理支付流程。
核心功能
- 支持 Stripe(国际支付)和微信支付(中国市场)
- 统一的 API 接口,轻松切换提供商
- 微信支付三种渠道:NATIVE(扫码)、JSAPI(公众号)、MINIPROGRAM_BRIDGE(小程序)
- Webhook 签名验证
- 订单查询和管理
- 完善的错误处理和日志记录
快速开始
安装
pnpm add @mono/paymentStripe 基础用法
import { createPaymentProvider } from "@mono/payment";
// 创建 Stripe 提供商
const stripeProvider = createPaymentProvider("stripe");
// 创建支付
const result = await stripeProvider.createPayment({
orderId: "order_123",
userId: "user_123",
planId: "monthly",
amount: 10,
currency: "usd",
});
// 跳转到支付页面
window.location.href = result.paymentUrl;微信支付基础用法
import { createWechatPayProvider, type WechatPaymentParams } from "@mono/payment";
// 从环境变量创建提供商
const wechatProvider = createWechatPayProvider();
// 创建 NATIVE 支付(扫码)
const params: WechatPaymentParams = {
orderId: "order_123",
userId: "user_123",
planId: "monthly",
amount: 99.99,
currency: "CNY",
channel: { type: "NATIVE" },
description: "月度订阅",
};
const result = await wechatProvider.createPayment(params);
// 显示二维码给用户扫描
console.log("二维码链接:", result.codeUrl);微信支付渠道选择
微信支付支持三种支付渠道,根据用户环境自动选择:
| 渠道 | 使用场景 | 必需参数 | 返回内容 |
|---|---|---|---|
| NATIVE | PC 网站扫码支付 | - | codeUrl(二维码链接) |
| JSAPI | 微信公众号内支付 | openid | jsapiParams(支付参数) |
| MINIPROGRAM_BRIDGE | 小程序支付 | openid | jsapiParams(支付参数) |
自动渠道选择
function selectWechatPayChannel(userAgent: string, openid?: string) {
// 小程序环境
if (userAgent.includes("miniProgram")) {
return { type: "MINIPROGRAM_BRIDGE", openid };
}
// 微信浏览器(移动端)
if (userAgent.includes("MicroMessenger") && openid) {
return { type: "JSAPI", openid };
}
// PC 或其他浏览器
return { type: "NATIVE" };
}实战示例
示例 1:NATIVE 扫码支付(PC)
// app/api/payment/wechat/native/route.ts
import { createWechatPayProvider, type WechatPaymentParams } from "@mono/payment";
import { NextResponse } from "next/server";
export async function POST(req: Request) {
try {
const { orderId, amount, userId } = await req.json();
const wechatProvider = createWechatPayProvider();
const params: WechatPaymentParams = {
orderId,
userId,
planId: "monthly",
amount,
currency: "CNY",
channel: { type: "NATIVE" },
description: "月度订阅",
};
const result = await wechatProvider.createPayment(params);
return NextResponse.json({
success: true,
codeUrl: result.codeUrl,
orderId: result.providerOrderId,
});
} catch (error) {
console.error("创建支付失败:", error);
return NextResponse.json(
{ error: "支付创建失败" },
{ status: 500 }
);
}
}前端展示二维码:
// components/WechatPayQRCode.tsx
import { QRCodeSVG } from "qrcode.react";
import { useState, useEffect } from "react";
export function WechatPayQRCode({ orderId, amount }: { orderId: string; amount: number }) {
const [codeUrl, setCodeUrl] = useState<string>("");
const [status, setStatus] = useState<"pending" | "paid" | "failed">("pending");
useEffect(() => {
// 创建支付
fetch("/api/payment/wechat/native", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ orderId, amount, userId: "current-user-id" }),
})
.then((res) => res.json())
.then((data) => setCodeUrl(data.codeUrl));
// 轮询订单状态
const interval = setInterval(async () => {
const res = await fetch(`/api/payment/status/${orderId}`);
const data = await res.json();
if (data.status === "paid") {
setStatus("paid");
clearInterval(interval);
}
}, 2000);
return () => clearInterval(interval);
}, [orderId, amount]);
if (status === "paid") {
return <div className="text-green-600">支付成功!</div>;
}
return (
<div className="flex flex-col items-center gap-4">
<h3 className="text-lg font-semibold">请使用微信扫码支付</h3>
{codeUrl && <QRCodeSVG value={codeUrl} size={256} />}
<p className="text-sm text-muted-foreground">
支付金额: ¥{amount.toFixed(2)}
</p>
</div>
);
}示例 2:JSAPI 公众号支付
// app/api/payment/wechat/jsapi/route.ts
import { createWechatPayProvider, type WechatPaymentParams } from "@mono/payment";
import { NextResponse } from "next/server";
export async function POST(req: Request) {
try {
const { orderId, amount, userId, openid } = await req.json();
if (!openid) {
return NextResponse.json(
{ error: "需要用户 openid" },
{ status: 400 }
);
}
const wechatProvider = createWechatPayProvider();
const params: WechatPaymentParams = {
orderId,
userId,
planId: "monthly",
amount,
currency: "CNY",
channel: {
type: "JSAPI",
openid, // 通过微信 OAuth 获取
},
description: "月度订阅",
};
const result = await wechatProvider.createPayment(params);
return NextResponse.json({
success: true,
jsapiParams: result.jsapiParams,
orderId: result.providerOrderId,
});
} catch (error) {
console.error("创建支付失败:", error);
return NextResponse.json(
{ error: "支付创建失败" },
{ status: 500 }
);
}
}前端调用微信 JSAPI:
// components/WechatJSAPIPay.tsx
"use client";
import { useState } from "react";
declare global {
interface Window {
WeixinJSBridge: any;
}
}
export function WechatJSAPIPay({ orderId, amount, openid }: {
orderId: string;
amount: number;
openid: string;
}) {
const [paying, setPaying] = useState(false);
const handlePay = async () => {
setPaying(true);
try {
// 创建支付
const res = await fetch("/api/payment/wechat/jsapi", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ orderId, amount, userId: "current-user-id", openid }),
});
const data = await res.json();
if (!data.success) {
throw new Error(data.error);
}
// 调用微信支付
if (typeof window.WeixinJSBridge === "undefined") {
alert("请在微信中打开");
return;
}
window.WeixinJSBridge.invoke(
"getBrandWCPayRequest",
data.jsapiParams,
(res: any) => {
if (res.err_msg === "get_brand_wcpay_request:ok") {
// 支付成功
window.location.href = "/payment-success";
} else {
// 支付失败或取消
alert("支付失败");
}
setPaying(false);
}
);
} catch (error) {
console.error("支付错误:", error);
alert("支付失败,请重试");
setPaying(false);
}
};
return (
<button
onClick={handlePay}
disabled={paying}
className="w-full bg-green-600 text-white py-3 rounded-lg disabled:opacity-50"
>
{paying ? "处理中..." : `微信支付 ¥${amount.toFixed(2)}`}
</button>
);
}示例 3:小程序支付
// 小程序端代码
// pages/payment/index.ts
Page({
data: {
orderId: "",
amount: 0,
},
async onPay() {
try {
// 调用后端创建支付
const res = await wx.request({
url: "https://yourdomain.com/api/payment/wechat/miniprogram",
method: "POST",
data: {
orderId: this.data.orderId,
amount: this.data.amount,
userId: "current-user-id",
openid: wx.getStorageSync("openid"),
},
});
if (!res.data.success) {
throw new Error(res.data.error);
}
// 调用小程序支付
const payResult = await wx.requestPayment(res.data.jsapiParams);
// 支付成功
wx.showToast({
title: "支付成功",
icon: "success",
});
// 跳转到成功页面
wx.redirectTo({
url: "/pages/payment-success/index",
});
} catch (error) {
console.error("支付失败:", error);
wx.showToast({
title: "支付失败",
icon: "error",
});
}
},
});示例 4:Webhook 处理
// app/api/webhooks/wechat-pay/route.ts
import { createWechatPayProvider } from "@mono/payment";
import { NextResponse } from "next/server";
import { db } from "@/lib/db";
export async function POST(req: Request) {
try {
const body = await req.text();
const signature = req.headers.get("wechatpay-signature") || "";
const wechatProvider = createWechatPayProvider();
const verification = await wechatProvider.handleWebhook(body, signature);
if (verification.success && verification.orderId) {
// 更新数据库订单状态
await db.order.update({
where: { id: verification.orderId },
data: {
status: "paid",
paidAt: new Date(),
},
});
// 发送支付成功通知
await sendPaymentSuccessNotification(verification.orderId);
// 返回成功响应(必须)
return NextResponse.json({
code: "SUCCESS",
message: "成功",
});
}
return NextResponse.json(
{ code: "FAIL", message: "验证失败" },
{ status: 400 }
);
} catch (error) {
console.error("Webhook 处理错误:", error);
return NextResponse.json(
{ code: "FAIL", message: "处理失败" },
{ status: 500 }
);
}
}示例 5:订单状态查询
// app/api/payment/status/[orderId]/route.ts
import { createWechatPayProvider } from "@mono/payment";
import { NextResponse } from "next/server";
export async function GET(
req: Request,
{ params }: { params: { orderId: string } }
) {
try {
const wechatProvider = createWechatPayProvider();
const orderStatus = await wechatProvider.queryOrder(params.orderId);
return NextResponse.json({
orderId: params.orderId,
status: orderStatus.status,
metadata: orderStatus.metadata,
});
} catch (error) {
console.error("查询订单失败:", error);
return NextResponse.json(
{ error: "查询失败" },
{ status: 500 }
);
}
}示例 6:订单超时关闭
// lib/payment/order-timeout.ts
import { createWechatPayProvider } from "@mono/payment";
import { db } from "@/lib/db";
export async function closeExpiredOrders() {
// 查找超过 30 分钟未支付的订单
const expiredOrders = await db.order.findMany({
where: {
status: "pending",
createdAt: {
lt: new Date(Date.now() - 30 * 60 * 1000),
},
},
});
const wechatProvider = createWechatPayProvider();
for (const order of expiredOrders) {
try {
// 关闭微信支付订单
const success = await wechatProvider.closeOrder(order.id);
if (success) {
// 更新数据库状态
await db.order.update({
where: { id: order.id },
data: { status: "closed" },
});
console.log(`订单 ${order.id} 已关闭`);
}
} catch (error) {
console.error(`关闭订单 ${order.id} 失败:`, error);
}
}
}
// 使用 cron job 定期执行
// 例如:每 10 分钟执行一次环境变量配置
Stripe
STRIPE_SECRET_KEY=sk_test_xxxxxxxx
STRIPE_WEBHOOK_SECRET=whsec_xxxxxxxx
NEXT_PUBLIC_APP_URL=https://yourdomain.com微信支付
# 微信支付配置
WECHAT_PAY_APP_ID=wx1234567890abcdef # 微信公众号/小程序 AppID
WECHAT_PAY_MCH_ID=1234567890 # 商户号
WECHAT_PAY_API_V3_KEY=your-32-character-key # API v3 密钥 (32字符)
WECHAT_PAY_SERIAL_NO=1234567890ABCDEF # 证书序列号
WECHAT_PAY_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC7VJTUt9Us8cKj
...
-----END PRIVATE KEY-----" # 商户私钥 (完整 PEM 格式)
WECHAT_PAY_NOTIFY_URL=https://yourdomain.com/api/webhooks/wechat-pay # 支付回调 URL (可选)如何获取微信支付凭证:
- AppID: 从微信公众平台或小程序平台获取
- 商户号 (mchId): 从微信支付商户平台获取
- API v3 密钥: 在商户平台 → 账户中心 → API 安全 → 设置 API 密钥(32 个字符)
- 证书序列号: 从商户平台 → 账户中心 → API 安全 → 管理证书 下载证书
- 商户私钥: 使用微信支付证书工具生成,妥善保管
API 参考
createPaymentProvider
创建支付提供商实例。
function createPaymentProvider(
provider: "stripe" | "wechat"
): PaymentProvider;createWechatPayProvider
创建微信支付提供商实例。
function createWechatPayProvider(
config?: WechatPayConfig
): WechatPayProvider;PaymentProvider.createPayment
创建支付订单。
async createPayment(params: PaymentParams): Promise<PaymentResult>;PaymentProvider.handleWebhook
处理支付回调。
async handleWebhook(
payload: string | Record<string, unknown>,
signature: string
): Promise<WebhookVerification>;WechatPayProvider.queryOrder
查询订单状态。
async queryOrder(orderId: string): Promise<{
status: "pending" | "paid" | "failed";
metadata?: Record<string, unknown>;
}>;WechatPayProvider.closeOrder
关闭未支付订单。
async closeOrder(orderId: string): Promise<boolean>;最佳实践
1. 安全性
- 保护私钥: 存储在环境变量中,永远不要提交到 git
- 验证 Webhook: 始终验证签名后再处理
- 使用 HTTPS: 生产环境所有支付 URL 必须使用 HTTPS
- 实现幂等性: 处理重复的 webhook 通知
2. 错误处理
try {
const result = await provider.createPayment(params);
// 处理成功
} catch (error) {
// 记录错误用于调试
console.error("支付创建失败:", error);
// 返回用户友好的错误
return { error: "无法处理支付,请重试" };
}3. 订单管理
- 生成唯一订单 ID(例如:
order_${Date.now()}_${randomString()}) - 在数据库中存储订单状态
- 实现订单超时机制
- 关闭过期的未支付订单
4. 测试
- 开发环境使用沙箱/测试环境
- 测试所有支付渠道
- 使用模拟数据测试 webhook 处理
- 验证签名验证逻辑
5. 监控
- 记录所有支付操作
- 监控 webhook 送达情况
- 跟踪支付成功率
- 设置失败告警
故障排查
微信支付问题
问题:"Missing required environment variable"
- 确保所有微信支付环境变量已设置
- 检查变量名称完全匹配
- 添加变量后重启服务器
问题:"Signature verification failed"
- 验证证书序列号是否正确
- 检查私钥格式(必须是 PEM 格式)
- 确保 API v3 密钥正好 32 个字符
问题:"OpenID is required for JSAPI payment"
- JSAPI 和 MINIPROGRAM_BRIDGE 渠道需要用户的 openid
- 通过微信 OAuth 流程获取 openid
- 在 channel 配置中传入 openid
问题:Webhook 未收到通知
- 验证 webhook URL 可公开访问(HTTPS)
- 检查防火墙/安全组设置
- 使用微信支付沙箱测试 webhook URL
- 确保 webhook 返回正确的响应格式
Stripe 问题
问题:"No such customer"
- 验证 Stripe 密钥是否正确
- 检查是否一致使用测试/生产模式
问题:Webhook 签名不匹配
- 验证 webhook 密钥与 Stripe 控制台匹配
- 确保使用原始请求体(不是解析后的 JSON)
相关文档
完整的 API 文档请参考 README.md。