本文目录导读:

在PHP项目中实现商品组合套餐,常见的思路有基于数据库的套餐定义和基于购物车逻辑的套餐计算两种方式,下面是一个完整的实现方案,包含数据库设计、PHP业务逻辑和前端交互示例。
核心思路
- 数据层:定义套餐表、套餐-商品关联表,存储套餐包含的子商品及数量。
- 业务层:
- 添加套餐到购物车时,将套餐视为一个整体(或拆分为子商品但标记为套餐)。
- 计算价格时,优先使用套餐优惠价。
- 展示层:前端可单独显示套餐,或混在商品列表中。
数据库设计(MySQL)
商品表(products)
CREATE TABLE `products` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(255) NOT NULL, `price` decimal(10,2) NOT NULL, -- 原价 `type` tinyint(1) DEFAULT '1', -- 1:普通商品, 2:套餐商品(可选) PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
套餐主表(product_packages)
CREATE TABLE `product_packages` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(255) NOT NULL, -- 套餐名称 `package_price` decimal(10,2) NOT NULL, -- 套餐优惠总价 `status` tinyint(1) DEFAULT '1', -- 1:启用 0:禁用 `created_at` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
套餐子商品关联表(package_items)
CREATE TABLE `package_items` ( `id` int(11) NOT NULL AUTO_INCREMENT, `package_id` int(11) NOT NULL, -- 套餐ID `product_id` int(11) NOT NULL, -- 子商品ID `quantity` int(11) NOT NULL DEFAULT 1, -- 数量 PRIMARY KEY (`id`), KEY `package_id` (`package_id`), KEY `product_id` (`product_id`), FOREIGN KEY (`package_id`) REFERENCES `product_packages`(`id`) ON DELETE CASCADE, FOREIGN KEY (`product_id`) REFERENCES `products`(`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
PHP 业务逻辑
获取套餐信息(含子商品)
// 获取单个套餐详情
function getPackageDetail($packageId) {
$pdo = new PDO('mysql:host=localhost;dbname=test', 'root', '');
// 查套餐主信息
$stmt = $pdo->prepare("SELECT * FROM product_packages WHERE id = ?");
$stmt->execute([$packageId]);
$package = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$package) return null;
// 查子商品
$stmt = $pdo->prepare("
SELECT p.id, p.name, p.price, pi.quantity
FROM package_items pi
JOIN products p ON pi.product_id = p.id
WHERE pi.package_id = ?
");
$stmt->execute([$packageId]);
$package['items'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
// 计算原价总价(用于展示节省金额)
$originalTotal = 0;
foreach ($package['items'] as $item) {
$originalTotal += $item['price'] * $item['quantity'];
}
$package['original_total'] = $originalTotal;
$package['save_amount'] = $originalTotal - $package['package_price'];
return $package;
}
添加套餐到购物车
方案A(推荐):将套餐作为一个整体条目
class Cart {
// 添加套餐到购物车
public function addPackage($packageId, $quantity = 1) {
$package = getPackageDetail($packageId);
if (!$package || $package['status'] == 0) {
throw new Exception('套餐不存在或已下架');
}
// 购物车会话存储结构
// $_SESSION['cart']['packages'][套餐ID] = ['package_id'=>1, 'quantity'=>2, ...]
$_SESSION['cart']['packages'][$packageId] = [
'package_id' => $packageId,
'name' => $package['name'],
'quantity' => $quantity,
'price' => $package['package_price'],
'items' => $package['items'] // 可选,用于展示子商品
];
}
}
方案B:拆分为子商品(用于库存扣减更精细的场合)
// 添加套餐时,将每个子商品逐条加入购物车,并标记套餐ID
public function addPackageAsItems($packageId, $packageQuantity = 1) {
$package = getPackageDetail($packageId);
foreach ($package['items'] as $item) {
for ($i = 0; $i < $item['quantity'] * $packageQuantity; $i++) {
$_SESSION['cart']['items'][] = [
'product_id' => $item['id'],
'name' => $item['name'],
'price' => 0, // 价格在结算时重新计算
'package_id' => $packageId, // 标记属于哪个套餐
'quantity' => 1
];
}
}
// 存储套餐优惠信息,用于结算
$_SESSION['cart']['package_discount'][$packageId] = $package['package_price'];
}
购物车价格计算
function calculateCartTotal() {
$total = 0;
// 处理普通商品
foreach ($_SESSION['cart']['products'] ?? [] as $item) {
$total += $item['price'] * $item['quantity'];
}
// 处理套餐(方案A)
foreach ($_SESSION['cart']['packages'] ?? [] as $pkg) {
$total += $pkg['price'] * $pkg['quantity'];
}
// 处理方案B(套餐拆分子商品)
if (isset($_SESSION['cart']['package_discount'])) {
$packageTotal = array_sum($_SESSION['cart']['package_discount']);
$total += $packageTotal;
}
return $total;
}
前端展示(Smarty / Blade / 原生PHP示例)
<!-- 展示可用套餐列表 -->
<div class="packages">
<?php foreach ($packages as $pkg): ?>
<div class="package-card">
<h3><?= htmlspecialchars($pkg['name']) ?></h3>
<p>套餐价:<span class="price">¥<?= $pkg['package_price'] ?></span></p>
<p>原价总计:<del>¥<?= $pkg['original_total'] ?></del></p>
<p>节省:<span class="save">¥<?= $pkg['save_amount'] ?></span></p>
<ul>
<?php foreach ($pkg['items'] as $item): ?>
<li><?= $item['name'] ?> × <?= $item['quantity'] ?></li>
<?php endforeach; ?>
</ul>
<button onclick="addPackage(<?= $pkg['id'] ?>)">加入购物车</button>
</div>
<?php endforeach; ?>
</div>
<script>
function addPackage(packageId) {
fetch('/cart/add-package', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({package_id: packageId, quantity: 1})
}).then(r => r.json()).then(data => {
alert('已加入购物车');
// 更新购物车角标
});
}
</script>
进阶功能与注意事项
| 功能点 | 实现思路 |
|---|---|
| 库存扣减 | 下单时逐商品扣减,注意套餐中每个子商品的数量要乘以套餐数量 |
| 套餐互斥/叠加 | 可在数据库加字段 exclusive,前端做校验 |
| 时间限时/限量 | 套餐表增加 start_time, end_time, max_sales 字段 |
| 可选子商品 | 增加 package_options 表,让用户选择不同口味、颜色等 |
| 动态价格 | 根据用户等级(VIP)或促销活动计算不同套餐价 |
| 组合优惠(满减/满赠) | 使用规则引擎配合数据库条件判断 |
性能优化建议(高并发场景)
- 缓存套餐数据:用Redis存储套餐定义(尤其是子商品列表),避免每次请求都查DB。
- 预计算价格:在订单结算阶段统一计算套餐折扣,而不是每次加购物车都算。
- 限流:热门秒杀套餐可用队列或Redis原子操作防止超卖。
实现商品组合套餐的关键是:
- 数据模型清晰:套餐主表 + 子商品关联表。
- 购物车存储:要么整体存储套餐(简单),要么拆分子商品(灵活)。
- 价格计算:区分套餐价和原价,前端展示优惠力度。
就是一套可直接落地的PHP项目套餐实现方案,可根据你的业务复杂度和数据量剪裁使用。