
stripe 的 custom_fields 不适用于商品变体选择与库存管理;正确做法是为每个变体创建独立的 product/price,并在应用层自行实现库存校验、预占与同步逻辑。
stripe 的 custom_fields 不适用于商品变体选择与库存管理;正确做法是为每个变体创建独立的 product/price,并在应用层自行实现库存校验、预占与同步逻辑。
在 Next.js 电商项目中集成 Stripe 时,开发者常误将 custom_fields 视为商品规格(如 T 恤尺码)的选择入口。但需明确:Stripe Checkout 的 custom_fields 本质是支付后补充信息的收集工具(如 Discord ID、定制刻字内容),而非商品建模机制。将其用于尺码选择会导致两个关键问题:
- 用户体验断裂:尺寸应在商品页选择,而非跳转至支付页才决策;
- 库存无法管控:Stripe 不提供任何库存字段、原子扣减或并发保护能力。
✅ 正确架构:变体即独立商品 + 应用层库存控制
1. 数据建模:为每个 SKU 创建专属 Price
不再使用 custom_fields,而是将「T 恤 - Small」「T 恤 - Medium」等视为完全独立的商品变体,各自对应唯一的 Product 和 Price(推荐使用 active: true 的 recurring 或 one-time price):
// 创建变体 Price(服务端调用,仅需执行一次)
const smallPrice = await stripe.prices.create({
product: 'prod_tshirt_basic', // 可复用同一 Product ID
unit_amount: 2999,
currency: 'usd',
nickname: 'T-Shirt - Small',
lookup_key: 'tshirt-small', // 关键!用于前端映射库存
});
const mediumPrice = await stripe.prices.create({
product: 'prod_tshirt_basic',
unit_amount: 2999,
currency: 'usd',
nickname: 'T-Shirt - Medium',
lookup_key: 'tshirt-medium',
});? 提示:使用 lookup_key 可避免硬编码 Price ID,便于后续维护和库存关联。
2. 前端:商品页渲染变体 + 实时库存状态
在 Next.js 商品页面中,通过 lookup_key 查询对应库存(例如从你的数据库或 Redis 获取):
// ProductPage.tsx
const variants = [
{ size: 'Small', lookupKey: 'tshirt-small', stock: 100 },
{ size: 'Medium', lookupKey: 'tshirt-medium', stock: 30 },
{ size: 'Large', lookupKey: 'tshirt-large', stock: 45 },
];
return (
<div>
<h2>T-Shirt</h2>
<div className="variants">
{variants.map(v => (
<button
key={v.lookupKey}
disabled={v.stock <= 0}
onClick={() => setSelectedVariant(v)}
>
{v.size} {v.stock > 0 ? `(In stock: ${v.stock})` : '(Out of stock)'}
</button>
))}
</div>
</div>
);3. 创建 Checkout Session:传入选定变体的 Price ID
用户选择尺寸后,服务端根据 lookup_key 查得对应 Price ID,构建 Session:
// POST /api/create-checkout-session
export default async function handler(req, res) {
const { variantLookupKey, quantity } = req.body;
// 1. 校验库存(关键!防止超卖)
const stock = await db.inventory.findUnique({
where: { lookupKey: variantLookupKey }
});
if (!stock || stock.available < quantity) {
return res.status(400).json({ error: 'Insufficient stock' });
}
// 2. 预占库存(乐观锁 or Redis INCR)
await db.inventory.update({
where: { lookupKey: variantLookupKey },
data: { available: { decrement: quantity } }
});
// 3. 创建 Session(仅含一个 line_item)
const session = await stripe.checkout.sessions.create({
success_url: `${baseUrl}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: baseUrl + '/cart',
payment_method_types: ['card'],
mode: 'payment',
line_items: [{
price: stock.priceId, // ← 绑定到具体变体 Price
quantity,
}],
});
res.json({ id: session.id });
}4. 支付成功后:确认扣减或回滚
监听 checkout.session.completed Webhook,验证支付状态并完成库存扣减;若支付失败(如用户关闭页面),需设置定时任务或前端回调释放预占库存:
// Webhook handler for 'checkout.session.completed'
if (event.data.object.payment_status === 'paid') {
// ✅ 真实扣减:更新库存为最终态
await db.inventory.update({
where: { lookupKey: variantLookupKey },
data: { reserved: { decrement: quantity } } // 若你用 reserved 字段做预占
});
} else {
// ⚠️ 支付未完成:需异步恢复库存(如 1 小时后自动释放)
}⚠️ 重要注意事项
- 不要依赖 Stripe 原生库存:Stripe 当前(2024)无库存字段、无库存 API、无事务性扣减能力;所有库存逻辑必须由你控制。
- 并发安全是核心挑战:高流量下需用数据库行锁(PostgreSQL SELECT ... FOR UPDATE)、Redis Lua 脚本或分布式锁保障 check + reserve 原子性。
- 自定义字段仍有价值场景:仅用于非业务关键的附加信息收集(如“是否需要电子发票?”、“备注配送要求”),且不影响商品定价与库存。
- 未来扩展建议:将库存服务抽象为独立模块,支持缓存(Redis)、预警(低库存通知)、多仓逻辑,与 Stripe 解耦。
综上,Stripe 是卓越的支付管道,而非商品目录或库存系统。将变体建模为独立 Price,并在应用层构建健壮的库存生命周期管理(查询 → 预占 → 确认/回滚),才是可扩展、可维护、符合用户体验的工程实践。










