SPU 与SKU:SPU 是Java 中的类,SKU 是Java 中的对象。
这些基本信息是属于SPU 里面的。而一个具体的版本: 黑色 128G IPhone Xs 就是属于SKU。SKU 都会共享SPU 的所有特征,比如128G 的IPhone Xs 都有基本信息和主体,这些信息统称为SPU 的规格与属性或者基本包装。
不同的手机产品,他们的规格性都是不一样的,但是他们的SPU 的主体属性是一样的。
决定手机的价格的是销售属性。
SPU 决定基本属性的值,SKU 决定销售属性的值。
基于以上的观点,设计出属性表:
机身颜色就代表一个属性,基本信息是包含机身颜色的分组,所以属性和分组是相互关联的。
属性分组表:
维护属性与分组之间关系的表:
商品本身具有属性,所以商品与属性相关联,因为每个商品都关联SPU ,所以也有SPU_ID。SPU 信息在另外一张表进行维护:spu_info。
spu_info 表维护的是每个商品的SPU 信息。
SPU 中包含许多的属性,这些属性在另外一张表进行维护:product_attr_value
每个商品有SPU,也有SKU。在sku_info
每一个SKU 都有自己的图片(不同颜色的手机)
在进行销售时,SKU 之间的属性会有不同的搭配(不同的颜色搭配不同的内存…),这些信息保存在sku_sale_attr_value 表中。
总体关系
要做到的效果
因为左边布局都是原来在category 中的菜单,所以这个菜单数据就可以抽取出来当成一个组件。
在其他类中引入该菜单组件。最终以标签的形式进行使用。
效果图:
表格中的前端代码直接从逆向生成的代码中获取,然后复制attrgroup.vue 的全部代码
里面需要导入一个AddOrUpdate 的组件
效果图:
实现功能:
当点击菜单列表中的数据时,右边表格能动态显示出属于它的数据。
这里要用到Vue 中的高级功能。父子组件传递数据。
- 子组件给父组件传递数据,事件机制:子组件给父组件发送一个事件,携带上数据。
给common的category 组件的tree 标签添加上node-click 事件,该事件有三个参数,依次为:传递给 data 属性的数组中该节点所对应的对象(数据库中的数据)、节点对应的 Node、节点组件本身。
那这些数据如何让attrgroup 知道呢?
1.先把数据从子组件发给父组件
第一个参数是给这次发送事件取名,随便取。后面都是给父组件发送的数据。
2.让父组件感知这些数据。
将子组件的事件名作为父组件的绑定事件,并写一个方法接收传输过来的参数
当我们点击一个三级分类,就要显示出他的信息,
在请求的路径上要携带上三级分类的id,如果没有三级分类,那么这里要传0。0代表查询所有
在service 层创建该方法
然后在ServiceImpl 中对其进行实现
修改前端的请求
效果图:
点击新增按钮时,所属分类选项应该是一个级联框。里面的数据是菜单中的数据。
在级联框中,数据的属性是这样的。
请求数据,就是请求Category 的getMenu() 中的数据
规定该方法在vue 创建时就已经把菜单数据请求到了。但是它里面的数据属性是这样的:
要把这些数据放进级联框中,必须要以级联框存储数据的方式来。
optiions : 级联框,里面的数据就是category
props
要想级联框中有值,就要将里面的value,label,children 都附上对应的值
这里的catId,name,children 都是category 中的值。
效果图:
发现问题:
当数据没有children,即children 是空数据时,页面依然会渲染出一个级联图。
解决:在后台把数据传给前端时,把chidren 为空的数据都不赋予children 字段。
当children 不为空的时候,才将children 字段进行返回。
该功能是源于Jackson 包下的功能。
注意一点,当我们
当绑定级联框中数据的时候,因为级联了很多数据,所以应该绑定一个数组,但是我们发送请求的时候是将数组的最后一个数据发送过去,所以要再定义一个catelogId 进行接收。
点击确定时候,发送的时候是发送级联框数组中最后一个数据。
分组修改 & 级联选择器回显
当点击修改按钮时,要回显所有数据
发现所属分类无法回显,为什么?
因为返回的数据只有当时新增时存储的级联选择器数组的最后一位数据。
我们希望能返回完成的数据
catelogPath 就是catelogIds.
那么data 数据就应该带上catelogPath 属性。
看到Controller 返回的AttrGroupEntity 对象中没有catelogPath 字段,那么就给它加上该字段。
因为在数据库字段中没有catelogPath 字段,所以希望在Service 层能有一个方法能将该字段返回。因为查出三级分类的id 是应该属于categoryService 的功能,所以为它新增一个这样的方法。
通过CategoryService 中的两个方法来整理出一个完成的catelogPath
然后数据就能正常显示了。
但是因为修改和新增都公用同一个对话框,所以里面在绑定的数据中,catelogPath 也是一样的,当点击完修改以后,catelogPath 数组就有了值;关闭了当前修改对话框,打开新增对话框时,catelogPath 数组还是有值,所以就要给对话框绑定一个事件:当关闭对话框时,清空catelogPath 的数据。
级联选择器可搜索功能:
在级联框中可以输入信息,并且进行数据的搜索。
只需要加上属性:filterable 即可
提示功能:
引入分页插件:
以为我们用的是Mybatis-plus,以及Spring-boot 框架。
这样前端的分页效果的ok 了
模糊查询功能:
接下来就要把老师给的代码中的moudles 中的product 和common 文件都替换到我们的项目中。
编写关联分类功能:
一个品牌可能会有多个分类,比如小米,即造手机,也有家具。一个分类下也会有多个品牌。这是一个多对多的关系。那么这时在数据库中就会有一张中间表。
对该接口进行维护。
从接口文档中看出,该接口的响应数据信息为下图红色框。
我们需要返回的数据:catelogId, catelogname 在数据库表:pms_category_brand_relation 都有,所以直接查询该表数据并返回即可。
接下来编写按下确定按钮时的保存操作。
当点击确定按钮时,根据触发的url ,在后端编写响应代码。
在开发接口文档中看出:请求参数有brandId 和catelogId
重写save(),因为逆向生成的save() 并不保存brand_name,catelog_name,因为可以通过brand_id,catelog_id 与其他表进行关联从而得到这两个数据。但是在电商系统中,关联查询是很浪费数据库资源的,所以宁愿在本表中亦或者在其他表中一点一点查,也不进行关联查询。所以我们改写的save() 要有brand_name,和catelog_name
品牌关联分类就写完了。
但是因为brand_name 和catelog_name 是其他表的字段,如果其他表的字段进行了修改,那么这里的数据也要进行修改。换句话说,因为brand_name 属于品牌部分,所以如果有对品牌进行修改,就要检测它的brand_name 是否也进行了修改,如果修改了,那么就连同此表中的数据一起修改。
因为更新的数据不仅仅是自己的,还要更新别人的,所以这里要开启事务。
关于更新catelog_name
为catelog_name 在Mapper 中自定义sql 语句
接下来编写属性分类的功能:
查询有三个逻辑:1,。单击左边三级数据时,根据菜单的catId 进行查询。 2. 在输入框中输入关键字,然后进行查询。 3. 单击左边三级数据时,输入框中也有数据。
为三级菜单节点添加上点击事件,一点击就能出发查询。
输入框数据与vue 的数据进行绑定
查询时,catId 作为url,而查询框中的key 则作为url 中的参数。
Controller:
对应sql 语句:
关联属性功能:因为每个产品都有自己的分组,分组中由又自己的属性。 要为分组添加关联属性,因为目前还没有数据,所以要现在数据库中添加一些数据。
因为Controller 接收的参数的字段和Entity 对象的字段会有些许不同,以前会直接修改Entity 对象字段,现在是直接创建另一个对象,只不过这个对象大部分属性还是和Entity 对象相同(甚至说Entity 对象有的字段VO 对象都会有,并且VO 对象还比Entity 多几个字段),还有一部分对象是为了迎合前端传来的数据或者后端传来的数据而创建的。
创建VO 对象:
属性保存,即要在属性自己的表保存,还要在维护分组和属性的表中进行信息的保存
编写功能:查询
因为查询都是要返回分页数据的,所以后端是直接返回一个page,里面包含数据信息和分页信息。
在AttrService 中
但是返回的Entity 对象中没有所属分类和所属分组字段,所以要编写一个VO 对象。
返回的对象具有AttrVo 对象的字段,还多两个catelogName 和groupName 字段。所以直接extend AttrVo 对象即可。
attr 表只记录catelog_id 而没有catelog_name,这个可以通过多余字段进行查询。但groupName 在另一张表中,且不能用联表查询,所以要进行分开查询。
AttrServcieImpl 层:
@Override
public PageUtils queryBaseAttrPage(Map<String, Object> params, Long catelogId, String type) {
QueryWrapper<AttrEntity> queryWrapper = new QueryWrapper<AttrEntity>().eq("attr_type","base".equalsIgnoreCase(type)?ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode():ProductConstant.AttrEnum.ATTR_TYPE_SALE.getCode());
if(catelogId != 0){
queryWrapper.eq("catelog_id",catelogId);
}
String key = (String) params.get("key");
if(!StringUtils.isEmpty(key)){
//attr_id attr_name
queryWrapper.and((wrapper)->{
wrapper.eq("attr_id",key).or().like("attr_name",key);
});
}
IPage<AttrEntity> page = this.page(
new Query<AttrEntity>().getPage(params),
queryWrapper
);
PageUtils pageUtils = new PageUtils(page);
//因为AttrEntity 中字段不能满足前端显示,所以要借助VO 对象
//下面操作都是将AttrEntity 对象的字段值转移到VO 对象中。
List<AttrEntity> records = page.getRecords();
List<AttrRespVo> respVos = records.stream().map((attrEntity) -> {
AttrRespVo attrRespVo = new AttrRespVo();
BeanUtils.copyProperties(attrEntity, attrRespVo);
//此时VO 对象中已经具有AttrEntity 对象的字段值
//1、设置分类和分组的名字
if("base".equalsIgnoreCase(type)){
//根据属性id 找到属性与分组表中的对象
AttrAttrgroupRelationEntity attrId = relationDao.selectOne(new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_id", attrEntity.getAttrId()));
//根据该对象找到属性对应的分组号
if (attrId != null && attrId.getAttrGroupId()!=null) {
AttrGroupEntity attrGroupEntity = attrGroupDao.selectById(attrId.getAttrGroupId());
attrRespVo.setGroupName(attrGroupEntity.getAttrGroupName());
}
}
//通过catelogId 在其他表中查出catelogName
CategoryEntity categoryEntity = categoryDao.selectById(attrEntity.getCatelogId());
if (categoryEntity != null) {
attrRespVo.setCatelogName(categoryEntity.getName());
}
return attrRespVo;
}).collect(Collectors.toList());
pageUtils.setList(respVos);
return pageUtils;
}
属性修改功能:点击修改按钮时,回显信息
最重要是回显所属分类和分组
因为要返回三级菜单的具体路径进行回显,所以根据请求的url 找到Controller 的对应方法,发现返回封装的对象是AttrEntity ,但这里面的字段太少了,所以又把具体路径字段封装在AttrPrepVo 对象中。
回显过程和一开始的查询过程大致相同:只是多了一步查询菜单的完整路径:
@Override
public AttrRespVo getAttrInfo(Long attrId) {
AttrRespVo respVo = new AttrRespVo();
AttrEntity attrEntity = this.getById(attrId);
BeanUtils.copyProperties(attrEntity,respVo);
if(attrEntity.getAttrType() == ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode()){
//1、设置分组信息
AttrAttrgroupRelationEntity attrgroupRelation = relationDao.selectOne(new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_id", attrId));
if(attrgroupRelation!=null){
respVo.setAttrGroupId(attrgroupRelation.getAttrGroupId());
AttrGroupEntity attrGroupEntity = attrGroupDao.selectById(attrgroupRelation.getAttrGroupId());
if(attrGroupEntity!=null){
respVo.setGroupName(attrGroupEntity.getAttrGroupName());
}
}
}
//2、设置分类信息
Long catelogId = attrEntity.getCatelogId();
Long[] catelogPath = categoryService.findCatelogPath(catelogId);
respVo.setCatelogPath(catelogPath);
CategoryEntity categoryEntity = categoryDao.selectById(catelogId);
if(categoryEntity!=null){
respVo.setCatelogName(categoryEntity.getName());
}
return respVo;
}
当点击确定按钮保存时,发送的则是update 请求,所以要去Controller 中修改有update 请求的方法
AttrService 层:
注意:这里遇到一个细节问题,当在新增规格的时候没有设置分组,那么后面就算修改成有分组的,它也不会回显。为什么? 因为它在新增操作时没有添加分组,即在维护分组与属性的表中并没有这条数据,所以就算后面进行修改,也因为找不到数据而修改了个寂寞。所以要进行判断:如果在查询维护分组和属性的表中查询到了数据,证明是修改操作;如果在此表中查不到数据,就得执行插入操作
编写销售属性功能(SKU):
在数据库表中,无论是销售属性(SKU)还是规格参数(SPU),都是放在同一张表中的。
他们之间的区分通过attr_type 的不同进行区分。0:销售属性,1:规格参数
发现销售属性发送的url 仅仅只是sale 不同,其他都是一样的,所以可以公用规格参数的方法。不过需要在url 中多携带一个参数: type。以便区分这两个参数
因为销售属性中不需要分组字段,所以在查询的时候也不需要经过分组字段的查询。
@Override
public PageUtils queryBaseAttrPage(Map<String, Object> params, Long catelogId, String type) {
//因为两个属性都共用一个字段,所以在查询之前都需要先对属性字段进行判断
QueryWrapper<AttrEntity> queryWrapper = new QueryWrapper<AttrEntity>().eq("attr_type","base".equalsIgnoreCase(type)?ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode():ProductConstant.AttrEnum.ATTR_TYPE_SALE.getCode());
if(catelogId != 0){
queryWrapper.eq("catelog_id",catelogId);
}
String key = (String) params.get("key");
if(!StringUtils.isEmpty(key)){
//attr_id attr_name
queryWrapper.and((wrapper)->{
wrapper.eq("attr_id",key).or().like("attr_name",key);
});
}
IPage<AttrEntity> page = this.page(
new Query<AttrEntity>().getPage(params),
queryWrapper
);
PageUtils pageUtils = new PageUtils(page);
//因为AttrEntity 中字段不能满足前端显示,所以要借助VO 对象
//下面操作都是将AttrEntity 对象的字段值转移到VO 对象中。
List<AttrEntity> records = page.getRecords();
List<AttrRespVo> respVos = records.stream().map((attrEntity) -> {
AttrRespVo attrRespVo = new AttrRespVo();
BeanUtils.copyProperties(attrEntity, attrRespVo);
//此时VO 对象中已经具有AttrEntity 对象的字段值
//1、设置分类和分组的名字
if("base".equalsIgnoreCase(type)){
//根据属性id 找到属性与分组表中的对象
AttrAttrgroupRelationEntity attrId = relationDao.selectOne(new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_id", attrEntity.getAttrId()));
//根据该对象找到属性对应的分组号
if (attrId != null && attrId.getAttrGroupId()!=null) {
AttrGroupEntity attrGroupEntity = attrGroupDao.selectById(attrId.getAttrGroupId());
attrRespVo.setGroupName(attrGroupEntity.getAttrGroupName());
}
}
//通过catelogId 在其他表中查出catelogName
CategoryEntity categoryEntity = categoryDao.selectById(attrEntity.getCatelogId());
if (categoryEntity != null) {
attrRespVo.setCatelogName(categoryEntity.getName());
}
return attrRespVo;
}).collect(Collectors.toList());
pageUtils.setList(respVos);
return pageUtils;
}
在查询中需要加判断,更新和保存的时候也需要判断。因为经常需要判断,有可能后面还会被其他对象引用到,所以在common 中编写了一个枚举类。好处:以后数据库如果修改了判断区分销售属性和规格参数的值,我们只需要需要该枚举类中的值即可,不用到代码中处处进行修改。
回显方法变成:
@Override
public AttrRespVo getAttrInfo(Long attrId) {
AttrRespVo respVo = new AttrRespVo();
AttrEntity attrEntity = this.getById(attrId);
BeanUtils.copyProperties(attrEntity,respVo);
if(attrEntity.getAttrType() == ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode()){
//1、设置分组信息
AttrAttrgroupRelationEntity attrgroupRelation = relationDao.selectOne(new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_id", attrId));
if(attrgroupRelation!=null){
respVo.setAttrGroupId(attrgroupRelation.getAttrGroupId());
AttrGroupEntity attrGroupEntity = attrGroupDao.selectById(attrgroupRelation.getAttrGroupId());
if(attrGroupEntity!=null){
respVo.setGroupName(attrGroupEntity.getAttrGroupName());
}
}
}
//2、设置分类信息
Long catelogId = attrEntity.getCatelogId();
Long[] catelogPath = categoryService.findCatelogPath(catelogId);
respVo.setCatelogPath(catelogPath);
CategoryEntity categoryEntity = categoryDao.selectById(catelogId);
if(categoryEntity!=null){
respVo.setCatelogName(categoryEntity.getName());
}
return respVo;
}
更新方法变成:
@Override
public AttrRespVo getAttrInfo(Long attrId) {
AttrRespVo respVo = new AttrRespVo();
AttrEntity attrEntity = this.getById(attrId);
BeanUtils.copyProperties(attrEntity,respVo);
if(attrEntity.getAttrType() == ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode()){
//1、设置分组信息
AttrAttrgroupRelationEntity attrgroupRelation = relationDao.selectOne(new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_id", attrId));
if(attrgroupRelation!=null){
respVo.setAttrGroupId(attrgroupRelation.getAttrGroupId());
AttrGroupEntity attrGroupEntity = attrGroupDao.selectById(attrgroupRelation.getAttrGroupId());
if(attrGroupEntity!=null){
respVo.setGroupName(attrGroupEntity.getAttrGroupName());
}
}
}
//2、设置分类信息
Long catelogId = attrEntity.getCatelogId();
Long[] catelogPath = categoryService.findCatelogPath(catelogId);
respVo.setCatelogPath(catelogPath);
CategoryEntity categoryEntity = categoryDao.selectById(catelogId);
if(categoryEntity!=null){
respVo.setCatelogName(categoryEntity.getName());
}
return respVo;
}
查询分组关联属性 & 删除关联
每个属性分组都能关联非常多的属性参数,这个操作就是在属性分组页面完成
点击关联按钮,能有相应的属性显示
查看url:
所以要在AttrGroupController 中编写一个接受该请求并返回数据:有id,属性名,可选值。这些属性在AttrEntity 都有,所以编写的Controller 中直接返回该对象就可以了。
通过attrGroup 找到attr 可以用attr_attrgroup_relation表,通过attr_group_id 找到对应的attr_id,再通过attr_id 在attr 表中找到对应的数据并返回
在获取数据,然后将这个数据用作为参数传到另一个方法时,一定要将这个数据进行非空判断。
编写删除属性与分组关系的功能
发送参数是attr_id ,attrGroup_id,那么Controller 可以编写一个vo 对象,这个对象包含这两个字段。
因为要删除的数据可能有多条或一条,并且不能发送多条sql 语句,所以要用批量删除。
批量删除的sql 语句:
因为数据库接收的数据是AttrgroupRelationEntity ,所以要把VO 对象的数据都传给该对象,然后再讲该对象封装成一个list,一遍Mybatis 编写sql 语句时候遍历。
查询分组为关联属性:
难点:哪些属性是当前分组能进行关联但是又未进行关联的。
当前分组能关联的,肯定是当前分类(三级菜单数据)下的,而且还是当前分类下没有被其他分组关联的属性
这个请求会查出当前分组没有关联的属性。
分类-分组-属性 关系图
查出这些属性的步骤:
1.通过catelogId 找到当前分类
2.通过当前分类找到属于该分类的分组
3.找出这些分组的所有属性
4.通过当前分类的catelogId 找到所有属性,并且这些属性只要规格参数,不要销售属性,然后利用分组的属性ID 将这些属性都进行排除(因为能通过分组查询到的属性都是已经关联好的了,所以都不要),那么剩下的就是当前分组能够进行显示关联的属性
AttrServiceImpl:
@Override
public PageUtils getNoRelationAttr(Map<String, Object> params, Long attrgroupId) {
//1、当前分组只能关联自己所属的分类里面的所有属性
AttrGroupEntity attrGroupEntity = attrGroupDao.selectById(attrgroupId);
Long catelogId = attrGroupEntity.getCatelogId();
//2、当前分组只能关联别的分组没有引用的属性
//2.1)、当前分类下的其他分组
List<AttrGroupEntity> group = attrGroupDao.selectList(new QueryWrapper<AttrGroupEntity>().eq("catelog_id", catelogId));
List<Long> collect = group.stream().map(item -> {
return item.getAttrGroupId();
}).collect(Collectors.toList());
//2.2)、这些分组关联的属性
List<AttrAttrgroupRelationEntity> groupId = relationDao.selectList(new QueryWrapper<AttrAttrgroupRelationEntity>().in("attr_group_id", collect));
List<Long> attrIds = groupId.stream().map(item -> {
return item.getAttrId();
}).collect(Collectors.toList());
//2.3)、查找出属于当前分类,且并没有和其他分组进行关联的属性
QueryWrapper<AttrEntity> wrapper = new QueryWrapper<AttrEntity>().eq("catelog_id", catelogId).eq("attr_type",ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode());
if(attrIds!=null && attrIds.size()>0){
wrapper.notIn("attr_id", attrIds);
}
//如果页面有进行模糊查询,就得在查询中加上
String key = (String) params.get("key");
if(!StringUtils.isEmpty(key)){
wrapper.and((w)->{
w.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;
}
点击确认新增按钮,将选中的新关联属性保存到数据库
发送的参数:
因为关联的数据可能会很多,所以使用批量保存
Service 层: