微服务商城系统(四)商品管理


     对于商品的管理来说, 新增 商品,需要 增加 SPU 和 SKU; 修改 商品,需要 修改 SPU 和 SKU (新增 与 修改,和上篇博客中管理的 tb_template、tb_spec、tb_para 模板、规格、参数表,还和 tb_sku 某款商品中某类商品的信息表、 tb_spu 某款商品中某类商品的信息表 有关);审核,需要 修改审核状态;上架下架,需要 修改上架下架状态。
     而 删除商品,分为 :

  • 逻辑删除:修改了删除状态,“找回商品”,找回的一定是逻辑删除的商品。
  • 物理删除:真实删除了数据

一、SPU 与 SKU

1、SPU 与 SKU 的概念
  • SPU:Standard Product Unit ,标准产品单位,描述某一款商品的公共属性。SPU 是商品信息聚合的最小单位,是一组 可复用、易检索 的 标准化信息 的集合,该集合 描述了一个产品的特性,是对 同款商品的公共属性 抽取。通俗点讲,属性值、特性相同的货品 就可以称为一个 SPU,比如 华为 nova6SE 手机是一个 SPU 。
        
  • SKU: stock keeping unit,库存量单位,即库存进出计量的单位, 可以是以 、盒、托盘 等为单位。 SKU 是物理上不可分割的最小存货单元。在使用时要根据不同业态,不同管理模式来处理。在服装、鞋类商品中 使用 最多 最普遍。是 某个库存单位的商品独有属性 (某个商品的独有属性),比如 华为nova6SE 红色 64G 就是一个 SKU。
        
    在这里插入图片描述

     把重复的属性值抽取出来,更节省空间,不过缺点是,需要进行管理时,要将两个表关联起来。

2、tb_spu、tb_sku 表结构分析
  • tb_spu 表
    在这里插入图片描述
         可以看到,对于同款商品来说,这些属性都是公共属性。

     其中,在 tb_category 分类表中,parent_id=0 的记录就是 “一级分类”:
在这里插入图片描述
二级分类是将选中的一级分类作为子分类,继续查询,比如 ” 865 鞋靴”,查询它的子分类:
在这里插入图片描述
同理,再看看 866 流行男鞋的子分类,这是 “三级分类”:
在这里插入图片描述
总得来说,就是通过父节点 ID 去查询子分类。

  • tb_sku 表
    在这里插入图片描述
         tb_spu 和 tb_sku 两张表通过 spu_id 进行外键关联。
        

二、商品管理

     在新增 和 修改 商品中,主要是对规格 和 参数 的选择。
     规格 ,比如说 尺寸、网络(3G?4G?5G?)、颜色,是根据模板 ,比如说 手机、电视,选择出来的,使用者选择的分类 比如说 11536 手机配件,可以查出它属于模板 手机。总得来说,就是 category —> template——>spec。
     参数,类似于我们点开“商品详情” 时展示的参数,也是通过 template 选择出来的。

1、代码生成

     上一篇实战中写了很多增删改查方法,现在我们使用一个开源的 基于 Framemarker 模板引擎的 代码生成器,即便是一个工程几百个表,也可以瞬间完成代码的构建,用户只需要建立数据表结构,运行 main 方法就可以快速构建生成微服务工程的 pojo、Service、Controller 各层,并且可以生成 swagger API 模板等。用户通过自己开发模板,也可以生成 php、C#、C++、数据库存储过程等其他编程语言的代码。
开源地址:https://github.com/shenkunlin/code-template.git
     把它 clone 下来,然后放在 ChangGou 项目中。
     需要修改配置文件 application.properties 里的路径:

#pojo包路径
pojoPackage=com.changgou.goods.pojo
#Dao包路径
mapperPackage=com.changgou.goods.dao
#service接口包路径
serviceInterfacePackage=com.changgou.goods.service
#service接口实现包路径
serviceInterfaceImplPackage=com.changgou.goods.service.impl
#controller包路径
controllerPackage=com.changgou.goods.controller
#feign包路径
feignPackage=com.changgou.goods.feign
#是否启用swagger
enableSwagger=true
#swagger-ui 的路径
swaggeruipath=com.changgou.swagger
#服务名字,用于生成feign
serviceName=goods

#数据源配置
url=jdbc:mysql://192.168.211.132:3306/changgou_goods
uname=root
pwd=123456
driver=com.mysql.jdbc.Driver

     然后运行 main 方法即可,它就会为我们生成 dao、service、feign、swagger 包及代码,不过是在 code_template 下生成的,而且因为缺少依赖,会爆红,所以我们需要把这些代码拷贝到 changgou-parent 项目中:(feign 暂时不拷)
在这里插入图片描述
     可以看到,这些方法只是对于该表的增删改查,查包括条件查询、分页查询 和 分页条件查询,不过对于多表关联的情况,并没有实现关联查询,这部分还是需要手写的。
     一会儿会用到 ID 生成,我们可以使用 idWorker,在启动类 GoodsApplication 中添加以下代码,用于创建 IdWorker,并将它交给 Spring 容器。

public IdWorker idWorker(){
        return new IdWorker(0,0);
    }

IdWorker 是个实体类,在它的注释中看到,它用于生成 分布式自增长 ID,整体上按照时间自增排序,并且整个分布式系统内不会产生 ID 碰撞(由 datacenter 和 机器ID 作区分), 并且效率较高,经测试,snowflake 每秒能够产生26万ID左右,完全满足需要。
    

2、查询分类品牌数据
  • 根据父 ID 查询所有子分类

     按照上文对三个级别分类,需要在 CategoryService 中补充 根据 父 ID 查询所有子分类的方法。
    
在接口中新增方法:

/* 根据分类的父节点 ID 查询 所有子节点集合*/
    List<Category> findByParentId(Integer id);     

实现:

   public List<Category> findByParentId(Integer pid) {
        Category category=new Category();
        category.setParentId(pid);
        return categoryMapper.select(category);
    }             

提供控制层代码:

@GetMapping(value = "/list/{pid}")
    public Result<List<Category>> findByParentId(@PathVariable Integer pid){
       List<Category> categories= categoryService.findByParentId(pid);
        return new Result<>(true, StatusCode.OK,"查询子节点成功!",categories);
    }

     刚进入界面时需要查询 pid=0 也就是父节点的数据,然后根据父节点会去查询子节点。
    

  • 根据分类 ID 查询品牌集合

     我们可以通过 brandId 连结 category_brand 和 brand 表。

在接口中添加方法:

	/**
     * 根据分类 ID 查询品牌集合,
      * @param categoryid
     * @return
     */
   List<Brand> findByCategory(Integer categoryid);

实现:

 public List<Brand> findByCategory(Integer categoryid) {
        return brandMapper.findByCategory(categoryid);
    }

在 dao 层提供方法:

public interface BrandMapper extends Mapper<Brand> {
    @Select("select tb.* from tb_brand tb, tb_category_brand tcb where tb.id=tcb.brand_id and tcb.category_id=#{category};")
    List<Category> findByCategory(Integer categoryid);
}

控制层:

	@GetMapping(value = "/category/{id}")
    public Result<List<Brand>> findBrandByCategory(@PathVariable(value = "id")Integer categoryId){
        //调用Service查询品牌数据
        List<Brand> categoryList = brandService.findByCategory(categoryId);
        return new Result<List<Brand>>(true,StatusCode.OK,"查询成功!",categoryList);
    }

    

  • 模板查询

     当 用户 选中了 分类 后,需要根据分类的 ID 查询出 对应的模板数据,并将模板的名字进行显示。可以通过 template_id 将 category 表 和 template 表进行连结。

在接口中添加方法:

Template findByCategoryId(Integer id);

实现:

   public Template findByCategoryId(Integer id) {
        Category category = categoryMapper.selectByPrimaryKey(id);
        return templateMapper.selectByPrimaryKey(category.getTemplateId());
    }

或者可以自己写 sql 语句,在 dao 层添加方法:

@Select("select tem.* from tb_template tem,tb_category cat where tem.id=cat.template_id and cat.id=#{cateId}")
    Template findByCategory(Integer cateId);

控制层:

	 @GetMapping(value = "/category/{id}")
    public Result<Template> findByCategoryId(@PathVariable(value = "id") Integer id) {
        //调用Service查询
        Template template = templateService.findByCategoryId(id);
        return new Result<Template>(true, StatusCode.OK, "查询成功", template);
    }
  • 规格查询、参数查询

     都是通过 template_id 连结 分类表 和 规格 / 参数表。通过分类查询模板对应的规格、参数信息,代码就不展示了,为了后续前端页面调用不出错,路径保持一致即可。

     可以访问路径验证一下效果:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3、新增商品

     因为在新增商品时,需要 spu、sku 的信息,而且 sku 是集合的形式,所以,我们在 pojo 包封装一个 Javabean ,把 spu 和 List 作为属性:

public class Goods implements Serializable {
    private Spu spu;
    private List<Sku> skuList;

/* 构造方法和getter、setter 方法略*/

因为在 tb_spu 和 tb_sku 表中,ID 都不是自增的,所以需要使用 IdWorker 产生 ID 。
先在 SpuService 中新增方法:

 void saveGoods(Goods goods);

实现:

public void saveGoods(Goods goods) {
        Spu spu = goods.getSpu();
        spu.setId(idWorker.nextId());

        // 商品一发布就上架
        spu.setIsMarketable("1");
        spuMapper.insertSelective(spu);

        // Sku 集合
        List<Sku> skuList = goods.getSkuList();
        for (Sku sku : skuList) {
            sku.setId(idWorker.nextId());

            // name 由 spu name、规格组成
            String spuName = spu.getName();

            // 在数据库里规格字段:{"电视音响效果":"立体声","电视屏幕尺寸":"20英寸","尺码":"165"}
            // 是 json 形式,需要把它转换成 map

            // 防止空指针
            if (StringUtils.isEmpty(sku.getSpec())) {
                sku.setSpec("{}");
            }
            Map<String, String> specMap = JSON.parseObject(sku.getSpec(), Map.class);
            for (Map.Entry<String, String> entry : specMap.entrySet()) {
                spuName += "" + entry.getValue();
            }

            Date date = new Date();
            sku.setCreateTime(date);
            sku.setUpdateTime(date);
            sku.setSpuId(spu.getId());

            // 三级分类
            sku.setCategoryId(spu.getCategory3Id());

            // 品牌名称是要通过查询才能得到的,所以需要先查询品牌信息
            Brand brand = brandMapper.selectByPrimaryKey(spu.getBrandId());
            sku.setBrandName(brand.getName());

            // 将 sku 添加到数据库中
            skuMapper.insertSelective(sku);
        }
    }

暂不提供控制层。


     在修改商品信息前需要先查询出商品信息。


4、查询商品

     根据 ID 查询商品详细信息,这里的 ID 应该是 spu 的 ID(在前端页面选中商品图片时,这时是大的分类,比如 华为手机,就相当于选中了商品的 spu,所以应该根据 ID 进行查询,等到商品选定后,就需要进入商品详情页,对规格、参数 等进行细选,比如 颜色、网络、内存。前端页面选中图片后,我们后端就需要给前端传商品大类 和 详情的规格、参数等信息,也就是说 spu 和 sku 都需要提供的,那就是 Goods 类对象咯):
在 spuService 中添加方法:

  Goods findGoodsById(Long id);

实现:

  public Goods findGoodsById(Long id) {
        // 查询 spu
        Spu spu=spuMapper.selectByPrimaryKey(id);

        // 查询 sku 
        Sku sku=new Sku();
        sku.setSpuId(id);
        List<Sku> skuList=skuMapper.select(sku);
        
        Goods goods=new Goods(spu,skuList);
        return goods;
    }

控制层:

 @GetMapping("/goods/{id}")
    public Result<Goods> findGoodsById(@PathVariable(value = "id")Long spuid) {
        Goods goods=spuService.findGoodsById(spuid);
        return new Result<>(true,StatusCode.OK,"查询商品成功",goods);
    }

运行结果;
在这里插入图片描述

5、修改商品

     思路是 先删除原先全部的 sku ,再把现在选择的 sku 进行添加。
     添加商品的页面不只是第一次添加商品的时候会访问的,后续也可以进行商品信息的更新的。所以在上面的 saveGoods 方法里添加逻辑,判断是第一次添加商品,还是后续更新商品:

		// 修改商品需要先删除原先全部的 sku,再进行添加
        // 区分是直接添加商品,还是因为修改商品才进行添加
        // 根据 spu ID 是否为空
        if (spu.getId() == null) {
            
            // 直接添加商品
            spu.setId(idWorker.nextId());

            // 商品一发布就上架
            spu.setIsMarketable("1");
            
            spuMapper.insertSelective(spu);
      }else{
            // 修改商品
            spuMapper.updateByPrimaryKey(spu);

            // 将原先 spu 对应的 sku 全部删除
            Sku sku=new Sku();
            sku.setSpuId(spu.getId());
            spuMapper.delete(spu);
        }

     剩下的代码部分不变,是对 sku 的处理,为集合中的每一个 sku 设置主键、名字、分类、品牌等属性,并把每一“个”商品对应的 sku 信息插入数据库中。
    

6、审核商品

在这里插入图片描述
     从 tb_spu 表中可以看到,默认情况下是 已下架、未审核状态。(审核状态是在spu 表里的,说明审核是以一款商品为单位的,而不是一个一个商品进行审核。)

     审核商品的需求是这样的:

  • 需要校验是否是被删除的商品,如果未删除,则修改审核状态为1,即 “ 已审核”,并设置为”已上架“;

     下架商品的需求是这样的:

  • 需要校验是否是被删除的商品,如果未删除,则设置为 ”已下架“。

(也就是说,审核和下架的前提都是商品未被删除,即 商品要存在,这个前提成立的话,审核和下架就能通过。)

在 spuService 中添加方法:

 public void audit(Long spuId);

实现:

public void audit(Long spuId) {
    // 先查询商品
    Spu spu=spuMapper.selectByPrimaryKey(spuId);

    // 判断商品是否被删除
    if(spu.getIsDelete().equalsIgnoreCase("1")){
        throw new RuntimeException("不能对已删除的商品进行审核");
    }

    // 审核通过,并自动上架
    spu.setStatus("1");
    spu.setIsMarketable("1");
    spuMapper.updateByPrimaryKeySelective(spu);
}

控制层:

  @PutMapping("/audit/{id}")
    public Result audit(@PathVariable Long id) {
        spuService.audit(id);
        return new Result(true, StatusCode.OK, "审核通过" );
    }
7、下架商品

在 spuService 中添加方法:

public void pull(Long spuId);

实现:

  public void pull(Long spuId) {
        // 先查询商品
        Spu spu=spuMapper.selectByPrimaryKey(spuId);

        // 判断商品是否被删除
        if(spu.getIsDelete().equalsIgnoreCase("1")){
            throw new RuntimeException("此商品已删除");
        }
        
        spu.setIsMarketable("0");
        spuMapper.updateByPrimaryKeySelective(spu);
    }

控制层:

 @PutMapping("/pull/{id}")
    public Result pull(@PathVariable(value = "id") Long id) {
        spuService.pull(id);
        return new Result(true, StatusCode.OK, "下架成功");
    }
8、上架商品

实现类:

	public void put(Long spuId) {
        Spu spu=spuMapper.selectByPrimaryKey(spuId);
        
        // 检查是否删除
        if(spu.getIsDelete().equals("1")){
            throw new RuntimeException("此商品已删除!");
        }
        
        // 检查是否进行了审核
        if(!spu.getStatus().equals(1)){
            throw new RuntimeException("未通过审核的商品不能上架!");
        }
        
        // 上架状态
        spu.setIsMarketable("1");
        spuMapper.updateByPrimaryKeySelective(spu);
    }

控制层:

@PutMapping("/put/{id}")
public Result put(@PathVariable Long id){
    spuService.put(id);
    return new Result(true,StatusCode.OK,"上架成功");
}

    

9、批量上架

     前端传递一组商品 ID,后端进行批量上下架处理。思路是 从 tb_spu 表中先查询出符合 ID 集的记录,然后检查这些记录是否未删除、通过审核。 (我感觉“通过审核” 和 “上架”的逻辑重复了。只要通过审核就自动上架了啊,为什么还会有专门的上架操作?)
在 service 层添加方法:

  public int putMany(Long[] ids);

实现:

public int putMany(Long[] ids) {
    // 未删除 已审核的才能上架
    // update tb_sku set IsMarkable=1 where id in(ids) and isdelete=0 and status=1
    Example example=new Example(Spu.class);
    Example.Criteria criteria=example.createCriteria();
    criteria.andIn("id", Arrays.asList(ids));
    criteria.andEqualTo("is_delete",0);
    criteria.andEqualTo("status","1");
    
    Spu spu=new Spu();
    // 上架
    spu.setIsMarketable("1");
    spuMapper.updateByExampleSelective(spu,example);
}

控制层:

  @PostMapping("/put/many")
    public Result putMany(@RequestBody Long[] ids){
        int count=spuService.putMany(ids);
        return new Result(true,StatusCode.OK,"成功上架"+count+"个商品")
    }

运行结果:
在这里插入图片描述
去数据库里查这条记录:
在这里插入图片描述

10、批量下架

     下架和上架的代码大同小异,这里只贴实现:

 public int pullMany(Long[] ids) {
        // 未删除 已上架的才能下架
        // update tb_sku set IsMarkable=0 where id in(ids) and isdelete=0 and status=1
        Example example=new Example(Spu.class);
        Example.Criteria criteria=example.createCriteria();
        criteria.andIn("id", Arrays.asList(ids));
        criteria.andEqualTo("isMarketable","1");
        criteria.andEqualTo("isDelete",0);

        Spu spu=new Spu();
        // 下架
        spu.setIsMarketable("0");
        return spuMapper.updateByExampleSelective(spu,example);
    }
11、删除商品

     商品列表中的删除商品功能,并非真正的删除,只是将删除标记的字段设置为1。在回收站中有恢复商品的功能,将删除标记的字段设置为 0。在回收站中有删除商品的功能,是真正的物理删除。
    

  • 逻辑删除商品
    在 service 层提供方法:
public void logicDelete(Long spuId);

实现:

    @Transactional
    @Override
    public void logicDelete(Long spuId) {
        Spu spu=spuMapper.selectByPrimaryKey(spuId);

        // 检查是否是已下架商品
        if(!spu.getIsMarketable().equalsIgnoreCase("0")){
            throw new RuntimeException("必须先下架再删除!");

        }
        spu.setIsDelete("1");

        // 需要把这个商品置为未审核
        spu.setStatus("0");
        spuMapper.updateByPrimaryKeySelective(spu);
    }

控制层:

	@DeleteMapping("/logic/delete/{id}")
    public Result logicDelete(@PathVariable Long id) {
        spuService.logicDelete(id);
        return new Result(true, StatusCode.OK, "逻辑删除成功!");
    }
  • 还原被删除的商品
    添加方法:
public void restore(Long spuId);

实现:

public void restore(Long spuId) {
        Spu spu=spuMapper.selectByPrimaryKey(spuId);
        
        // 检查是否是未删除的商品
        if(!spu.getIsDelete().equalsIgnoreCase("1")){
            throw new RuntimeException("此商品未删除!");
        }
        
        // 逻辑恢复
        spu.setIsDelete("0");
        
        // 设置为未审核
        spu.setStatus("0");
        
        spuMapper.updateByPrimaryKeySelective(spu);
    }

控制层:

	@PutMapping("/restore/{id}")
    public Result restore(@PathVariable Long id){
        spuService.restore(id);
        return new Result(true,StatusCode.OK,"数据恢复成功!");
    }
  • 物理删除商品
public void delete(Long id);

实现(原先代码生成器帮我们生成了,现在需要加上物理删除的前提):

 public void delete(Long id) {
        // 检查是否被逻辑删除,必须先逻辑删除,才能被物理删除
        Spu spu=spuMapper.selectByPrimaryKey(id);
        if(!spu.getIsDelete().equalsIgnoreCase("1")){
            throw new RuntimeException("此商品不能删除!");
        }
        spuMapper.deleteByPrimaryKey(id);
    }
🎉 补充:为什么要实现 Serializable 接口

     实现序列化 Serializable接口 ,可以将一个对象的状态 (各个属性值)保存起来,然后在适当的时候再获得。
     序列化的过程就是对象写入字节流 (序列化) 和从字节流中读取对象(反序列化), 允许一个对象在虚拟机之间传送 (或者经过一段空间,如在RMI中;或者经过一段时间,比如数据流被保存到一个文件中)。对象序列化可以对对象进行深层复制。
     Java 对象序列化 将那些实现了 Serializable接口的对象 转换成一个字节序列,并能够以后将这个字节序列完全恢复为原来的对象。利用对象的序列化,可以实现轻量级持久性,这意味着一个对象的生存周期并不取决于程序是否正在执行,它可以生存于程序的调用之间。通过将一个序列化对象写入磁盘,然后在重新调用程序时恢复该对象,就能够实现持久性的效果。
    

三、总结

     spu 是某款商品公有的属性,sku 是某个商品独有的属性。在前端页面中先选定大的商品,比如 华为Nova6SE,然后再选择一些颜色、规格、网络等,也就是说对商品进行操作,既需要 spu ,又需要 sku,所以封装一个 javabean,把 spu 和 List作为属性。这一篇还是增删改查,而且有代码生成器给我们写好了增删改查。要注意的是一些细节上的逻辑设计,比如说 :
     审核商品需要先判断商品是否被删除,审核通过需要改变审核状态,并自动上架;
     上架商品需要先判断商品是否被删除,是否被审核;
     下架商品需要先判断商品是否被删除,需要先被删除才能下架的;
     逻辑删除商品需要先判断商品是否已下架,并把商品设置为未审核状态(否则上架时是没经过审核的,这样的情况在这个情景下没有差别,但是如果审核的逻辑比较复杂,是不能不审核就上架的。)
     恢复商品需要先判断商品是否已删除,并把商品设置为未审核状态。
     物理删除商品需要先判断是否进行果逻辑删除。

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值