1、SPU与SKU介绍

1)SPU

SPU:Standard Product Unit(标准化产品单元) 。是商品信息聚合的最小单位,是一组可复用、易检索的标准化信息的集合,该集合描述了一 个产品的特性。
image.png
image.png
iphoneX 是 SPU、MI 8 是 SPU
iphoneX 64G 黑曜石 是 SKU
MI8 8+64G+黑色 是 SKU

2)SKU

SKU:Stock Keeping Unit(库存量单位)。即库存进出计量的基本单元,可以是以件,盒,托盘等为单位。SKU 这是对于大型连锁超市 DC(配送中心)物流管理的一个必要的方法。现在已经被引申为产品统一编号的简称,每种产品均对应有唯一的 SKU 号

3)基本属性【规格参数】与销售属性

每个分类下的商品共享规格参数,与销售属性。只是有些商品不一定要用这个分类下全部的属性;

  • 属性是以三级分类组织起来的
  • 规格参数中有些是可以提供检索的
  • 规格参数也是基本属性,他们具有自己的分组
  • 属性的分组也是以三级分类组织起来的
  • 属性名确定的,但是值是每一个商品不同来决定的
  • SPU对应基本属性,即SPU下的所有SKU的基本属性都相同
  • 销售属性用于区分SPU下的不同SKU

image.png
image.png
image.png
image.png

2、属性分组

image.png

1)数据库设计

  1. create table pms_attr_group (
  2. attr_group_id bigint not null auto_increment comment '分组id',
  3. attr_group_name char(20) comment '组名',
  4. sort int comment '排序',
  5. descript varchar(255) comment '描述',
  6. icon varchar(255) comment '组图标',
  7. catelog_id bigint comment '所属分类id',
  8. primary key (attr_group_id)
  9. );
  10. alter table pms_attr_group comment '属性分组';

2)AttrGroupEntity

  1. /**
  2. * 属性分组
  3. */
  4. @Data
  5. @TableName("pms_attr_group")
  6. public class AttrGroupEntity implements Serializable {
  7. private static final long serialVersionUID = 1L;
  8. /**
  9. * 分组id
  10. */
  11. @TableId
  12. private Long attrGroupId;
  13. /**
  14. * 组名
  15. */
  16. private String attrGroupName;
  17. /**
  18. * 排序
  19. */
  20. private Integer sort;
  21. /**
  22. * 描述
  23. */
  24. private String descript;
  25. /**
  26. * 组图标
  27. */
  28. private String icon;
  29. /**
  30. * 所属分类id
  31. */
  32. private Long catelogId;
  33. @TableField(exist = false)
  34. private Long[] catelogPath;
  35. }

3)获取分类下的属性分组

① AttrGroupController

  1. /**
  2. * 获取分类下的属性分组
  3. * 路径变量必须用@PathVariable标注
  4. */
  5. @RequestMapping("/list/{catelogId}")
  6. //@RequiresPermissions("product:attrgroup:list")
  7. public R list(@RequestParam Map<String, Object> params,
  8. @PathVariable("catelogId") Long catelogId){
  9. PageUtils page = attrGroupService.queryPage(params, catelogId);
  10. return R.ok().put("page", page);
  11. }

② AttrGroupServiceImpl

  1. /**
  2. * 属性分组列表查询
  3. */
  4. @Override
  5. public PageUtils queryPage(Map<String, Object> params, Long catelogId) {
  6. String key = (String) params.get("key");
  7. LambdaQueryWrapper<AttrGroupEntity> wrapper = new LambdaQueryWrapper<AttrGroupEntity>();
  8. if(!StringUtils.isEmpty(key)){
  9. wrapper.and(obj -> {
  10. // 根据分组id精确查询,或者根据分组名称模糊查询
  11. obj.eq(AttrGroupEntity::getAttrGroupId,key).or().like(AttrGroupEntity::getAttrGroupName,key);
  12. });
  13. }
  14. // 查询所有数据(进入属性分组页面,默认查询所有数据)
  15. if(catelogId == 0){
  16. IPage<AttrGroupEntity> page = this.page(new Query<AttrGroupEntity>().getPage(params), wrapper);
  17. return new PageUtils(page);
  18. }else {
  19. // 带分类的查询
  20. wrapper.eq(AttrGroupEntity::getCatelogId, catelogId);
  21. IPage<AttrGroupEntity> page = this.page(new Query<AttrGroupEntity>().getPage(params), wrapper);
  22. return new PageUtils(page);
  23. }
  24. }

③ 分页工具类PageUtils

  1. public class PageUtils implements Serializable {
  2. private static final long serialVersionUID = 1L;
  3. /**
  4. * 总记录数
  5. */
  6. private int totalCount;
  7. /**
  8. * 每页记录数
  9. */
  10. private int pageSize;
  11. /**
  12. * 总页数
  13. */
  14. private int totalPage;
  15. /**
  16. * 当前页数
  17. */
  18. private int currPage;
  19. /**
  20. * 列表数据
  21. */
  22. private List<?> list;
  23. /**
  24. * 分页
  25. * @param list 列表数据
  26. * @param totalCount 总记录数
  27. * @param pageSize 每页记录数
  28. * @param currPage 当前页数
  29. */
  30. public PageUtils(List<?> list, int totalCount, int pageSize, int currPage) {
  31. this.list = list;
  32. this.totalCount = totalCount;
  33. this.pageSize = pageSize;
  34. this.currPage = currPage;
  35. this.totalPage = (int)Math.ceil((double)totalCount/pageSize);
  36. }
  37. /**
  38. * 分页
  39. */
  40. public PageUtils(IPage<?> page) {
  41. this.list = page.getRecords();
  42. this.totalCount = (int)page.getTotal();
  43. this.pageSize = (int)page.getSize();
  44. this.currPage = (int)page.getCurrent();
  45. this.totalPage = (int)page.getPages();
  46. }
  47. 其他getset*****
  48. public List<?> getList() {
  49. return list;
  50. }
  51. public void setList(List<?> list) {
  52. this.list = list;
  53. }
  54. }

④ 查询参数Query

  1. public class Query<T> {
  2. public IPage<T> getPage(Map<String, Object> params) {
  3. return this.getPage(params, null, false);
  4. }
  5. public IPage<T> getPage(Map<String, Object> params,
  6. String defaultOrderField,
  7. boolean isAsc) {
  8. //分页参数
  9. long curPage = 1;
  10. long limit = 10;
  11. if(params.get(Constant.PAGE) != null){
  12. curPage = Long.parseLong((String)params.get(Constant.PAGE));
  13. }
  14. if(params.get(Constant.LIMIT) != null){
  15. limit = Long.parseLong((String)params.get(Constant.LIMIT));
  16. }
  17. //分页对象
  18. Page<T> page = new Page<>(curPage, limit);
  19. //分页参数
  20. params.put(Constant.PAGE, page);
  21. //排序字段
  22. //防止SQL注入(因为sidx、order是通过拼接SQL实现排序的,会有SQL注入风险)
  23. String orderField = SQLFilter.sqlInject((String)params.get(Constant.ORDER_FIELD));
  24. String order = (String)params.get(Constant.ORDER);
  25. //前端字段排序
  26. if(StringUtils.isNotEmpty(orderField) && StringUtils.isNotEmpty(order)){
  27. if(Constant.ASC.equalsIgnoreCase(order)) {
  28. return page.addOrder(OrderItem.asc(orderField));
  29. }else {
  30. return page.addOrder(OrderItem.desc(orderField));
  31. }
  32. }
  33. //没有排序字段,则不排序
  34. if(StringUtils.isBlank(defaultOrderField)){
  35. return page;
  36. }
  37. //默认排序
  38. if(isAsc) {
  39. page.addOrder(OrderItem.asc(defaultOrderField));
  40. }else {
  41. page.addOrder(OrderItem.desc(defaultOrderField));
  42. }
  43. return page;
  44. }
  45. }

4)查询属性分组详情

image.png

① AttrGroupController

  1. // 查询属性分组详情
  2. @RequestMapping("/info/{attrGroupId}")
  3. public R info(@PathVariable("attrGroupId") Long attrGroupId){
  4. AttrGroupEntity attrGroup = attrGroupService.getById(attrGroupId);
  5. Long catelogId = attrGroup.getCatelogId();
  6. Long[] path = categoryService.findCatelogPath(catelogId);
  7. attrGroup.setCatelogPath(path);
  8. return R.ok().put("attrGroup", attrGroup);
  9. }

② CategoryServiceImpl

  1. /**
  2. * 找到catelogId的完整路径;
  3. * [父/子/孙]
  4. * @param catelogId
  5. * @return [2,25,225]
  6. */
  7. @Override
  8. public Long[] findCatelogPath(Long catelogId) {
  9. List<Long> paths = new ArrayList<>();
  10. List<Long> finalPath = findParentPath(catelogId, paths);
  11. [225,25,2] -> [2,25,225]
  12. Collections.reverse(finalPath);
  13. // 集合转数组
  14. return finalPath.toArray(new Long[finalPath.size()]);
  15. }
  16. // 得到的结果为:225,25,2
  17. private List<Long> findParentPath(Long catelogId, List<Long> paths) {
  18. // 收集当前节点ID
  19. paths.add(catelogId);
  20. CategoryEntity categoryEntity = baseMapper.selectById(catelogId);
  21. if(categoryEntity.getParentCid() != 0){
  22. // 判断父节点id是否为0,然后递归查询以上的所有节点数据
  23. findParentPath(categoryEntity.getParentCid(), paths);
  24. }
  25. return paths;
  26. }

3、规格属性

image.png

1)数据库设计

  1. create table pms_attr (
  2. attr_id bigint not null auto_increment comment '属性id',
  3. attr_name char(30) comment '属性名',
  4. search_type tinyint comment '是否需要检索[0-不需要,1-需要]',
  5. icon varchar(255) comment '属性图标',
  6. value_select char(255) comment '可选值列表[用逗号分隔]',
  7. attr_type tinyint comment '属性类型[0-销售属性,1-基本属性,2-既是销售属性又是基本属性]',
  8. enable bigint comment '启用状态[0 - 禁用,1 - 启用]',
  9. catelog_id bigint comment '所属分类',
  10. show_desc tinyint comment '快速展示【是否展示在介绍上;0-否 1-是】,在sku中仍然可以调整',
  11. primary key (attr_id)
  12. );
  13. alter table pms_attr comment '商品属性';
  14. create table pms_attr_attrgroup_relation (
  15. id bigint not null auto_increment comment 'id',
  16. attr_id bigint comment '属性id',
  17. attr_group_id bigint comment '属性分组id',
  18. attr_sort int comment '属性组内排序',
  19. primary key (id)
  20. );
  21. alter table pms_attr_attrgroup_relation comment '属性&属性分组关联';

2)新增规格参数

① AttrVo

  1. /***
  2. * 封装请求和响应的数据
  3. */
  4. @Data
  5. public class AttrVo {
  6. /**
  7. * 属性id
  8. */
  9. private Long attrId;
  10. /**
  11. * 属性名
  12. */
  13. private String attrName;
  14. /**
  15. * 是否需要检索[0-不需要,1-需要]
  16. */
  17. private Integer searchType;
  18. /**
  19. * 属性图标
  20. */
  21. private String icon;
  22. /**
  23. * 值类型
  24. */
  25. private Integer valueType;
  26. /**
  27. * 可选值列表[用逗号分隔]
  28. */
  29. private String valueSelect;
  30. /**
  31. * 属性类型[0-销售属性,1-基本属性,2-既是销售属性又是基本属性]
  32. */
  33. private Integer attrType;
  34. /**
  35. * 启用状态[0 - 禁用,1 - 启用]
  36. */
  37. private Long enable;
  38. /**
  39. * 所属分类
  40. */
  41. private Long catelogId;
  42. /**
  43. * 快速展示【是否展示在介绍上;0-否 1-是】,在sku中仍然可以调整
  44. */
  45. private Integer showDesc;
  46. /**
  47. * 分组id
  48. */
  49. private Long attrGroupId;
  50. }

② AttrController

  1. /**
  2. * 保存
  3. */
  4. @RequestMapping("/save")
  5. public R save(@RequestBody AttrVo attr){
  6. attrService.saveAttr(attr);
  7. return R.ok();
  8. }

③ AttrServiceImpl

  1. @Transactional
  2. @Override
  3. public void saveAttr(AttrVo attr) {
  4. AttrEntity attrEntity = new AttrEntity();
  5. BeanUtils.copyProperties(attr,attrEntity);
  6. // 1、保存基本数据
  7. this.save(attrEntity);
  8. // 2、若前端选择了所属分组,且选择基本属性,则需要保存与分组的关联关系
  9. if(attr.getAttrGroupId() != null && attr.getAttrType() == ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode()){
  10. AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity();
  11. relationEntity.setAttrId(attrEntity.getAttrId()); // attr.getAttrId()为空
  12. relationEntity.setAttrGroupId(attr.getAttrGroupId());
  13. attrAttrgroupRelationDao.insert(relationEntity);
  14. }
  15. }

3)规格参数列表

① AttrRespVo

  1. @Data
  2. public class AttrRespVo extends AttrVo{
  3. /**
  4. * 规格参数所属分类名称
  5. */
  6. private String catelogName;
  7. /**
  8. * 所属分组名称
  9. */
  10. private String groupName;
  11. /**
  12. * 分类path
  13. */
  14. private Long[] catelogPath;
  15. }

② AttrController

  1. /**
  2. * 列表查询
  3. * @param params
  4. * @param catelogId
  5. * @return
  6. */
  7. @GetMapping("/{attrType}/list/{catelogId}")
  8. public R baseAttrList(@RequestParam Map<String, Object> params,
  9. @PathVariable("catelogId") Long catelogId,
  10. @PathVariable("attrType") String attrType){
  11. PageUtils page = attrService.queryBaseAttrPage(params, catelogId, attrType);
  12. return R.ok().put("page", page);
  13. }

③ AttrServiceImpl

  1. @Override
  2. public PageUtils queryBaseAttrPage(Map<String, Object> params, Long catelogId, String attrType) {
  3. LambdaQueryWrapper<AttrEntity> wrapper = new LambdaQueryWrapper<>();
  4. int code = "base".equalsIgnoreCase(attrType) ? ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode()
  5. : ProductConstant.AttrEnum.ATTR_TYPE_SALE.getCode();
  6. wrapper.eq(AttrEntity::getAttrType, code);
  7. String key = (String) params.get("key");
  8. if(!StringUtils.isEmpty(key)){
  9. wrapper.and(obj -> {
  10. // 根据属性id精确查询,或根据属性名称模糊查询
  11. obj.eq(AttrEntity::getAttrId,key).or().like(AttrEntity::getAttrName,key);
  12. });
  13. }
  14. // 如果传有分类id,则关联查询
  15. if(catelogId != 0){
  16. wrapper.eq(AttrEntity::getCatelogId,catelogId);
  17. }
  18. // 否则查询所有
  19. IPage<AttrEntity> page = this.page(new Query<AttrEntity>().getPage(params), wrapper);
  20. // 获取实体信息,并进行再次封装到AttrRespVo
  21. List<AttrEntity> records = page.getRecords();
  22. List<AttrRespVo> list = records.stream().map(attrEntity -> {
  23. AttrRespVo attrRespVo = new AttrRespVo();
  24. BeanUtils.copyProperties(attrEntity, attrRespVo);
  25. // 1、如果是基本属性,需要设置分组的名字
  26. if("base".equalsIgnoreCase(attrType)){
  27. LambdaQueryWrapper<AttrAttrgroupRelationEntity> wrapper1 = new LambdaQueryWrapper<AttrAttrgroupRelationEntity>();
  28. wrapper1.eq(AttrAttrgroupRelationEntity::getAttrId, attrEntity.getAttrId());
  29. // 一个属性最多只会对应单个分类下的单个分组
  30. AttrAttrgroupRelationEntity relationEntity = attrAttrgroupRelationDao.selectOne(wrapper1);
  31. if (relationEntity != null) {
  32. AttrGroupEntity attrGroupEntity = attrGroupDao.selectById(relationEntity.getAttrGroupId());
  33. if(attrGroupEntity != null){
  34. attrRespVo.setGroupName(attrGroupEntity.getAttrGroupName());
  35. }
  36. }
  37. }
  38. // 2、设置分类的名字
  39. CategoryEntity categoryEntity = categoryDao.selectById(attrEntity.getCatelogId());
  40. if (categoryEntity != null) {
  41. attrRespVo.setCatelogName(categoryEntity.getName());
  42. }
  43. return attrRespVo;
  44. }).collect(Collectors.toList());
  45. PageUtils pageUtils = new PageUtils(page);
  46. pageUtils.setList(list);
  47. return pageUtils;
  48. }

image.png

4)查询规格参数详情

① AttrController

  1. /**
  2. * 信息
  3. */
  4. @RequestMapping("/info/{attrId}")
  5. public R info(@PathVariable("attrId") Long attrId){
  6. AttrRespVo attrRespVo = attrService.getAttrInfo(attrId);
  7. return R.ok().put("attr", attrRespVo);
  8. }

② AttrServiceImpl

  1. /**
  2. * 查询属性详情,并返回分类及分组信息
  3. * @param attrId
  4. * @return
  5. */
  6. @Override
  7. public AttrRespVo getAttrInfo(Long attrId) {
  8. AttrRespVo attrRespVo = new AttrRespVo();
  9. // 1、返回当前属性的详情信息
  10. AttrEntity attrEntity = baseMapper.selectById(attrId);
  11. BeanUtils.copyProperties(attrEntity,attrRespVo);
  12. // 2、返回分类的全路径信息
  13. List<Long> paths = new ArrayList<>();
  14. Long catelogId = attrEntity.getCatelogId();
  15. paths = this.findFinalPath(catelogId, paths);
  16. Collections.reverse(paths);
  17. Long[] longs = paths.toArray(new Long[paths.size()]);
  18. attrRespVo.setCatelogPath(longs);
  19. // 3、若是基本属性,需要返回分组信息
  20. if(attrEntity.getAttrType() == ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode()){
  21. LambdaQueryWrapper<AttrAttrgroupRelationEntity> wrapper = new LambdaQueryWrapper<>();
  22. wrapper.eq(AttrAttrgroupRelationEntity::getAttrId, attrId);
  23. AttrAttrgroupRelationEntity relationEntity = attrAttrgroupRelationDao.selectOne(wrapper);
  24. if(relationEntity != null){
  25. attrRespVo.setAttrGroupId(relationEntity.getAttrGroupId());
  26. AttrGroupEntity attrGroupEntity = attrGroupDao.selectById(relationEntity.getAttrGroupId());
  27. if(attrGroupEntity != null){
  28. attrRespVo.setGroupName(attrGroupEntity.getAttrGroupName());
  29. }
  30. }
  31. }
  32. return attrRespVo;
  33. }
  34. private List<Long> findFinalPath(Long catelogId, List<Long> paths) {
  35. // 1、首先将自身加入分类路径
  36. paths.add(catelogId);
  37. // 2、将对应的父id加入分类路径
  38. CategoryEntity categoryEntity = categoryDao.selectById(catelogId);
  39. if(categoryEntity.getParentCid() != 0){ // 只要不是顶层分类,就递归查询父分类
  40. findFinalPath(categoryEntity.getParentCid(),paths);
  41. }
  42. return paths;
  43. }

4)修改规格参数

① AttrController

  1. /**
  2. * 修改
  3. */
  4. @RequestMapping("/update")
  5. public R update(@RequestBody AttrVo attr){
  6. attrService.updateAttr(attr);
  7. return R.ok();
  8. }

② AttrServiceImpl

  1. /**
  2. * 修改属性
  3. * @param attr
  4. */
  5. @Transactional
  6. @Override
  7. public void updateAttr(AttrVo attr) {
  8. // 1、修改自身数据
  9. AttrEntity attrEntity = new AttrEntity();
  10. BeanUtils.copyProperties(attr,attrEntity);
  11. baseMapper.updateById(attrEntity);
  12. // 2、若是基本属性,需要修改分组
  13. if(attrEntity.getAttrType() == ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode()){
  14. AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity();
  15. relationEntity.setAttrGroupId(attr.getAttrGroupId());
  16. relationEntity.setAttrId(attr.getAttrId());
  17. // 2.1 查询先前是否存在关联关系
  18. LambdaQueryWrapper<AttrAttrgroupRelationEntity> wrapper = new LambdaQueryWrapper<>();
  19. wrapper.eq(AttrAttrgroupRelationEntity::getAttrId, relationEntity.getAttrId());
  20. Integer count = attrAttrgroupRelationDao.selectCount(wrapper);
  21. if(count > 0){
  22. // 2.2 修改操作
  23. if(attr.getAttrGroupId() != null){
  24. attrAttrgroupRelationDao.update(relationEntity,wrapper);
  25. }else {
  26. // 删除原分组
  27. attrAttrgroupRelationDao.delete(wrapper);
  28. }
  29. }else {
  30. // 2.3 新增操作
  31. if(attr.getAttrGroupId() != null) {
  32. attrAttrgroupRelationDao.insert(relationEntity);
  33. }
  34. }
  35. }
  36. }

4、销售属性

image.png

  1. package com.atguigu.common.constant;
  2. public class ProductConstant {
  3. public enum AttrEnum{
  4. ATTR_TYPE_BASE(1,"基本属性"),
  5. ATTR_TYPE_SALE(0,"销售属性");
  6. private int code;
  7. private String msg;
  8. AttrEnum(int code,String msg){
  9. this.code = code;
  10. this.msg = msg;
  11. }
  12. public int getCode() {
  13. return code;
  14. }
  15. public String getMsg() {
  16. return msg;
  17. }
  18. }
  19. }