业务背景
SKU属性、SKU规格、SKU
一个权益产品(EquityProduct)下可能有多个权益商品(EquityGoods),而每个权益商品又可以有多个SKU(Stock Keeping Unit,库存单位)。每个SKU由多个属性组成,如配置字段、车辆信息等,每个属性其下是具体的规格。----》一定要理解SKU、SKU属性(skuSpecsName)和SKU规格(skuSpecsField)之间的关系。
例如宝马是一个权益商品,宝马25号是权益商品下具体的一个商品,它的SKU 属性是由商品类型决定的(例如商品为手机,涉及到的参数就是容量,颜色,而商品为车,涉及到的参数就是重量,尺寸),也就是说车作为一个抽象出来的最小库存单位,它的SKU属性可能定义为[颜色、尺寸、重量],而每一个SKU属性以颜色为例,可能在后台设置的就是5种配色(SKU规格),同理其他两种SKU属性,而我们唯一确定一个SKU(具体的一辆车),就是SKU规格的不同组合,例如为[红色、S、1.5t] 和 [蓝色、S、1.5t] ,它们对应就是两辆车。
现在就需要保证SKU规格是不应该存在重复的,我们知道SKU规格本质上就是属性值的组合,如果不同的 SKU 配置有重复的SKU规格组合,可能会导致库存管理混乱或者用户选择错误。
当然正常来说我们如果在用户进行新增SKU的时候,进行限制实际上是可以规避这类事情,它主要是用于这种批量导入SKU规格的时候,对这种混乱顺序需要做一层校验,避免出先相同规格的SKU导致两条记录的。
如果还是不是理解上面三者的关系,可以参考:
关键词:规格项乱序后解决重复生产sku场景
代码实现
我们先看是怎么实现的,以新增sku为例:
@Override
@Transactional(rollbackFor = Exception.class)
public void insertSku(EquityGoodsDTO equityGoodsdto) {
Long goodsId = equityGoodsdto.getGoodsId();
checkShelf(goodsId);
skuInsert(equityGoodsdto);
}
/**
* 商品状态校验 ---业务逻辑可不过多关注
* @param goodsId
*/
private void checkShelf(Long goodsId) {
EquityGoods equityGoods = Optional.ofNullable(baseMapper.selectById(goodsId))
.orElseThrow(() -> new ServiceException("商品不存在"));
if (!Objects.equals(equityGoods.getStatus(), 1)) {
throw new ServiceException("待上架商品允许修改");
}
}
核心部分–插入SKU
业务流程:
针对经销商操作的后台程序(toB)
- 新增权益产品
- 配置多个权益商品
- 为每个权益商品配置sku
首先权益商品是需要先绑定权益产品才能进行新建,这里是分为两部分来进行保存的,权益产品的配置是保存在product表中,而下面的延保配置才算是权益商品,一个权益产品其下可以配置多个权益商品。
新增权益商品的流程是下载模版,得到一个excel文件,再将数据填写其中,之后导入该excel,解析其中数据并对业务数据做校验,无误则新增成功。因为这里的商品属性是一个套餐,它是需要绑定实体的(车、商品),下一步就是配置sku,但这里sku实际上是由后端开发来维护的,所以从某种程度上来说这里的sku实际上是一个简单版本的,因为它交由用户是配置sku(sku是查询已添加好的),而用户是不是能够直接新增sku。
从系统角度来说,因为权益平台作为的是一个权益发放平台,它所要做的是对接原系统传过来的sku即可,并不需要去单独来做车型产品的sku新增。
对sku的不重复问题,一个最为简单的方法就是在由用户配置好其具体的SKU规格之后,我们先按照字母大小进行排序,排完序之后我们再与数据库进行一次校验,如果存在则不允许插入。
核心逻辑:
public void checkAttrRepeat(Long goodsId) {
// 获取商品信息
EquityGoods equityGoods = new EquityGoods(goodsId, "conf1,conf2"); // 模拟商品信息
Long productId = 100L; // 模拟获取的产品ID
// 获取产品配置
Map<String, List<EquityProductConfig>> configMap = mockMapper.getProductConfigs(productId)
.stream().collect(Collectors.groupingBy(EquityProductConfig::getConfCode));
// 获取商品 SKU
List<EquityGoodsSku> goodsSkus = mockMapper.getGoodsSkus(goodsId);
// 获取 SKU 配置字段
List<String> skuSpecsField = List.of(equityGoods.getSkuSpecsField().split(","));
// 重复检测
List<String> attrList = new ArrayList<>();
Map<String, List<String>> repeatMap = new HashMap<>();
goodsSkus.forEach(sku -> {
String confCode = sku.getConfCode();
EquityProductConfig productConfig = configMap.get(confCode).get(0);
// 生成属性组合
String values = skuSpecsField.stream().map(checkField -> {
return Optional.ofNullable(productConfig.getConfName()).orElse("");
}).collect(Collectors.joining());
String attr = values + "@";
attrList.add(attr);
repeatMap.putIfAbsent(attr, new ArrayList<>());
repeatMap.get(attr).add(confCode);
});
// 检查是否存在重复组合
for (int i = 0; i < attrList.size(); i++) {
for (int j = i + 1; j < attrList.size(); j++) {
if (attrList.get(i).contains(attrList.get(j)) || attrList.get(j).contains(attrList.get(i))) {
String repeatAttrFirst = attrList.get(i);
String repeatAttrSecond = attrList.get(j);
String firstSku = String.join("、", repeatMap.get(repeatAttrFirst));
String secondSku = String.join("、", repeatMap.get(repeatAttrSecond));
String errorMsg = String.format("sku属性重复:[属性:【%s】配置:【%s】与属性:【%s】配置:【%s】]",
repeatAttrFirst, firstSku, repeatAttrSecond, secondSku);
throw new RuntimeException(errorMsg);
}
}
}
}
新增商品
在电商系统中,如果要新增一个商品,它是需要保存多方的数据,而每一个其下又是各种配置项,这种处理逻辑如果不做好分类到后面就很容易绕进去,建议以这样的形式来设置前端请求参数:
其中每一个List都代表是和商品表关联的,通过productId来联系的,在新增商品的过程,它实际是按照顺序进行添加的,它是一个流程链,用于收集所有用户填的信息,并依次将其保存在对应的表中
其中比较重要就是配置商品的SKU属性、规格,最后下单的时候也是通过SKU为单位来处理,这里我们需要首先确定需要配置的SKU属于哪一种类型,因为不同商品类型具有不同的SKU属性(例如衣物就是尺码,颜色,而手机就是内存等),下面这里就是以衣服为例,当然这里实际上根据用户选择的商品类型(product_attribute_category_id),类型为服装- T恤,那么会展示属于T恤的SKU属性(在product_attribute中)查询其SKU属性表来做的。
用户在配置SKU属性、规格后,会将其信息保存在sku_stock表中,同时关联其库存信息
product_attribute表结构:
例如这里我们product_attribute_category_id假设为1,那么我们将会展示[尺寸,颜色,商品编号,适用季节,适用人群,上市时间,袖长],对应我们看图也是这样。
注意这里用户并不会接触到对具体的SKU属性或规格的添加,我们规定的逻辑是,如果用户需要新增的商品包含一种特色的SKU属性,例如 图案 ,他要用到这个SKU属性,就需要先进行在后台进行新增SKU属性,流程如下:
可以选择添加SKU属性,也可以选择编辑原有SKU属性,修改SKU规格
这里的SKU属性、规格都会保存到上面的表 product_attribute 中
-
如果是添加原来没有的SKU属性,点击新增SKU属性,再配置该SKU属性其下SKU规格参数,这里的type标识为参数(1)
-
而如果是修改原来的SKU属性的SKU规格,点击编辑SKU属性,这里的type标识为规格(0)
具体的我们来看看这里是如何做SKU规格重复问题是怎么解决的?
很遗憾在原代码逻辑中,这里插入是没有做额外的处理,所以它插入数据库,可能导致相同的sku属性可以插入多条,这样导致在展示C端界面的时候读取对应的SKU属性就会有问题。
代码实现
新建商品流程:
@Override
public int create(PmsProductParam productParam) {
int count;
//创建商品
PmsProduct product = productParam;
product.setId(null);
productMapper.insertSelective(product);
//根据促销类型设置价格:会员价格、阶梯价格、满减价格
Long productId = product.getId();
//会员价格
relateAndInsertList(memberPriceDao, productParam.getMemberPriceList(), productId);
//阶梯价格
relateAndInsertList(productLadderDao, productParam.getProductLadderList(), productId);
//满减价格
relateAndInsertList(productFullReductionDao, productParam.getProductFullReductionList(), productId);
//处理sku的编码
handleSkuStockCode(productParam.getSkuStockList(),productId);
//添加sku库存信息
relateAndInsertList(skuStockDao, productParam.getSkuStockList(), productId);
//添加商品参数,添加自定义商品规格
relateAndInsertList(productAttributeValueDao, productParam.getProductAttributeValueList(), productId);
//关联专题
relateAndInsertList(subjectProductRelationDao, productParam.getSubjectProductRelationList(), productId);
//关联优选
relateAndInsertList(prefrenceAreaProductRelationDao, productParam.getPrefrenceAreaProductRelationList(), productId);
count = 1;
return count;
}
很有意思的一点,这里relateAndInsertList方法利用反射首先对填充数据进行校验和设置其id、productId为null,因为数据库是采用主键自增的顺序,确保数据库自动生成每个新记录的唯一 ID,避免重复或手动指定的 ID 冲突,:
/**
* 建立和插入关系表操作
*
* @param dao 可以操作的dao
* @param dataList 要插入的数据
* @param productId 建立关系的id
*/
private void relateAndInsertList(Object dao, List dataList, Long productId) {
try {
// 如果传入的 dataList 为空,则直接返回,避免后续操作
if (CollectionUtils.isEmpty(dataList)) return;
// 遍历 dataList 中的每一项,设置其 ID 为 null(用于确保插入时 ID 是由数据库生成)
for (Object item : dataList) {
// 获取并调用 "setId" 方法,设置 ID 为 null
Method setId = item.getClass().getMethod("setId", Long.class);
setId.invoke(item, (Long) null); // 设置为 null,表示插入新记录,数据库会生成 ID
// 获取并调用 "setProductId" 方法,设置与商品的关联 ID
Method setProductId = item.getClass().getMethod("setProductId", Long.class);
setProductId.invoke(item, productId); // 设置与商品的 ID 关联
}
// 获取并调用 dao 的 "insertList" 方法,批量插入 dataList 中的数据
Method insertList = dao.getClass().getMethod("insertList", List.class);
insertList.invoke(dao, dataList);
} catch (Exception e) {
// 捕获任何异常并记录日志,抛出 RuntimeException 异常
LOGGER.warn("创建产品出错:{}", e.getMessage());
throw new RuntimeException(e.getMessage());
}
}
数据库设计
新增商品,由用户按照表单链的形式填充参数,这里productId 由数据库自增,不需要用户单独传入,所有填充的数据存放在product表中
product
填写完商品的基本信息(名称,标题)之后,需要配置商品的SKU属性,我们需要知道用户新增商品具体属于哪一个模块,由用户进行选择,传入product_attribute_category_id,这里可以看看具体由哪些模块。
product_category
拿到product_attribute_category_id,之后我们需要去查看该商品对应模块预设的SKU属性,这个属性是提前配置好的,这里查询product_attribute,显示所有的SKU属性、规格。
product_attribute:商品属性
用户按照需求选择勾选SKU规格(这里也建议用表格的形式,用户需要先下载excel表格,再填写excel并导入表格的形式来填写),系统会显示勾选的SKU规格的不同组合,用户需要具体配置每件SKU的价格、库存,所有数据将保存在 sku_stock 表中
sku_stock
该表后续将在下单的时候用于判断当前购买的SKU库存是否充足。
至此与新增商品相关的表逻辑就完结了。
其他功能,例如展示当前大模块有多少SKU属性、SKU规格,对应下表中的attribute_count,param_count,可以通过这个product_attribute_category_id来进行关联