Laravel Cashier实现用户注册时同步创建Stripe订阅的问题
解决方案:Laravel注册+Stripe订阅一步完成
你当前的核心问题是流程顺序错误:不能先创建用户再生成Setup Intent,因为此时表单已经提交,无法将client_secret回传给前端完成Stripe支付验证。正确的逻辑是:先在表单展示阶段生成Setup Intent,让前端完成支付方式确认后,再把注册信息+支付凭证一起提交给后端,最终创建用户和订阅。
步骤1:合并注册与支付表单
把Laravel默认注册表单和支付表单整合,让用户一次性填写所有信息:
<form id="payment-form" action="{{ route('register') }}" method="POST"> @csrf <input type="hidden" name="plan" id="plan" value="{{ $plan->id }}"> <!-- 注册信息 --> <div class="row"> <div class="col-xl-4 col-lg-4"> <div class="form-group"> <label>First Name</label> <input type="text" name="first_name" class="form-control" required> </div> </div> <div class="col-xl-4 col-lg-4"> <div class="form-group"> <label>Last Name</label> <input type="text" name="last_name" class="form-control" required> </div> </div> <div class="col-xl-4 col-lg-4"> <div class="form-group"> <label>Email</label> <input type="email" name="email" class="form-control" required> </div> </div> <div class="col-xl-6 col-lg-6"> <div class="form-group"> <label>Password</label> <input type="password" name="password" class="form-control" required> </div> </div> <div class="col-xl-6 col-lg-6"> <div class="form-group"> <label>Confirm Password</label> <input type="password" name="password_confirmation" class="form-control" required> </div> </div> </div> <!-- 支付信息 --> <div class="row"> <div class="col-xl-4 col-lg-4"> <div class="form-group"> <label>Name on Card</label> <input type="text" id="card-holder-name" class="form-control" required> </div> </div> <div class="col-xl-4 col-lg-4"> <div class="form-group"> <label>Card Details</label> <div id="card-element"></div> </div> </div> <div class="col-xl-12 col-lg-12"> <hr> <button type="submit" class="btn btn-primary" id="card-button" data-secret="{{ $setupIntent->client_secret }}">Register & Subscribe</button> </div> </div> </form>
步骤2:在表单展示阶段生成Setup Intent
修改注册页面的控制器方法,直接调用Stripe API生成Setup Intent(无需提前创建用户):
// App/Http/Controllers/Auth/RegisteredUserController.php use Stripe\Stripe; use Stripe\SetupIntent; public function create() { Stripe::setApiKey(config('cashier.secret')); // 生成Setup Intent,用于收集支付方式 $setupIntent = SetupIntent::create([ 'payment_method_types' => ['card'], ]); $plan = Plan::find(request()->query('plan')); // 假设通过URL参数传递计划ID return view('auth.register', compact('setupIntent', 'plan')); }
步骤3:前端完成Stripe支付验证
添加Stripe JS代码,在表单提交前验证支付方式,获取payment_method凭证:
<script src="https://js.stripe.com/v3/"></script> <script> const stripe = Stripe('{{ config('cashier.key') }}'); const elements = stripe.elements(); const cardElement = elements.create('card'); cardElement.mount('#card-element'); const cardHolderName = document.getElementById('card-holder-name'); const cardButton = document.getElementById('card-button'); const clientSecret = cardButton.dataset.secret; cardButton.addEventListener('click', async (e) => { e.preventDefault(); // 确认支付方式,获取payment_method const { setupIntent, error } = await stripe.confirmCardSetup( clientSecret, { payment_method: { card: cardElement, billing_details: { name: cardHolderName.value } } } ); if (error) { alert(error.message); // 处理错误提示 return; } // 将payment_method添加到表单并提交 const form = document.getElementById('payment-form'); const paymentMethodInput = document.createElement('input'); paymentMethodInput.type = 'hidden'; paymentMethodInput.name = 'payment_method'; paymentMethodInput.value = setupIntent.payment_method; form.appendChild(paymentMethodInput); form.submit(); }); </script>
步骤4:后端创建用户并完成订阅
修改store方法,接收payment_method参数,创建用户后直接完成订阅:
// App/Http/Controllers/Auth/RegisteredUserController.php public function store(Request $request) { $request->validate([ 'first_name' => ['required', 'string', 'max:255'], 'last_name' => ['required', 'string', 'max:255'], 'email' => ['required', 'string', 'email', 'max:255', 'unique:users'], 'password' => ['required', 'string', 'min:8', 'confirmed'], 'plan' => ['required', 'exists:plans,id'], 'payment_method' => ['required', 'string'], ]); $plan = Plan::find($request->plan); $userID = uniqid('u-'); // 创建用户 $user = User::create([ 'unique_id' => $userID, 'first_name' => $request->first_name, 'last_name' => $request->last_name, 'name' => $request->first_name . ' ' . $request->last_name, 'email' => $request->email, 'password' => Hash::make($request->password), 'user_role' => 'customer', 'status' => 'active', // 订阅成功后直接激活 'show_in_directory' => $request->has('show_in_directory') ? 'on' : 'off', ]); event(new Registered($user)); Auth::login($user); // 使用payment_method创建订阅 try { $user->newSubscription('default', $plan->stripe_plan) ->create($request->payment_method, [ 'email' => $user->email, ]); return view('subscription_success'); } catch (\Exception $e) { // 订阅失败时删除用户,避免无效数据 $user->delete(); return back()->withErrors(['payment' => $e->getMessage()]); } }
关键说明
- 移除了原代码中硬编码的密码,改为用户自主输入,保证安全性
- 流程调整为:展示表单→生成Setup Intent→前端验证支付→提交所有信息→后端创建用户+订阅
- 订阅失败时自动删除用户,避免数据库中存在未完成支付的无效账户
内容的提问来源于stack exchange,提问作者John Hubler
相关产品推荐
相关产品推荐

