本文目录导读:

在 PHP 项目中实现订阅支付功能,通常涉及支付网关集成、定期扣款逻辑、用户订阅状态管理以及Webhook(网络钩子)处理,以下是实现该功能的详细指南,以 Stripe 为例(最常用且文档清晰),同时也包含支付宝/微信支付的思路。
核心流程概述
- 用户选择订阅计划:显示不同价格与周期(月/年)。
- 创建支付意向:后端调用支付网关 API 创建订阅。
- 前端确认支付:用户输入卡信息/扫码确认。
- Webhook 异步通知:支付网关通知你的服务器支付成功/失败。
- 更新本地数据库:记录订阅状态、到期时间。
- 定期扣款:支付网关自动在周期结束时再次扣款(如果启用自动续费)。
- 取消/退款:用户或管理员操作。
技术选型
- 支付网关:Stripe(国际)、Lemon Squeezy(适合 SaaS)、支付宝/微信支付(国内)。
- PHP 库:
stripe/stripe-php、paypal/rest-api-sdk-php等。 - 数据库:存储
users、subscriptions、plans表。 - 定时任务:用于处理本地续费状态校验(可选,但依赖 Webhook 更可靠)。
数据库表设计
-- 订阅计划表
CREATE TABLE `plans` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`name` VARCHAR(100) NOT NULL, -- 如“专业版月付”
`price_cents` INT NOT NULL, -- 单位:分(避免浮点)
`currency` CHAR(3) DEFAULT 'USD',
`interval` ENUM('month','year') NOT NULL,
`stripe_price_id` VARCHAR(100) NULL, -- Stripe 价格ID
`features` TEXT NULL,
`is_active` TINYINT DEFAULT 1
);
-- 用户订阅表
CREATE TABLE `subscriptions` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`user_id` INT NOT NULL,
`plan_id` INT NOT NULL,
`stripe_subscription_id` VARCHAR(100) NULL, -- 支付网关的唯一订阅ID
`status` ENUM('active','canceled','past_due','incomplete','trialing','expired') DEFAULT 'incomplete',
`current_period_start` DATETIME NULL,
`current_period_end` DATETIME NULL,
`canceled_at` DATETIME NULL,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`),
FOREIGN KEY (`plan_id`) REFERENCES `plans`(`id`)
);
使用 Stripe 实现订阅(详细步骤)
1 安装依赖
composer require stripe/stripe-php
2 后端 API:创建订阅
// src/Controller/SubscriptionController.php
use Stripe\Stripe;
use Stripe\Checkout\Session;
class SubscriptionController
{
public function createCheckoutSession($userId, $priceId)
{
Stripe::setApiKey($_ENV['STRIPE_SECRET_KEY']);
// 1. 查找用户(假设你有用户对象)
$user = User::find($userId);
if (!$user->stripe_customer_id) {
// 首次订阅需要先创建 Customer
$customer = \Stripe\Customer::create([
'email' => $user->email,
'metadata' => ['user_id' => $userId],
]);
$user->stripe_customer_id = $customer->id;
$user->save();
}
// 2. 创建 Checkout Session (订阅模式)
$session = Session::create([
'customer' => $user->stripe_customer_id,
'mode' => 'subscription', // 关键:订阅模式
'line_items' => [[
'price' => $priceId, // 你在Stripe Dashboard 创建的价格ID
'quantity' => 1,
]],
// 支付成功后的跳转地址
'success_url' => $_ENV['APP_URL'] . '/subscription/success?session_id={CHECKOUT_SESSION_ID}',
'cancel_url' => $_ENV['APP_URL'] . '/subscription/cancel',
'metadata' => [
'user_id' => $userId,
],
]);
// 3. 返回给前端:重定向到 Stripe 支付页面
header('Location: ' . $session->url);
exit;
}
}
3 前端处理
<!-- 简单示例:点击按钮跳转到 Stripe Checkout --> <a href="/api/subscription/create?priceId=price_xxxx" class="btn">订阅 月付 $10</a>
Stripe Checkout 会自动处理卡号输入、3D 验证等。
4 Webhook 处理(关键:异步确认订阅状态)
Stripe 在支付完成后会向你的服务器发送 POST 请求,你需要暴露一个公开 URL 接收事件。
// webhook.php
use Stripe\Webhook;
use Stripe\Event;
$payload = @file_get_contents('php://input');
$sig_header = $_SERVER['HTTP_STRIPE_SIGNATURE'];
$endpoint_secret = $_ENV['STRIPE_WEBHOOK_SECRET'];
try {
$event = Webhook::constructEvent($payload, $sig_header, $endpoint_secret);
} catch (\UnexpectedValueException $e) {
http_response_code(400);
exit;
} catch (\Stripe\Exception\SignatureVerificationException $e) {
http_response_code(400);
exit;
}
// 处理订阅相关事件
switch ($event->type) {
case 'checkout.session.completed':
$session = $event->data->object;
// 获取 subscription ID
$subscriptionId = $session->subscription;
// 获取用户ID (从 metadata)
$userId = $session->metadata->user_id;
// 更新数据库:创建订阅记录
$this->activateSubscription($userId, $subscriptionId);
break;
case 'invoice.payment_succeeded':
// 每月续费成功时触发
$invoice = $event->data->object;
$subscriptionId = $invoice->subscription;
// 更新本地订阅的到期时间
$this->renewSubscription($subscriptionId);
break;
case 'customer.subscription.updated':
case 'customer.subscription.deleted':
// 取消订阅、暂停等
$subscription = $event->data->object;
$this->syncSubscriptionStatus($subscription);
break;
default:
// 其他事件可忽略或记录日志
echo 'Received unknown event type: ' . $event->type;
}
http_response_code(200);
重要:必须使用 HTTPS,且 Webhook 端点不要放在公网可随意访问的位置(至少要求 Stripe 的 IP 白名单)。
国内支付(支付宝/微信)订阅实现思路
支付宝和微信的订阅(自动扣款) 需要通过签约接口实现,例如支付宝的 周期性扣款 或微信支付的 委托代扣。
1 流程差异
- 创建签约:后端调用支付宝
alipay.user.agreement.page.sign或微信papay签约接口。 - 用户确认:用户跳转到支付宝/微信页面同意协议。
- 异步通知:签约成功/失败后,回调你的服务器(同样需要 Webhook)。
- 主动扣款:周期到达时,由你的服务器调用扣款接口(而非自动扣款)。
- 处理结果:扣款成功/失败后继续重置订阅周期或标记失败。
注意:国内平台的订阅需要用户手动签约,且微信的自动扣款目前仅限特定行业(如水电煤)。
2 PHP 示例(支付宝签约)
// 使用官方SDK: composer require alipaysdk/easysdk
use Alipay\EasySDK\Kernel\Factory;
use Alipay\EasySDK\Kernel\Config;
$config = new Config();
$config->protocol = 'https';
$config->gatewayHost = 'openapi.alipay.com';
$config->appId = '你的APPID';
$config->signType = 'RSA2';
// ... 其他配置
Factory::setOptions($config);
// 创建签约请求
$result = Factory::payment()->pay()->signRequestParams(
'订阅计划名称',
'external_agreement_no', // 你系统的唯一签约号
'product_code', // 固定值:CYCLE_PAY_AUTH
'period', // 1
'period_type', // MONTH
'total_amount', // 10.00
);
// 返回给前端一个 form 表单或跳转 URL
header('Location: ' . $result['pageRedirectionUrl']);
本地订阅状态管理
无论是 Stripe 还是国内网关,都需要在本地维护一个 subscriptions 表的准确状态。
- Webhook 更新:如上所示,在
invoice.payment_succeeded时更新current_period_end。 - 定时任务:每天运行一次,检查
current_period_end < NOW()且状态为active的记录,将其标记为expired。
// cron job 示例 (每天凌晨执行)
$expiredSubscriptions = Subscription::where('status', 'active')
->where('current_period_end', '<', now())
->update(['status' => 'expired']);
安全性 & 最佳实践
| 注意事项 | 说明 |
|---|---|
| 永远不要在客户端存储支付敏感信息 | 卡号、CVV 等通过 Stripe Elements 或 Checkout 处理,你的服务器只接收 token/ID。 |
| 使用 Webhook 签名验证 | 确保收到的回调确实来自支付网关,而非伪造。 |
| 幂等性处理 | Webhook 可能重复发送,需在数据库用 stripe_subscription_id 做唯一索引并去重。 |
| 日志记录 | 记录所有 Webhook 事件、订阅状态变更,方便排查问题。 |
| 用户通知 | 支付失败、即将到期时发送邮件/短信通知用户。 |
完整示例项目结构(推荐)
├── public/
│ ├── index.php # 入口
│ └── webhook.php # 接收支付网关回调
├── src/
│ ├── Controller/
│ │ ├── SubscriptionController.php
│ │ └── WebhookController.php
│ ├── Model/
│ │ ├── User.php
│ │ └── Subscription.php
│ └── Service/
│ └── StripeService.php # 封装 Stripe API 调用
├── config/
│ └── stripe.php
├── composer.json
└── .env # 存放密钥
- 选型:推荐 Stripe(国际)或 Lemon Squeezy(免去税务合规烦恼),国内用支付宝/微信但需注意签约模式。
- 核心组件:数据库设计、Webhook 处理、定时任务。
- 核心原则:绝对信任 Webhook 而非前端返回的数据,不要仅依赖用户跳转成功页面来激活订阅。
按照上述步骤,你可以在 PHP 项目中逐步搭建一个安全、可扩展的订阅支付系统,如果需要更详细的某个环节(如支付宝签约的具体参数),请提供更多信息,我可以继续补充。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播