1.1 三表结构的核心模型
LikeShop 的商品体系采用经典的主表(SPU)+ 规格表 + SKU 表三层架构;三张表的分工如下:
| 表名 | 职责 | 关键字段 |
|---|---|---|
| ls_goods | 商品基础信息(SPU) | name、image、category_id、status、goods_type |
| ls_goods_spec | 规格名称与规格值定义 | spec_name、spec_value |
| ls_goods_item | SKU 组合的价格与库存 | goods_id、spec_value_ids、spec_value_str、sell_price、stock |
ls_goods_spec 存储商品的可选规格,比如“颜色”这个规格下有“红色、蓝色”两个规格值,“尺寸”这个规格下有“S、M、L”三个规格值;ls_goods_item 则是规格值组合的笛卡尔积结果,每一条记录对应一个独立的 SKU,拥有自己的价格和库存。
1.2 ls_goods_item 的字段语义
ls_goods_item 是价格和库存的真正存储位置;核心字段包括:
| 字段名 | 数据类型 | 注释 |
|---|---|---|
| goods_id | int | 商品id |
| spec_value_ids | varchar | 多个规格id,逗号隔开 |
| spec_value_str | varchar | 多个规格名称,逗号隔开 |
| sell_price | decimal | 销售价格 |
| stock | int | 库存 |
spec_value_ids 是 SKU 的唯一标识,由规格值 ID 按有序组合拼接而成;比如“蓝色,4G”对应“5,7”,“蓝色,6G”对应“5,8”;下单时用户选择的规格组合,计算通过 spec_value_ids 来定位具体的 SKU。
商品详情接口返回的数据结构清晰地印证了这个设计:
json
{
"goods_spec": [
{
"name": "选择颜色",
"spec_value": [
{"id": 5, "value": "蓝色"},
{"id": 6, "value": "白色"}
]
},
{
"name": "选择版本",
"spec_value": [
{"id": 7, "value": "4G"},
{"id": 8, "value": "6G"}
]
}
],
"goods_item": [
{
"id": 1,
"spec_value_ids": "5,7",
"spec_value_str": "蓝色,4G",
"price": "2088.00",
"stock": 1000
}
]
}
1.3 原生 SKU 体系的四个核心缺口
缺口一:规格数量受限。原生设计在 UI 层面通常只支持两级规格(如“颜色 + 尺寸”),当商品需要三级以上规格(如“颜色 + 尺寸 + 材质”)时,前端编辑器和后端计算逻辑都无法支撑;虽然 ls_goods_spec 表本身没有限制规格数量,但业务逻辑层对规格层级做了硬编码假设。
缺口二:规格属性不可自定义。原生的规格名和规格值依赖于预设模版或手动输入,缺乏“按商品分类预置规格属性”的能力;例如,服装分类应该自动带出“尺码、颜色、材质”三个规格维度,但原生系统无法实现这种分类级别的规格属性绑定。
缺口三:无批量导入能力。商品数量较多时,逐个手动添加商品的效率极低;原生系统没有提供 Excel 批量导入商品和 SKU 库存的功能,运营人员需要耗费大量时间在重复的表单填写上。
缺口四:SKU 组合的笛卡尔积计算在服务端硬编码。当规格数量动态变化时,SKU 组合的生成逻辑需要手动适配,缺乏通用的笛卡尔积计算引擎。
1.4 二开策略定位
遵循 LikeShop的“扩展优先”原则,SKU 体系改造采用以下策略:
- 不修改 ls_goods_item 的核心字段结构,通过扩展规格定义层来支持多级规格
- 新增分类规格属性表,实现“分类 → 规格属性模版 → 商品规格”的联动
- 批量导入采用 PhpSpreadsheet 流式读取 + 分批事务插入,避免内存溢出和超时
- SKU 笛卡尔积计算封装为独立 Service,支持任意数量的规格维度
2.1 分类规格属性模版表
新建分类规格属性表,实现按商品分类预置规格维度的能力:
sql
CREATE TABLE `ls_category_spec_attr` (
`id` int UNSIGNED NOT NULL AUTO_INCREMENT,
`category_id` int UNSIGNED NOT NULL COMMENT '分类ID',
`attr_name` varchar(30) NOT NULL COMMENT '规格属性名称,如"颜色""尺寸""材质"',
`attr_values` text COMMENT '预设规格值,JSON数组,如["红色","蓝色"]',
`is_required` tinyint NOT NULL DEFAULT 0 COMMENT '是否必填:0-否,1-是',
`sort` int NOT NULL DEFAULT 0 COMMENT '排序',
`status` tinyint NOT NULL DEFAULT 1 COMMENT '状态:0-禁用,1-启用',
`create_time` int UNSIGNED NOT NULL DEFAULT 0,
`update_time` int UNSIGNED NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
KEY `idx_category_id` (`category_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='分类规格属性模板表';
该表使得“服装”分类可以预置“尺码、颜色、材质”三个规格属性,“数码”分类可以预置“颜色、内存、版本”三个规格属性;发布商品选择分类后,系统自动带出该分类下的规格属性模版。
2.2 商品规格表的结构扩展
在 ls_goods_spec 表中新增字段,支持规格属性的扩展能力:
sql
ALTER TABLE `ls_goods_spec`
ADD COLUMN `spec_type` tinyint NOT NULL DEFAULT 1 COMMENT '规格类型:1-普通规格,2-自定义规格' AFTER `spec_name`,
ADD COLUMN `sort` int NOT NULL DEFAULT 0 COMMENT '规格排序' AFTER `spec_value`,
ADD COLUMN `is_required` tinyint NOT NULL DEFAULT 0 COMMENT '是否必填:0-否,1-是' AFTER `sort`;
spec_type 标识该规格是来自分类模版(普通规格)还是商家自定义添加(自定义规格);sort 字段控制规格在商品详情页的展示顺序。
2.3 多规格笛卡尔积计算引擎
原生 SKU 组合的计算逻辑在服务端硬编码,只能处理固定数量的规格;改造后需要实现通用的笛卡尔积计算引擎,支持任意数量的规格维度:
php
class SkuGeneratorService
{
/**
* 生成多规格 SKU 组合
* @param array $specGroups 规格分组,格式:
* [
* ['spec_name' => '颜色', 'spec_values' => [['id'=>5,'value'=>'蓝色'], ['id'=>6,'value'=>'白色']]],
* ['spec_name' => '尺寸', 'spec_values' => [['id'=>7,'value'=>'S'], ['id'=>8,'value'=>'M']]],
* ['spec_name' => '材质', 'spec_values' => [['id'=>9,'value'=>'棉'], ['id'=>10,'value'=>'涤纶']]]
* ]
* @return array SKU 组合列表
*/
public function generateSkus($specGroups)
{
if (empty($specGroups)) {
return [];
}
// 提取每个规格分组的值列表
$valueGroups = [];
foreach ($specGroups as $group) {
$values = array_column($group['spec_values'], 'id');
if (empty($values)) {
throw new Exception("规格「{$group['spec_name']}」没有配置规格值");
}
$valueGroups[] = $values;
}
// 计算笛卡尔积
$combinations = $this->cartesianProduct($valueGroups);
// 构建 SKU 数据结构
$skus = [];
foreach ($combinations as $combination) {
$specValueIds = implode(',', $combination);
$specValueStr = $this->buildSpecValueStr($specGroups, $combination);
$skus[] = [
'spec_value_ids' => $specValueIds,
'spec_value_str' => $specValueStr,
'sell_price' => 0.00,
'stock' => 0,
];
}
return $skus;
}
/**
* 递归计算笛卡尔积
*/
private function cartesianProduct($arrays, $index = 0, $current = [])
{
if ($index >= count($arrays)) {
return [$current];
}
$result = [];
foreach ($arrays[$index] as $value) {
$next = $current;
$next[] = $value;
$result = array_merge($result, $this->cartesianProduct($arrays, $index + 1, $next));
}
return $result;
}
/**
* 构建规格值展示字符串
*/
private function buildSpecValueStr($specGroups, $combination)
{
$strs = [];
foreach ($specGroups as $i => $group) {
foreach ($group['spec_values'] as $value) {
if ($value['id'] == $combination[$i]) {
$strs[] = $value['value'];
break;
}
}
}
return implode(',', $strs);
}
}
2.4 多级规格的 SKU 匹配逻辑
当规格数量动态变化时,下单时的 SKU 匹配逻辑需要适配;原生通过 spec_value_ids 字段串匹配,改造后需要增加对任意数量规格的支持:
php
class SkuMatcherService
{
/**
* 根据用户选择的规格值匹配 SKU
* @param int $goodsId 商品ID
* @param array $selectedValueIds 用户选择的规格值ID数组,如 [5, 7, 9]
*/
public function matchSku($goodsId, $selectedValueIds)
{
// 排序保证顺序一致
sort($selectedValueIds);
$specValueIds = implode(',', $selectedValueIds);
// 优先精确匹配
$sku = GoodsItemModel::where('goods_id', $goodsId)
->where('spec_value_ids', $specValueIds)
->find();
if ($sku) {
return $sku;
}
// 降级:遍历该商品所有 SKU,做集合匹配(处理顺序不一致的情况)
$allSkus = GoodsItemModel::where('goods_id', $goodsId)->select();
foreach ($allSkus as $item) {
$itemIds = explode(',', $item['spec_value_ids']);
sort($itemIds);
if ($itemIds === $selectedValueIds) {
return $item;
}
}
throw new Exception('未找到匹配的规格组合');
}
}
2.5 后台配置
在“商品管理 → 分类管理”中,为每个分类增加“规格属性模版”配置区块:
| 配置项 | 类型 | 说明 |
|---|---|---|
| 规格属性名称 | 文本输入 | 如“颜色”“尺寸”“材质” |
| 预设规格值 | 标签输入 | 每个规格值回车确认,如“红色”“蓝色” |
| 是否必填 | 开关 | 发布商品时该规格属性是否必须配置 |
| 排序 | 数字输入 | 控制规格属性的展示顺序 |
在商品发布页面,选择分类后自动带出该分类下的规格属性模版,商家可在此基础上增删规格值,也可添加自定义规格属性。
3.1 批量导入的整体流程
批量导入的完整流程为:下载导入模版 → 填写商品数据 → 上传 Excel 文件 → 后端解析验证 → 分批写入数据库;LikeShop 原生支持列表导出功能,数据导出采用 PhpExcel 使数据更加直观;批量导入作为导出的逆向操作,需要复用已有的数据模型和验证规则。
3.2 导入模版设计
模版分为两个 Sheet:
Sheet1:商品基本信息(一行一个商品)
| 商品名称 | 商品分类 | 商品类型 | 商品主图 | 轮播图 | 商品详情 | 运费模板 | 单位 | 品牌 |
|---|---|---|---|---|---|---|---|---|
| iPhone 15 | 手机数码 | 实物 | url1 | url1,url2 | ... | 默认模板 | 台 | Apple |
Sheet2:SKU 规格信息(一行一个 SKU)
| 商品名称 | 规格1名称 | 规格1值 | 规格2名称 | 规格2值 | 规格3名称 | 规格3值 | 销售价 | 库存 | 成本价 |
|---|---|---|---|---|---|---|---|---|---|
| iPhone 15 | 颜色 | 黑色 | 内存 | 256G | 版本 | 国行 | 6999 | 100 | 6500 |
| iPhone 15 | 颜色 | 白色 | 内存 | 256G | 版本 | 国行 | 6999 | 80 | 6500 |
两个 Sheet 通过“商品名称”关联;如果商品只有单一规格(无多规格),Sheet2 中的规格列留空即可。
3.3 PhpSpreadsheet 流式读取
使用 PhpSpreadsheet 实现 Excel 批量导入时,必须先安装依赖: composer require phpoffice/phpspreadsheet;TP6 项目必须确保 PHP ≥ 7.4,否则类加载会静默失败;在控制器顶部写全命名空间 use PhpOffice\PhpSpreadsheet\IOFactory,漏掉 PhpOffice\ 前缀,运行时必报“Class not found”。
php
class GoodsImportService
{
/**
* 解析导入文件
*/
public function parseFile($filePath, $ext)
{
// 1. 根据扩展名创建显式 Reader
if ($ext === 'xlsx') {
$reader = IOFactory::createReader('Xlsx');
} else {
$reader = IOFactory::createReader('Xls');
}
// 2. 关闭非数据解析,内存占用直降60%以上
$reader->setReadDataOnly(true);
// 3. 加载文件并获取工作表
$spreadsheet = $reader->load($filePath);
// 4. 解析商品基本信息 Sheet
$goodsSheet = $spreadsheet->getSheet(0);
$goodsList = $this->parseGoodsSheet($goodsSheet);
// 5. 解析 SKU 规格 Sheet
$skuSheet = $spreadsheet->getSheet(1);
$skuList = $this->parseSkuSheet($skuSheet);
// 6. 释放内存
$spreadsheet->disconnectWorksheets();
unset($spreadsheet);
return ['goods' => $goodsList, 'skus' => $skuList];
}
/**
* 解析商品基本信息 Sheet
*/
private function parseGoodsSheet($worksheet)
{
$goodsList = [];
$headers = [];
// 读取表头(第一行)
$headerRow = $worksheet->getRowIterator(1, 1)->current();
$cellIterator = $headerRow->getCellIterator();
$cellIterator->setIterateOnlyExistingCells(false);
foreach ($cellIterator as $cell) {
$headers[] = trim($cell->getFormattedValue());
}
// 读取数据行(从第二行开始)
$rowIterator = $worksheet->getRowIterator(2);
foreach ($rowIterator as $row) {
$rowData = [];
$cellIterator = $row->getCellIterator();
$cellIterator->setIterateOnlyExistingCells(false);
foreach ($cellIterator as $index => $cell) {
if (isset($headers[$index])) {
$rowData[$headers[$index]] = trim($cell->getFormattedValue());
}
}
// 跳过空行
if (empty(array_filter($rowData))) continue;
$goodsList[] = $rowData;
}
return $goodsList;
}
/**
* 解析 SKU 规格 Sheet
*/
private function parseSkuSheet($worksheet)
{
// 与 parseGoodsSheet 逻辑类似,读取 SKU 行数据
// 返回格式:[['商品名称'=>..., '规格1名称'=>..., '规格1值'=>..., ...], ...]
return $this->parseGenericSheet($worksheet);
}
}
3.4 数据验证与清洗
导入前的数据验证是保证数据质量的关键环节;参考 ThinkPHP 批量导入的最佳实践,需要先验证文件格式与大小,再清洗数据并映射字段:
php
class GoodsImportValidator
{
/**
* 验证导入数据
*/
public function validate($goodsList, $skuList)
{
$errors = [];
$goodsNames = [];
// 1. 验证商品基本信息
foreach ($goodsList as $index => $goods) {
$rowNum = $index + 2; // Excel 行号
// 必填字段校验
if (empty($goods['商品名称'])) {
$errors[] = "第{$rowNum}行:商品名称不能为空";
continue;
}
// 商品名称重复校验
if (in_array($goods['商品名称'], $goodsNames)) {
$errors[] = "第{$rowNum}行:商品名称「{$goods['商品名称']}」重复";
}
$goodsNames[] = $goods['商品名称'];
// 分类存在性校验
if (!empty($goods['商品分类'])) {
$catId = CategoryModel::where('name', $goods['商品分类'])
->where('status', 1)
->value('id');
if (!$catId) {
$errors[] = "第{$rowNum}行:分类「{$goods['商品分类']}」不存在或已禁用";
}
}
// 商品类型校验
if (!in_array($goods['商品类型'], ['实物', '虚拟'])) {
$errors[] = "第{$rowNum}行:商品类型只能为「实物」或「虚拟」";
}
}
// 2. 验证 SKU 数据
foreach ($skuList as $index => $sku) {
$rowNum = $index + 2;
if (empty($sku['商品名称'])) {
$errors[] = "SKU第{$rowNum}行:商品名称不能为空";
continue;
}
// 商品名称必须存在于商品列表中
if (!in_array($sku['商品名称'], $goodsNames)) {
$errors[] = "SKU第{$rowNum}行:商品「{$sku['商品名称']}」在商品信息中不存在";
}
// 价格和库存校验
if (!is_numeric($sku['销售价']) || $sku['销售价'] < 0) {
$errors[] = "SKU第{$rowNum}行:销售价格式不正确";
}
if (!is_numeric($sku['库存']) || $sku['库存'] < 0) {
$errors[] = "SKU第{$rowNum}行:库存格式不正确";
}
}
return $errors;
}
}
3.5 分批事务写入
数据验证通过后,进入数据库写入阶段;大批量数据插入需要分批处理,每批 200-400 条,并在事务中执行:
php
class GoodsImportWriter
{
/**
* 执行导入
*/
public function import($goodsList, $skuList)
{
// 按商品名称分组 SKU
$skuGroups = [];
foreach ($skuList as $sku) {
$skuGroups[$sku['商品名称']][] = $sku;
}
$successCount = 0;
$batchSize = 50; // 每批处理 50 个商品
Db::startTrans();
try {
foreach (array_chunk($goodsList, $batchSize) as $goodsBatch) {
foreach ($goodsBatch as $goodsData) {
// 1. 创建商品(SPU)
$goodsId = $this->createGoods($goodsData);
// 2. 创建规格和 SKU
$skus = $skuGroups[$goodsData['商品名称']] ?? [];
$this->createSkus($goodsId, $goodsData['商品名称'], $skus);
$successCount++;
}
}
Db::commit();
} catch (\Exception $e) {
Db::rollback();
throw new Exception('导入失败:' . $e->getMessage());
}
return $successCount;
}
/**
* 创建商品主记录
*/
private function createGoods($data)
{
$catId = CategoryModel::where('name', $data['商品分类'])
->where('status', 1)
->value('id');
return GoodsModel::create([
'name' => $data['商品名称'],
'category_id' => $catId ?: 0,
'goods_type' => $data['商品类型'] == '实物' ? 'real' : 'virtual',
'image' => $data['商品主图'] ?? '',
'carousel' => $data['轮播图'] ?? '',
'unit' => $data['单位'] ?? '件',
'status' => 0, // 默认下架,审核通过后上架
'create_time' => time(),
])->id;
}
/**
* 创建 SKU 记录
*/
private function createSkus($goodsId, $goodsName, $skus)
{
if (empty($skus)) {
// 无规格商品,创建默认 SKU
GoodsItemModel::create([
'goods_id' => $goodsId,
'spec_value_ids'=> '',
'spec_value_str'=> '',
'sell_price' => 0,
'stock' => 0,
'create_time' => time(),
]);
return;
}
// 解析规格分组
$specGroups = $this->parseSpecGroups($skus);
// 生成规格定义
$specService = new SpecService();
$specService->saveSpecs($goodsId, $specGroups);
// 创建 SKU
foreach ($skus as $skuData) {
$specValueIds = $this->buildSpecValueIds($specGroups, $skuData);
$specValueStr = $this->buildSpecValueStr($skuData);
GoodsItemModel::create([
'goods_id' => $goodsId,
'spec_value_ids'=> $specValueIds,
'spec_value_str'=> $specValueStr,
'sell_price' => $skuData['销售价'] ?? 0,
'stock' => $skuData['库存'] ?? 0,
'create_time' => time(),
]);
}
}
/**
* 从 SKU 数据中解析规格分组
*/
private function parseSpecGroups($skus)
{
$groups = [];
foreach ($skus as $sku) {
for ($i = 1; $i <= 5; $i++) { // 支持最多5级规格
$nameKey = "规格{$i}名称";
$valueKey = "规格{$i}值";
if (!empty($sku[$nameKey]) && !empty($sku[$valueKey])) {
$groups[$sku[$nameKey]][$sku[$valueKey]] = true;
}
}
}
// 转换为标准格式
$result = [];
foreach ($groups as $name => $values) {
$result[] = [
'spec_name' => $name,
'spec_values' => array_keys($values),
];
}
return $result;
}
}
3.6 导入结果反馈
导入完成后,向用户反馈导入结果:
php
public function getImportResult($successCount, $errors = [])
{ successCount, 'error_count' => count(errors, ];
if ($successCount > 0 && empty($errors)) {
$result['message'] = "成功导入 {$successCount} 个商品";
} elseif ($successCount > 0 && !empty($errors)) {
$result['message'] = "部分导入成功:{$successCount} 个商品成功,"
. count($errors) . " 条数据存在错误";
} else {
$result['message'] = '导入失败,请检查数据格式';
}
return $result;
}
3.7 后台配置
在“商品管理 → 商品列表”页面增加“批量导入”入口:
| 功能 | 说明 |
|---|---|
| 下载导入模板 | 下载包含商品信息和 SKU 规格两个 Sheet 的 Excel 模板 |
| 上传导入文件 | 支持 .xlsx 和 .xls 格式,文件大小限制 10MB |
| 导入预览 | 上传后先预览解析结果和验证错误,确认后再执行导入 |
| 导入日志 | 记录每次导入的时间、操作人、成功/失败数量 |
4.1 库存批量更新模版
除了商品全量导入,商家还需要对已有商品的 SKU 库存进行批量更新;设计专用的库存更新模版:
| 商品ID | 商品名称 | SKU ID | 规格值 | 库存 | 成本价 |
|---|---|---|---|---|---|
| 15 | iPhone 15 | 1 | 黑色,256G,国行 | 200 | 6500 |
| 15 | iPhone 15 | 2 | 白色,256G,国行 | 150 | 6500 |
通过“SKU ID”精确定位,避免因规格值字符串匹配的歧义。
4.2 库存更新逻辑
php
class StockUpdateService
{
/**
* 批量更新 SKU 库存
*/
public function batchUpdateStock($stockList)
{
$updated = 0;
$errors = [];
Db::startTrans();
try {
foreach ($stockList as $index => $item) {
$skuId = (int)$item['SKU ID'];
$stock = (int)$item['库存'];
if ($skuId <= 0) {
$errors[] = "第" . ($index + 2) . "行:SKU ID 无效";
continue;
}
if ($stock < 0) {
$errors[] = "第" . ($index + 2) . "行:库存不能为负数";
continue;
}
// 校验 SKU 是否存在
$sku = GoodsItemModel::find($skuId);
if (!$sku) {
$errors[] = "第" . ($index + 2) . "行:SKU ID {$skuId} 不存在";
continue;
}
// 更新库存
GoodsItemModel::where('id', $skuId)->update([
'stock' => $stock,
'update_time' => time(),
]);
// 记录库存变动日志
StockLogModel::create([
'goods_id' => $sku['goods_id'],
'item_id' => $skuId,
'change_num' => $stock - $sku['stock'],
'before_num' => $sku['stock'],
'after_num' => $stock,
'remark' => 'Excel批量导入更新',
'create_time'=> time(),
]);
$updated++;
}
Db::commit();
} catch (\Exception $e) {
Db::rollback();
throw $e;
}
return ['updated' => $updated, 'errors' => $errors];
}
}
4.3 库存预警联动
批量更新库存后,系统自动检查是否触发库存预警;当 stock 低于预警阈值时,写入预警记录:
php
private function checkStockWarning($skuId, $goodsId, $stock)
{
$warningThreshold = ConfigModel::get('stock_warning_threshold', 10);
if ($stock < $warningThreshold && $stock >= 0) {
// 避免重复预警
$exists = StockWarningModel::where('goods_id', $goodsId)
->where('item_id', $skuId)
->where('status', 0) // 未处理
->find();
if (!$exists) {
StockWarningModel::create([
'goods_id' => $goodsId,
'item_id' => $skuId,
'stock' => $stock,
'threshold' => $warningThreshold,
'status' => 0,
'create_time' => time(),
]);
MessageService::sendToShop($goodsId, '库存预警',
"商品 SKU 库存仅剩 {$stock} 件,请及时补货");
}
}
}
5.1 数据库变更清单
| 操作 | 表名 | 说明 |
|---|---|---|
| CREATE | ls_category_spec_attr | 分类规格属性模板表 |
| ALTER | ls_goods_spec | 新增 spec_type、sort、is_required 字段 |
| CREATE | ls_stock_log | 库存变动日志表 |
| CREATE | ls_stock_warning | 库存预警记录表 |
5.2 后端开发清单
| 模块 | 文件/类 | 工作内容 |
|---|---|---|
| 数据层 | CategorySpecAttrModel | 分类规格属性 Model |
| 数据层 | GoodsSpecModel | 规格表 Model,新增字段映射 |
| 数据层 | StockLogModel / StockWarningModel | 库存日志和预警 Model |
| 业务层 | SkuGeneratorService | 多规格笛卡尔积计算引擎 |
| 业务层 | SkuMatcherService | SKU 匹配服务,支持任意数量规格 |
| 业务层 | GoodsImportService | Excel 解析和导入主流程 |
| 业务层 | GoodsImportValidator | 导入数据验证和清洗 |
| 业务层 | GoodsImportWriter | 分批事务写入 |
| 业务层 | StockUpdateService | 库存批量更新和预警检查 |
| 后台接口 | GoodsImportController | 模板下载、文件上传、导入执行接口 |
| 后台接口 | StockController | 库存批量更新接口 |
5.3 后台前端开发清单
| 页面 | 工作内容 |
|---|---|
| 分类管理 → 规格属性 | 分类规格属性模板的增删改查,预设规格值配置 |
| 商品发布 → 规格设置 | 选择分类后自动带出规格属性模板,支持添加自定义规格 |
| 商品列表 → 批量导入 | 下载模板、上传文件、导入预览、执行导入 |
| 商品列表 → 批量库存更新 | 下载库存模板、上传文件、更新预览、执行更新 |
5.4 开发注意事项
Excel 读取的内存控制:使用 PhpSpreadsheet 时,必须设置 setReadDataOnly(true) 跳过公式和样式解析,内存占用直降 60%以上;对于超过 1000 行的数据,建议使用行迭代器逐行读取,而非一次性加载全部数据到内存。
SKU 唯一性约束:ls_goods_item 表的 goods_id + spec_value_ids 组合应建立唯一索引,防止批量导入时产生重复的 SKU 记录;导入前先查询已存在的 SKU,已存在则执行更新而非插入。
事务边界的控制:批量导入时,每批商品的处理在独立事务中执行;如果某批数据出现问题,只需回滚该批次,不影响已成功导入的数据;批次大小建议设为 50 个商品,避免事务过大导致锁表时间过长。
规格数量的上限:虽然笛卡尔积计算引擎支持任意数量的规格,但从用户体验和性能角度考虑,建议限制为最多 5 级规格;超过 5 级规格时,SKU 数量会呈指数级增长,给前端渲染和库存管理带来巨大压力。
兼容性:ls_goods_spec 表新增字段全部设置默认值,保证存量商品数据不受影响;spec_type 默认为1(普通规则),存量规格记录的该字段值自动为 1;批量导入功能作为独立的后台模块开发,不修改原生的商品发布和编辑逻辑。
