PHP项目中如何实现订阅支付功能?

wen PHP项目 5

本文目录导读:

PHP项目中如何实现订阅支付功能?

  1. 核心流程概述
  2. 技术选型
  3. 数据库表设计
  4. 使用 Stripe 实现订阅(详细步骤)
  5. 国内支付(支付宝/微信)订阅实现思路
  6. 本地订阅状态管理
  7. 安全性 & 最佳实践
  8. 完整示例项目结构(推荐)

在 PHP 项目中实现订阅支付功能,通常涉及支付网关集成定期扣款逻辑用户订阅状态管理以及Webhook(网络钩子)处理,以下是实现该功能的详细指南,以 Stripe 为例(最常用且文档清晰),同时也包含支付宝/微信支付的思路。


核心流程概述

  1. 用户选择订阅计划:显示不同价格与周期(月/年)。
  2. 创建支付意向:后端调用支付网关 API 创建订阅。
  3. 前端确认支付:用户输入卡信息/扫码确认。
  4. Webhook 异步通知:支付网关通知你的服务器支付成功/失败。
  5. 更新本地数据库:记录订阅状态、到期时间。
  6. 定期扣款:支付网关自动在周期结束时再次扣款(如果启用自动续费)。
  7. 取消/退款:用户或管理员操作。

技术选型

  • 支付网关:Stripe(国际)、Lemon Squeezy(适合 SaaS)、支付宝/微信支付(国内)。
  • PHP 库stripe/stripe-phppaypal/rest-api-sdk-php 等。
  • 数据库:存储 userssubscriptionsplans 表。
  • 定时任务:用于处理本地续费状态校验(可选,但依赖 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 流程差异

  1. 创建签约:后端调用支付宝 alipay.user.agreement.page.sign 或微信 papay 签约接口。
  2. 用户确认:用户跳转到支付宝/微信页面同意协议。
  3. 异步通知:签约成功/失败后,回调你的服务器(同样需要 Webhook)。
  4. 主动扣款:周期到达时,由你的服务器调用扣款接口(而非自动扣款)。
  5. 处理结果:扣款成功/失败后继续重置订阅周期或标记失败。

注意:国内平台的订阅需要用户手动签约,且微信的自动扣款目前仅限特定行业(如水电煤)。

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                    # 存放密钥

  1. 选型:推荐 Stripe(国际)或 Lemon Squeezy(免去税务合规烦恼),国内用支付宝/微信但需注意签约模式。
  2. 核心组件:数据库设计、Webhook 处理、定时任务。
  3. 核心原则绝对信任 Webhook 而非前端返回的数据,不要仅依赖用户跳转成功页面来激活订阅。

按照上述步骤,你可以在 PHP 项目中逐步搭建一个安全、可扩展的订阅支付系统,如果需要更详细的某个环节(如支付宝签约的具体参数),请提供更多信息,我可以继续补充。

抱歉,评论功能暂时关闭!