1.SaaS计费系统的典型需求
软件即服务(SaaS)模式的计费系统需要处理:订阅计划(月付/年付/按量)、折扣与试用期、续费与升级降级、发票与税务、多支付渠道(支付宝/Stripe/PayPal)、订阅生命周期管理(激活、暂停、取消)。PHP凭借成熟的电商类库和快速迭代能力,成为许多SaaS初创公司构建计费模块的首选。
2.订阅计划的数据建模
PHP后端使用Laravel+MySQL存储订阅相关表:
plans表:定义订阅计划(名称、价格、周期、特性描述)。价格通常以最小货币单位(分)存储为整数,避免浮点误差。
subscriptions表:用户当前订阅记录(plan_id、status、start_at、end_at、cancel_at_period_end等)。状态包括:trialing、active、past_due、canceled、ended。
invoices表:每期生成的账单记录(金额、支付状态、pdf链接)。
transactions表:支付网关返回的交易流水(transaction_id、payment_method、response_json)。
Eloquent模型定义关联关系,使得查询用户当前订阅非常便捷。
参考:https://npqev.cn
3.订阅生命周期管理
使用PHP事件驱动模式处理关键状态变化:
用户发起订阅:创建subscription记录,调用支付网关预授权或扣款。成功后将status改为active,设置end_at为当前时间+计费周期。
定期扣款(Cron任务):每日检查所有即将到期且未取消的订阅,生成发票,发起扣款。如果扣款失败,status改为past_due,并重试多次;最终失败则自动取消。Cron脚本在PHPCLI模式运行,需要注意防止并发重叠(使用Laravel的withoutOverlapping中间件)。
用户取消订阅:设置cancel_at_period_end为true,status仍为active直到end_at到达。然后通过监听事件发送问卷调查。
到期处理:另一个Cron将end_at<now的过期订阅状态改为ended,并清理相关权限。
4.Webhook处理外部支付事件
支付网关会异步发送Webhook(如支付成功、退款、争议)。PHP端点接收支付网关的签名请求,验证后更新本地状态。由于Webhook可能重复发送,需要幂等处理:使用transaction_id唯一索引防止重复处理。Laravel框架的dispatchNow或放入队列执行,避免阻塞网关响应。
5.按量计费的处理
对于按使用量计费(如API调用次数、存储空间),PHP需要收集用量事件(每发生一次,写入Redis计数器或消息队列)。月末汇总用量,根据阶梯价格计算费用,生成额外账单。由于计算量可能很大,通常通过异步任务处理,并将结果与固定订阅费合并。
6.发票与税务处理
PHP生成PDF发票可以使用Dompdf或wkhtmltopdf。为了支持国际税务(增值税、GST),需要集成税务服务(如TaxJar)或使用第三方SDK。PHP的moneyphp/money库处理货币转换和格式化。
参考:https://oqmyh.cn
7.实际案例:一款在线协作工具的计费系统
该工具提供免费版、专业版($15/月)、企业版(定制价格)。使用LaravelCashier(官方订阅管理包)简化开发,支付网关对接Stripe。团队2人两周完成核心计费功能,后续扩展了:
优惠码系统:支持首月折扣或免费试用延长。
用量限制:专业版限制项目数50个,超出后前端提示升级。
发票自助下载:用户面板可查看历史发票。
税务合规:自动根据用户注册国家/地区收取对应VAT。
系统运行两年,处理了5000+活跃订阅,每月计费流水$80K+,未出现扣款错误或重复计费问题。
8.常见陷阱与解决方案
货币精度:始终使用最小单位(分),不要使用float。
时区问题:订阅起止时间统一存储为UTC,但展示时按用户时区转换。
试用到正式切换:试用期结束前发送提醒邮件,若用户未绑定支付方式则自动降级为免费版。
支付网关连接失败:使用队列重试机制,最多重试3次,指数退避。
并发扣款:对同一用户使用Redis分布式锁,防止多次同时生成账单。
9.总结
PHP在SaaS计费系统中可以做到既可靠又高效。利用现代框架和官方包(CashierforStripe/Paddle),开发团队可以专注于业务逻辑而非底层支付细节。对于早期SaaS产品,PHP计费模块是性价比最高的选择之一。
参考:https://bgnno.cn