本文目录导读:

在 PHP 中实现 SaaS 定价,核心不是用 PHP 写死价格表,而是设计一套灵活、可扩展的定价引擎,配合数据库、API 和前端交互。
根据你 SaaS 的复杂度,我将方案分为三个层级,你可以根据自己的需求选择。
第一层级:简易版(适合 MVP 或早期阶段)
如果套餐不多(比如只有 3 个固定套餐),直接在代码里写 match 或 switch 即可,无需数据库。
优点:开发快,易读。 缺点:改价需改代码,无法做复杂优惠。
示例代码:config/plans.php
<?php
// 定义套餐(硬编码)
return [
'free' => [
'name' => '免费版',
'price_monthly' => 0,
'features' => ['10 个项目', '1GB 存储'],
'limits' => ['projects' => 10, 'storage_mb' => 1024],
],
'pro' => [
'name' => '专业版',
'price_monthly' => 29.00,
'price_yearly' => 290.00, // 年付 10 个月价格
'features' => ['无限项目', '100GB 存储', 'API 访问'],
'limits' => ['projects' => PHP_INT_MAX, 'storage_mb' => 102400],
],
'enterprise' => [
'name' => '企业版',
'price_monthly' => 99.00,
'price_yearly' => 990.00,
'features' => ['无限项目', '无限存储', '专属客服'],
'limits' => ['projects' => PHP_INT_MAX, 'storage_mb' => PHP_INT_MAX],
],
];
在控制器中使用:
<?php
// 获取当前用户的套餐ID(例如从数据库 user 表读取)
$userPlanId = $user->plan_id; // 'free', 'pro', 'enterprise'
$plans = require 'config/plans.php';
// 用户请求升级到 'pro'
$requestedPlan = 'pro';
$price = ($billingCycle === 'yearly')
? $plans[$requestedPlan]['price_yearly']
: $plans[$requestedPlan]['price_monthly'];
第二层级:数据库驱动版(适合大多数常规 SaaS)
这是最推荐的方式,支持后台配置,适合产品迭代。
数据表设计:
-- 套餐主表
CREATE TABLE `plans` (
`id` VARCHAR(50) PRIMARY KEY, -- 代码标识: basic, pro
`name` VARCHAR(100) NOT NULL, -- 显示名称
`description` TEXT,
`is_active` BOOLEAN DEFAULT TRUE
);
-- 价格表(处理多币种、多周期)
CREATE TABLE `plan_prices` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`plan_id` VARCHAR(50) NOT NULL,
`billing_cycle` ENUM('monthly', 'yearly', 'quarterly') NOT NULL,
`currency` CHAR(3) DEFAULT 'USD',
`amount` DECIMAL(10,2) NOT NULL,
UNIQUE KEY `unique_plan_cycle` (`plan_id`, `billing_cycle`, `currency`)
);
-- 功能/限额表(用于后端鉴权)
CREATE TABLE `plan_features` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`plan_id` VARCHAR(50) NOT NULL,
`feature_key` VARCHAR(50) NOT NULL, -- 'max_projects'
`feature_value` INT/TEXT NOT NULL -- 数量或 JSON
);
PHP 定价服务类:
<?php
class PricingService {
public function __construct(private PDO $db) {}
// 核心方法:获取套餐价格
public function getPrice(string $planId, string $cycle, string $currency = 'USD'): float {
$stmt = $this->db->prepare(
"SELECT amount FROM plan_prices
WHERE plan_id = ? AND billing_cycle = ? AND currency = ?"
);
$stmt->execute([$planId, $cycle, $currency]);
return (float) $stmt->fetchColumn();
}
// 获取所有可用套餐(用于前台展示)
public function getActivePlans(): array {
$plans = $this->db->query("SELECT * FROM plans WHERE is_active = TRUE")->fetchAll();
foreach ($plans as &$plan) {
// 获取价格列表
$priceStmt = $this->db->prepare("SELECT * FROM plan_prices WHERE plan_id = ?");
$priceStmt->execute([$plan['id']]);
$plan['prices'] = $priceStmt->fetchAll();
}
return $plans;
}
// 鉴权:检查用户是否可以升级(例如处理按量计费超额)
public function canAccessFeature(string $planId, string $featureKey): bool {
// 逻辑:查询 plan_features 表
}
}
第三层级:高级用例(复杂计费、按量计费、多租户)
如果你的 SaaS 涉及按 API 调用次数、存储 GB 数、GPU 时间等计量计费,PHP 中需要引入计量表和配额校验。
核心逻辑:
- 订阅合同表: 记录用户当前订阅的套餐和周期。
- 用量事件表: 记录每次 API 调用或资源消耗。
- 配额检查中间件: 在用户每次操作前,计算当前时段内用量总和,超过限制则返回 403。
<?php
// 中间件示例(伪代码)
function checkQuota(string $userId, string $resourceType, int $amount) {
$max = getPlanLimit($userId, $resourceType); // 从缓存/数据库读取最大配额
$used = getUsage($userId, $resourceType); // 计算这一周期内(比如本月)的用量总和
// 当用量超过时,返回错误
if ($used + $amount > $max) {
throw new \Exception('资源配额不足,请升级套餐。');
}
}
高级“按需付费”逻辑: 当用户在升级套餐时,要计算按比例计费(Proration)。
<?php
// 计算升级费用
public function calculateUpgradeCost($currentPlan, $newPlan, $daysRemaining, $daysInPeriod) {
$oldMonthlyCost = $currentPlan['monthly_price'];
$newMonthlyCost = $newPlan['monthly_price'];
// 已付的旧套餐剩余价值
$credit = $oldMonthlyCost * ($daysRemaining / $daysInPeriod);
// 新套餐总价
$newCost = $newMonthlyCost;
// 应付差额(需要向 Stripe 收费的金额)
$amountDue = max(0, $newCost - $credit);
return $amountDue;
}
关键点:前端与支付网关集成
定价策略的实现几乎总是需要结合支付网关(如 Stripe、Paddle 或国内的海底捞、支付宝)。
-
方案 A(推荐): 在 PHP 后端生成
Checkout Session,将套餐 ID(pro_yearly)发送给 Stripe,由 Stripe 计算金额。// Stripe 客户端直接收取固定金额,简单安全 $session = \Stripe\Checkout\Session::create([ 'line_items' => [[ 'price' => 'price_123abc', // Stripe 后台定义的套餐价格ID 'quantity' => 1, ]], 'mode' => 'subscription', 'metadata' => ['plan_id' => 'pro'], ]); -
方案 B: 由 PHP 计算好价格,展示给用户确认,再同步价格到支付网关。
总结与最佳实践
如果你要开始做,我建议:
- 不要硬编码:即使现在只有 3 个套餐,也请建两个数据表(
plans和plan_prices)。 - “限额”与“价格”分离:价格表是给财务看的,
plan_features是给后端代码看的(用于校验用户权限)。 - 缓存价格表:将
plans和plan_prices缓存到 Redis 或 Yac 中,避免每次请求都查询数据库。 - 时间敏感:价格修改时,考虑历史订单价格不变,所以要记录订单快照(下单时的套餐信息+价格)。
如果你需要针对具体的场景(如结合 Stripe 订阅、处理试用期),可以补充细节,我帮你写对应的代码。