{{wikiTitle}}
商品流程相关说明
CRMEB PRO 商品流程相关说明
概述
本文档详细说明 CRMEB PRO 系统中商品管理的完整流程,包括商品发布、编辑、上下架、库存管理等业务流程。
商品状态说明
上架状态 (is_show)
| 状态值 | 说明 |
|---|---|
| 0 | 下架(仓库中) |
| 1 | 上架(销售中) |
删除状态 (is_del)
| 状态值 | 说明 |
|---|---|
| 0 | 正常 |
| 1 | 已删除(回收站) |
审核状态 (is_verify)
| 状态值 | 说明 |
|---|---|
| 0 | 待审核 |
| 1 | 审核通过 |
| 2 | 审核未通过 |
规格类型 (spec_type)
| 状态值 | 说明 |
|---|---|
| 0 | 单规格 |
| 1 | 多规格 |
目录结构
app/
├── controller/admin/v1/product/ # 后台商品控制器
│ └── StoreProduct.php
├── services/product/ # 商品服务层
│ ├── product/ # 商品核心服务
│ │ ├── StoreProductServices.php
│ │ └── StoreProductAttrServices.php
│ ├── category/ # 分类服务
│ ├── brand/ # 品牌服务
│ ├── label/ # 标签服务
│ ├── shipping/ # 运费模板服务
│ ├── sku/ # SKU服务
│ └── stock/ # 库存服务
├── dao/product/ # 商品数据访问层
└── model/product/ # 商品模型
商品发布流程
1. 获取商品表单数据
接口: GET /adminapi/product/product/create
// app/controller/admin/v1/product/StoreProduct.php
public function create()
{
// 获取创建商品所需的表单数据
return app('json')->success([
'cateList' => $this->getCateList(), // 分类列表
'tempList' => $this->getTempList(), // 运费模板
'labelList' => $this->getLabelList(), // 标签列表
'brandList' => $this->getBrandList(), // 品牌列表
'ensureList' => $this->getEnsureList(), // 保障服务
'specsColumn' => $this->getSpecsColumn(), // 规格模板
]);
}
2. 保存商品
接口: POST /adminapi/product/product
请求参数:
{
"store_name": "商品名称",
"store_info": "商品简介",
"cate_id": [1, 2],
"image": "主图URL",
"slider_image": ["轮播图1", "轮播图2"],
"price": "99.00",
"ot_price": "199.00",
"cost": "50.00",
"stock": 100,
"unit_name": "件",
"spec_type": 1,
"description": "商品详情HTML",
"temp_id": 1,
"is_show": 1,
"is_hot": 0,
"is_new": 1,
"items": [...],
"attrs": [...]
}
服务层处理:
// app/services/product/product/StoreProductServices.php
public function save(array $data)
{
return $this->transaction(function () use ($data) {
// 1. 处理商品主表数据
$productData = $this->formatProductData($data);
// 2. 保存商品
$product = $this->dao->save($productData);
// 3. 保存商品分类关联
$this->saveCateRelation($product['id'], $data['cate_id']);
// 4. 保存商品属性(规格)
if ($data['spec_type'] == 1) {
$this->saveProductAttr($product['id'], $data['items'], $data['attrs']);
} else {
$this->saveProductSingleAttr($product['id'], $data);
}
// 5. 保存商品描述
$this->saveDescription($product['id'], $data['description']);
// 6. 保存商品标签关联
if (!empty($data['label_id'])) {
$this->saveLabelRelation($product['id'], $data['label_id']);
}
// 7. 触发商品创建事件
event('product.create', [$product]);
return $product;
});
}
3. 保存商品属性(多规格)
// app/services/product/sku/StoreProductAttrServices.php
public function saveProductAttr(int $productId, array $items, array $attrs, int $type = 0)
{
// 1. 删除旧的属性数据
$this->deleteProductAttr($productId, $type);
// 2. 保存属性名称(如:颜色、尺码)
foreach ($items as $item) {
$this->attrDao->save([
'product_id' => $productId,
'attr_name' => $item['value'],
'attr_values' => implode(',', $item['detail']),
'type' => $type,
]);
}
// 3. 保存属性值(SKU)
foreach ($attrs as $attr) {
$attrValueData = [
'product_id' => $productId,
'suk' => $attr['detail']['value1'] . ',' . ($attr['detail']['value2'] ?? ''),
'stock' => $attr['stock'],
'sales' => $attr['sales'] ?? 0,
'price' => $attr['price'],
'image' => $attr['pic'],
'cost' => $attr['cost'],
'ot_price' => $attr['ot_price'],
'bar_code' => $attr['bar_code'] ?? '',
'weight' => $attr['weight'] ?? 0,
'volume' => $attr['volume'] ?? 0,
'brokerage' => $attr['brokerage'] ?? 0,
'brokerage_two' => $attr['brokerage_two'] ?? 0,
'type' => $type,
'unique' => $this->makeUnique($productId, $attr['detail']),
];
$this->attrValueDao->save($attrValueData);
}
// 4. 更新商品总库存
$this->updateProductStock($productId);
}
商品编辑流程
1. 获取商品详情
接口: GET /adminapi/product/product/{id}
// app/services/product/product/StoreProductServices.php
public function getInfo(int $id)
{
// 1. 获取商品基本信息
$product = $this->dao->get($id);
// 2. 获取商品属性
$product['items'] = $this->getProductAttr($id);
$product['attrs'] = $this->getProductAttrValue($id);
// 3. 获取关联分类
$product['cate_id'] = $this->getCateIds($id);
// 4. 获取关联标签
$product['label_id'] = $this->getLabelIds($id);
// 5. 获取商品描述
$product['description'] = $this->getDescription($id);
return $product;
}
2. 更新商品
接口: PUT /adminapi/product/product/{id}
// app/services/product/product/StoreProductServices.php
public function update(int $id, array $data)
{
return $this->transaction(function () use ($id, $data) {
// 1. 更新商品主表
$productData = $this->formatProductData($data);
$this->dao->update($id, $productData);
// 2. 更新分类关联
$this->updateCateRelation($id, $data['cate_id']);
// 3. 更新商品属性
if ($data['spec_type'] == 1) {
$this->saveProductAttr($id, $data['items'], $data['attrs']);
} else {
$this->saveProductSingleAttr($id, $data);
}
// 4. 更新商品描述
$this->saveDescription($id, $data['description']);
// 5. 更新标签关联
$this->updateLabelRelation($id, $data['label_id'] ?? []);
// 6. 清除商品缓存
$this->clearProductCache($id);
return true;
});
}
商品上下架
上架商品
接口: PUT /adminapi/product/product/set_show/{id}/1
// app/services/product/product/StoreProductServices.php
public function setShow(int $id, int $isShow)
{
// 1. 验证商品状态
$product = $this->dao->get($id);
if ($isShow == 1) {
// 上架验证
if ($product['stock'] <= 0) {
throw new AdminException('商品库存不足,无法上架');
}
if (empty($product['image'])) {
throw new AdminException('请先上传商品主图');
}
}
// 2. 更新状态
$this->dao->update($id, ['is_show' => $isShow]);
// 3. 触发事件
event('product.status', [$product, $isShow]);
// 4. 清除缓存
$this->clearProductCache($id);
return true;
}
批量上下架
接口: PUT /adminapi/product/product/set_show
public function batchSetShow(array $ids, int $isShow)
{
foreach ($ids as $id) {
$this->setShow($id, $isShow);
}
return true;
}
商品删除与恢复
移入回收站
接口: DELETE /adminapi/product/product/{id}
public function delete(int $id)
{
// 软删除(移入回收站)
$this->dao->update($id, ['is_del' => 1]);
// 同时下架商品
$this->dao->update($id, ['is_show' => 0]);
// 触发删除事件
event('product.delete', [$id]);
return true;
}
从回收站恢复
接口: PUT /adminapi/product/product/restore/{id}
public function restore(int $id)
{
$this->dao->update($id, ['is_del' => 0]);
return true;
}
彻底删除
接口: DELETE /adminapi/product/product/destroy/{id}
public function destroy(int $id)
{
return $this->transaction(function () use ($id) {
// 1. 删除商品主表
$this->dao->delete($id);
// 2. 删除商品属性
$this->attrDao->delete(['product_id' => $id]);
$this->attrValueDao->delete(['product_id' => $id]);
// 3. 删除分类关联
$this->cateRelationDao->delete(['product_id' => $id]);
// 4. 删除商品描述
$this->descriptionDao->delete(['product_id' => $id]);
// 5. 删除标签关联
$this->labelRelationDao->delete(['product_id' => $id]);
return true;
});
}
库存管理
库存服务目录
app/services/product/stock/
├── StoreProductStockServices.php # 库存服务
├── StoreProductStockRecordServices.php # 库存记录服务
└── StoreProductStockJobServices.php # 库存任务服务
修改库存
// app/services/product/sku/StoreProductAttrValueServices.php
public function updateStock(int $productId, string $unique, int $num, string $type = 'dec')
{
return $this->transaction(function () use ($productId, $unique, $num, $type) {
// 1. 更新SKU库存
if ($type == 'dec') {
$this->dao->decStock($productId, $unique, $num);
} else {
$this->dao->incStock($productId, $unique, $num);
}
// 2. 更新商品总库存
$this->updateProductTotalStock($productId);
// 3. 记录库存变动
$this->recordStockChange($productId, $unique, $num, $type);
return true;
});
}
库存变动记录
// app/services/product/stock/StoreProductStockRecordServices.php
public function recordStockChange(int $productId, string $unique, int $num, string $type, string $remark = '')
{
$data = [
'product_id' => $productId,
'unique' => $unique,
'type' => $type,
'number' => $num,
'balance' => $this->getStockBalance($productId, $unique),
'add_time' => time(),
'remark' => $remark,
];
$this->dao->save($data);
// 触发库存变动事件
event('product.stock.record', [$data]);
}
出入库管理
// 入库操作
public function inStock(int $productId, string $unique, int $num, string $remark = '')
{
$this->updateStock($productId, $unique, $num, 'inc');
$this->recordStockChange($productId, $unique, $num, 'in', $remark);
event('product.stock.create', [$productId, 'in', $num]);
}
// 出库操作
public function outStock(int $productId, string $unique, int $num, string $remark = '')
{
$this->updateStock($productId, $unique, $num, 'dec');
$this->recordStockChange($productId, $unique, $num, 'out', $remark);
event('product.stock.create', [$productId, 'out', $num]);
}
商品分类管理
分类服务
// app/services/product/category/StoreProductCategoryServices.php
class StoreProductCategoryServices extends BaseServices
{
// 获取分类树形结构
public function getCateTree(int $pid = 0)
{
$list = $this->dao->getList(['pid' => $pid, 'is_show' => 1]);
foreach ($list as &$item) {
$item['children'] = $this->getCateTree($item['id']);
}
return $list;
}
// 保存分类
public function save(array $data)
{
// 验证父级分类
if ($data['pid'] > 0) {
$parent = $this->dao->get($data['pid']);
if (!$parent) {
throw new AdminException('父级分类不存在');
}
}
return $this->dao->save($data);
}
}
商品标签管理
标签服务
// app/services/product/label/StoreProductLabelServices.php
class StoreProductLabelServices extends BaseServices
{
// 获取标签列表
public function getLabelList(int $cateId = 0)
{
return $this->dao->getList([
'label_cate' => $cateId,
'status' => 1,
]);
}
// 商品关联标签
public function saveProductLabel(int $productId, array $labelIds)
{
// 清除旧关联
$this->relationDao->delete(['product_id' => $productId]);
// 添加新关联
foreach ($labelIds as $labelId) {
$this->relationDao->save([
'product_id' => $productId,
'label_id' => $labelId,
]);
}
}
}
商品品牌管理
品牌服务
// app/services/product/brand/StoreBrandServices.php
class StoreBrandServices extends BaseServices
{
// 获取品牌列表
public function getBrandList(int $cateId = 0)
{
return $this->dao->getList([
'brand_category_id' => $cateId,
'is_show' => 1,
], 'id,brand_name');
}
}
运费模板管理
运费计算服务
// app/services/product/shipping/ShippingTemplatesServices.php
class ShippingTemplatesServices extends BaseServices
{
// 计算运费
public function computedPostage(int $tempId, int $cityId, int $num, float $weight, float $volume)
{
// 1. 获取运费模板
$template = $this->dao->get($tempId);
// 2. 判断是否包邮
if ($this->checkFreeShipping($template, $cityId, $num, $weight, $volume)) {
return 0;
}
// 3. 获取地区运费规则
$rule = $this->getRegionRule($tempId, $cityId);
// 4. 计算运费
switch ($template['type']) {
case 1: // 按件数
return $this->computedByNum($rule, $num);
case 2: // 按重量
return $this->computedByWeight($rule, $weight);
case 3: // 按体积
return $this->computedByVolume($rule, $volume);
}
return 0;
}
}
商品相关事件
| 事件名 | 说明 | 触发时机 |
|---|---|---|
product.create |
商品创建 | 保存商品后 |
product.delete |
商品删除 | 删除商品后 |
product.status |
商品状态变更 | 上下架后 |
product.reply |
商品评价 | 提交评价后 |
product.collect |
商品收藏 | 收藏商品后 |
product.stock.create |
库存变动 | 出入库后 |
product.stock.record |
库存记录 | 记录变动后 |
商品相关数据表
| 表名 | 说明 |
|---|---|
eb_store_product |
商品主表 |
eb_store_product_attr |
商品属性表 |
eb_store_product_attr_value |
商品属性值表(SKU) |
eb_store_product_cate |
商品分类关联表 |
eb_store_product_description |
商品描述表 |
eb_store_product_relation |
商品关系表(收藏/浏览) |
eb_store_product_reply |
商品评价表 |
eb_store_category |
商品分类表 |
eb_store_product_label |
商品标签表 |
eb_store_brand |
商品品牌表 |
eb_shipping_templates |
运费模板表 |
eb_shipping_templates_region |
运费模板地区表 |
商品列表查询示例
// 前台商品列表
public function getProductList(array $where, int $page, int $limit)
{
return $this->dao->getList([
'is_show' => 1,
'is_del' => 0,
'keyword' => $where['keyword'] ?? '',
'cate_id' => $where['cate_id'] ?? 0,
'price_min' => $where['price_min'] ?? 0,
'price_max' => $where['price_max'] ?? 0,
'brand_id' => $where['brand_id'] ?? 0,
], $page, $limit, [
'id', 'store_name', 'image', 'price', 'ot_price', 'sales', 'stock'
], 'sort desc, id desc');
}
最佳实践
- 商品数据验证: 保存前进行完整的数据验证
- 事务处理: 涉及多表操作时使用事务
- 缓存清理: 修改商品后及时清理相关缓存
- 事件触发: 关键操作后触发相应事件
- 库存安全: 使用数据库锁防止超卖
注意事项
- 商品状态联动:修改商品
is_show状态时,需同步处理参与的活动商品状态(秒杀、拼团、砍价等) - 多规格库存:多规格商品的总库存应等于各 SKU 库存之和,更新单个 SKU 库存后需同步更新商品总库存
- 图片存储:商品图片建议使用独立存储服务(如 OSS),避免占用服务器磁盘空间
- 软删除机制:商品使用软删除(
is_del=1),已产生订单的商品不要物理删除 - 价格精度:价格字段使用 DECIMAL 类型,计算时注意浮点数精度问题
- 分类层级:分类支持多级嵌套,前端展示时注意递归获取子分类
- 运费模板:商品必须关联运费模板,否则无法正确计算运费
- 商品审核:开启审核功能后,新增或编辑的商品需要等待审核通过才能上架
- SEO 优化:商品关键词和描述会影响搜索引擎收录,建议填写完整
常见问题
Q: 商品保存失败提示”规格数据错误”?
A: 检查 SKU 数据格式是否正确:1) 规格属性和属性值不能为空;2) SKU 的unique字段必须唯一;3) 价格和库存必须为有效数值Q: 修改商品后前台显示未更新?
A: 商品数据有缓存,修改后需要清理缓存。检查ProductServices中的cacheTag是否正确清理Q: 如何批量修改商品价格或库存?
A: 使用后台的批量管理功能,或通过ProductServices::batchUpdate()方法批量更新Q: 商品下架后订单中的商品信息会受影响吗?
A: 不会。订单创建时会快照商品信息到订单详情表,商品后续变更不影响已创建的订单Q: 多规格商品如何设置不同规格的不同图片?
A: 在eb_store_product_attr_value表的image字段存储每个 SKU 的专属图片Q: 商品排序规则是什么?
A: 默认按sort字段降序排列,sort相同时按id降序。可在后台设置商品的排序值
