PAYATHON 2026

如何用代码实现每月动态金额的 Recurring Billing?

支付小周

明确结论

可以实现。这个需求通常叫按量计费(usage-based billing / metered billing),与固定金额订阅不同。

系统需要完成两项工作:

  1. 持续记录客户的实际使用量。
  2. 每个计费周期结束后,由代码汇总使用量并计算应付金额,再通过支付平台自动生成账单和扣款。

例如,每周收费 $10,月度费用可以这样计算:

amount = usedWeeks × 10

如果客户分别使用了 1、3、2 周,对应账单就是 $10、$30、$20,不需要人工修改订阅价格。

推荐的实现方式

通常有两种方案。

方案一:使用支付平台的按量计费功能

Stripe Billing 等订阅计费平台提供过按量计费或计量事件相关功能。业务系统负责上报使用量,平台在计费周期结束时汇总数据并生成账单。

这种方式适合以下情况:

  • 使用量可以按次数、时长、流量等单位统计;
  • 计费规则相对稳定;
  • 希望由支付平台处理账单、自动扣款、失败重试和收据。

不同平台的 API 和计量模型可能发生调整,接入前应查看所选平台当前的官方文档。

方案二:业务系统计算金额,支付平台负责扣款

如果计费规则比较复杂,可以由业务系统计算金额,再按月创建动态金额的发票。

基本流程如下:

记录使用事件
    ↓
计费周期结束
    ↓
汇总本月使用量
    ↓
计算应付金额
    ↓
创建账单明细
    ↓
生成并确认发票
    ↓
使用客户保存的付款方式自动扣款

这种方式更灵活。折扣、封顶价格、阶梯价格和补偿金额都可以由业务代码控制。

数据设计

不要只在用户表中保存一个”本月使用周数”字段。更稳妥的做法是保存不可变的使用记录:

usage_events
- id
- customer_id
- quantity
- occurred_at
- billing_period
- status

例如:

[
  {
    "id": "evt_1001",
    "customer_id": "cus_123",
    "quantity": 1,
    "occurred_at": "2026-08-03T00:00:00Z"
  },
  {
    "id": "evt_1002",
    "customer_id": "cus_123",
    "quantity": 1,
    "occurred_at": "2026-08-10T00:00:00Z"
  }
]

月底按客户和计费周期汇总:

SELECT customer_id, SUM(quantity) AS used_weeks
FROM usage_events
WHERE occurred_at >= :period_start
  AND occurred_at < :period_end
GROUP BY customer_id;

JavaScript 计算示例

金额应使用最小货币单位。例如,美元应使用美分,以免产生浮点数误差:

function calculateMonthlyCharge(usedWeeks) {
  if (!Number.isInteger(usedWeeks) || usedWeeks < 0) {
    throw new Error("usedWeeks must be a non-negative integer");
  }

  const pricePerWeekInCents = 1000;
  return usedWeeks * pricePerWeekInCents;
}

console.log(calculateMonthlyCharge(1)); // 1000,即 $10
console.log(calculateMonthlyCharge(3)); // 3000,即 $30
console.log(calculateMonthlyCharge(2)); // 2000,即 $20

创建动态发票的示例

下面用 Stripe 风格的 Node.js API 展示整体思路。实际字段和调用顺序需要根据当前 SDK 版本及账户配置确认。

import Stripe from "stripe";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);

async function billCustomer({
  customerId,
  usedWeeks,
  billingPeriod,
}) {
  const amount = usedWeeks * 1000;

  if (amount === 0) {
    return {
      customerId,
      billingPeriod,
      amount: 0,
      status: "no_charge",
    };
  }

  const invoice = await stripe.invoices.create(
    {
      customer: customerId,
      collection_method: "charge_automatically",
      auto_advance: false,
      metadata: {
        billing_period: billingPeriod,
      },
    },
    {
      idempotencyKey: `invoice:${customerId}:${billingPeriod}`,
    }
  );

  await stripe.invoiceItems.create(
    {
      customer: customerId,
      invoice: invoice.id,
      amount,
      currency: "usd",
      description: `${billingPeriod}: ${usedWeeks} service week(s)`,
      metadata: {
        used_weeks: String(usedWeeks),
      },
    },
    {
      idempotencyKey: `invoice-item:${customerId}:${billingPeriod}`,
    }
  );

  const finalizedInvoice = await stripe.invoices.finalizeInvoice(
    invoice.id
  );

  return {
    invoiceId: finalizedInvoice.id,
    amount,
    status: finalizedInvoice.status,
  };
}

客户需要提前保存可用于后续扣款的付款方式,并完成相应授权。发票确认后是否立即扣款,是由平台自动推进,还是由代码调用支付接口,需要根据实际业务流程配置。

定时结算任务

可以每月运行一次定时任务:

async function runMonthlyBilling(periodStart, periodEnd) {
  const usageSummaries = await getUsageSummaries(
    periodStart,
    periodEnd
  );

  for (const usage of usageSummaries) {
    await billCustomer({
      customerId: usage.paymentCustomerId,
      usedWeeks: usage.usedWeeks,
      billingPeriod: usage.billingPeriod,
    });

    await markUsageAsBilled(
      usage.customerId,
      usage.billingPeriod
    );
  }
}

在生产环境中,扣款请求刚提交时不能直接将账单标记为支付成功。系统应监听支付平台的 Webhook,并根据发票支付成功、支付失败等事件更新账单状态。

必须注意的问题

防止重复扣款

定时任务可能因为超时或重试而执行多次。每个”客户 + 计费周期”都必须有唯一的账单编号,支付请求也要设置幂等键。

数据库中还应添加唯一约束,例如:

UNIQUE (customer_id, billing_period)

固定计费周期边界

需要明确月份采用哪个时区,以及计费周期是否按自然月计算。例如:

[2026-08-01 00:00:00, 2026-09-01 00:00:00)

建议采用左闭右开区间,避免月末事件被重复计算。

处理延迟上报

如果使用数据可能延迟到达,可以采用以下方式处理:

  • 在月末预留结算缓冲时间;
  • 将迟到的数据计入下一个周期;
  • 为已经出具的账单创建补充账单或抵扣项。

具体规则应提前写入计费协议。

不要直接用浮点数计算金额

不要这样计算:

const amount = 10.1 * 100;

美分、分或其他最小货币单位应始终以整数形式保存。涉及比例、税率或折扣时,可以使用十进制定点库,并明确舍入规则。

处理支付失败

自动扣款并不保证付款一定成功,还需要处理以下情况:

  • 银行卡过期;
  • 余额不足;
  • 需要额外身份验证;
  • 重试失败;
  • 客户取消付款授权。

系统应监听 Webhook,并将账单状态区分为 pending、paid、failed、void 等。

选择建议

如果价格只是”使用量 × 单价”,可以优先考虑支付平台提供的按量计费功能。

如果金额取决于阶梯价格、最低消费、月度封顶、优惠券或人工补偿等复杂业务规则,更适合由业务系统计算金额,再调用支付平台创建动态发票。

业务系统负责准确记录和计算使用量,支付平台负责保存付款方式、生成账单并执行扣款。

备注:内容仅供参考。