如何从 Laravel Cashier 获取下一次账单日期?
结论
下一次账单日期通常要从支付平台的订阅对象中读取,不能直接通过 Cashier 的 Billable trait 获取。以 Laravel Cashier Stripe 为例,先取得 Stripe Subscription,再读取当前计费周期的结束时间 current_period_end:
use Carbon\Carbon;
$subscription = $user->subscription('default');
$stripeSubscription = $subscription->asStripeSubscription();
$nextBillingDate = Carbon::createFromTimestamp(
$stripeSubscription->current_period_end
);
current_period_end 是 Unix 时间戳,通常对应当前订阅周期结束和下一个计费周期开始的时间。
在页面中显示
显示日期时,应将其明确转换为应用时区:
use Carbon\Carbon;
$subscription = $user->subscription('default');
if ($subscription) {
$stripeSubscription = $subscription->asStripeSubscription();
$nextBillingDate = Carbon::createFromTimestamp(
$stripeSubscription->current_period_end
)->setTimezone(config('app.timezone'));
echo $nextBillingDate->format('Y-m-d H:i:s');
}
如果创建订阅时使用的名称不是 default,请替换为实际名称。例如:
$subscription = $user->subscription('main');
为什么不能直接从 Billable 获取
Cashier 的本地订阅记录主要保存订阅状态、套餐和取消时间等信息。下一次续费日期则由 Stripe 的计费周期决定,并可能受到以下因素影响:
- 免费试用
- 套餐切换
- 按比例计费
- 自定义 billing cycle anchor
- 暂停或恢复订阅
- Subscription Schedule
- 在 Stripe 后台直接修改订阅
因此,这个日期通常应以远程 Stripe Subscription 中的数据为准。
需要注意的情况
订阅不存在
调用前应先检查用户是否有对应的订阅:
$subscription = $user->subscription('default');
if (!$subscription) {
// 用户没有该订阅
}
订阅即将取消
如果用户设置了在当前周期结束时取消订阅,current_period_end 仍然存在,但此时它表示订阅结束日期,不是下一次扣费日期:
if ($subscription->onGracePeriod()) {
$endsAt = $subscription->ends_at;
}
这种情况下,不应将该时间显示为“下次扣费时间”。
免费试用期
订阅处于试用期时,首次可能扣费的日期通常取决于 trial_end:
$stripeSubscription = $subscription->asStripeSubscription();
$timestamp = $stripeSubscription->trial_end
?? $stripeSubscription->current_period_end;
$billingDate = Carbon::createFromTimestamp($timestamp)
->setTimezone(config('app.timezone'));
字段位置可能因 Stripe API 版本变化
上述代码适用于 current_period_end 位于 Stripe Subscription 对象上的 Cashier 和 Stripe API 组合。较新的 Stripe API 版本可能会将计费周期信息放在 Subscription Item 上。如果订阅对象中没有该字段,需要检查实际返回的数据结构,并从对应的订阅项中读取:
$stripeSubscription = $subscription->asStripeSubscription();
$item = $stripeSubscription->items->data[0];
$nextBillingDate = Carbon::createFromTimestamp(
$item->current_period_end
)->setTimezone(config('app.timezone'));
如果一个订阅包含多个计费周期不同的订阅项,就没有唯一的“下一次账单日期”。此时需要分别处理各个订阅项,或者查询 Stripe 的 upcoming invoice。
计费周期的结束时间也不代表银行卡一定会在该时刻成功扣款。失败重试、手动开票和异步支付都会影响实际付款时间。
备注:内容仅供参考。
