Stripe优惠券验证问题:无法传递有效券至服务端并应用折扣
问题解决:Stripe优惠券无法在结算时应用的问题
核心问题分析
你当前的代码存在两个关键问题:
- 请求体数据结构错误:你将
validCoupon作为数组cartItems的附加属性添加,但JSON序列化数组时会忽略非索引属性,导致服务端根本接收不到validCoupon数据。 - 前端暴露Stripe密钥:直接在前端使用
NEXT_PUBLIC_STRIPE_SECRET_KEY调用Stripe API会泄露密钥,这是严重的安全风险。
修复步骤
1. 客户端Cart.js修改
重构请求体,将购物车商品和优惠券信息封装成一个独立对象,而非给数组附加属性:
const handleCheckout = async () => { const stripeClient = await getStripe(); // 构造包含购物车和优惠券的请求体 const checkoutData = { cartItems: cartItems, validCoupon: validCoupon }; const response = await fetch('/api/stripe', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(checkoutData) }); if (!response.ok) { const error = await response.json(); toast.error(error.message || '结算请求失败'); return; } const data = await response.json(); toast.loading('Redirecting...'); stripeClient.redirectToCheckout({sessionId: data.id}); }
同时,将优惠券验证逻辑移到服务端(避免暴露密钥),新建pages/api/validate-coupon.js:
// pages/api/validate-coupon.js import Stripe from 'stripe'; const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); export default async function handler(req, res) { if (req.method !== 'POST') { res.setHeader('Allow', 'POST'); return res.status(405).end('Method Not Allowed'); } try { const { couponId } = req.body; const coupon = await stripe.coupons.retrieve(couponId); res.status(200).json({ valid: true, coupon }); } catch (err) { res.status(400).json({ valid: false, message: '优惠券无效或不存在' }); } }
修改前端checkCoupon函数调用服务端验证:
const checkCoupon = async () => { if(isCouponAdded){ toast.success("Coupon already added!"); return; } try { const response = await fetch('/api/validate-coupon', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ couponId: coupontext }) }); const result = await response.json(); if (result.valid) { toast.success("Coupon is valid!"); setValidCoupon(result.coupon); setisCouponAdded(true); } else { toast.error(result.message); } } catch (err) { toast.error("Error validating coupon"); } }
2. 服务端stripe.js修改
正确解析请求体中的cartItems和validCoupon,避免遍历到优惠券数据:
import Stripe from 'stripe'; // 注意:不要用NEXT_PUBLIC_前缀,密钥不能暴露在前端 const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); export default async function handler(req, res) { if (req.method === 'POST') { try { const { cartItems, validCoupon } = req.body; const discounts = []; if(validCoupon && validCoupon.id) { discounts.push({coupon: validCoupon.id}); } let shipping_options = []; let total_amount = 0; for (const item of cartItems) { total_amount += item.price * item.quantity; } if (total_amount >= 40) { shipping_options = [ { shipping_rate: 'shr_1MMq3iJFNgzNJo7s2nVvfATq' }, ]; } else { shipping_options = [ { shipping_rate: "shr_1MMq2YJFNgzNJo7sbwDNnqhz"} ]; } const params = { submit_type: 'pay', mode: 'payment', discounts: discounts, payment_method_types: ['card'], billing_address_collection: 'auto', shipping_address_collection: { allowed_countries: ["PT"], }, shipping_options: shipping_options, line_items: cartItems.map((item) => { const img = item.image[0].asset._ref; const newImage = img.replace('image-', 'https://cdn.sanity.io/images/REDACTED/production/').replace('-png', '.png'); return { price_data: { currency: 'eur', product_data: { name: item.name, images: [newImage], }, unit_amount: item.price * 100, }, quantity: item.quantity } }), success_url: `${req.headers.origin}/success`, cancel_url: `${req.headers.origin}/`, } const session = await stripe.checkout.sessions.create(params); res.status(200).json(session); } catch (err) { res.status(err.statusCode || 500).json(err.message); } } else { res.setHeader('Allow', 'POST'); res.status(405).end('Method Not Allowed'); } }
额外注意事项
- 确保
STRIPE_SECRET_KEY没有用NEXT_PUBLIC_前缀,只在服务端环境变量中配置,绝对不能暴露给前端。 - 优惠券验证必须在服务端完成,前端只负责输入和展示结果,避免恶意用户伪造优惠券信息。
内容的提问来源于stack exchange,提问作者André Gomes
相关产品推荐
相关产品推荐

