基于MERN Stack+Stripe的订阅用户月度信用自动重置方案问询
问题解决方案
一、信用额度存储位置的合理性
把信用额度存在userSchema里是完全合理的。信用属于用户的核心属性,和用户强绑定,直接存储在用户档案中能让查询、修改操作更直接高效,符合MERN Stack的常规数据设计逻辑。
建议在userSchema中补充几个辅助字段,方便后续逻辑处理:
currentCredits: 存储当前可用信用数subscriptionStatus: 标记订阅状态(如active/past_due/canceled)lastBillingDate: 记录上次成功扣费的日期stripeCustomerId: 关联用户对应的Stripe客户IDstripeSubscriptionId: 关联用户的Stripe订阅ID
二、实现订阅续费后的信用重置与未续费清空逻辑
核心依赖Stripe Webhooks触发后端操作,这是最实时可靠的方式,同时可搭配定时任务做兜底处理。
1. 配置Stripe Webhooks
监听以下关键事件:
invoice.paid: 发票支付成功(即续费完成)invoice.payment_failed: 发票支付失败(续费失败)customer.subscription.updated: 订阅状态更新(如用户主动取消订阅)
2. 后端处理代码(Node.js/Express)
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); const User = require('../models/User'); const express = require('express'); const router = express.Router(); // 处理Stripe Webhook请求 router.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => { const sig = req.headers['stripe-signature']; const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET; let event; try { event = stripe.webhooks.constructEvent(req.body, sig, webhookSecret); } catch (err) { return res.status(400).send(`Webhook Error: ${err.message}`); } // 处理续费成功事件 if (event.type === 'invoice.paid') { const invoice = event.data.object; const user = await User.findOne({ stripeCustomerId: invoice.customer }); if (user) { user.currentCredits = 10; user.lastBillingDate = new Date(); user.subscriptionStatus = 'active'; await user.save(); } } // 处理续费失败事件 if (event.type === 'invoice.payment_failed') { const invoice = event.data.object; const user = await User.findOne({ stripeCustomerId: invoice.customer }); if (user) { user.currentCredits = 0; user.subscriptionStatus = 'past_due'; await user.save(); } } // 处理订阅取消事件 if (event.type === 'customer.subscription.updated') { const subscription = event.data.object; if (subscription.status === 'canceled') { const user = await User.findOne({ stripeCustomerId: subscription.customer }); if (user) { user.currentCredits = 0; user.subscriptionStatus = 'canceled'; await user.save(); } } } res.json({ received: true }); }); module.exports = router;
3. 定时任务兜底(可选)
为避免Webhook通知丢失,可添加每日定时任务,主动校验用户订阅状态:
const cron = require('node-cron'); const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); const User = require('../models/User'); // 每天凌晨2点执行校验 cron.schedule('0 2 * * *', async () => { // 校验活跃订阅用户的信用是否需要重置 const activeUsers = await User.find({ subscriptionStatus: 'active' }); for (const user of activeUsers) { const subscription = await stripe.subscriptions.retrieve(user.stripeSubscriptionId); const currentPeriodEnd = new Date(subscription.current_period_end * 1000); // 若当前已过订阅周期结束日,且订阅仍活跃,重置信用 if (currentPeriodEnd < new Date() && subscription.status === 'active') { user.currentCredits = 10; user.lastBillingDate = new Date(); await user.save(); } } // 清空逾期/取消订阅用户的信用 await User.updateMany( { subscriptionStatus: { $in: ['past_due', 'canceled'] } }, { currentCredits: 0 } ); });
4. 前端配合逻辑
用户访问时,先从后端获取subscriptionStatus和currentCredits:
- 若状态为
past_due/canceled,前端禁用信用消费功能,提示用户续费 - 展示当前信用数时,直接读取后端返回的
currentCredits即可
三、示例场景验证
- 用户11月1日订阅成功:后端收到
invoice.paid事件,将currentCredits设为10,状态标记为active - 当月使用8个信用后剩余2个,12月1日续费成功:后端再次触发
invoice.paid事件,将currentCredits重置为10 - 若12月1日续费失败:后端收到
invoice.payment_failed事件,将currentCredits清空为0,状态标记为past_due
内容的提问来源于stack exchange,提问作者CNSA
相关产品推荐
相关产品推荐

