用Cashier初始化Stripe订阅需四步:安装并运行php artisan cashier:install;配置.env中的STRIPE_KEY、STRIPE_SECRET和STRIPE_WEBHOOK_SECRET;用户模型使用Billable trait且数据库含stripe_id字段;创建订阅时传入price ID(如price_1Qx...)而非已弃用的plan ID。

如何用 Cashier 初始化 Stripe 订阅
Cashier 是 Laravel 官方维护的订阅计费封装,它不处理支付网关底层通信,而是把 Stripe(或 Paddle)的 API 调用、Webhook 验证、模型状态同步等常见逻辑收拢到 Eloquent 层。你得先装包、配密钥、迁移表,再绑定模型 —— 少一步,createSubscription() 就会抛 BadMethodCallException 或静默失败。
-
composer require laravel/cashier后必须运行php artisan cashier:install(Laravel 10+),它会自动注册服务提供者、发布配置、生成迁移 -
STRIPE_KEY和STRIPE_SECRET必须设在.env,且STRIPE_WEBHOOK_SECRET要和 Stripe Dashboard 里创建的 webhook endpoint 完全一致,否则WebhookController收不到有效事件 - 用户模型需 use
Billabletrait,并确保数据库有stripe_id字段(migration 会加,但如果你手动删过或跳过迁移,newSubscription()会因找不到关联字段而报SQLSTATE[42S22]
订阅创建时怎么选 price 而不是 plan
Stripe 已弃用 plan 概念,Cashier v14+(对应 Laravel 9+)强制使用 Price 对象 ID(如 price_1Qx...)。硬写旧 plan ID 会触发 InvalidRequestException: No such price —— 这不是 Cashier 的 bug,是 Stripe API 拒绝了请求。
- 在 Stripe Dashboard 创建 Product 后,为其添加 recurring
Price,复制那个以price_开头的 ID,传给user->newSubscription('main', 'price_xxx') - 别在代码里写死 price ID;建议查库缓存或配置表映射,比如
config('billing.prices.monthly'),避免上线后改价格还得改 PHP 文件 - 如果要用 metered billing(按用量计费),price 类型必须是
recurring+usage,且调用incrementUsage()前要确认 subscription 状态为active,否则返回空或报错
Webhook 处理失败的典型原因和验证方式
Cashier 自带 WebhookController,但它只负责路由分发,不自动重试或记录原始 payload。很多「订阅没更新」问题其实源于 webhook 被丢弃:签名验不过、路由未注册、中间件阻断、或 DB 写入失败后静默退出。
- 确保
webhook/stripe路由在routes/web.php中启用,且没被VerifyCsrfToken拦截(Cashier 的路由默认已排除 CSRF) - 用
php artisan tinker手动触发测试:Stripe::webhook()->constructEvent($payload, $signature, config('cashier.webhook_secret')),能过说明密钥和 payload 格式没问题 - 监听
Illuminate\Foundation\Http\Events\RequestHandled或加日志到handleWebhook方法里,确认事件是否真正进入处理流程;常见坑是 DB transaction 中抛异常导致部分更新回滚,比如subscription()->update(['ends_at' => ...])失败后,整个事务 rollback,但日志里看不到
取消订阅后还能不能恢复?怎么控制 grace period
Cashier 默认调用 cancelNow() 会立即终止订阅并清空 ends_at;而 cancel() 是软取消:保留 ends_at,让订阅自然到期。两者行为差异直接影响用户能否调用 resume() —— 只有 ends_at 不为空时,resume() 才有效,否则抛 LogicException: User is not on trial and is not subscribed to this subscription.
- 若想让用户「取消但保留剩余周期」,用
$user->subscription('main')->cancel(),别用cancelNow() - grace period 由 Stripe 控制,Cashier 不干预;但你可以通过
onGracePeriod()方法判断当前状态,并在 Blade 中隐藏续订按钮或显示倒计时 - 注意
resume()不会重试失败的支付;如果上次扣款失败导致 subscription 进入incomplete_expired状态,必须先createAsStripeCustomer()补全 payment method,再resume(),否则仍失败
真正麻烦的是跨期 proration(计费周期调整)和 tax calculation,Cashier 把这些交给 Stripe 的 proration_behavior 和 automatic_tax 参数控制,PHP 层几乎不干预 —— 别试图在模型里手动算差价,Stripe 返回的 invoice 已含精确金额。


















