本文目录导读:

在 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 项目中逐步搭建一个安全、可扩展的订阅支付系统,如果需要更详细的某个环节(如支付宝签约的具体参数),请提供更多信息,我可以继续补充。