大纲
1.商品中心的专业术语
2.商品中心的基本业务系统
3.商品中心整体架构设计以及运行流程
4.商品B端—商品编码生成逻辑
5.商品B端—商品核心数据模型
6.商品B端—转换建品请求数据为商品模型数据
7.商品B端—商品建品时商品编号补全与审核配置
8.商品B端—商品审核前的草稿数据保存逻辑
9.商品B端—不需审核的建品流程持久化逻辑
10.商品B端—审核工单分页列表和商品草稿查询
11.商品B端—商品审核时的敏感字段diff计算逻辑
12.商品B端—对草稿中的商品进行审核的逻辑
13.商品B端—商品属性+买手+品类的数据维护
14.商品C端—通用缓存读写组件的实现逻辑
15.商品C端—接口代码实现逻辑
1.商品中心的专业术语
一.ITEM
商品售卖展示单位,仅⽤于销售时展示使⽤。
二.SKU
SKU是Stock Keeping Unit(库存单位),即库存进出计量的单位。可以是以件、盒、托盘等为单位,如:iPhoneX + ⿊⾊ + 256G。
三.SPU
SPU是Standard Product Unit标准化产品单元,是对某一类标准产品的共同特征属性的描述。SPU是商品信息聚合的最⼩单位,如:iPhoneX就是SPU。SPU的出现是为了满足在叶子类目下对商品进行进一步抽象的需求。比如手机就是叶子类目,虽然可以添加苹果手机或者华为手机这样的类目,但这样添加就比较麻烦了,可能会导致类目树就会变得非常庞大。所以SPU是一个介于叶子类目和商品之间的概念,是对类目的细化。因此SPU通常由"后台类目 + 关键属性"唯一确定。
四.CSPU
CSPU也就是子标准化产品单元,即SPU的细分,Child SPU。CSPU通常由"后台类目 + 关键属性 + 销售属性"唯一确定。比如手机类型下,品牌和型号这两个属性可以确定一个SPU,但还不能确定一个CSPU,需要额外的销售属性才能确定一个CSPU。以苹果手机为例,品牌是iPhone、型号是X、颜色为黑色、存储为256G,两个关键属性是品牌和型号,两个销售属性为颜色和存储。
五.运营品类
运营品类是⼀种抽象的概念,例如:运动裤、⼿机。每一个商品都会有所属的品类,比如iPhone X这个SPU会属于手机这个品类。不同的电商平台会对品类进行不同的划分,手机品类在有的平台是一级品类,在有的平台是电子产品品类的子品类。
六.前台类⽬
多级品类可以构成前台类⽬,例如:男T恤 + 男短裤可归属到男装类⽬。电商网站首页里,左侧都会有一颗类目树,这个类目树就是前台类目。
七.SKU规格
⽤来区分单品的主要指标。例如⼿机商品由颜⾊、内存两种规格区分单品,每个规格有多个可选值。从每个规格选出一个值,拼凑起来的组合就可以唯一确定一款商品SKU。颜色规格:白色、黑色、粉色、天蓝色;内存:128G、256G、512G。
八.原料商品
只采购不销售的商品,只有采购属性如包材或原材料,例如:吸管、开瓶器。
九.普通商品
⼜采购⼜销售的商品,有库存和销售属性。
十.组套商品
不采购只销售的商品,共享库存和销售属性,例如:原料商品 + 普通商品组合为⼀个商品。开瓶器是原料商品,红酒是普通商品,开瓶器 + 红酒就是一个组套商品。开瓶器不能单卖但需要采购,用户购买红酒时不用关注开瓶器,开瓶器会和红酒打包在一起进行展示和售卖。
十一.虚拟商品
不采购只销售,只有虚拟库存,只有销售属性,例如:会员卡、虚拟卡、购物卡、游戏点卡。这些虚拟商品没有必要去进行采购,用户支付后也不需要履约签收。用户完成对虚拟商品的支付后,商品直接可以展示在用户的会员中心里。
十二.售卖区
商品可以在哪⾥卖,售卖范围配置:按城市配置、按卖家组配置。有的商品只能在部分城市可以售卖,部分城市是没法售卖的。在某些区域里,商品的库存不好发货,可能会显示该区域无货。
仓库会分成两种:微仓和大仓,微仓就是微型的小仓库,大仓就是大型的大仓库。大仓可以辐射很大一片区域的发货,仓库容量很大,里面可以放很多商品。微仓也叫前置仓,在一个城市里,可以设置微仓。可以将该城市经常购买的,库存量消耗比较大的商品,放到多个微仓里。这样距离消费者就会更近一些,发货也可以更快一些。
十三.卖家类型
类型一:⾃营,类型二:POP。自营就是商品是由平台自己来采购、入仓、售卖,POP(Platform Open Plan)意思是平台开放计划,POP就是第三方卖家入驻平台开店售卖自己的商品。
十四.商品状态
可售:商品配置了售卖区并且状态为可售
可补:商品可售且微仓可补货状态
可采:商品可售且⼤仓可采货状态
准备上架:建品后为此状态,表示可采和可补
试销上架:上架状态,表示处于试销阶段
上架:正式上架售卖
预下架:售完不展示商品,表示不可采和可补
下架:不可采和不可补
停售:永久下架,已淘汰
十五.商品价格
商城价:⾮会员⽤户购买商品的价格
会员价:会员⽤户购买的价格
营销价:促销活动价
秒杀价:秒杀活动价格,⼀⼝价
2.商品中心的基本业务系统
(1)商品基础服务
(2)商品类型与采购销售之间的关系
(3)商品中心的业务系统
(1)商品基础服务
服务一:提供从建品到下架期间可采可补可售管理的商品全流程服务
服务二:对商品基本信息、品牌信息、运营品类、前台类⽬、仓配信息、标签信息、品控信息、销售信息、推⼴信息等进⾏精细化管理与运营
服务三:通过权限收敛,可以很好把控并记录⽤户操作⾏为,使流程更加规范
服务四:通过提效⼯具,业务⽅可以批量处理商品相关⼯作,降低⼈⼒成本
(2)商品类型与采购销售之间的关系
(3)商品中心的业务系统
商品中心的系统主要会分为两类:一个是面向B端,一个是面向C端。面向B端的系统,主要由公司运营来使用,对商品进行精细化管理。面向C端的系统,则会对C端用户提供各种商品浏览和查询的接口。
一.价格中心系统
商品价格管理,提供全流程价格管控和分析。⽀持功能:价格查询、价格设置、审核流程、历史价格查询与趋势分析等。
二.商品卖家系统
商品售卖⽅,这⾥将卖家定义为卖家树,⽤户可以定位到多个卖家。商品基于卖家售卖,⽤户在当前覆盖的区域内可浏览到相应卖家的商品。将多个卖家合并为⼀个⼤的卖家称为卖家组,也称为售卖区,售卖区之间的逻辑处理称为售卖区管理(可售区域)。
三.商品⽣命周期系统
商品的状态分为:准备上架、试销上架、上架、预下架、下架、停售。为了更好的管理商品,需要对商品进⾏⼀套⽣命周期管理。⽤于考核商品,降低滞销率、资⾦成本以及影响商品的可采可补逻辑。
四.商品库存系统
商品库存需要分卖家设置,卖家 + 商品 + 库存关系定位具体商品库存数量。
五.商品标签系统
需要打上特殊标签的商品,例如:爆款,后台进⾏标签 + 标签组 + 商品管理。
六.属性库系统
商品关联的属性,涉及四种属性:关键属性、销售属性、⾮关键属性、导购属性。
七.商品品控系统
把控商品质量,在商品⼊库前进⾏取样检测,给出质检报告。合格商品允许⼊库,不合格商品不允许⼊库。将可售卖商品关联上质检报告,展示给⽤户。
3.商品中心整体架构设计以及运行流程
(1)商品中心整体架构
(2)商品新建编辑流程
(1)商品中心整体架构
(2)商品新建编辑流程
- //商品服务
- @DubboService(version = "1.0.0", interfaceClass = ProductApi.class, retries = 0)
- public class ProductApiImpl implements ProductApi {
- @Autowired
- private ProductService productService;
- //建品/编辑商品接口
- @Override
- public JsonResult<ProductDTO> product(ProductRequest request) {
- try {
- ProductDTO productDTO = productService.product(request);
- return JsonResult.buildSuccess(productDTO);
- } catch (ProductBizException e) {
- log.error("biz error: request={}", JSON.toJSONString(request), e);
- return JsonResult.buildError(e.getErrorCode(), e.getErrorMsg());
- } catch (Exception e) {
- log.error("system error: request={}", JSON.toJSONString(request), e);
- return JsonResult.buildError(e.getMessage());
- }
- }
- ...
- }
复制代码
4.商品B端—商品编码生成逻辑- //商品编码
- @Service
- public class ProductNoManagerImpl implements ProductNoManager {
- //6位序列号
- private static final int width = 6;
- @Autowired
- private ProductAutoNoMapper productAutoNoMapper;
- //生成商品编码
- @Override
- public String generateProductNo(Integer sourceType) {
- ProductTypeEnum productTypeEnum = ProductTypeEnum.getByCode(sourceType);
- if (productTypeEnum == null) {
- throw new ProductBizException(ProductErrorCodeEnum.PARAM_ERROR);
- }
- return getProductNo(productTypeEnum.getValue());
- }
- //获取组装后的商品编码,商品的prefixNo是100000
- private String getProductNo(String prefixNo) {
- //有一张ProductAutoNo表专门用于生成商品ID
- //分库分表也可以利用此来实现基于数据库的内存缓存分段的发号器
- ProductAutoNoDO productAutoNoDO = new ProductAutoNoDO();
- productAutoNoMapper.insert(productAutoNoDO);
- Long autoNo = productAutoNoDO.getId();//获取自增ID
- return prefixNo + IDUtils.genId(autoNo, width);//数字混淆算法
- }
- }
复制代码
5.商品B端—商品核心数据模型
6.商品B端—转换建品请求数据为商品模型数据
前端的建品请求数据比较复杂,需要和后端的商品模型数据匹配起来,所以需要进行数据转换。这种数据转换,通常会用Builder模式来实现。
7.商品B端—商品建品时商品编号补全与审核配置- @Service
- public class ProductServiceImpl implements ProductService {
- ...
- //建品/编辑商品
- @Transactional(rollbackFor = Exception.class)
- @Override
- @ParamsValidate
- public ProductDTO product(ProductRequest productRequest) {
- //入参检查
- checkProductRequestParam(productRequest);
- //商品数据处理
- ProductDTO productDTO = handleProduct(productRequest);
- //返回商品信息
- return productDTO;
- }
- ...
- //商品数据处理
- private ProductDTO handleProduct(ProductRequest productRequest) {
- //构建商品的全量信息
- FullProductData fullProductData = buildProduct(productRequest);
- //是否构建填充itemId
- Boolean createFlag = whetherBuildProductItemId(fullProductData);
- //判断是否需要审核,根据配置的sku或者item或者品类信息,如果需要审核,只处理草稿表中的数据,正式表中的数据不动
- if (productAuditRepository.needAudit(fullProductData, createFlag)) {
- //需要审核,则正式表中的数据不变更,只新增草稿表记录
- FullDraftData fullDraftData = buildDraft(fullProductData, AuditTypeEnum.GOODS.getCode());
- //保存草稿信息
- productAuditRepository.saveDraft(fullDraftData);
- return new ProductDTO(null, null);
- }
- //如果不需要审核,则保存商品信息
- this.saveOrUpdateDBProduct(fullProductData, createFlag);
- //发送消息通知订阅方
- sendUpdateProductMessage(fullProductData);
- //返回商品返回结果
- return new ProductDTO(fullProductData.getItemInfoDO().getItemId(), buildProductSkuIds(fullProductData));
- }
- //是否需要构建商品的ItemId
- private Boolean whetherBuildProductItemId(FullProductData fullProductData) {
- //ITEM信息
- ItemInfoDO itemInfoDO = fullProductData.getItemInfoDO();
- //新增
- if (StringUtils.isEmpty(itemInfoDO.getItemId())) {
- //保质期
- ItemShelfLifeDO itemShelfLifeDO = fullProductData.getItemShelfLifeDO();
- //生成Item的Id
- String itemId = createItemId();
- //赋值itemId
- itemInfoDO.setItemId(itemId);
- itemShelfLifeDO.setItemId(itemId);
- //SKU信息
- List<SkuInfoDO> skuInfoDOList = fullProductData.getSkuInfoDOList();
- for (SkuInfoDO skuInfoDO : skuInfoDOList) {
- //对每个SKU也生成ID
- String skuId = productNoManager.generateProductNo(ProductTypeEnum.SKU.getCode());
- skuInfoDO.setSkuId(skuId);
- skuInfoDO.setItemId(itemId);
- }
- //视频图片
- List<ItemVideoImgDO> itemVideoImgDOList = fullProductData.getItemVideoImgDOList();
- for (ItemVideoImgDO itemVideoImgDO : itemVideoImgDOList) {
- itemVideoImgDO.setItemId(itemId);
- }
- //属性扩展
- AttributeExtendDO attributeExtendDO = fullProductData.getAttributeExtendDO();
- attributeExtendDO.setParticipateId(itemInfoDO.getItemId());
- attributeExtendDO.setParticipateType(ProductTypeEnum.ITEM.getCode());
- return true;
- }
- return false;
- }
- //创建ItemId
- private String createItemId() {
- String itemId = productNoManager.generateProductNo(ProductTypeEnum.ITEM.getCode());
- return itemId;
- }
- ...
- }
- //商品审核 资源管理
- @Repository
- public class ProductAuditRepository {
- ...
- //验证是否需要审核
- public Boolean needAudit(FullProductData fullProductData, Boolean createFlag) {
- ItemInfoDO itemInfoDO = fullProductData.getItemInfoDO();
- Integer count = 0;
- if (!createFlag) {
- //1.首先判断 商品审核内容配置表 中是否有对应的skuId
- List<String> skuIds = fullProductData.getSkuInfoDOList().stream().map(SkuInfoDO::getSkuId).collect(Collectors.toList());
- count = countByCustomIds(skuIds, AuditCustomTypeEnum.SKU);
- if (count > 0) {
- return true;
- }
- //2.是否有对应的item
- count = countByCustomIds(Collections.singletonList(itemInfoDO.getItemId()), AuditCustomTypeEnum.ITEM);
- if (count > 0) {
- return true;
- }
- }
- //3.验证是否有对应的categoryId
- List<Integer> categoryIds = Arrays.asList(itemInfoDO.getFirstCategoryId(), itemInfoDO.getSecondCategoryId(), itemInfoDO.getThirdCategoryId());
- count = countByCustomIds(categoryIds, AuditCustomTypeEnum.CATEGORY);
- //当商品审核内容配置表中有相应的品类数据,则需要审核,否则不需要审核
- return count > 0;
- }
- ...
- }
复制代码
8.商品B端—商品审核前的草稿数据保存逻辑
[code]@Servicepublic class ProductServiceImpl implements ProductService { ... //建品/编辑商品 @Transactional(rollbackFor = Exception.class) @Override @ParamsValidate public ProductDTO product(ProductRequest productRequest) { //入参检查 checkProductRequestParam(productRequest); //商品数据处理 ProductDTO productDTO = handleProduct(productRequest); //返回商品信息 return productDTO; } ... //商品数据处理 private ProductDTO handleProduct(ProductRequest productRequest) { //构建商品的全量信息 FullProductData fullProductData = buildProduct(productRequest); //是否构建填充itemId Boolean createFlag = whetherBuildProductItemId(fullProductData); //判断是否需要审核,根据配置的sku或者item或者品类信息,如果需要审核,只处理草稿表中的数据,正式表中的数据不动 if (productAuditRepository.needAudit(fullProductData, createFlag)) { //需要审核,则正式表中的数据不变更,只新增草稿表记录 FullDraftData fullDraftData = buildDraft(fullProductData, AuditTypeEnum.GOODS.getCode()); //保存草稿信息 productAuditRepository.saveDraft(fullDraftData); return new ProductDTO(null, null); } //如果不需要审核,则保存商品信息 this.saveOrUpdateDBProduct(fullProductData, createFlag); //发送消息通知订阅方 sendUpdateProductMessage(fullProductData); //返回商品返回结果 return new ProductDTO(fullProductData.getItemInfoDO().getItemId(), buildProductSkuIds(fullProductData)); } ... //根据商品数据构建商品草稿数据 private FullDraftData buildDraft(FullProductData fullProductData, Integer auditType) { ProductDraftBuilder productDraftBuilder = new ProductDraftBuilder(fullProductData); FullDraftData fullDraftData = productDraftBuilder.buildDraftMain(auditType) .buildDraftImgList() .build(); return fullDraftData; } ...}//商品审核 资源管理@Repositorypublic class ProductAuditRepository { ... //保存草稿信息 public void saveDraft(FullDraftData fullDraftData) { //1.保存工单信息 AuditInfoDO auditInfoDO = saveAudit(fullDraftData); //2.保存工单审核历史信息 saveAuditHistory(auditInfoDO); //3.保存草稿信息 saveDraftMain(fullDraftData, auditInfoDO.getId()); //4.保存草稿图片信息 saveDraftImgBatch(fullDraftData); } //保存工单信息 private AuditInfoDO saveAudit(FullDraftData fullDraftData) { AuditInfoDO auditInfoDO = auditConverter.converterDO(fullDraftData.getDraftMainDO()); auditInfoDO.initCommon(); int count = auditInfoMapper.insert(auditInfoDO); if (count |