SPU:standard product unit(标准化产品单元):是商品信息聚合的最小单位,是一组可复用、易检索的标准化信息的集合,该集合描述了一个产品的特性。如iphone13是SPU,它是一个产品的集合
**SKU:stock keeping unit(库存量单位):**库存进出计量的基本单元,可以是件/盒/托盘等单位。
SKU是对于大型连锁超市DC配送中心物流管理的一个必要的方法。现在已经被引申为产品统一编号
的简称,每种产品对应有唯一的SKU号。如iphone13ProMax 1T 蓝色 是SKU,包子店中肉包子是SKU,素包子是SKU,水煎包是SKU…
像这里的商品介绍,规格与包装
都是属于SPU的属性。它们都属于是规格参数
像版本,颜色等都属是SKU的销售属性
(1)属性关系-规格参数-销售属性-三级分类 关联关系
每个三级分类下有各自的属性分组表通过id和catelogid关联
,能查出每个分类下的属性分组
属性分组表和属性表通过一个属性&属性关联表
进行关联,能查出每个属性分组下的属性
最终这样的关系我们可以查出每个分类的属性分组
和每个属性分组对应的属性
(2)通过思维导图来理解
手机是一级分类,它下面又有属性组,每个属性组又有各自的属性
(3)SPU-SKU属性表
商品属性表和属性表通过attrid
和id
进行关联,能查出每个spu的属性
sku销售属性表是为了表示spu下不同sku
,比如1号spu在此表有两个sku,这两个sku有不同的销售属性,是通过和属性表关联获取
的
(4)通过思维导图来理解
像网络、像素一般是固定不可选的所以是SPU属性
而内存、容量、颜色等可选的就为SKU销售属性
(1)重新执行“sys_menus.sql”,完善菜单。
正常我们是在系统管理里自定义添加,步骤都是一样的,其实在前端页面添加就是把数据提交到mall_admin表中,这里我们直接把提供的sql语句导入即可!
如下结果:
(2)实现点击菜单的左边,能够实现在右边展示数据
这个页面就是三级分类和一个表格显示在一块对吧,属于是父子组件交互
前端不具体写了,我们直接导入代码,效果如下:
因为数据库没数据,所以这里不显示
开发接口就是开发Controller、service、dao
在线接口文档如下https://easydoc.net/s/78237135/ZUqEdvA4/OXTgKobR
别人告诉你需要什么功能,需要返回什么样的数据,你就通过接口的形式把他们呢实现出来即可!
以后工作了也是这种形式,主要是开发接口为多,前端其实不用写太多,能看懂即可!!!
思路:
这一部分都是CRUD相关的代码,所以要好好练好好写!!!
接口如下:
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);
}
}
测试
这一部分主要是做属性分组的数据回显的
接口如下:
前端这里省略,需要去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到放到一个集合里
怎样写好递归?
- 确定参数值和返回值
- 确定终止条件
- 递归逻辑
三者缺一不可!!!
//找到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
接口如下:
什么是规格参数?
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;
}
测试
如下:
这些属性的分类和所属分组怎么查呢?
规格参数表(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;
}
测试
可以看出所属分类和分组都是由这条请求查询的,那么我们改这个接口功能就行
相当于在
原来查询基础上
返回分类路径信息
,分组信息
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;
}
测试
提交修改分类和分组是无效的?
更改用的还是默认的update方法,所以我们改update接口!
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);
}
}
出现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=
这个接口有问题!
所以我们就去后端改这个接口即可!
controller
规格参数和销售参数的区别在于type的值,
type为 1是规格参数
,type为0是销售参数
这里采用一个方法当两个来用!
@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;
}
可以看到,销售属性回显是不需要所属分组的
但是
销售属性
和规格参数
用的是同一个回显方法
,我们也进行更改,只有是规格参数的时候才进行分组回显
!
在原分组回显的逻辑上加上判断,后面逻辑不变!
if (attrEntity.getAttrType() == ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode()) {
...
}
销售属性
和规格参数
用的是同一个修改方法
,销售属性进行修改时,会对关联表进行一个级联更新,但销售属性不需要所以也在对关联表级联更新的时候进行判断,只有销售属性修改的时候才进行级联更新!
if (attrEntity.getAttrType() == ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode()) {
...
}
销售属性
和规格参数
用的是同一个保存方法
,销售属性进行保存时,会对关联表进行一个级联保存,但销售属性不需要所以也在对关联表级联保存的时候进行判断,只有销售属性保存的时候才进行级联保存!
if (attrEntity.getAttrType() == ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode()) {
...
}