@wecode-team/payment-stripe
v0.0.4
Published
We0 Stripe SDK —— PaymentIntent 收款,自动 Webhook 注册
Readme
@wecode-team/payment-stripe 集成指南
当用户选择「Stripe 支付」功能时,在当前 Razzle / Express 项目中集成 @wecode-team/payment-stripe SDK。
SDK 自动从 CMS 公共数据存储读取 Stripe 密钥,业务侧只需传 supabase 实例。
1. 安装依赖
npm install @wecode-team/payment-stripepackage.json 的 dependencies 中确保:
"@wecode-team/payment-stripe": "latest"运行环境:Node.js 18+
2. 核心参数
2.1 createStripeClient — 创建客户端
唯一必填参数:supabase 实例。
const client = await createStripeClient({ supabase });强约束(必须遵守):
- 只传
supabase,禁止传config参数覆盖读取规则。PROJECT_ID必填,SDK 只通过 CMS 公共数据存储读取配置。 supabase必须使用 service role key 创建,不能用 anon key(anon key 无法读取配置表)。- 禁止手动拼接 Stripe key 或 webhook secret,全部由 SDK 自动管理。
${PROJECT_ID}__config__ 仅作为逻辑 table_name,并不是物理表。SDK 会先通过 TableFieldsStore 查询 table_fields,确认该逻辑表的字段定义已注册;确认后再通过 SupabasePublicDataStore 从物理表 public_data_0 读取 namespace=payment 的配置。自动注册 webhook 后,signing secret 也会合并回同一条公共数据记录。
正确写法:
// ✅ 正确:只传 supabase,配置由 SDK 内部读取
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!,
);
const client = await createStripeClient({ supabase });错误写法:
// ❌ 错误:传 config 覆盖读取规则
const client = await createStripeClient({
supabase,
config: { secretKey: "sk_test_xxx" }, // 禁止手动传 key
});
// ❌ 错误:用 anon key
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_ANON_KEY!, // anon key 无法读取配置表
);2.2 createCheckoutSession — 创建支付会话
type CreateCheckoutSessionInput = {
amount: number; // 金额,单位为**最小货币单位**(美分 = 1/100 USD)
currency: string; // 货币代码,如 "usd"、"cny"
orderId: string; // 业务订单 ID,必须唯一
description: string; // 支付描述,用户可在 Stripe 页面看到
successUrl: string; // 支付成功回调地址
cancelUrl: string; // 支付取消回调地址
metadata?: Record<string, string>; // 自定义元数据,会随 webhook 返回
};
type StripeCheckoutSessionResult = {
sessionId: string; // Stripe Checkout Session ID
orderId: string; // 业务订单 ID(回传)
url: string; // 前端跳转地址
};强约束:
amount单位是最小货币单位:USD 传美分($19.99 →1999),CNY 传分(¥19.99 →1999)。orderId必须全局唯一,建议格式:order_${userId}_${timestamp}。successUrl和cancelUrl必须是完整的绝对 URL,不能是相对路径。successUrl中可用{CHECKOUT_SESSION_ID}占位符,Stripe 会自动替换为实际 session ID。
2.3 verifyWebhook — 验签
type StripeWebhookEvent = {
checkoutSessionId?: string;
paymentId: string;
orderId: string;
status: "success" | "failed" | "pending";
amount: number;
raw: unknown;
};强约束(必须遵守):
body必须是原始请求体字符串,不能先JSON.parse()再传入。- Webhook 路由必须使用
express.raw({ type: "application/json" })中间件。 headers至少包含stripe-signature字段。
正确写法:
// ✅ 正确:express.raw 中间件 + 原始 body
router.post(
"/api/payment/webhook",
express.raw({ type: "application/json" }),
async (req, res) => {
const event = await client.verifyWebhook(
{ "stripe-signature": req.headers["stripe-signature"] as string },
req.body.toString("utf8"), // 原始 buffer 转 string
);
},
);错误写法:
// ❌ 错误:先 JSON.parse 再传入
router.post("/api/payment/webhook", async (req, res) => {
const body = JSON.parse(req.body); // 已经丢失原始格式,验签必然失败
const event = await client.verifyWebhook(headers, body);
});
// ❌ 错误:使用 express.json() 中间件
router.post(
"/api/payment/webhook",
express.json(), // json() 会自动 parse,body 已不是原始格式
async (req, res) => { ... },
);2.4 registerWebhook — 自动注册
type RegisterStripeWebhookInput = {
url: string; // webhook 回调地址,必须是公网可访问的绝对 URL
description?: string; // 描述
enabledEvents?: string[]; // 监听事件列表
};默认监听事件:checkout.session.completed、checkout.session.async_payment_failed、payment_intent.succeeded、payment_intent.payment_failed
说明:SDK 自动把 signing secret 写回 CMS 公共数据中的原配置记录,不需要手动填写。
2.5 其他 API
| 方法 | 说明 |
|------|------|
| client.listWebhookEndpoints() | 列出已注册的 webhook endpoint |
| client.createPaymentIntent(input) | 创建 PaymentIntent,仅用于明确需要 Intent 流程时 |
| client.isTestMode | 当前是否为测试模式(sk_test_*) |
| initStripe(options) / stripe | 初始化默认单例并复用 |
| verifyStripeWebhook(headers, body, webhookSecret, secretKey) | 低级验签函数,适用于已有自己 key 来源的场景 |
3. 默认落地路径
本项目使用 Razzle / Express,不要按 Next.js App Router 理解。
AI 必须生成以下两个接口,不需要等用户定义:
3.1 POST /api/payment/create-checkout-session
职责:接收前端请求,创建 Stripe Checkout Session,返回跳转 URL。
放置位置:src/routes/payment.ts 或 src/api/payment.ts(根据项目现有路由结构调整)。
import express from "express";
import { createClient } from "@supabase/supabase-js";
import { createStripeClient } from "@wecode-team/payment-stripe";
const router = express.Router();
router.post("/api/payment/create-checkout-session", async (req, res) => {
try {
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!,
);
const client = await createStripeClient({ supabase });
const session = await client.createCheckoutSession({
amount: req.body.amount,
currency: req.body.currency,
orderId: req.body.orderId,
description: req.body.description,
successUrl: req.body.successUrl,
cancelUrl: req.body.cancelUrl,
metadata: req.body.metadata,
});
res.json({ code: 200, message: "success", data: session });
} catch (error) {
const message = (error as Error).message;
console.error("[payment] create-checkout-session failed:", message);
res.status(500).json({ code: 500, message });
}
});3.2 POST /api/payment/webhook
职责:接收 Stripe webhook 回调,验签,处理支付结果。
放置位置:同上,与 checkout 路由放在同一文件。
router.post(
"/api/payment/webhook",
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 createStripeClient({ supabase });
const event = await client.verifyWebhook(
{ "stripe-signature": req.headers["stripe-signature"] as string | undefined },
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({ received: true });
} catch (error) {
const message = (error as Error).message;
console.error("[payment] webhook verification failed:", message);
res.status(400).json({ code: 400, message });
}
},
);3.3 前端跳转
const res = await fetch("/api/payment/create-checkout-session", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
amount: 1999,
currency: "usd",
orderId: `order_${userId}_${Date.now()}`,
description: "Pro plan",
successUrl: `${window.location.origin}/payment/result?status=success&session_id={CHECKOUT_SESSION_ID}`,
cancelUrl: `${window.location.origin}/payment/result?status=cancel`,
}),
});
if (!res.ok) {
const error = await res.json();
// 展示错误提示
toast.error(error.message || "支付创建失败,请稍后重试");
return;
}
const result = await res.json();
window.location.assign(result.data.url);3.4 Webhook 自动注册(平台场景)
在项目部署时自动注册 webhook,不要引导用户手动跑 stripe listen。
const deployBaseUrl = resolveDeployUrl(sessionId); // 平台层约定
const endpoint = await client.registerWebhook({
url: `${deployBaseUrl}/api/payment/webhook`,
description: "We0 generated project webhook",
});
// SDK 自动把 signing secret 写回配置中心,不需要手填4. 安全边界
强约束(必须遵守):
| # | 约束 | 违反后果 |
|---|------|----------|
| 1 | 禁止将 secret key / webhook secret 暴露给前端 | 密钥泄露,资金风险 |
| 2 | Webhook 路由必须保留原始 body,不能先 JSON.parse | 验签必然失败 |
| 3 | amount 参数必须在服务端校验范围(如最低 $1.00) | 防止前端篡改金额 |
| 4 | create-checkout-session 建议加登录态校验 | 防止未登录用户调用 |
| 5 | 禁止在前端硬编码 Stripe key | 密钥泄露 |
5. 错误处理
5.1 SDK 层错误
| 场景 | 错误信息 | 处理方式 |
|------|----------|----------|
| 未设置项目标识 | PROJECT_ID is required | 设置当前 CMS 项目的 PROJECT_ID 环境变量 |
| 配置结构未注册 | is not registered in table_fields | 确认逻辑配置表的字段结构已写入 table_fields |
| 配置数据不存在 | No config row found | 确认 public_data_0 中已有该逻辑表的 namespace=payment 数据 |
| Supabase 查询失败 | Failed to check config table / Failed to load config data | 检查 SUPABASE_URL、SUPABASE_SERVICE_ROLE_KEY 和数据库权限 |
| Stripe key 无效 | Invalid API Key | 在配置中心更新正确的 STRIPE_SECRET_KEY |
| Webhook 验签失败 | Webhook signature verification failed | 确认 body 是原始格式,确认 signing secret 一致 |
5.2 前端错误处理
前端必须处理以下场景:
// 1. 网络错误
if (!res.ok) {
const error = await res.json().catch(() => ({ message: "网络错误" }));
toast.error(error.message || "支付创建失败,请稍后重试");
return;
}
// 2. 跳转失败(Stripe 页面加载失败)
window.location.assign(result.data.url);
// 无需额外处理,浏览器会显示 Stripe 的错误页面
// 3. 回调页面
// success 页面:展示支付成功 + 订单详情
// cancel 页面:展示支付取消 + 重新支付按钮6. 平台生成场景职责边界
| 角色 | 职责 |
|------|------|
| 平台层 | 根据 sessionId 推导 deploy url |
| AI | 注册支付相关 route,调用 SDK |
| SDK | 读取 key、注册 webhook、写回 secret、验签 |
| 用户 | 不需要手填任何 key 或 secret |
默认路径是"真实 deploy url 自动注册 webhook",而不是让 AI 引导用户去跑 stripe listen。
7. 规则摘要
- 唯一必填参数:
createStripeClient({ supabase }),禁止传config覆盖读取规则。 - 两个默认接口:
POST /api/payment/create-checkout-session+POST /api/payment/webhook,AI 直接生成,不需要等用户定义。 - Webhook 原始 body:必须用
express.raw()中间件,不能先JSON.parse()。 - 前端只做跳转:发请求 → 拿
data.url→window.location.assign(),不处理支付逻辑。 - 不要发明抽象层:禁止生成
PaymentService、StripeManager、PaymentProvider等,直接 route → SDK。 - 不要引导用户手动配置:SDK 自动从 Supabase 读取 key、自动注册 webhook、自动写回 secret。
- 安全底线:secret key / webhook secret 绝不暴露给前端,
amount服务端校验。
