This commit is contained in:
zjf committed 2025-08-25 20:38:20 +08:00
1 parent fa2734ca89
commit 089d10a72d
3 files changed
+459 -2

No files matched your search

@@ -0,0 +1,229 @@
# 商品SPU口味功能说明
## 概述
本功能为商品SPU添加了口味集合管理能力,支持在新增和编辑商品时管理多种口味,每种口味可以独立设置库存数量。
## 功能特性
### 1. 新增商品SPU时的口味处理
- 支持同时添加多个口味
- 每个口味自动关联到对应的SPU
- 自动设置默认库存为0(如果未指定)
- 口味数据与商品SPU在同一事务中保存
### 2. 编辑商品SPU时的口味处理
- **新增口味**:ID为null的口味会被识别为新增
- **更新口味**:ID不为null的口味会被识别为更新
- **删除口味**:提交数据中不包含的口味会被自动删除
- 支持混合操作(同时新增、更新、删除)
### 3. 数据完整性
- 自动维护口味与SPU的关联关系
- 支持级联删除(删除SPU时自动删除相关口味)
- 事务保证数据一致性
## 数据结构
### SmErpTasteStockEntity(口味库存实体)
```java
public class SmErpTasteStockEntity {
private Long id; // 主键ID
private Long spuId; // 关联的SPU ID
private String tasteName; // 口味名称
private Double stock; // 该口味的库存数量
private LocalDateTime createTime; // 创建时间
private LocalDateTime updateTime; // 更新时间
}
```
### SmErpSpuEntity(商品SPU实体)
```java
public class SmErpSpuEntity {
// ... 其他字段 ...
@Schema(description="口味集合")
@TableField(exist = false)
private List<SmErpTasteStockEntity> smErpTasteStockEntityList;
}
```
## 使用方法
### 1. 新增商品SPU(包含口味)
```java
@PostMapping
public R save(@RequestBody SmErpSpuEntity supermarketSpu) {
supermarketSpu.setDeptId(SecurityUtils.getUser().getDeptId());
return smErpSpuService.saveSpu(supermarketSpu);
}
```
**请求示例:**
```json
{
"productCode": "SPU001",
"productName": "果汁饮料",
"spec": "500ml",
"status": "0",
"stock": 100.0,
"smErpTasteStockEntityList": [
{
"tasteName": "原味",
"stock": 50.0
},
{
"tasteName": "草莓味",
"stock": 30.0
},
{
"tasteName": "橙子味",
"stock": 20.0
}
],
"smErpUnitEntityList": [
{
"unit": "瓶",
"packageNum": 1,
"isBase": true
}
]
}
```
### 2. 编辑商品SPU(修改口味)
```java
@PutMapping
public R updateById(@RequestBody SmErpSpuEntity supermarketSpu) {
return smErpSpuService.updateSpuById(supermarketSpu);
}
```
**请求示例(更新现有口味,新增口味,删除口味):**
```json
{
"id": 1,
"productCode": "SPU001",
"productName": "果汁饮料",
"spec": "500ml",
"status": "0",
"stock": 120.0,
"smErpTasteStockEntityList": [
{
"id": 1, // 有ID表示更新
"tasteName": "原味",
"stock": 60.0 // 更新库存
},
{
"id": 2, // 有ID表示更新
"tasteName": "草莓味",
"stock": 40.0 // 更新库存
},
{
"tasteName": "葡萄味", // 无ID表示新增
"stock": 20.0
}
// 不包含巧克力味,会被自动删除
],
"smErpUnitEntityList": [
{
"id": 1,
"unit": "瓶",
"packageNum": 1,
"isBase": true
}
]
}
```
## 业务逻辑
### 新增逻辑
1. 验证商品基本信息
2. 保存商品SPU主表
3. 创建库存记录
4. **处理口味集合**:
- 遍历口味列表
- 设置SPU ID关联
- 设置默认库存(如果未指定)
- 批量保存口味数据
5. 处理包装单位
6. 处理辅助条码
### 编辑逻辑
1. 更新商品SPU主表
2. 更新库存记录
3. 处理包装单位(新增/更新/删除)
4. 处理辅助条码(新增/更新/删除)
5. **处理口味集合**:
- 分离新增和更新的口味
- 新增口味:设置SPU ID,设置默认库存
- 更新口味:保持ID,更新数据
- 删除口味:计算需要删除的口味ID,批量删除
## 注意事项
### 1. 数据一致性
- 所有操作都在事务中执行
- 删除操作会级联删除相关数据
- 口味ID必须与SPU ID匹配
### 2. 性能考虑
- 使用批量操作提高性能
- 避免N+1查询问题
- 合理设置批量大小
### 3. 业务规则
- 口味名称在同一SPU下必须唯一
- 库存数量不能为负数
- 删除SPU时自动清理相关口味
### 4. 错误处理
- 提供详细的错误信息
- 支持事务回滚
- 记录操作日志
## 测试用例
项目包含完整的测试用例,覆盖以下场景:
- 新增商品SPU(包含口味)
- 编辑商品SPU(更新口味)
- 编辑商品SPU(删除口味)
- 混合操作(新增+更新+删除)
运行测试:
```bash
mvn test -Dtest=SmErpSpuServiceTest
```
## 扩展功能
### 1. 口味库存管理
- 支持口味级别的库存调整
- 库存预警功能
- 库存盘点功能
### 2. 口味统计分析
- 口味销售统计
- 口味库存分析
- 口味偏好分析
### 3. 口味权限控制
- 基于角色的口味管理权限
- 口味数据访问控制
- 操作审计日志
## 相关文件
- `SmErpSpuServiceImpl.java` - 服务实现类
- `SmErpTasteStockEntity.java` - 口味实体类
- `SmErpSpuEntity.java` - 商品SPU实体类
- `SmErpSpuServiceTest.java` - 测试类
- `README_TASTE_FEATURE.md` - 本文档
## 技术支持
如有问题或建议,请联系开发团队或提交Issue。
@@ -22,7 +22,7 @@
<result property="delFlag" column="del_flag"/>
<result property="tenantId" column="tenant_id"/>
<result property="isBase" column="is_base"/>
<result property="taste" column="taste"/>
<result property="tasteName" column="taste_name"/>
<result property="tasteId" column="taste_id"/>
</resultMap>
@@ -86,7 +86,7 @@
u.spu_id,
u.cost_price,
u.is_base,
u.taste
u.taste_name
FROM sm_erp_unit u
LEFT JOIN sm_erp_spu s
ON u.spu_id = s.id
@@ -0,0 +1,228 @@
package com.bsd.pjcloud.supermarket.service;
import com.bsd.pjcloud.supermarket.entity.SmErpSpuEntity;
import com.bsd.pjcloud.supermarket.entity.SmErpTasteStockEntity;
import com.bsd.pjcloud.supermarket.entity.SmErpUnitEntity;
import com.bsd.pjcloud.supermarket.entity.SmErpAuxiliaryCodeEntity;
import com.bsd.pjcloud.common.core.util.R;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
/**
* 商品SPU服务测试类
* 测试口味集合的新增、更新和删除功能
*
* @author pjcloud
*/
@SpringBootTest
@Transactional
public class SmErpSpuServiceTest {
@Autowired
private SmErpSpuService smErpSpuService;
@Test
public void testSaveSpuWithTastes() {
// 创建测试商品SPU
SmErpSpuEntity spu = new SmErpSpuEntity();
spu.setProductCode("TEST001");
spu.setProductName("测试商品");
spu.setSpec("500ml");
spu.setStatus("0");
spu.setStock(100.0);
spu.setInventoryLowerLimit(10);
spu.setInventoryUpperLimit(1000);
// 创建口味集合
List<SmErpTasteStockEntity> tastes = new ArrayList<>();
SmErpTasteStockEntity taste1 = new SmErpTasteStockEntity();
taste1.setTasteName("原味");
taste1.setStock(50.0);
tastes.add(taste1);
SmErpTasteStockEntity taste2 = new SmErpTasteStockEntity();
taste2.setTasteName("草莓味");
taste2.setStock(30.0);
tastes.add(taste2);
spu.setSmErpTasteStockEntityList(tastes);
// 创建包装单位
List<SmErpUnitEntity> units = new ArrayList<>();
SmErpUnitEntity unit = new SmErpUnitEntity();
unit.setUnit("瓶");
unit.setPackageNum(1);
unit.setIsBase(true);
units.add(unit);
spu.setSmErpUnitEntityList(units);
// 执行新增
R result = smErpSpuService.saveSpu(spu);
// 验证结果
assertTrue(result.isOk(), "新增商品SPU应该成功");
assertNotNull(spu.getId(), "商品SPU应该有ID");
// 验证口味是否保存成功
assertNotNull(tastes.get(0).getId(), "第一个口味应该有ID");
assertNotNull(tastes.get(1).getId(), "第二个口味应该有ID");
assertEquals(spu.getId(), tastes.get(0).getSpuId(), "口味应该关联到正确的SPU");
assertEquals(spu.getId(), tastes.get(1).getSpuId(), "口味应该关联到正确的SPU");
}
@Test
public void testUpdateSpuWithTastes() {
// 先创建一个商品SPU
SmErpSpuEntity spu = new SmErpSpuEntity();
spu.setProductCode("TEST002");
spu.setProductName("测试商品2");
spu.setSpec("1L");
spu.setStatus("0");
spu.setStock(200.0);
spu.setInventoryLowerLimit(20);
spu.setInventoryUpperLimit(2000);
// 创建初始口味
List<SmErpTasteStockEntity> initialTastes = new ArrayList<>();
SmErpTasteStockEntity initialTaste = new SmErpTasteStockEntity();
initialTaste.setTasteName("原味");
initialTaste.setStock(100.0);
initialTastes.add(initialTaste);
spu.setSmErpTasteStockEntityList(initialTastes);
// 创建包装单位
List<SmErpUnitEntity> units = new ArrayList<>();
SmErpUnitEntity unit = new SmErpUnitEntity();
unit.setUnit("瓶");
unit.setPackageNum(1);
unit.setIsBase(true);
units.add(unit);
spu.setSmErpUnitEntityList(units);
// 新增商品
R saveResult = smErpSpuService.saveSpu(spu);
assertTrue(saveResult.isOk(), "新增商品应该成功");
// 更新商品,修改口味
List<SmErpTasteStockEntity> updatedTastes = new ArrayList<>();
// 更新原口味
SmErpTasteStockEntity updatedTaste1 = new SmErpTasteStockEntity();
updatedTaste1.setId(initialTaste.getId()); // 设置ID表示更新
updatedTaste1.setTasteName("原味");
updatedTaste1.setStock(150.0); // 更新库存
updatedTastes.add(updatedTaste1);
// 新增口味
SmErpTasteStockEntity newTaste = new SmErpTasteStockEntity();
newTaste.setTasteName("巧克力味");
newTaste.setStock(80.0);
updatedTastes.add(newTaste);
spu.setSmErpTasteStockEntityList(updatedTastes);
// 执行更新
R updateResult = smErpSpuService.updateSpuById(spu);
// 验证结果
assertTrue(updateResult.isOk(), "更新商品SPU应该成功");
// 验证口味更新
assertNotNull(updatedTaste1.getId(), "更新的口味应该保持ID");
assertNotNull(newTaste.getId(), "新增的口味应该有ID");
assertEquals(spu.getId(), updatedTaste1.getSpuId(), "更新的口味应该关联到正确的SPU");
assertEquals(spu.getId(), newTaste.getSpuId(), "新增的口味应该关联到正确的SPU");
}
@Test
public void testUpdateSpuRemoveTastes() {
// 先创建一个商品SPU,包含多个口味
SmErpSpuEntity spu = new SmErpSpuEntity();
spu.setProductCode("TEST003");
spu.setProductName("测试商品3");
spu.setSpec("250ml");
spu.setStatus("0");
spu.setStock(150.0);
spu.setInventoryLowerLimit(15);
spu.setInventoryUpperLimit(1500);
// 创建多个口味
List<SmErpTasteStockEntity> initialTastes = new ArrayList<>();
SmErpTasteStockEntity taste1 = new SmErpTasteStockEntity();
taste1.setTasteName("原味");
taste1.setStock(50.0);
initialTastes.add(taste1);
SmErpTasteStockEntity taste2 = new SmErpTasteStockEntity();
taste2.setTasteName("草莓味");
taste2.setStock(50.0);
initialTastes.add(taste2);
SmErpTasteStockEntity taste3 = new SmErpTasteStockEntity();
taste3.setTasteName("巧克力味");
taste3.setStock(50.0);
initialTastes.add(taste3);
spu.setSmErpTasteStockEntityList(initialTastes);
// 创建包装单位
List<SmErpUnitEntity> units = new ArrayList<>();
SmErpUnitEntity unit = new SmErpUnitEntity();
unit.setUnit("瓶");
unit.setPackageNum(1);
unit.setIsBase(true);
units.add(unit);
spu.setSmErpUnitEntityList(units);
// 新增商品
R saveResult = smErpSpuService.saveSpu(spu);
assertTrue(saveResult.isOk(), "新增商品应该成功");
// 更新商品,只保留部分口味(删除巧克力味)
List<SmErpTasteStockEntity> updatedTastes = new ArrayList<>();
// 保留原味
SmErpTasteStockEntity updatedTaste1 = new SmErpTasteStockEntity();
updatedTaste1.setId(taste1.getId());
updatedTaste1.setTasteName("原味");
updatedTaste1.setStock(60.0);
updatedTastes.add(updatedTaste1);
// 保留草莓味
SmErpTasteStockEntity updatedTaste2 = new SmErpTasteStockEntity();
updatedTaste2.setId(taste2.getId());
updatedTaste2.setTasteName("草莓味");
updatedTaste2.setStock(40.0);
updatedTastes.add(updatedTaste2);
// 不包含巧克力味,应该被删除
spu.setSmErpTasteStockEntityList(updatedTastes);
// 执行更新
R updateResult = smErpSpuService.updateSpuById(spu);
// 验证结果
assertTrue(updateResult.isOk(), "更新商品SPU应该成功");
// 验证口味数量
assertEquals(2, updatedTastes.size(), "更新后应该只有2个口味");
// 验证保留的口味
assertTrue(updatedTastes.stream().anyMatch(t -> "原味".equals(t.getTasteName())), "应该保留原味");
assertTrue(updatedTastes.stream().anyMatch(t -> "草莓味".equals(t.getTasteName())), "应该保留草莓味");
// 验证删除的口味
assertFalse(updatedTastes.stream().anyMatch(t -> "巧克力味".equals(t.getTasteName())), "不应该包含巧克力味");
}
}