You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于MERN Stack+Stripe的订阅用户月度信用自动重置方案问询

问题解决方案

一、信用额度存储位置的合理性

把信用额度存在userSchema里是完全合理的。信用属于用户的核心属性,和用户强绑定,直接存储在用户档案中能让查询、修改操作更直接高效,符合MERN Stack的常规数据设计逻辑。

建议在userSchema中补充几个辅助字段,方便后续逻辑处理:

  • currentCredits: 存储当前可用信用数
  • subscriptionStatus: 标记订阅状态(如active/past_due/canceled)
  • lastBillingDate: 记录上次成功扣费的日期
  • stripeCustomerId: 关联用户对应的Stripe客户ID
  • stripeSubscriptionId: 关联用户的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.10 18:25:25