【Lilishop商城】No4-7.业务逻辑的代码开发,涉及到:商品模块

  仅涉及后端,全部目录看顶部专栏,代码、文档、接口路径在: 

【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.核心业务逻辑

  1. 根据商品DTO生成商品和商品skulist的基本信息,保存商品和skulist,然后生成并保存其对应的子表信息;
  2. 添加商品成功后,如果商品是审核通过且上架状态,则需要根据商品信息生成es商品sku索引列表,进行分词存储后,保存es商品sku索引列表。

D2.实际操作业务逻辑

接下来我们就需要针对核心业务逻辑进行进一步实现了!

在介绍业务逻辑时,会涉及到一些其他代码结构,有需要说明的就用绿色底纹标注,然后在后面的代码逻辑里面详细介绍。

GoodsStoreController#save:

  1. 拿到入参DTO,调用service方法添加商品;
  2. 返回ResultUtil.success();有异常则进入异常拦截返回异常;

GoodsServiceImpl#addGoods:

  1. 构造器new一个商品PO对象,并将DTO里的基本信息set给商品PO,其中可以校验DTO信息是否有效;
  2. 根据业务,检查并set商品信息:1.判断商品类型,是虚拟的还是实物的,进而配置配送模板;2.判断商品id是否存在,是新增还是修改;3.判断商品是否需要审核;4.判断当前用户是否为店铺,并设置店铺信息;
  3. 将商品图册列表中第一个图片set为商品默认图片,并拿到set缩略图等;
  4. set商品参数,参数转为JSON类型保存;
  5. save商品信息;【到这个步骤商品基本信息已设置完毕】;
  6. 判断商品DTO里是否有 GoodsGalleryList 商品图册信息,save商品图册信息;【子表关联】
  7. 判断商品DTO里是否有 skuList 商品规格信息,for循环使用构造器new一个商品skuPO对象,并将商品基本信息set给商品skuPO,然后将商品规格信息 set 给商品skuPO;
  8. 根据销售模式渲染,如果是批发模式需要再渲染sku信息和save批发信息;【子表关联】
  9. set图册列表中第一个为商品图册默认图片,并拿到set缩略图等;
  10. 循环商品sku,将规格json里面图片列表的第一个set为商品sku默认图片,并拿到set缩略图等;
  11. 批量save商品sku信息;【子表关联】
  12. 修改商品库存为商品sku的库存总和
  13. 如果商品是已审核通过且上架状态的,则发送生成es商品sku索引的rocketMq消息,只需要传递商品id就可以。
  14. 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工具的使用

  1. String JSONUtil#toJsonStr(java.lang.Object) 将对象转为json
  2. T JSONUtil#toBean(java.lang.String, java.lang.Class<T>) 将json转为对象
  3. List<T> JSONUtil#toList(java.lang.String, java.lang.Class<T>) 将json转为对象list

猜你喜欢

转载自blog.csdn.net/vaevaevae233/article/details/128628548
今日推荐