仅涉及后端,全部目录看顶部专栏,代码、文档、接口路径在:
【Lilishop商城】记录一下B2B2C商城系统学习笔记~_清晨敲代码的博客-CSDN博客
全篇会结合业务介绍重点设计逻辑,其中重点包括接口类、业务类,具体的结合源代码分析,源码读起来也不复杂~
谨慎:源代码中有一些注释是错误的,有的注释意思完全相反,有的注释对不上号,我在阅读过程中就顺手更新了,并且在我不会的地方添加了新的注释,所以在读源代码过程中一定要谨慎啊!
目录
A1.商品模块
B0.前言
C1.商品的关联表、子表分析
C2.商品的关联表、子表的操作分析(可跳过)
B1.新增商品
C1.商品DTO类分析 GoodsOperationDTO
C2.业务逻辑
D1.核心业务逻辑
D2.实际操作业务逻辑
C3.代码逻辑
D1.构造器new一个商品PO对象
D2.根据业务,检查并set商品信息
D3.拿到set缩略图【商品缩略图统一在这里说明】
D4.set商品参数
D5.构造器new一个商品skuPO对象
D6.销售模式渲染
D7.修改商品库存为商品sku的库存总和
D8.发送生成es商品sku索引的rocketMq消息
D9.构建出es商品sku索引EsGoodsIndex列表信息
D10.分词存储
D11.save es商品sku索引
C3.总结
A2.第三方工具记录(可略过)
B1.hutool
C1.JSONUtil工具的使用
商品模块是商城系统的核心,不是简单的增删改查,毕竟会涉及到很多的业务表,所以看起来简单但是业务还是比较复杂的,每一个接口(可以理解为一个业务操作)都可能涉及到很多业务操作。
之前在 No3 详细设计里面已经分析过数据结构设计和接口了,接下来就从接口方面先分析,一定要结合着数据结构~
我们只分析重点接口,相似的就放一起记录了~
A1.商品模块
B0.前言
开始前,先记录一下商品模块的信息哈。
C1.商品的关联表、子表分析
首先,商品表会关联:商品分类、商品品牌、商品规格、商品参数、商品单位、运费模板、店铺分类 这七张表,但是只有商品分类、商品品牌、运费模板、店铺分类是关联到商品表的!剩余的商品规格、商品参数、商品单位是弱关联的,并未关联到商品表,仅仅是作为可选项而已,是将数据直接保存到商品表里的。
商品分类:必须选到末级,是多选的,所以可以将商品分类id存放一起用逗号隔开;
商品品牌:是单选的,直接存储就可;
运费模板:是单选的,直接存储就可;
店铺分类:是多选的,所以可以将店铺分类id存放一起用逗号隔开;
商品规格:规格是对应商品sku的,是保存到商品sku表里的,并且是保存数据不是标识;
商品参数:由于参数是M端自定义的,并且是多项的,所以存储为 json 类型是最合适的
商品单位:直接保存的数据内容,不会产生关联的;
其次,商品表创建后,会产生子表:商品sku表、商品图册表、批发规则表、
商品sku表的数据一部分是和商品表的信息一样,一部分是来源于规格表。
C2.商品的关联表、子表的操作分析(可跳过)
在这简单说明一下,因为商品的七个关联表可能会互相影响,也会和商品表互相影响,例如,商品品牌和商品分类,商品分类是会关联商品品牌的,如果禁用已关联商品分类的商品品牌,此时就会提醒:分类已经绑定品牌,请先解除关联:[\"手机\",\"耳机/耳麦\"]。
所以一旦涉及到关联的情况,就一定要对照业务设计考虑到各种数据操作情况,如果设计未给出逻辑一定要问清楚!!!非常重要!!!
这里就不详细描写了,后续分析上面七个表时,在详细分析~~~~
B1.新增商品
C1.商品DTO类分析 GoodsOperationDTO
此类就是接口入参类型,因为商品信息比较复杂,所以需要添加一个 DTO 类,此DTO类就包含上一篇分析的接口入参类型啦~~~
重点说引用类型哈,
因为商品参数属性、批发属性是复杂且多数的,并且是有字段属性规则的,也及时可以抽象出一个类,所以直接用的自定义引用对象接收;其中有参数组类GoodsParamsDTO和组内商品参数类GoodsParamsItemDTO、WholesaleDTO;
而商品规格属性也复杂且多数的,但是是没有字段属性规则,不能抽象出一个具体的字段类,所以是用map集合接收~~~
下面是部分字段~~~
public class GoodsOperationDTO implements Serializable {
//与其他表无关联的,基本数据类型的业务基本信息;
@ApiModelProperty(hidden = true)
private String goodsId;
@ApiModelProperty(value = "商品名称", required = true)
@NotEmpty(message = "商品名称不能为空")
@Length(max = 50, message = "商品名称不能超过50个字符")
private String goodsName;
。。。
//与其他表关联的业务基本信息,可理解为外键;
@ApiModelProperty(value = "商品分类path,逗号隔开")
private String categoryPath;
@ApiModelProperty(value = "店铺分类id,逗号隔开", required = true)
@Size(max = 200, message = "选择了太多店铺分类")
private String storeCategoryPath;
。。。
//引用类型的业务基本信息;
@ApiModelProperty(value = "商品参数")
private List<GoodsParamsDTO> goodsParamsDTOList;
@ApiModelProperty(value = "sku列表,因为无法匹配pojo类,所以是用map接收")
@Valid
private List<Map<String, Object>> skuList;
。。。
//业务校验/判断的字段;
@ApiModelProperty(value = "是否有规格", hidden = true)
private String haveSpec;
@ApiModelProperty(value = "是否重新生成sku数据")
private Boolean regeneratorSkuFlag = true;
。。。
}
C2.业务逻辑
核心业务逻辑是说该接口主要的业务操作,不结合其他复用情况或者业务情况的!!!
而实际操作业务逻辑是结合了复用代码和其他业务情况的操作,所以实际操作肯定会有一些判断或其他代码逻辑。
所以核心业务逻辑主要是说明该接口具体是做了什么,是针对前端理解、设计理解、后端理解而言的,而实际操作业务逻辑是说明该接口怎么用业务逻辑实现,仅针对后端理解。
D1.核心业务逻辑
- 根据商品DTO生成商品和商品skulist的基本信息,保存商品和skulist,然后生成并保存其对应的子表信息;
- 添加商品成功后,如果商品是审核通过且上架状态,则需要根据商品信息生成es商品sku索引列表,进行分词存储后,保存es商品sku索引列表。
D2.实际操作业务逻辑
接下来我们就需要针对核心业务逻辑进行进一步实现了!
在介绍业务逻辑时,会涉及到一些其他代码结构,有需要说明的就用绿色底纹标注,然后在后面的代码逻辑里面详细介绍。
GoodsStoreController#save:
- 拿到入参DTO,调用service方法添加商品;
- 返回ResultUtil.success();有异常则进入异常拦截返回异常;
GoodsServiceImpl#addGoods:
- 构造器new一个商品PO对象,并将DTO里的基本信息set给商品PO,其中可以校验DTO信息是否有效;
- 根据业务,检查并set商品信息:1.判断商品类型,是虚拟的还是实物的,进而配置配送模板;2.判断商品id是否存在,是新增还是修改;3.判断商品是否需要审核;4.判断当前用户是否为店铺,并设置店铺信息;
- 将商品图册列表中第一个图片set为商品默认图片,并拿到set缩略图等;
- set商品参数,参数转为JSON类型保存;
- save商品信息;【到这个步骤商品基本信息已设置完毕】;
- 判断商品DTO里是否有 GoodsGalleryList 商品图册信息,save商品图册信息;【子表关联】
- 判断商品DTO里是否有 skuList 商品规格信息,for循环使用构造器new一个商品skuPO对象,并将商品基本信息set给商品skuPO,然后将商品规格信息 set 给商品skuPO;
- 根据销售模式渲染,如果是批发模式需要再渲染sku信息和save批发信息;【子表关联】
- set图册列表中第一个为商品图册默认图片,并拿到set缩略图等;
- 循环商品sku,将规格json里面图片列表的第一个set为商品sku默认图片,并拿到set缩略图等;
- 批量save商品sku信息;【子表关联】
- 修改商品库存为商品sku的库存总和;
- 如果商品是已审核通过且上架状态的,则发送生成es商品sku索引的rocketMq消息,只需要传递商品id就可以。
- MQ执行时,通过商品ID拿到商品信息和商品skulist信息,然后根据这两个信息构建出es商品sku索引EsGoodsIndex列表信息,将根据列表里面的商品参数和商品名称进行分词存储后,save es商品sku索引;
下面就贴一下service的添加商品方法的截图,具体的代码逻辑看下面的代码逻辑分析~~
C3.代码逻辑
D1.构造器new一个商品PO对象
首先,我们知道商品PO表里面都是商品的基本信息,所以可以直接通过PO类的构造器进行赋值,由于商品DTO表较为复杂、使用DTO对象构造PO对象前需要校验、有其他业务也会复用到DTO对象构造PO对象,所以就手动给PO类中添加此类型的构造方法~
@EqualsAndHashCode(callSuper = true)
@Data
@TableName("li_goods")
@ApiModel(value = "商品")
public class Goods extends BaseEntity {
。。。
public Goods(GoodsOperationDTO goodsOperationDTO) {
//基本信息赋值
this.goodsName = goodsOperationDTO.getGoodsName();
this.categoryPath = goodsOperationDTO.getCategoryPath();
this.storeCategoryPath = goodsOperationDTO.getStoreCategoryPath();
this.brandId = goodsOperationDTO.getBrandId();
this.templateId = goodsOperationDTO.getTemplateId();
this.recommend = goodsOperationDTO.getRecommend();
this.sellingPoint = goodsOperationDTO.getSellingPoint();
this.salesModel = goodsOperationDTO.getSalesModel();
this.goodsUnit = goodsOperationDTO.getGoodsUnit();
this.intro = goodsOperationDTO.getIntro();
this.mobileIntro = goodsOperationDTO.getMobileIntro();
this.goodsVideo = goodsOperationDTO.getGoodsVideo();
this.price = goodsOperationDTO.getPrice();
if (goodsOperationDTO.getGoodsParamsDTOList() != null && goodsOperationDTO.getGoodsParamsDTOList().isEmpty()) {
this.params = JSONUtil.toJsonStr(goodsOperationDTO.getGoodsParamsDTOList());
}
//判断是否立即上架
this.marketEnable = Boolean.TRUE.equals(goodsOperationDTO.getRelease()) ? GoodsStatusEnum.UPPER.name() : GoodsStatusEnum.DOWN.name();
this.goodsType = goodsOperationDTO.getGoodsType();
//商品评分,初始100
this.grade = 100D;
//循环sku,判定sku是否有效,根据销售模式、商品类型
/*
sn 、quantity:是任何销售模式下、任何商品类型下都有的
price、cost:是零售销售模式下、任何商品类型下有的
weight:是任何销售模式下、商品实物类型下有的
*/
for (Map<String, Object> sku : goodsOperationDTO.getSkuList()) {
//判定参数不能为空
if (!sku.containsKey("sn") || sku.get("sn") == null) {
throw new ServiceException(ResultCode.GOODS_SKU_SN_ERROR);
}
if (!sku.containsKey("quantity") || StringUtil.isEmpty(sku.get("quantity").toString()) || Convert.toInt(sku.get("quantity").toString()) < 0) {
throw new ServiceException(ResultCode.GOODS_SKU_QUANTITY_ERROR);
}
//判断参数是否有效,并且是非批发销售模式下的
if ((!sku.containsKey("price") || StringUtil.isEmpty(sku.get("price").toString()) || Convert.toDouble(sku.get("price")) <= 0)
//非批发销售模式。添加此判断是因为成本和价格仅针对零售销售模式而言,但是前端有可能会先填写过零售模式的规格后又修改为批发模式,就会导致price参数不对,如果是批发销售模式此参数就无所谓了
&& !goodsOperationDTO.getSalesModel().equals(GoodsSalesModeEnum.WHOLESALE.name())) {
throw new ServiceException(ResultCode.GOODS_SKU_PRICE_ERROR);
}
if ((!sku.containsKey("cost") || StringUtil.isEmpty(sku.get("cost").toString()) || Convert.toDouble(sku.get("cost")) <= 0)
//非批发销售模式
&& !goodsOperationDTO.getSalesModel().equals(GoodsSalesModeEnum.WHOLESALE.name())) {
throw new ServiceException(ResultCode.GOODS_SKU_COST_ERROR);
}
//虚拟商品没有重量字段
if (this.goodsType.equals(GoodsTypeEnum.PHYSICAL_GOODS.name()) &&
(!sku.containsKey("weight") || sku.containsKey("weight") && (StringUtil.isEmpty(sku.get("weight").toString()) || Convert.toDouble(sku.get("weight").toString()) < 0))) {
throw new ServiceException(ResultCode.GOODS_SKU_WEIGHT_ERROR);
}
sku.values().forEach(i -> {
if (CharSequenceUtil.isBlank(i.toString())) {
throw new ServiceException(ResultCode.MUST_HAVE_GOODS_SKU_VALUE);
}
});
}
}
。。。
}
D2.根据业务,检查并set商品信息
仅仅靠构造器new的PO对象不是最完全的,可能会有特殊业务需要再次修改,所以添加一个专门处理特殊业务的防范来解决。
例如,这里就需要添加一下的业务:
//cn.lili.modules.goods.serviceimpl.GoodsServiceImpl
/**
* 根据业务,检查商品信息
* 如果商品是虚拟商品则无需配置配送模板
* 如果商品是实物商品需要配置配送模板
* 判断商品id是否存在。修改商品时会复用此方法
* 判断商品是否需要审核。系统配置里面设置的
* 判断当前用户是否为店铺,并设置店铺信息
*
* @param goods 商品
*/
private void checkGoods(Goods goods) {
//判断商品类型,是虚拟的还是实物的
switch (goods.getGoodsType()) {
case "PHYSICAL_GOODS":
if ("0".equals(goods.getTemplateId())) {
throw new ServiceException(ResultCode.PHYSICAL_GOODS_NEED_TEMP);
}
break;
case "VIRTUAL_GOODS":
if (!"0".equals(goods.getTemplateId())) {
goods.setTemplateId("0");
}
break;
default:
throw new ServiceException(ResultCode.GOODS_TYPE_ERROR);
}
//检查商品是否存在--修改商品时使用
if (goods.getId() != null) {
this.checkExist(goods.getId());
} else {
//评论次数
goods.setCommentNum(0);
//购买次数
goods.setBuyCount(0);
//购买次数
goods.setQuantity(0);
//商品评分
goods.setGrade(100.0);
}
//获取商品系统配置决定是否审核
Setting setting = settingService.get(SettingEnum.GOODS_SETTING.name());
GoodsSetting goodsSetting = JSONUtil.toBean(setting.getSettingValue(), GoodsSetting.class);
//set审核状态
goods.setAuthFlag(Boolean.TRUE.equals(goodsSetting.getGoodsCheck()) ? GoodsAuthEnum.TOBEAUDITED.name() : GoodsAuthEnum.PASS.name());
//判断当前用户是否为店铺
if (Objects.requireNonNull(UserContext.getCurrentUser()).getRole().equals(UserEnums.STORE)) {
StoreVO storeDetail = this.storeService.getStoreDetail();
if (storeDetail.getSelfOperated() != null) {
goods.setSelfOperated(storeDetail.getSelfOperated());
}
goods.setStoreId(storeDetail.getId());
goods.setStoreName(storeDetail.getStoreName());
goods.setSelfOperated(storeDetail.getSelfOperated());
} else {
throw new ServiceException(ResultCode.STORE_NOT_LOGIN_ERROR);
}
}
D3.拿到set缩略图【商品缩略图统一在这里说明】
商品和商品sku都有自己单独的默认图,以及默认图的缩略图、小图。
此默认图和图册列表是有区别的,一般会是由图册列表里的第一张图作为默认图。
- 商品默认图,可用于S端商品列表的列表中显示的图片;
- 商品sku默认图,可用于B端搜索商品时列表中展示的图片;
- 商品图册,暂时还不知道用在哪里,但是商品sku也就是商品规格的图片默认是拿取的商品图册的【S端新增/修改商品时】,但是sku图册是可以修改的;
- 商品sku图册,可用于B端打开商品详情时,左侧显示的商品图片;
用户在新增/修改商品时,会上传图片,然后拿到图片存储url。【上传方法见cn.lili.controller.common.UploadController#upload,是上传到阿里云的OSS上】
然后新增商品时就会根据上方原图url,拿到其缩略图等信息,由于可以复用,所以抽象成一个方法。
商品sku的默认图也是如此,这里就不复述了
D4.set商品参数
这个就是记录一下,商品参数是作为 JSON 存储的,后续会在获取商品信息等功能中转成商品参数的类型,没有复杂的业务。重点就是前端传值格式一定要和后端对应的pojo对象类型一致~~否则json转格式转不成功的!
D5.构造器new一个商品skuPO对象
商品会有不同规格组成的商品sku,例如XX手机,有黄色内存60G、 白色内存60G、黄色内存120G、 白色内存120G的四种商品sku。就像我们逛淘宝时要买一部手机,必须选择规格型号确定商品sku后才能下单。
所以商品sku也是比较复杂,是基于部分商品信息又增加了sku规格信息,相当于是商品的子表。由于很多业务功能例如添加秒杀商品的功能中,是根据商品sku操作的,所以shop系统是直接将部分商品信息存储到了sku表里面,这样查询时直接查询sku表就可以了,如果修改商品信息了,也会修改商品sku里面对应的信息,然后新增或更新~
说的很简单,但是具体是有些复杂滴。
首先,由于sku是多个,所以需要for循环创建GoodsSku并set他的信息,每个GoodsSku里面的规格信息分为两种,一种是原始固定必有的("sn", "cost", "price", "quantity", "weight"),一种是用户自定义的("颜色","内存"等),对于固定的,我们使用固定字段就可以,对于自定义的就只能使用json形式存储了,也方便获取。
由于构建skulist都很多地方都会复用,所以直接抽象成Builder更方便~
D6.销售模式渲染
首先要知道,为啥销售模式渲染放在这里?
销售模式分为两种:零售、批发。零售很简单就是正常的商品规格添加就行,而批发会是一种特殊的商品sku。
先来看页面设计:
所以可以知道,批发模式下,批发规格是跟着批发模式而定的,并且所有商品sku价格是一致的~~~
注意:批发模式下是需要填写商品单个重量的,这个重量是所有商品sku的重量哦,前端会设置sku重量为统一的重量的~
了解了关系,那么我们就可以直接在编辑sku信息时,进行批发模式的操作,可以理解为对于sku数据的渲染。
shop系统是提供了一个单独的销售模式渲染抽象类,然后实现了一个批发模式渲染类,通过这个类来将商品sku和批发业务类关联起来。
D7.修改商品库存为商品sku的库存总和
这个其实没啥逻辑,就是我疑惑,为啥将库存总和放在新增sku列表里面,调用数据库修改?
因为前面保存商品时没法儿直接拿到库存总和,而在这GoodsSkuServiceImpl#add方法拿到sku列表后的sku保存又需要商品ID,所以要么在保存商品前循环拿到商品库存保存,进行前置处理,要么在这里通过 skulist 拿到库存后进行后置处理。
D8.发送生成es商品sku索引的rocketMq消息
对于店铺S端来说,主要就是添加成功商品及商品sku信息。由于会员B端需要浏览搜索商品,且数据量较大, 所以我们引入了es搜索引擎。那么当商品等信息存储到mysql里面后,就需要根据业务判断是否要添加到es搜索引擎里面,也就是在es里面生成es商品sku索引!!!
由于es生成索引的业务是主要针对会员B端的,并不影响店铺S端添加商品的业务,所以可以将此操作添加到mp里面,相当于是异步操作~~~
我们直接将商品id作为传递的消息,mq监听到之后,先通过商品id拿到商品信息及商品sku信息,然后在进行其他操作,这里不会直接传递商品信息一是数据量较大,二是无法保证是商品的实时数据。
D9.构建出es商品sku索引EsGoodsIndex列表信息
这个步骤就是麻烦,不复杂,我们先通过商品id拿到商品信息和商品sku列表信息,然后设置每一个es商品sku对象值就好了。
这里需要注意的是,es商品sku对象字段肯定要符合前端展示的。并且一些状态值一定不能缺少!!!
D10.分词存储
这里记录一下,分析B端获取商品时再详细描述分词。
D11.save es商品sku索引
这里记录一下,由于就是普通的es保存,所以直接使用 ElasticsearchRepository 接口的方式就好啦!
C3.总结
以上分析的新增商品接口逻辑中,将重点的逻辑描写了出来,其中涉及到的状态判断(例如商品审核状态的设置)就不详细说了,一定是要跟着设计走的。
还有一件事,就是新增商品接口额和编辑商品接口逻辑是类似的哦,但是又有些不同。所以会有一些复用的方法~~~,复用的方法,我们重点在编辑逻辑里面介绍吧~~~
A2.第三方工具记录(可略过)
B1.hutool
C1.JSONUtil工具的使用
- String JSONUtil#toJsonStr(java.lang.Object) 将对象转为json
- T JSONUtil#toBean(java.lang.String, java.lang.Class<T>) 将json转为对象
- List<T> JSONUtil#toList(java.lang.String, java.lang.Class<T>) 将json转为对象list