1、SPU与SKU介绍
1)SPU
SPU:Standard Product Unit(标准化产品单元) 。是商品信息聚合的最小单位,是一组可复用、易检索的标准化信息的集合,该集合描述了一 个产品的特性。

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
2、属性分组
1)数据库设计
create table pms_attr_group (attr_group_id bigint not null auto_increment comment '分组id',attr_group_name char(20) comment '组名',sort int comment '排序',descript varchar(255) comment '描述',icon varchar(255) comment '组图标',catelog_id bigint comment '所属分类id',primary key (attr_group_id));alter table pms_attr_group comment '属性分组';
2)AttrGroupEntity
/*** 属性分组*/@Data@TableName("pms_attr_group")public class AttrGroupEntity implements Serializable {private static final long serialVersionUID = 1L;/*** 分组id*/@TableIdprivate Long attrGroupId;/*** 组名*/private String attrGroupName;/*** 排序*/private Integer sort;/*** 描述*/private String descript;/*** 组图标*/private String icon;/*** 所属分类id*/private Long catelogId;@TableField(exist = false)private Long[] catelogPath;}
3)获取分类下的属性分组
① AttrGroupController
/*** 获取分类下的属性分组* 路径变量必须用@PathVariable标注*/@RequestMapping("/list/{catelogId}")//@RequiresPermissions("product:attrgroup:list")public R list(@RequestParam Map<String, Object> params,@PathVariable("catelogId") Long catelogId){PageUtils page = attrGroupService.queryPage(params, catelogId);return R.ok().put("page", page);}
② AttrGroupServiceImpl
/*** 属性分组列表查询*/@Overridepublic PageUtils queryPage(Map<String, Object> params, Long catelogId) {String key = (String) params.get("key");LambdaQueryWrapper<AttrGroupEntity> wrapper = new LambdaQueryWrapper<AttrGroupEntity>();if(!StringUtils.isEmpty(key)){wrapper.and(obj -> {// 根据分组id精确查询,或者根据分组名称模糊查询obj.eq(AttrGroupEntity::getAttrGroupId,key).or().like(AttrGroupEntity::getAttrGroupName,key);});}// 查询所有数据(进入属性分组页面,默认查询所有数据)if(catelogId == 0){IPage<AttrGroupEntity> page = this.page(new Query<AttrGroupEntity>().getPage(params), wrapper);return new PageUtils(page);}else {// 带分类的查询wrapper.eq(AttrGroupEntity::getCatelogId, catelogId);IPage<AttrGroupEntity> page = this.page(new Query<AttrGroupEntity>().getPage(params), wrapper);return new PageUtils(page);}}
③ 分页工具类PageUtils
public class PageUtils implements Serializable {private static final long serialVersionUID = 1L;/*** 总记录数*/private int totalCount;/*** 每页记录数*/private int pageSize;/*** 总页数*/private int totalPage;/*** 当前页数*/private int currPage;/*** 列表数据*/private List<?> list;/*** 分页* @param list 列表数据* @param totalCount 总记录数* @param pageSize 每页记录数* @param currPage 当前页数*/public PageUtils(List<?> list, int totalCount, int pageSize, int currPage) {this.list = list;this.totalCount = totalCount;this.pageSize = pageSize;this.currPage = currPage;this.totalPage = (int)Math.ceil((double)totalCount/pageSize);}/*** 分页*/public PageUtils(IPage<?> page) {this.list = page.getRecords();this.totalCount = (int)page.getTotal();this.pageSize = (int)page.getSize();this.currPage = (int)page.getCurrent();this.totalPage = (int)page.getPages();}其他get、set*****public List<?> getList() {return list;}public void setList(List<?> list) {this.list = list;}}
④ 查询参数Query
public class Query<T> {public IPage<T> getPage(Map<String, Object> params) {return this.getPage(params, null, false);}public IPage<T> getPage(Map<String, Object> params,String defaultOrderField,boolean isAsc) {//分页参数long curPage = 1;long limit = 10;if(params.get(Constant.PAGE) != null){curPage = Long.parseLong((String)params.get(Constant.PAGE));}if(params.get(Constant.LIMIT) != null){limit = Long.parseLong((String)params.get(Constant.LIMIT));}//分页对象Page<T> page = new Page<>(curPage, limit);//分页参数params.put(Constant.PAGE, page);//排序字段//防止SQL注入(因为sidx、order是通过拼接SQL实现排序的,会有SQL注入风险)String orderField = SQLFilter.sqlInject((String)params.get(Constant.ORDER_FIELD));String order = (String)params.get(Constant.ORDER);//前端字段排序if(StringUtils.isNotEmpty(orderField) && StringUtils.isNotEmpty(order)){if(Constant.ASC.equalsIgnoreCase(order)) {return page.addOrder(OrderItem.asc(orderField));}else {return page.addOrder(OrderItem.desc(orderField));}}//没有排序字段,则不排序if(StringUtils.isBlank(defaultOrderField)){return page;}//默认排序if(isAsc) {page.addOrder(OrderItem.asc(defaultOrderField));}else {page.addOrder(OrderItem.desc(defaultOrderField));}return page;}}
4)查询属性分组详情
① AttrGroupController
// 查询属性分组详情@RequestMapping("/info/{attrGroupId}")public R info(@PathVariable("attrGroupId") Long attrGroupId){AttrGroupEntity attrGroup = attrGroupService.getById(attrGroupId);Long catelogId = attrGroup.getCatelogId();Long[] path = categoryService.findCatelogPath(catelogId);attrGroup.setCatelogPath(path);return R.ok().put("attrGroup", attrGroup);}
② CategoryServiceImpl
/*** 找到catelogId的完整路径;* [父/子/孙]* @param catelogId* @return [2,25,225]*/@Overridepublic Long[] findCatelogPath(Long catelogId) {List<Long> paths = new ArrayList<>();List<Long> finalPath = findParentPath(catelogId, paths);[225,25,2] -> [2,25,225]Collections.reverse(finalPath);// 集合转数组return finalPath.toArray(new Long[finalPath.size()]);}// 得到的结果为:225,25,2private List<Long> findParentPath(Long catelogId, List<Long> paths) {// 收集当前节点IDpaths.add(catelogId);CategoryEntity categoryEntity = baseMapper.selectById(catelogId);if(categoryEntity.getParentCid() != 0){// 判断父节点id是否为0,然后递归查询以上的所有节点数据findParentPath(categoryEntity.getParentCid(), paths);}return paths;}
3、规格属性
1)数据库设计
create table pms_attr (attr_id bigint not null auto_increment comment '属性id',attr_name char(30) comment '属性名',search_type tinyint comment '是否需要检索[0-不需要,1-需要]',icon varchar(255) comment '属性图标',value_select char(255) comment '可选值列表[用逗号分隔]',attr_type tinyint comment '属性类型[0-销售属性,1-基本属性,2-既是销售属性又是基本属性]',enable bigint comment '启用状态[0 - 禁用,1 - 启用]',catelog_id bigint comment '所属分类',show_desc tinyint comment '快速展示【是否展示在介绍上;0-否 1-是】,在sku中仍然可以调整',primary key (attr_id));alter table pms_attr comment '商品属性';create table pms_attr_attrgroup_relation (id bigint not null auto_increment comment 'id',attr_id bigint comment '属性id',attr_group_id bigint comment '属性分组id',attr_sort int comment '属性组内排序',primary key (id));alter table pms_attr_attrgroup_relation comment '属性&属性分组关联';
2)新增规格参数
① AttrVo
/**** 封装请求和响应的数据*/@Datapublic class AttrVo {/*** 属性id*/private Long attrId;/*** 属性名*/private String attrName;/*** 是否需要检索[0-不需要,1-需要]*/private Integer searchType;/*** 属性图标*/private String icon;/*** 值类型*/private Integer valueType;/*** 可选值列表[用逗号分隔]*/private String valueSelect;/*** 属性类型[0-销售属性,1-基本属性,2-既是销售属性又是基本属性]*/private Integer attrType;/*** 启用状态[0 - 禁用,1 - 启用]*/private Long enable;/*** 所属分类*/private Long catelogId;/*** 快速展示【是否展示在介绍上;0-否 1-是】,在sku中仍然可以调整*/private Integer showDesc;/*** 分组id*/private Long attrGroupId;}
② AttrController
/*** 保存*/@RequestMapping("/save")public R save(@RequestBody AttrVo attr){attrService.saveAttr(attr);return R.ok();}
③ AttrServiceImpl
@Transactional@Overridepublic void saveAttr(AttrVo attr) {AttrEntity attrEntity = new AttrEntity();BeanUtils.copyProperties(attr,attrEntity);// 1、保存基本数据this.save(attrEntity);// 2、若前端选择了所属分组,且选择基本属性,则需要保存与分组的关联关系if(attr.getAttrGroupId() != null && attr.getAttrType() == ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode()){AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity();relationEntity.setAttrId(attrEntity.getAttrId()); // attr.getAttrId()为空relationEntity.setAttrGroupId(attr.getAttrGroupId());attrAttrgroupRelationDao.insert(relationEntity);}}
3)规格参数列表
① AttrRespVo
@Datapublic class AttrRespVo extends AttrVo{/*** 规格参数所属分类名称*/private String catelogName;/*** 所属分组名称*/private String groupName;/*** 分类path*/private Long[] catelogPath;}
② AttrController
/*** 列表查询* @param params* @param catelogId* @return*/@GetMapping("/{attrType}/list/{catelogId}")public R baseAttrList(@RequestParam Map<String, Object> params,@PathVariable("catelogId") Long catelogId,@PathVariable("attrType") String attrType){PageUtils page = attrService.queryBaseAttrPage(params, catelogId, attrType);return R.ok().put("page", page);}
③ AttrServiceImpl
@Overridepublic PageUtils queryBaseAttrPage(Map<String, Object> params, Long catelogId, String attrType) {LambdaQueryWrapper<AttrEntity> wrapper = new LambdaQueryWrapper<>();int code = "base".equalsIgnoreCase(attrType) ? ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode(): ProductConstant.AttrEnum.ATTR_TYPE_SALE.getCode();wrapper.eq(AttrEntity::getAttrType, code);String key = (String) params.get("key");if(!StringUtils.isEmpty(key)){wrapper.and(obj -> {// 根据属性id精确查询,或根据属性名称模糊查询obj.eq(AttrEntity::getAttrId,key).or().like(AttrEntity::getAttrName,key);});}// 如果传有分类id,则关联查询if(catelogId != 0){wrapper.eq(AttrEntity::getCatelogId,catelogId);}// 否则查询所有IPage<AttrEntity> page = this.page(new Query<AttrEntity>().getPage(params), wrapper);// 获取实体信息,并进行再次封装到AttrRespVoList<AttrEntity> records = page.getRecords();List<AttrRespVo> list = records.stream().map(attrEntity -> {AttrRespVo attrRespVo = new AttrRespVo();BeanUtils.copyProperties(attrEntity, attrRespVo);// 1、如果是基本属性,需要设置分组的名字if("base".equalsIgnoreCase(attrType)){LambdaQueryWrapper<AttrAttrgroupRelationEntity> wrapper1 = new LambdaQueryWrapper<AttrAttrgroupRelationEntity>();wrapper1.eq(AttrAttrgroupRelationEntity::getAttrId, attrEntity.getAttrId());// 一个属性最多只会对应单个分类下的单个分组AttrAttrgroupRelationEntity relationEntity = attrAttrgroupRelationDao.selectOne(wrapper1);if (relationEntity != null) {AttrGroupEntity attrGroupEntity = attrGroupDao.selectById(relationEntity.getAttrGroupId());if(attrGroupEntity != null){attrRespVo.setGroupName(attrGroupEntity.getAttrGroupName());}}}// 2、设置分类的名字CategoryEntity categoryEntity = categoryDao.selectById(attrEntity.getCatelogId());if (categoryEntity != null) {attrRespVo.setCatelogName(categoryEntity.getName());}return attrRespVo;}).collect(Collectors.toList());PageUtils pageUtils = new PageUtils(page);pageUtils.setList(list);return pageUtils;}
4)查询规格参数详情
① AttrController
/*** 信息*/@RequestMapping("/info/{attrId}")public R info(@PathVariable("attrId") Long attrId){AttrRespVo attrRespVo = attrService.getAttrInfo(attrId);return R.ok().put("attr", attrRespVo);}
② AttrServiceImpl
/*** 查询属性详情,并返回分类及分组信息* @param attrId* @return*/@Overridepublic AttrRespVo getAttrInfo(Long attrId) {AttrRespVo attrRespVo = new AttrRespVo();// 1、返回当前属性的详情信息AttrEntity attrEntity = baseMapper.selectById(attrId);BeanUtils.copyProperties(attrEntity,attrRespVo);// 2、返回分类的全路径信息List<Long> paths = new ArrayList<>();Long catelogId = attrEntity.getCatelogId();paths = this.findFinalPath(catelogId, paths);Collections.reverse(paths);Long[] longs = paths.toArray(new Long[paths.size()]);attrRespVo.setCatelogPath(longs);// 3、若是基本属性,需要返回分组信息if(attrEntity.getAttrType() == ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode()){LambdaQueryWrapper<AttrAttrgroupRelationEntity> wrapper = new LambdaQueryWrapper<>();wrapper.eq(AttrAttrgroupRelationEntity::getAttrId, attrId);AttrAttrgroupRelationEntity relationEntity = attrAttrgroupRelationDao.selectOne(wrapper);if(relationEntity != null){attrRespVo.setAttrGroupId(relationEntity.getAttrGroupId());AttrGroupEntity attrGroupEntity = attrGroupDao.selectById(relationEntity.getAttrGroupId());if(attrGroupEntity != null){attrRespVo.setGroupName(attrGroupEntity.getAttrGroupName());}}}return attrRespVo;}private List<Long> findFinalPath(Long catelogId, List<Long> paths) {// 1、首先将自身加入分类路径paths.add(catelogId);// 2、将对应的父id加入分类路径CategoryEntity categoryEntity = categoryDao.selectById(catelogId);if(categoryEntity.getParentCid() != 0){ // 只要不是顶层分类,就递归查询父分类findFinalPath(categoryEntity.getParentCid(),paths);}return paths;}
4)修改规格参数
① AttrController
/*** 修改*/@RequestMapping("/update")public R update(@RequestBody AttrVo attr){attrService.updateAttr(attr);return R.ok();}
② AttrServiceImpl
/*** 修改属性* @param attr*/@Transactional@Overridepublic void updateAttr(AttrVo attr) {// 1、修改自身数据AttrEntity attrEntity = new AttrEntity();BeanUtils.copyProperties(attr,attrEntity);baseMapper.updateById(attrEntity);// 2、若是基本属性,需要修改分组if(attrEntity.getAttrType() == ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode()){AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity();relationEntity.setAttrGroupId(attr.getAttrGroupId());relationEntity.setAttrId(attr.getAttrId());// 2.1 查询先前是否存在关联关系LambdaQueryWrapper<AttrAttrgroupRelationEntity> wrapper = new LambdaQueryWrapper<>();wrapper.eq(AttrAttrgroupRelationEntity::getAttrId, relationEntity.getAttrId());Integer count = attrAttrgroupRelationDao.selectCount(wrapper);if(count > 0){// 2.2 修改操作if(attr.getAttrGroupId() != null){attrAttrgroupRelationDao.update(relationEntity,wrapper);}else {// 删除原分组attrAttrgroupRelationDao.delete(wrapper);}}else {// 2.3 新增操作if(attr.getAttrGroupId() != null) {attrAttrgroupRelationDao.insert(relationEntity);}}}}
4、销售属性

package com.atguigu.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;}}}



