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
*/
@TableId
private 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
/**
* 属性分组列表查询
*/
@Override
public 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]
*/
@Override
public 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,2
private List<Long> findParentPath(Long catelogId, List<Long> paths) {
// 收集当前节点ID
paths.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
/***
* 封装请求和响应的数据
*/
@Data
public 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
@Override
public 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
@Data
public 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
@Override
public 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);
// 获取实体信息,并进行再次封装到AttrRespVo
List<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
*/
@Override
public 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
@Override
public 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;
}
}
}