深入剖析积分/信用系统从简单数据库字段到复杂多桶策略的演变过程,涉及重置规则、过期策略、按账单日续期、升级/降级处理等实际工程问题。
如果你的产品卖的是积分、Token、分钟数或 API 调用,你的团队里大概有人说过这么一句话:「往 users 表加一列 credits 就完事了。」这是一个合理的第一步。也是大多数团队开始构建一个他们根本没打算自己维护的计费系统的起点。
以下是有了真实客户之后,这一列会变成什么样子。
按月付费的客户有一个额度,会定期重置。然后他们又买了一个叠加包,这个包不应该重置。接着你给他们发了一个注册奖励,30 天后过期。现在「他们有多少积分?」变成了一道对多个桶的求和,每个桶有各自的规则,而且你得决定先消耗哪个桶——通常是最快过期的那一个。
额度在客户的账单日续期,而不是每月 1 号。17 号注册的客户在每月 17 号续期。9 号升级的客户可能从现在起在每月 9 号续期,也可能沿用 17 号,取决于你怎么算按比例的部分。你的续期任务现在需要知道每个客户的锚定日期。
今天升级了,额度怎么算?是现在就拿完整的新额度、按比例分配,还是下次续期再给新额度?降级更麻烦:要是客户已经用掉的超过了小套餐包含的数量呢?无论你怎么选,都需要一份记录,记录哪个套餐在哪个时间段适用,否则后面没法解释账单。
你的应用先查余额,再跑任务,然后扣减。在并行请求的情况下,两个任务可能同时看到「还剩 10 个积分」,两个都去跑了,都做了扣减。客户最后变成了 −10。对于便宜的任务这无所谓。对于视频渲染或一批图片生成,这就是真金白银。解决办法是在工作开始前锁定容量,工作结束后再结算。
如果你在任务前锁定了或扣减了积分,但任务失败了,客户应该拿回这些积分。如果你的 Worker 在任务中途崩溃了,没有人会来触发退款。所以锁需要有过期时间,而且必须有东西来清理它们。
你需要对每个客户设置限速,不只是总额限制,而且通常需要在多个时间窗口下同时限制,比如每分钟和每小时。
客户宁愿在 80% 的时候去充值,也不愿意等到发现积分归零时任务已经失败了。这意味着要跟踪每个客户每个周期的阈值,而且只在越过那条线的时候通知一次,而不是在此之后的每次请求都通知。
周期结束时,财务想要一个精确的数字:这个客户用了多少、包含多少、超出多少。这个数字一旦周期关闭就不能再动了。
以上这些单独拎出来都不难。合在一起,就是一个小的、有状态的、对并发敏感的系統,它坐落在你最昂贵的操作前面,而且每次你的定价一变它就得跟着变。这才是原始估算里最常漏掉的那部分。
这就是我们在构建 Meterbase 解决的问题。你只需要设置一次计量器、套餐和额度,你的代码在工作前打一个调用,工作后打一个调用:
import { Meterbase } from "mbase-sdk"
const meterbase = new Meterbase({ apiKey: process.env.METERBASE_API_KEY! })
const check = await meterbase.check({
customer_id: "acct_1",
meter_id: "ai_tokens",
quantity: 2_000, // an estimate
})
if (!check.allowed) throw new Error(`Refused: ${check.reason}`)
const reply = await generateReply(prompt)
await meterbase.track({
customer_id: "acct_1",
meter_id: "ai_tokens",
quantity: reply.usage.total_tokens,
})
额度、叠加包、套餐变更、锁定、限速和阈值 Webhook 都封装在这两个调用后面。Meterbase 不是计费系统:Stripe 或 Paddle 仍然收钱,你只需要读取每个客户一个周期的用量来出账单。
如果你现在正在构建这个,或者你已经构建完了但厌倦了维护它,我们很想听听你是怎么处理的:meterbase.tech。