本文目录导读:

在PHP项目中实现商品规格管理(SKU管理),核心在于多对多关系的数据结构设计,以及规格组合与库存的动态生成,下面是一个通用的、经过项目实践检验的实现方案。
核心概念
- 规格名:如“颜色”、“尺寸”、“版本”。
- 规格值:如“红色”、“XL”、“256G”。
- SKU(Stock Keeping Unit):库存量单位,即“颜色:红色,尺寸:XL,版本:256G”这一具体组合,拥有独立的库存、价格、图片等。
数据库表结构设计(推荐方案)
这是最常用、扩展性最好的方案,使用四张表。
商品表 (products)
CREATE TABLE `products` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(255) NOT NULL DEFAULT '' COMMENT '商品名称', `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '状态 1上架 0下架', `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
规格名表 (spec_names)
CREATE TABLE `spec_names` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `product_id` int(11) NOT NULL COMMENT '关联商品ID', `name` varchar(100) NOT NULL DEFAULT '' COMMENT '规格名,如:颜色,尺寸', `sort_order` int(11) NOT NULL DEFAULT '0' COMMENT '排序', PRIMARY KEY (`id`), KEY `product_id` (`product_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
规格值表 (spec_values)
CREATE TABLE `spec_values` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `spec_name_id` int(11) NOT NULL COMMENT '关联规格名ID', `value` varchar(100) NOT NULL DEFAULT '' COMMENT '规格值,如:红色,XL', `sort_order` int(11) NOT NULL DEFAULT '0' COMMENT '排序', PRIMARY KEY (`id`), KEY `spec_name_id` (`spec_name_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
SKU 表 (sku_stock)
CREATE TABLE `sku_stock` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `product_id` int(11) NOT NULL COMMENT '商品ID', `sku_code` varchar(100) NOT NULL DEFAULT '' COMMENT 'SKU编码(唯一)', `price` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '售价', `stock` int(11) NOT NULL DEFAULT '0' COMMENT '库存', `image` varchar(255) NOT NULL DEFAULT '' COMMENT '该SKU专属图片', `spec_value_ids` varchar(255) NOT NULL DEFAULT '' COMMENT '规格值ID组合,如 1,4,7 方便查询', PRIMARY KEY (`id`), KEY `product_id` (`product_id`), KEY `sku_code` (`sku_code`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
PHP 业务逻辑实现(以 Laravel 为例,其他框架类似)
模型关联(可选,但推荐)
// Product.php (商品模型)
public function specNames() {
return $this->hasMany(SpecName::class);
}
public function skus() {
return $this->hasMany(SkuStock::class);
}
// SpecName.php (规格名模型)
public function specValues() {
return $this->hasMany(SpecValue::class);
}
保存规格(后端接收数据)
前端提交的数据格式通常是:
{
"product_name": "T恤",
"specs": [
{ "name": "颜色", "values": ["红色", "蓝色"] },
{ "name": "尺寸", "values": ["M", "L", "XL"] }
],
"skus": [
{ "specs": {"颜色":"红色","尺寸":"M"}, "price":99, "stock":100, "image":"red_m.jpg" },
{ "specs": {"颜色":"红色","尺寸":"L"}, "price":109, "stock":50, "image":"red_l.jpg" }
]
}
控制器处理逻辑:
public function store(Request $request) {
// 1. 保存商品基本信息
$product = Product::create(['name' => $request->product_name]);
// 2. 保存规格名和规格值
$specValueIdMap = []; // 记录 "规格名_规格值" -> 数据库ID
foreach ($request->specs as $spec) {
$specName = SpecName::create([
'product_id' => $product->id,
'name' => $spec['name']
]);
foreach ($spec['values'] as $value) {
$specValue = SpecValue::create([
'spec_name_id' => $specName->id,
'value' => $value
]);
$specValueIdMap[$spec['name'] . '_' . $value] = $specValue->id;
}
}
// 3. 保存SKU
foreach ($request->skus as $skuData) {
$ids = [];
foreach ($skuData['specs'] as $specName => $specValue) {
$ids[] = $specValueIdMap[$specName . '_' . $specValue];
}
sort($ids); // 排序后组合,便于查询
SkuStock::create([
'product_id' => $product->id,
'sku_code' => implode('-', $ids) . '-' . $product->id,
'price' => $skuData['price'],
'stock' => $skuData['stock'],
'image' => $skuData['image'],
'spec_value_ids' => implode(',', $ids)
]);
}
return success();
}
前端展示与选择(关键)
当用户选择规格时,需要根据已选的规格值,动态过滤可选的SKU。
数据结构示例(传给前端):
{
"specs": [
{
"name": "颜色",
"values": [
{"id":1, "value":"红色", "disabled": false},
{"id":2, "value":"蓝色", "disabled": true} // 该规格组合缺货
]
},
{
"name": "尺寸",
"values": [
{"id":3, "value":"M", "disabled": false},
{"id":4, "value":"L", "disabled": false}
]
}
],
"skus": [
{"id":1, "spec_value_ids": "1,3", "price":99, "stock":100, "image":"red_m.jpg"},
{"id":2, "spec_value_ids": "1,4", "price":109, "stock":0, "image":"red_l.jpg"}
],
"default_sku": null
}
前端JavaScript核心逻辑(以Vue为例):
// 当用户点击某个规格值时
function selectSpec(specNameIndex, specValueIndex) {
// 1. 切换选中状态
// 2. 根据当前已选中的规格值ID组合,去sku列表中查找匹配的SKU
// 3. 根据匹配结果,高亮或禁用未选中的规格值
// 4. 更新展示的价格、库存、图片
// 简化示例:遍历所有规格值,检查是否存在包含该规格值的SKU
this.specs.forEach((spec, i) => {
spec.values.forEach(val => {
// 检查是否有SKU包含 val.id 且 所有已选规格值也包含
val.disabled = !this.skus.some(sku => {
const skuIds = sku.spec_value_ids.split(',').map(Number);
// sku必须包含当前val,且包含所有已选的其他规格值
return skuIds.includes(val.id) && this.selectedSpecIds.every(selectedId =>
selectedId === val.id || skuIds.includes(selectedId)
);
});
});
});
}
高级优化建议
- 规格组合图片:SKU表增加
image字段,可为不同规格组合设置不同主图,提升用户体验。 - 规格组合编码:
sku_code可用规格值ID拼接(如1-4-7),或使用商品ID+随机码,用于仓库管理。 - 批量编辑:后台提供按规格名批量更新价格/库存的功能,如“红色尺码统一加10元”。
- 规格值排序:前端拖动排序,后端用
sort_order字段控制展示顺序。 - 缓存优化:SKU量大的商品(如几十个规格组合),可将规格树和SKU列表缓存到Redis,减少数据库查询。
- 缺货规格隐藏:对于库存为0的SKU,其规格值可显示为灰色并不可点击。
常见问题与解决方案
- 不同规格组合价格差异大:不要让所有SKU共用主表价格,而是以SKU表的价格为准。
- 规格值数量过多:不建议超过3-4个规格维度(如颜色、尺寸),否则SKU组合数会呈指数级增长(如10色10尺寸5版本=500个SKU),管理成本极高。
- 无规格商品:特殊处理,直接使用商品的默认价格和库存,SKU表只存一条记录,
spec_value_ids为空。
- 设计核心:
spec_names+spec_values+sku_stock三表 +products主表。 - 核心逻辑:SKU表通过
spec_value_ids字段存储组合关系,前端通过该字段实现规格联动。 - 最佳实践:前后端分离,前端负责动态渲染与禁用逻辑,后端负责数据维护与存储。
这个方案适用于大多数电商系统(如商城、服装、电子产品),扩展性好,易于维护,如果SKU量特别大(如上万),可以考虑使用专门的多SKU插件或自建缓存层。