好,各位未来的全栈大师,欢迎来到今天的“PHP与Stripe的爱恨情仇”研讨会。别紧张,我们不搞那些虚头巴脑的“区块链”或者“元宇宙”,咱们今天就来聊聊怎么用PHP这个老牌劲旅,去驾驭Stripe这个支付界的“华尔街之狼”。
为什么要用Stripe?因为它快、因为它准、因为它不需要你自己去管银行那帮挑剔的柜员。用PHP?因为PHP依然是Web开发界的“瑞士军刀”,虽然有人说它老了,但只要你能驾驭它,它依然是处理后台逻辑最犀利的武器。
今天的讲座主题只有一个:如何用PHP构建一个坚不可摧的订阅支付系统。
为了让我们这堂课不至于变成催眠曲,我会把内容拆解成三个阶段:“搞清楚你在卖什么(产品定义)”、“前端怎么骗用户掏钱(界面交互)”、“后台怎么接住钱(PHP核心逻辑与Webhook)”。
准备好了吗?系好安全带,我们出发。
第一部分:工欲善其事——环境准备与API密钥
在写代码之前,我们得先搞清楚武器在哪。Stripe的PHP SDK其实就像是一个打包好的工具箱,我们得先把它买回来。
1. 安装“武器”
别去GitHub手动下载那个死沉的库,用Composer。Composer是PHP的包管理器,就像是你家里的垃圾桶,虽然有时候不想要,但它把东西整理得井井有条。
在你的项目根目录下,打开终端,敲下这句咒语:
composer require stripe/stripe-php
2. API密钥:你的身份证
Stripe给开发者提供两张身份证:Publishable Key(公钥)和Secret Key(私钥)。
- 公钥:发给别人看,别藏着掖着,把它贴在前端HTML里,用来让Stripe弹出支付框。
- 私钥:你的底牌,绝对不能暴露给前端!绝对不能!把它放在PHP的后端代码里,它是我们跟Stripe服务器对话的“暗号”。
在你登录Stripe Dashboard的时候,你会看到这两把钥匙。记住了吗?记不住你就等着报错吧。
第二部分:定义产品——别上来就跳钱,先告诉别人你卖啥
Stripe很聪明,它不关心“我要收100块钱”,它关心的是“我要卖什么商品”。所以,我们必须先创建一个Product(产品)和一个Price(价格)。
为什么要有两个对象?因为Stripe的设计哲学是“一切皆对象”。产品是死的,价格是活的。如果以后你想涨价,你只需要改Price,不用改Product;如果以后你想换个商品,你只需要改Product,不用改所有价格。
代码示例:创建产品和价格
我们写个简单的PHP脚本,假装我们在卖一个“年度高级会员订阅”。
<?php
require 'vendor/autoload.php';
// 1. 初始化Stripe客户端,把你的Secret Key塞进去
StripeStripe::setApiKey('sk_test_your_secret_key_here');
try {
// 2. 创建产品
$product = StripeProduct::create([
'name' => '极客PHP大师班 - 年度会员',
'description' => '包含所有视频教程和源码,终身有效。',
]);
// 3. 创建价格
// recurring:表示这是订阅
// interval:扣费周期
// currency:货币单位
$price = StripePrice::create([
'product' => $product->id, // 关联到刚才创建的产品
'unit_amount' => 199900, // 金额,单位是分(CNY:1999.00元 = 199900分)
'currency' => 'cny', // 人民币
'recurring' => [
'interval' => 'year', // 每年扣一次
],
'billing_scheme' => 'per_unit', // 每单位计费
]);
echo "产品创建成功!ID: " . $product->id . "<br>";
echo "价格创建成功!ID: " . $price->id . "<br>";
// 为了演示,我们打印出返回的JSON结构,让你看看Stripe喜欢怎么唠叨
echo "<pre>";
print_r(json_encode(['product' => $product, 'price' => $price], JSON_PRETTY_PRINT));
echo "</pre>";
} catch (Error $e) {
echo '发生错误: ' . $e->getMessage();
}
专家点评:
注意看 unit_amount,单位是分。如果你写了199900,那是1999元。如果你写错了,Stripe不会告诉你“你钱给少了”,它会直接告诉你“余额不足”或者“无效金额”。这是新手最容易踩的坑,记住了:分。
第三部分:前端交互——让用户乖乖刷卡
现在我们有了产品和价格,接下来得有个地方让用户输入卡号。如果你自己去写一个输入框来校验卡号,那简直是对人类智商的侮辱。你会遇到校验算法(Luhn算法)、脱敏处理、卡片类型识别等一系列麻烦事。
Stripe Elements就是来解决这个问题的。它是一堆漂亮的、自动校验的、能自适应不同手机屏幕的输入框组件。
代码示例:Stripe Elements前端
这是一个HTML文件,你需要把它放在你的PHP项目的public目录下。
<!DOCTYPE html>
<html>
<head>
<title>订阅页面</title>
<!-- 引入Stripe JS库 -->
<script src="https://js.stripe.com/v3/"></script>
</head>
<body>
<h1>加入PHP大师班,开启付费之旅</h1>
<p>价格:¥1999/年</p>
<form id="payment-form">
<!-- 这里是Stripe提供的元素容器 -->
<div id="card-element"></div>
<div id="card-errors" role="alert"></div>
<button id="submit" type="submit">订阅</button>
</form>
<script>
// 用你的公钥初始化Stripe
const stripe = Stripe('pk_test_your_publishable_key_here');
const elements = stripe.elements();
// 创建一个Card Element
const cardElement = elements.create('card');
// 把它渲染到HTML里
cardElement.mount('#card-element');
// 监听表单提交
const form = document.getElementById('payment-form');
form.addEventListener('submit', async (event) => {
event.preventDefault(); // 阻止表单默认提交,我们要用AJAX处理
// 调用后端PHP接口
const response = await fetch('/create-subscription.php', {
method: 'post',
body: JSON.stringify({
email: document.getElementById('email').value, // 你得自己加个邮箱输入框
items: [{
price: 'price_your_price_id_here', // 填入刚才生成的Price ID
}],
}),
});
const result = await response.json();
if (result.error) {
// 显示错误信息
const errorElement = document.getElementById('card-errors');
errorElement.textContent = result.error.message;
} else {
// 支付成功,重定向到支付页面
stripe.redirectToCheckout({ sessionId: result.sessionId });
}
});
</script>
</body>
</html>
专家点评:
看,前端代码是不是很干净?我们没有写任何校验逻辑。这就是Stripe的强大之处。接下来,我们要去写那个create-subscription.php,这是真正的核心战场。
第四部分:PHP后端——接收请求并创建Checkout Session
前端已经把请求扔过来了,我们要在后端创建一个 Checkout Session。这是Stripe里的一个高级概念,它把支付链接、产品信息、客户信息全部打包在一个Session里。
当你创建了Checkout Session后,Stripe会返回一个 sessionId。前端拿到这个ID,调用 stripe.redirectToCheckout() 就能跳转到Stripe官方的结算页面。这比你自己写个支付页面要安全得多,因为所有PCI-DSS合规的工作都由Stripe完成了。
代码示例:create-subscription.php
<?php
require 'vendor/autoload.php';
StripeStripe::setApiKey('sk_test_your_secret_key_here');
header('Content-Type: application/json');
// 1. 获取前端传来的JSON数据
$inputJSON = file_get_contents('php://input');
$input = json_decode($inputJSON, true);
if (!isset($input['email']) || !isset($input['items'])) {
http_response_code(400);
echo json_encode(['error' => '缺少必要参数']);
exit;
}
try {
// 2. 创建Checkout Session
// 这个Session会自动创建一个Stripe Customer,并创建一个Subscription
$checkout_session = StripeCheckoutSession::create([
'payment_method_types' => ['card'],
'line_items' => $input['items'],
'mode' => 'subscription', // 模式:订阅
'success_url' => 'https://www.yourdomain.com/success?session_id={CHECKOUT_SESSION_ID}',
'cancel_url' => 'https://www.yourdomain.com/cancel',
// 3. 设置客户邮箱,Stripe会尝试根据邮箱匹配现有客户
'customer_email' => $input['email'],
// 4. 开启Email通知
'subscription_creation_batching' => true,
]);
// 5. 返回Session ID给前端
echo json_encode(['sessionId' => $checkout_session->id]);
} catch (Error $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
专家点评:
这里有个重点:mode 设置为 subscription。这意味着当用户完成支付后,Stripe不会直接从用户的卡里扣钱,而是会创建一个订阅。这个订阅有一个 status(状态),默认是 trialing(试用中)或者 active(活跃)。
你可能会问:“我什么时候能拿到钱?”
别急,Stripe每3天会发起一次结算周期,也就是把这一周或一个月里,所有订阅成功的交易的钱,一次性转账到你的银行账户。
第五部分:Webhook回调——那是Stripe在给你发“暗号”
这是今天最关键、也是最容易被忽略的部分。没有Webhook,你的支付系统就是瞎子。
为什么?
当用户在Stripe页面点击“支付”时,那是用户和Stripe的交互。而当你打开数据库,想把“用户已付费”这个状态更新为“是”时,那个时刻PHP代码还没有运行!因为用户还没付完钱呢。
所以,我们需要一个机制,让Stripe在我们不知道的情况下,主动给我们发一条消息(Webhook),通知我们:“嘿,老大,那个叫张三的哥们付完钱了,赶紧去更新数据库!”
1. 设置Webhook Secret
在Stripe Dashboard -> Developers -> Webhooks -> Add Endpoint,选择你的服务器URL。Stripe会给一个 Signing Secret,格式像 whsec_xxxxx。保存好这个Secret,它用于验证消息的来源。
2. 接收并验证Webhook
PHP怎么接收Webhook?很简单,$_POST['payload_json']。
但最最重要的是验证签名。这是为了防止黑客伪造请求,说“我付钱了”,结果让你把所有会员都升级成高级版。
<?php
require 'vendor/autoload.php';
StripeStripe::setApiKey('sk_test_your_secret_key_here');
// 你的Webhook Secret
$endpoint_secret = 'whsec_your_webhook_secret_here';
// 获取Stripe发来的原始数据
$payload = @file_get_contents('php://input');
$sig_header = $_SERVER['HTTP_STRIPE_SIGNATURE'];
$event = null;
try {
// 验证签名
$event = StripeWebhook::constructEvent(
$payload, $sig_header, $endpoint_secret
);
} catch(UnexpectedValueException $e) {
// 无效的Payload
http_response_code(400);
exit;
} catch(StripeExceptionSignatureVerificationException $e) {
// 无效的签名
http_response_code(400);
exit;
}
// 根据事件类型处理不同的业务逻辑
switch ($event->type) {
case 'checkout.session.completed':
// 场景1:用户通过Checkout页面支付完成
$session = $event->data->object;
handleCheckoutSessionCompleted($session);
break;
case 'customer.subscription.created':
// 场景2:一个新的订阅被创建(通常是Trial期间)
$subscription = $event->data->object;
handleSubscriptionCreated($subscription);
break;
case 'customer.subscription.updated':
// 场景3:订阅状态更新(比如从Trial转为Active,或者涨价了)
$subscription = $event->data->object;
handleSubscriptionUpdated($subscription);
break;
case 'invoice.payment_failed':
// 场景4:扣费失败(用户卡没钱了)
$invoice = $event->data->object;
handleInvoicePaymentFailed($invoice);
break;
default:
// 忽略其他事件
echo "Received unknown event type: " . $event->type;
}
// 处理Checkout完成逻辑
function handleCheckoutSessionCompleted($session) {
// 从Session里找到用户ID和订阅ID
$customerId = $session->customer;
$subscriptionId = $session->subscription;
// TODO: 在这里去更新你的数据库
// 1. 把用户的账号状态改成“已激活”
// 2. 发送欢迎邮件
// 3. 开启VIP权限
echo "Webhook received: checkout.session.completed";
}
// 处理扣费失败逻辑
function handleInvoicePaymentFailed($invoice) {
// TODO: 给用户发邮件警告:“你的卡没钱了,快充值!”
// TODO: 记录日志,准备启动催收流程
echo "Webhook received: invoice.payment_failed";
}
专家点评:
注意看 handleCheckoutSessionCompleted 函数。我们拿到了 $session->subscription。这个ID就是通往用户订阅详情的钥匙。去Stripe Dashboard里查一下,你会发现它长得非常像 sub_1234567890。
第六部分:订阅的生命周期管理——进阶操作
光知道付钱了还不行,订阅是有生命的。它会经历创建、试用、扣费、延期、延期、延期、延期……直到你取消它。
1. 查询订阅详情
用户付完钱后,你肯定想知道他买了啥。PHP可以通过订阅ID查到所有信息。
<?php
$subscription = StripeSubscription::retrieve('sub_1234567890');
// 查看状态
echo "状态: " . $subscription->status . "<br>";
// active, past_due, canceled, trialing, unpaid
// 查看价格ID
echo "价格ID: " . $subscription->items->data[0]->price->id . "<br>";
// 查看当前周期(Next billing date)
echo "下次扣费时间: " . $subscription->current_period_end . "<br>"; // Unix时间戳
2. 暂停/恢复订阅
有时候用户不想取消,只是想暂停一个月。Stripe支持这个操作。
$subscription = StripeSubscription::retrieve('sub_1234567890');
// 暂停
$subscription->cancel_at_period_end = true;
$subscription->save();
// 恢复
$subscription->cancel_at_period_end = false;
$subscription->save();
3. 升级订阅(涨价)
如果你想给老用户涨价,千万别直接删了重建。那样用户体验极差。
你应该创建一个新的Price,然后把用户的订阅替换掉。
// 1. 获取当前订阅
$subscription = StripeSubscription::retrieve('sub_1234567890');
// 2. 添加新的价格项(假设新价格是1999元,旧价格是999元)
$subscription->items->create([
'price' => 'price_new_higher_price_id',
]);
// 3. 保存,Stripe会自动在下一个周期生效
$subscription->save();
第七部分:常见坑与排雷指南(这很重要,听好了)
做了这么多,代码也跑通了,但总有几个坑会绊倒你。作为专家,我得把这些坑填上。
1. 冷却时间
这是一个新手杀手。
当你创建一个Subscription时,Stripe不会立即扣费。它会先进入 trialing(试用)状态。这个状态通常持续14天。
在14天内,你作为开发者,去查询 $subscription->status,它依然是 trialing。你以为没生效?其实是在等试用期结束。
解决方案:在代码里判断 current_period_start 和 current_period_end 的时间戳,不要只看状态。
2. API的幂等性
Stripe的API设计很优雅,但你要注意。当你重复调用 Subscription::create,如果参数一样,Stripe通常会报错(Duplicate object creation)。你需要用 try-catch 捕获这个错误,不要让程序崩了。
3. 货币单位
再次强调,Stripe里没有“元”这个单位。它只有分。如果你的代码逻辑里涉及到金额计算,一定要转换。
$amountInCents = 199900; // 1999元
$amountInYuan = $amountInCents / 100;
4. Webhook的重试机制
Stripe的Webhook不是发一次就完事的。如果发送到你的服务器时,你的服务器没响应(比如代码报错,或者网线拔了),Stripe会尝试重试。重试次数很多,非常顽固。
解决方案:你的Webhook接口代码必须极其稳定。如果出错了,一定要抛出异常或者返回HTTP 5xx错误,让Stripe知道失败了,否则它会一直重试,直到你的服务器挂掉。
5. 隐私保护
不要在日志里打印用户的卡号(如果Stripe返回了的话)。那是违法的。
不要在前端用 var_dump 调试API返回的JSON。那是给黑客看的。
如果你在本地开发,请务必设置一个代理(比如Charles或者Laravel Tunnel),把API请求转发到Stripe,否则前端会报错“CORS Policy”。
第八部分:Dashboard里的真相
虽然我们写了代码,但有时候Stripe不会告诉你所有事情。你需要去Stripe Dashboard里“看”。
- Subscriptions(订阅):这里能看到所有订阅的列表。你可以手动取消某个用户的订阅,这叫“后端控制权”。
- Invoices(发票):这是用户看到的账单。你可以点击“Send”手动发一封账单邮件。
- Reconciliation(对账):每个月Stripe会给发对账单,你需要把上面的金额和你App里的收入流水对一下。
结语:掌控金钱的艺术
好了,各位同学,今天的讲座到这里就接近尾声了。
我们今天讲了:
- 如何用Composer拿到PHP的“核武器”。
- 如何定义产品和价格,把抽象的钱变成具体的ID。
- 如何用前端优雅地展示支付界面。
- 如何用后端创建Checkout Session。
- 最重要的,如何用Webhook像接电话一样接住Stripe的通知。
支付系统是Web开发中最核心、也是最敏感的部分。写好代码只是第一步,做好日志、做好监控、做好安全验证才是专家的体现。
当你看到那个 checkout.session.completed 的Webhook触发,你的数据库更新,用户的VIP权限开启,那一刻,你会明白“金钱流转”的魔力。
现在,去写代码吧!记得把API Key藏好,别被黑客薅了羊毛。如果有问题,欢迎在GitHub上提Issue,或者在这个群里吐槽。下课!