谷粒商城--SPU和SKU(属性分组、规格参数、销售属性)

SPU和SKU

SPU:standard product unit(标准化产品单元):是商品信息聚合的最小单位,是一组可复用、易检索的标准化信息的集合,该集合描述了一个产品的特性。如iphone13是SPU,它是一个产品的集合

**SKU:stock keeping unit(库存量单位):**库存进出计量的基本单元,可以是件/盒/托盘等单位。

SKU是对于大型连锁超市DC配送中心物流管理的一个必要的方法。现在已经被引申为产品统一编号的简称,每种产品对应有唯一的SKU号。如iphone13ProMax 1T 蓝色 是SKU,包子店中肉包子是SKU,素包子是SKU,水煎包是SKU…

规格参数和销售属性

像这里的商品介绍,规格与包装都是属于SPU的属性。它们都属于是规格参数

image-20220806105620408

像版本,颜色等都属是SKU的销售属性

image-20220806105802342

表的关系理解

(1)属性关系-规格参数-销售属性-三级分类 关联关系

每个三级分类下有各自的属性分组表通过id和catelogid关联,能查出每个分类下的属性分组

属性分组表和属性表通过一个属性&属性关联表进行关联,能查出每个属性分组下的属性

最终这样的关系我们可以查出每个分类的属性分组和每个属性分组对应的属性

image-20220806110205216

(2)通过思维导图来理解

手机是一级分类,它下面又有属性组,每个属性组又有各自的属性

image-20220807230244792

(3)SPU-SKU属性表

商品属性表和属性表通过attridid进行关联,能查出每个spu的属性

sku销售属性表是为了表示spu下不同sku,比如1号spu在此表有两个sku,这两个sku有不同的销售属性,是通过和属性表关联获取

image-20220806110506836

(4)通过思维导图来理解

像网络、像素一般是固定不可选的所以是SPU属性

而内存、容量、颜色等可选的就为SKU销售属性

image-20220807231607457

导入前端代码

(1)重新执行“sys_menus.sql”,完善菜单。

正常我们是在系统管理里自定义添加,步骤都是一样的,其实在前端页面添加就是把数据提交到mall_admin表中,这里我们直接把提供的sql语句导入即可!

如下结果:

image-20220806100125647

(2)实现点击菜单的左边,能够实现在右边展示数据

image-20220806112654338

这个页面就是三级分类和一个表格显示在一块对吧,属于是父子组件交互

前端不具体写了,我们直接导入代码,效果如下:

image-20220807232635607

因为数据库没数据,所以这里不显示

完善后端接口

接口开发流程

什么是开发接口?

开发接口就是开发Controller、service、dao

在线接口文档如下https://easydoc.net/s/78237135/ZUqEdvA4/OXTgKobR
别人告诉你需要什么功能,需要返回什么样的数据,你就通过接口的形式把他们呢实现出来即可!

以后工作了也是这种形式,主要是开发接口为多,前端其实不用写太多,能看懂即可!!!

开发思路

思路:

  • 根据接口文档写接口
  • 代码中大部分接口已自动生成(MBG),所以我们在前端页面看哪个请求失败
  • 后端改接口

这一部分都是CRUD相关的代码,所以要好好练好好写!!!

属性分组详情

显示属性分组

接口如下:

image-20220810220705409

controller

    @RequestMapping("/list/{catelogId}")
    public R list(@RequestParam Map<String, Object> params,
                  @PathVariable("catelogId") Long catelogId){
//        PageUtils page = attrGroupService.queryPage(params);

        PageUtils page = attrGroupService.queryPage(params, catelogId);
        return R.ok().put("page", page);
    }

service

这里注意,前端有两个查询按钮

查询和查询全部

这两个都要有模糊查询的功能!

PageUtils queryPage(Map<String, Object> params, Long catelogId);


@Override
public PageUtils queryPage(Map<String, Object> params, Long catelogId) {
    //多条件查询
    String key = (String) params.get("key");
    QueryWrapper<AttrGroupEntity> wrapper = new QueryWrapper<>();
    if (!StringUtils.isEmpty(key)) {
        wrapper.and((obj) -> {
            obj.eq("attr_group_id",key).or().like("attr_group_name",key);
        });
    }
    if (catelogId == 0) {
        //如果是默认的是查全部的一级分类
        IPage<AttrGroupEntity> page = this.page(
            new Query<AttrGroupEntity>().getPage(params),
            wrapper);
        return new PageUtils(page);
    } else {
        wrapper.eq("catelog_id", catelogId);
        IPage<AttrGroupEntity> page = this.page(
            new Query<AttrGroupEntity>().getPage(params), wrapper);
        return new PageUtils(page);
    }
}

测试

image-20220811091620116

属性分组回显

这一部分主要是做属性分组的数据回显的

接口如下:

image-20220810220631503

前端这里省略,需要去elementui找组件,改数据

controller

/**
 * 信息
 */
@RequestMapping("/info/{attrId}")
public R info(@PathVariable("attrId") Long attrId){
	AttrEntity attr = attrService.getById(attrId);

    Long catelogId = attr.getCatelogId();
    Long[] path = categoryService.findCatelogPath(catelogId);
    attr.setCatelogPath(path);

    return R.ok().put("attr", attr);
}

service

获取分类路径id

通过递归操作完成

过程

给一个分类id,不断的查它的父类id直到查不到为止,最后把查询到的id到放到一个集合里

怎样写好递归?

  1. 确定参数值和返回值
  2. 确定终止条件
  3. 递归逻辑

三者缺一不可!!!

//找到catelogId的完整路径:[父/子/孙]
@Override
public Long[] findCatelogPath(Long catelogId) {
    ArrayList<Long> list = new ArrayList<>();
    List<Long> parentPath = findParentPath(catelogId, list);//1.确定递归参数和返回值

    Collections.reverse(parentPath);
    return (Long[]) list.toArray(new Long[parentPath.size()]);
}

private List<Long> findParentPath(Long catelogId,ArrayList<Long> list){
    //3.递归逻辑
    list.add(catelogId);
    CategoryEntity entity = this.getById(catelogId);
    if (entity.getParentCid()!=0){//2.递归终止条件
        findParentPath(entity.getParentCid(),list);
    }
    return list;
}

测试

返回属性的父路径id

image-20220811092606249

规格参数详情

接口如下:
image-20220810220725125

什么是规格参数?

image-20220808231624265

保存规格参数

controller

/**
 * 保存
 */
@RequestMapping("/save")
public R save(@RequestBody AttrVo vo){
	attrService.saveAttr(vo);
    return R.ok();
}

service

这里注意,因为添加规格参数的时候会有选择属性组,因为属性组和属性是通过关联关系表连接的所以要有级联操作。

在往pms_attr表插入数据的时候,pms_attr_group_relation也要插入

小bug:这里有个注意点,当添加规格参数的时候如果没有指定规格参数所属分组,那么就不应该在关联表中保存关联关系!!!

@Override
public void saveAttr(AttrVo attr) {
    AttrEntity attrEntity = new AttrEntity();
    //1.将前端接收数据的对象vo赋值给attrEntity对象,从而更新数据库
    BeanUtils.copyProperties(attr, attrEntity);
    this.save(attrEntity);

    if (attr.getAttrType() == ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode() && attr.getAttrGroupId() != null) {
        //2.保存关联关系
        //因为属性组和属性是通过关联关系表连接的
        AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity();
        relationEntity.setAttrGroupId(attr.getAttrGroupId());
        relationEntity.setAttrId(attrEntity.getAttrId());
        relationService.save(relationEntity);
    }

}
显示规格参数

controller

/**
 * 显示规格参数
 */
@GetMapping("/base/list/{catelogId}")
public R baseAttrList(@RequestParam Map<String, Object> params,
                      @PathVariable("catelogId") Integer catelogId) {
    PageUtils page = attrService.queryBaseAttrPage(params, catelogId);
    return R.ok().put("page", page);
}

service

//分页查询规格参数
@Override
public PageUtils queryBaseAttrPage(Map<String, Object> params, Integer catelogId) {
    QueryWrapper<AttrEntity> wrapper = new QueryWrapper<>();
    if (catelogId != 0) {
        //如果不是一级分类,那么查询的时候加上where catelog_id = ?
        wrapper.eq("catelog_id", catelogId);
    }

    //多条件模糊查询
    //搜索框里的key不但可以对catelog_id进行模糊查询,对attr_name也模糊查询
    String key = (String) params.get("key");
    if (!StringUtils.isEmpty(key)) {
        wrapper.eq("attr_id", key).or().like("attr_name", key);
    }

    //多条件分页查询
    IPage<AttrEntity> page = this.page(
        new Query<AttrEntity>().getPage(params),
        wrapper);

    PageUtils pageUtils = new PageUtils(page);
    return pageUtils;
}

测试

如下:

image-20220810205809739

这些属性的分类和所属分组怎么查呢?

规格参数表(pms_attr)中,有所属分类的信息,可以直接调用分类的service进行查询

那分组信息怎么查询呢?规格参数表中没有所属分类相关的信息…

这里我们就要借助第三张表,属性和分组表(pms_attr_attrgroup_relation)进行查询

通过规格参数表(pms_attr)获得attr_id,之后在调用属性和分组表的service获得属性和分组表的实体类,从而获得该属性的分组

下面通过stream流的方式,通过map给list集合中的每一项做映射给新实体类(AttrRespVo)赋值,最后返回AttrRespVo

小bug:这里显示规格参数的时候,会显示规格。参数对应的分组、分类,那么如果它们查出对象分组id或分类id为空那就不设置名字if (attrId != null && attrId.getAttrGroupId() != null) {…}

//分页查询规格参数
@Override
public PageUtils queryBaseAttrPage(Map<String, Object> params, String type, Integer catelogId) {
    QueryWrapper<AttrEntity> wrapper = new QueryWrapper<AttrEntity>()
        .eq("attr_type", "base".equalsIgnoreCase(type) ? ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode() : ProductConstant.AttrEnum.ATTR_TYPE_SALE.getCode());
    if (catelogId != 0) {
        //如果不是一级分类,那么查询的时候加上where catelog_id = ?
        //IgnoreCase忽略大小写
        wrapper.eq("catelog_id", catelogId);
    }

    //多条件模糊查询
    //搜索框里的key不但可以对catelog_id进行模糊查询,对attr_name也模糊查询
    String key = (String) params.get("key");
    if (!StringUtils.isEmpty(key)) {
        wrapper.eq("attr_id", key).or().like("attr_name", key);
    }

    //多条件分页查询
    IPage<AttrEntity> page = this.page(
        new Query<AttrEntity>().getPage(params),
        wrapper);

    PageUtils pageUtils = new PageUtils(page);


    List<AttrEntity> list = page.getRecords();
    //        .map()这个方法是对被筛选过后的流进行映射,一般是对属性进行赋值。
    List<AttrRespVo> resultList = list.stream().map(item -> {
        AttrRespVo attrRespvo = new AttrRespVo();
        BeanUtils.copyProperties(item, attrRespvo);
        //设置分类和分组的名字
        if ("base".equalsIgnoreCase(type)) {
            AttrAttrgroupRelationEntity attrId = relationService.
                getOne(new QueryWrapper<AttrAttrgroupRelationEntity>()
                       .eq("attr_id", item.getAttrId()));
            if (attrId != null && attrId.getAttrGroupId() != null) {
                //attrgroupRelationEntity.getAttrGroupId()也可以,这里可以直接放进去对象
                AttrGroupEntity attrGroupEntity = attrGroupService.getById(attrId.getAttrGroupId());
                attrRespvo.setGroupName(attrGroupEntity.getAttrGroupName());
            }
        }

        CategoryEntity categoryEntity = categoryService.getById(item.getCatelogId());
        if (categoryEntity != null) {
            attrRespvo.setCatelogName(categoryEntity.getName());
        }
        //返回最后的封装结果
        return attrRespvo;
    }).collect(Collectors.toList());

    //返回的结果是一个集合
    pageUtils.setList(resultList);

    //        返回分页后的集合对象
    return pageUtils;
}

AttrRespVo

@Data
public class AttrRespVo extends AttrVo {
    private String catelogName;
    private String  groupName;
}

测试

image-20220811084206902

规格参数回显

可以看出所属分类和分组都是由这条请求查询的,那么我们改这个接口功能就行

相当于在原来查询基础上返回分类路径信息分组信息

image-20220810221107404

controller

/**
 * 信息
 */
@RequestMapping("/info/{attrId}")
public R info(@PathVariable("attrId") Long attrId) {
    AttrRespVo respVo = attrService.getAttrInfo(attrId);
    return R.ok().put("attr", respVo);
}

service

@Override
public AttrRespVo getAttrInfo(Long attrId) {
    AttrRespVo respVo = new AttrRespVo();
    AttrEntity attrEntity = this.getById(attrId);
    BeanUtils.copyProperties(attrEntity, respVo);

    /**
     * 设置分组信息
     */
    AttrAttrgroupRelationEntity attrgroupRelationEntity = relationService.
            getOne(new QueryWrapper<AttrAttrgroupRelationEntity>()
                    .eq("attr_id", attrEntity.getAttrId()));
    if (attrgroupRelationEntity != null){
        respVo.setAttrGroupId(attrgroupRelationEntity.getAttrGroupId());

        Long attrGroupId = attrgroupRelationEntity.getAttrGroupId();
        AttrGroupEntity attrGroupEntity = attrGroupService.getById(attrGroupId);
        if (attrGroupEntity != null) {
            respVo.setGroupName(attrGroupEntity.getAttrGroupName());
        }
    }


    /**
     * 设置分类信息
     */
    Long catelogId = attrEntity.getCatelogId();
    //有了分类的完整路径,接下来就设置分类名字
    Long[] catelogPath = categoryService.findCatelogPath(catelogId);
    respVo.setCatelogPath(catelogPath);

    //获得分类名字
    CategoryEntity categoryEntity = categoryService.getById(catelogId);
    if (categoryEntity != null) {
        respVo.setCatelogName(categoryEntity.getName());
    }

    return respVo;
}

测试

image-20220810232827723

修改Or增加

提交修改分类和分组是无效的?

更改用的还是默认的update方法,所以我们改update接口!

image-20220810232652674

controller

/**
 * 修改
 */
@RequestMapping("/update")
public R update(@RequestBody AttrVo attr) {
    attrService.updateAttr(attr);

    return R.ok();
}

service

这里做了优化,对于规格参数中没有所属分组的,如果指定了不在是修改而是添加!

怎么判断规格参数有没有所属分组呢?

拿attr_id去pms_attr_attrgroup_relation表中查询,如果改attr_id存在与该表,那就修改关联关系

如果没有数据,那么就在此表添加数据!

@Transactional
@Override
public void updateAttr(AttrVo attr) {
    AttrEntity attrEntity = new AttrEntity();
    BeanUtils.copyProperties(attr, attrEntity);
    this.updateById(attrEntity);
    //修改分组关联
    AttrAttrgroupRelationEntity attrAttrgroupRelationEntity = new AttrAttrgroupRelationEntity();

    attrAttrgroupRelationEntity.setAttrGroupId(attr.getAttrGroupId());
    attrAttrgroupRelationEntity.setAttrId(attr.getAttrId());

    //统计attr_id的关联属性,如果没有初始分组,则进行添加操作;有则进行修改操作
    Integer count = relation.selectCount(new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_id", attr.getAttrId()));
    if (count > 0) {
        relation.update(attrAttrgroupRelationEntity, new UpdateWrapper<AttrAttrgroupRelationEntity>().eq("attr_id", attr.getAttrId()));
    } else {
        relation.insert(attrAttrgroupRelationEntity);
    }
}
spu规格维护

出现400页面,在数据库添加

INSERT INTO sys_menu (menu_id, parent_id, name, url, perms, type, icon, order_num) VALUES (76, 37, '规格维护', 'product/attrupdate', '', 2, 'log', 0);

更新index.js,哪里更新?找老师的源码

controller

@PostMapping("/update/{spuId}")
public R updateSpuAttr(@PathVariable("spuId") Long spuId,
                       @RequestBody List<ProductAttrValueEntity> entities){

    productAttrValueService.updateSpuAttr(spuId,entities);

    return R.ok();
}

impl

这里的修改其实是先把原来的spu_id下的属性都删除掉

之后在把前端传来的属性集合进行批量保存

@Transactional(rollbackFor = Exception.class)
@Override
public void updateSpuAttr(Long spuId, List<ProductAttrValueEntity> entities) {
    //1、删除spuId之前对应的所有属性
    this.baseMapper.delete(new QueryWrapper<ProductAttrValueEntity>().eq("spu_id",spuId));

    //2、添加商品规格信息
    List<ProductAttrValueEntity> collect = entities.stream().map(item -> {
        item.setSpuId(spuId);
        return item;
    }).collect(Collectors.toList());

    //批量新增
    this.saveBatch(collect);
}

销售属性详情

显示销售属性

如图http://localhost:88/api/product/attr/sale/list/0?t=1660181297434&page=1&limit=10&key=这个接口有问题!

所以我们就去后端改这个接口即可!

image-20220811092412567

controller

规格参数和销售参数的区别在于type的值,type为 1是规格参数type为0是销售参数

这里采用一个方法当两个来用!

image-20220811094628873

@GetMapping("/{attrType}/list/{catelogId}")
public R baseAttrList(@RequestParam Map<String, Object> params,
                      @PathVariable("attrType") String type,
                      @PathVariable("catelogId") Integer catelogId) {
    PageUtils page = attrService.queryBaseAttrPage(params, type, catelogId);
    return R.ok().put("page", page);
}

service

在原来对规格参数的基础上加了限制条件,如果是规格参数那就是WHERE attr_type = 1,否则就是WHERE attr_type = 0;

下面的逻辑和查询规格参数一致,都要模糊查询

这里为了使代码更通用,1和0的值我们写一个常量来控制,如过后期换值了我们直接更改常量的值即可

ProductConstant

package com.caq.common.constant;

public class ProductConstant {

    public enum AttrEnum{
        ATTR_TYPE_BASE(1,"基本属性"),
        ATTR_TYPE_SALE(0,"销售属性");

        private int code;

        private String msg;

        AttrEnum(int code,String msg){
            this.code = code;
            this.msg = msg;
        }

        public int getCode(){
            return code;
        }

        public String getMsg(){
            return msg;
        }
    }
}

在原来对规格参数的基础上加了限制条件,如果是规格参数那就是WHERE attr_type = 1,否则就是WHERE attr_type = 0;

@Override
    public PageUtils queryBaseAttrPage(Map<String, Object> params, String type, Integer catelogId) {
        QueryWrapper<AttrEntity> wrapper = new QueryWrapper<AttrEntity>()
                .eq("attr_type", "base".equalsIgnoreCase(type) ? ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode() : ProductConstant.AttrEnum.ATTR_TYPE_SALE.getCode());

        if (catelogId != 0) {
            //如果不是一级分类,那么查询的时候加上where catelog_id = ?
            //IgnoreCase忽略大小写
            wrapper.eq("catelog_id", catelogId);
        }

        //多条件模糊查询
        //搜索框里的key不但可以对catelog_id进行模糊查询,对attr_name也模糊查询
        String key = (String) params.get("key");
        if (!StringUtils.isEmpty(key)) {
            wrapper.eq("attr_id", key).or().like("attr_name", key);
        }

        //多条件分页查询
        IPage<AttrEntity> page = this.page(
                new Query<AttrEntity>().getPage(params),
                wrapper);

        PageUtils pageUtils = new PageUtils(page);

        List<AttrEntity> list = page.getRecords();
//        .map()这个方法是对被筛选过后的流进行映射,一般是对属性进行赋值。
        List<AttrRespVo> resultList = list.stream().map(item -> {
            AttrRespVo attrRespvo = new AttrRespVo();
            BeanUtils.copyProperties(item, attrRespvo);
            AttrAttrgroupRelationEntity attrgroupRelationEntity = relationService.
                    getOne(new QueryWrapper<AttrAttrgroupRelationEntity>()
                            .eq("attr_id", item.getAttrId()));

            if (attrgroupRelationEntity != null) {
                //attrgroupRelationEntity.getAttrGroupId()也可以,这里可以直接放进去对象
                AttrGroupEntity attrGroupEntity = attrGroupService.getById(attrgroupRelationEntity);
                attrRespvo.setGroupName(attrGroupEntity.getAttrGroupName());
            }

            CategoryEntity categoryEntity = categoryService.getById(item.getCatelogId());
            if (categoryEntity != null) {
                attrRespvo.setCatelogName(categoryEntity.getName());
            }
            //返回最后的封装结果
            return attrRespvo;
        }).collect(Collectors.toList());

        //返回的结果是一个集合
        pageUtils.setList(resultList);

//        返回分页后的集合对象
        return pageUtils;
    }
销售属性回显

可以看到,销售属性回显是不需要所属分组的

但是销售属性规格参数用的是同一个回显方法,我们也进行更改,只有是规格参数的时候才进行分组回显

image-20220811104643952

在原分组回显的逻辑上加上判断,后面逻辑不变!

if (attrEntity.getAttrType() == ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode()) {
    ...
}

image-20220811104920477

修改销售属性

销售属性规格参数用的是同一个修改方法,销售属性进行修改时,会对关联表进行一个级联更新,但销售属性不需要

所以也在对关联表级联更新的时候进行判断,只有销售属性修改的时候才进行级联更新!

if (attrEntity.getAttrType() == ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode()) {
    ...
}

image-20220811105227416

保存销售属性

销售属性规格参数用的是同一个保存方法,销售属性进行保存时,会对关联表进行一个级联保存,但销售属性不需要

所以也在对关联表级联保存的时候进行判断,只有销售属性保存的时候才进行级联保存!

if (attrEntity.getAttrType() == ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode()) {
    ...
}

image-20220811105415725

购物商城Spu-Sku数据库设计主要是为了管理商品的库存销售信息。Spu(Standard Product Unit)是商品的标准产品单位,通常指的是一组具有相同特征但可能有不同规格的商品,例如同一款衣服的不同颜色或尺码。Sku(Stock Keeping Unit)是商品的库存管理单位,是对Spu的具体细分,用于区分不同规格属性的商品。 在数据库设计中,可以建立两个主要的表:SpuSku表。Spu表用于存储商品的基本信息,包括商品的名称、描述、品牌、分类等。此外,可以为Spu表添加一些扩展字段,例如商品的图片、销售状态等。 Sku表用于存储商品的具体规格库存信息,其中包括Spu的外键关联、商品的属性规格、价格库存数量等。通过外键关联,可以将Sku与其对应的Spu关联起来,实现SpuSku的多对一关系。同时,可以在Sku表中添加一些扩展字段,例如商品的条形码、上架时间等。 为了提高查询效率,可以在Sku表中添加索引,例如根据商品的价格、库存数量、销售状态等字段进行索引,以快速获取满足条件的商品信息。 此外,为了提高系统的可维护性可扩展性,可以添加一些辅助表,例如属性属性值表,用于管理商品的属性信息。属性表用于存储商品的属性名称,属性值表用于存储属性的具体取值范围。 总之,购物商城Spu-Sku数据库设计需要考虑SpuSku之间的关联关系,以及商品的基本信息规格信息的存储管理。通过合理的设计优化索引,可以提高系统的查询性能用户体验。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小蔡coding

口袋空空

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值