商品服务-API-属性分组
1. 品牌管理基本概念
1.1 SPU和SKU
SPU:Standard Product Unit(标准化产品单元)
- 是商品信息聚合的最小单位,是一组可复用、易检索的标准化信息的集合,该集合描述了一个产品的特性。
SKU:Stock Keeping Unit(库存量单位)
- 即库存进出计量的基本单元,可以是以件,盒,托盘等为单位。
- SKU 这是对于大型连锁超市DC(配送中心)物流管理的一个必要的方法。现在已经被引申为产品统一编号的简称,每种产品均对应有唯一的SKU号。
例:
iphoneX 是SPU、MI8 是SPU
iphoneX 64G 黑曜石是SKU
MI8 8+64G 黑色是SKU
1.2 基本属性【规格参数】与销售属性
每个分类下的商品共享规格参数,与销售属性。只是有些商品不一定要用这个分类下全部的属性;
- 属性是以三级分类组织起来的
- 规格参数中有些是可以提供检索的
- 规格参数也是基本属性,他们具有自己的分组
- 属性的分组也是以三级分类组织起来的
- 属性名确定的,但是值是每一个商品不同来决定的
决定SKU的属性称为销售属性。
决定SPU的属性称为基本属性【规格参数】。
2. 属性分组展示功能

2.1 前端页面效果
复制逆向代码生成的
attrgroup.vue和attrgroup-add-or-update到product目录下。
- 在
<template>标签体内将页面分割为6栅格和18栅格两部分。 - 6栅格部分展示提取出来的公共三级分类
category.vue
<el-col :span="6">
<category></category>
</el-col>
import Category from "../common/category.vue";
- 18栅格部分展示三级分类表格。(将逆向生成的前端直接复制即可。)
<el-col :span="18">
<div class="mod-config">
<el-form :inline="true" :model="dataForm" @keyup.enter.native="getDataList()">
<el-form-item>
<el-input v-model="dataForm.key" placeholder="参数名" clearable></el-input>
</el-form-item>
<el-form-item>
<el-button @click="getDataList()">查询</el-button>
<el-button v-if="isAuth('product:attrgroup:save')" type="primary" @click="addOrUpdateHandle()">新增</el-button>
<el-button v-if="isAuth('product:attrgroup:delete')" type="danger" @click="deleteHandle()" :disabled="dataListSelections.length <= 0">批量删除</el-button>
</el-form-item>
</el-form>
<el-table :data="dataList" border v-loading="dataListLoading" @selection-change="selectionChangeHandle" style="width: 100%;">
<el-table-column type="selection" header-align="center" align="center" width="50">
</el-table-column>
<el-table-column prop="attrGroupId" header-align="center" align="center" label="分组id">
</el-table-column>
<el-table-column prop="attrGroupName" header-align="center" align="center" label="组名">
</el-table-column>
<el-table-column prop="sort" header-align="center" align="center" label="排序">
</el-table-column>
<el-table-column prop="descript" header-align="center" align="center" label="描述">
</el-table-column>
<el-table-column prop="icon" header-align="center" align="center" label="组图标">
</el-table-column>
<el-table-column prop="catelogId" header-align="center" align="center" label="所属分类id">
</el-table-column>
<el-table-column fixed="right" header-align="center" align="center" width="150" label="操作">
<template slot-scope="scope">
<el-button type="text" size="small" @click="addOrUpdateHandle(scope.row.attrGroupId)">修改</el-button>
<el-button type="text" size="small" @click="deleteHandle(scope.row.attrGroupId)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" :current-page="pageIndex" :page-sizes="[10, 20, 50, 100]" :page-size="pageSize" :total="totalPage"
layout="total, sizes, prev, pager, next, jumper">
</el-pagination>
<!-- 弹窗, 新增 / 修改 -->
<add-or-update v-if="addOrUpdateVisible" ref="addOrUpdate" @refreshDataList="getDataList"></add-or-update>
</div>
</el-col>
整体效果

2.2 父子组件传递数据
要求: 点击左侧栅格的三级分类,右边表格自动展示该分类的属性。
- 左侧是三级分类是引入的子组件(category.vue),子组件给父组件传递数据需要使用事件机制。
- 子组件给父组件发送一个事件,携带数据。
- https://element.eleme.cn/#/zh-CN/component/tree

2.2.1 子组件获取点击数据
- 给组件绑定一个nodeclick事件,当该树形结构变点击后,会回调返回三个参数。
- 将这个事件绑定我们自定义的方法,就可以获取这三个参数值。
<el-tree :data="menus" :props="defaultProps" node-key="catId" ref="menuTree" @node-click="nodeclick"></el-tree>
methods: {
getMenus() {
this.$http({
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get",
}).then(({ data }) => {
// 将后端请求获取的data传递给menus。
this.menus = data.page;
});
},
nodeclick(data,node,component){
console.log("树节点被点击了",data,node,component)
}
},
- 当点击一个树形数据时,可以在子组件获取到点击的数据。

2.2.2 通过事件将数据传给父组件
- 通过
this.$emit()和父组件进行绑定,并传递数据。
子组件
nodeclick(data,node,component){
console.log("树节点被点击了",data,node,component);
//向父组件发送事件(携带参数)和父组件的一个方法进行绑定。
this.$emit("tree-node-click",data,node,component)
}
父组件
- 收到子组件散发的事件,和自身组件的一个方法进行绑定。
<category @tree-node-click="treenodeclick"></category>
treenodeclick(data, node, component) {
console.log("父组件收到数据", data);
},

2.2 后端获取数据
前端获取点击后的节点数据,并且能够传递给父组件,父组件可以根据该id向后端发请求,获取该id下的所有属性数据。
- 根据接口文档编写代码获取数据

Query和QueryWapper用法
- https://blog.csdn.net/bird_tp/article/details/105587582
- https://blog.csdn.net/thc1987/article/details/79347054
2.2.1 Controller层
@RequestMapping("/list/{catelogId}")
public R list(@RequestParam Map<String, Object> params,
@PathVariable("catelogId") Long catelogId){
//根据catelogId查询,返回分页数据
PageUtils page = attrGroupService.queryPage(params,catelogId);
return R.ok().put("page", page);
}
2.2.2 Service层
前端传递的params:
- 当前页,每页显示几条,检索字。
params: this.$http.adornParams({
page: this.pageIndex,
limit: this.pageSize,
key: this.dataForm.key,
}),
service层根据前端传递的catId判断查询数据,将数据封装为page对象返回。
- catId为0(默认查所有数据)
- catId不为0(查符合条件的数据)
- key不为空(根据key进行sql拼接)
@Override
public PageUtils queryPage(Map<String, Object> params, Long catelogId) {
String key = (String) params.get("key");
if (catelogId == 0 && StringUtils.isEmpty(key)) {
//查询所有,第一个参数是IPage(分页数据,包括当前页和每页显示数)
//第二个参数是查询条件(即sql),没有条件表示查询所有该泛型的数据。
IPage<AttrGroupEntity> page = this.page(
new Query<AttrGroupEntity>().getPage(params),
new QueryWrapper<AttrGroupEntity>()
);
//使用工具类解析IPage对象,获取分页信息(当前页,每页数,总页数,总数据...)
return new PageUtils(page);
} else if (catelogId == 0 && !StringUtils.isEmpty(key)) {
//查询所有,且有关键字时
//select * from pms_attr_group where (attr_group_id=key or attr_group_name like %key%)
QueryWrapper<AttrGroupEntity> wrapper = new QueryWrapper<AttrGroupEntity>();
wrapper.and((obj) -> {
obj.eq("catelog_id", key).or().like("attr_group_name", key);
});
IPage<AttrGroupEntity> page = this.page(
new Query<AttrGroupEntity>().getPage(params), wrapper
);
//使用工具类解析IPage对象,获取分页信息(当前页,每页数,总页数,总数据...)
return new PageUtils(page);
} else {
//根据catelogId和检索关键字查
//select * from pms_attr_group where catelog_id = catelogId and (attr_group_id=key or attr_group_name like %key%)
QueryWrapper<AttrGroupEntity> wrapper = new QueryWrapper<AttrGroupEntity>().eq("catelog_id", catelogId);
if (!StringUtils.isEmpty(key)) {
//有检索条件
wrapper.and((obj) -> {
obj.eq("catelog_id", key).or().like("attr_group_name", key);
});
}
IPage<AttrGroupEntity> page = this.page(
new Query<AttrGroupEntity>().getPage(params), wrapper
);
//使用工具类解析IPage对象,获取分页信息(当前页,每页数,总页数,总数据...)
return new PageUtils(page);
}
}
2.3 前后端联调测试
2.3.1 前端
//接收子组件点击后的节点数据,将其和方法绑定
<category @tree-node-click="treenodeclick"></category>
//如果是三级节点,给catId赋值,重新发请求查询
treenodeclick(data, node, component) {
if (node.level == 3) {
this.catId = data.catId;
this.getDataList(); //重新获取列表
}
},
//向后端发请求获取page数据,catId默认是0
getDataList() {
this.dataListLoading = true;
this.$http({
url: this.$http.adornUrl(`/product/attrgroup/list/${this.catId}`),
method: "get",
params: this.$http.adornParams({
page: this.pageIndex,
limit: this.pageSize,
key: this.dataForm.key,
}),
}).then(({ data }) => {
if (data && data.code === 0) {
this.dataList = data.page.list;
this.totalPage = data.page.totalCount;
} else {
this.dataList = [];
this.totalPage = 0;
}
this.dataListLoading = false;
});
},
2.3.2测试

请求体

携带key时发送请求


3. 属性分组新增功能
-
所属分类id应该是一个级联选择器

- prop绑定的是选中的属性值。
- 只需为 Cascader 的
options属性指定选项数组即可渲染出一个级联选择器。 options绑定全部的三级分类数据
<el-form-item label="所属分类id" prop="catelogId">
<el-cascader v-model="dataForm.catelogIds" :options="categorys" :props="props"></el-cascader>
</el-form-item>
data() {
return {
categorys: [],
}
}
methods: {
getCategorys() {
this.$http({
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get",
}).then(({ data }) => {
// 将后端请求获取的data传递给menus。
this.categorys = data.page;
});
},
},
//组件创建的时候就调用方法获取全部的三级分类
created(){
this.getCategorys();
}
- 指定显示的值,提交的值,和下级的值。
props:{
value:"catId",
label:"name",
children:"children"
},

出现一个问题,查询出来的三级分类还有一个空的chidredn


解决方法
- 给返回实体的children属性添加一个注解
// 用来存放该分类的子分类。不存在数据库表,需要加注解标注
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@TableField(exist =false)
private List<CategoryEntity> children;
3.1 点击修改回显选择
其他属性回显正常,catlogId需要是一个层级结构,如:[2,25,225]才回显正常。

3.1.1 后端修改返回值
controller层
- controller层需要修改一下返回信息,添加一个完整路径属性
AttrGroupController
@RequestMapping("/info/{attrGroupId}")
// @RequiresPermissions("product:attrgroup:info")
public R info(@PathVariable("attrGroupId") Long attrGroupId){
AttrGroupEntity attrGroup = attrGroupService.getById(attrGroupId);
Long catelogId = attrGroup.getCatelogId();
//根据属性分组id查询分类id(225),根据分类id查询完整路径[2,25,225]
Long[] paths=categoryService.findPath(catelogId);
//将完整路径赋值给新的返回实体
attrGroup.setCatelogPath(paths);
return R.ok().put("data", attrGroup);
}
service层
CategoryService
- 根据传入的分类id查询完整路径,然后对数组进行反转即可
@Override
public Long[] findPath(Long catelogId) {
ArrayList<Long> list = new ArrayList<>();
ArrayList<Long> pList = this.getPath(list, catelogId);
Collections.reverse(pList);
//数组转集合
return list.toArray(new Long[pList.size()]);
}
//递归获取完整路径
private ArrayList<Long> getPath(ArrayList<Long> list,Long sId){
CategoryEntity pCategory = this.getById(sId);
list.add(sId);
//如果该分类父分类不为0,说明上面还有分类,继续递归查找。
if (pCategory.getParentCid()!=0){
getPath(list,pCategory.getParentCid());
}
return list;
}
测试
@RunWith(SpringRunner.class)
@SpringBootTest()
public class GulimallProductApplicationTests {
@Autowired
CategoryService categoryService;
@Test
public void testPath(){
Long[] path = categoryService.findPath(225L);
System.out.println(Arrays.asList(path));
}
}

属性分组实体类
- 添加完整路径属性
@TableField(exist =false)
private Long[] catelogPath;
重启项目,前端点击修改按钮。查看后端返回结果。

3.1.2 前端修改展示内容
- 修改接收后端传递数据
dataForm: {
attrGroupId: 0,
attrGroupName: "",
sort: "",
descript: "",
icon: "",
catelogId:0,
catelogPath:[]
},
------------------------------------------------
init(id) {
this.dataForm.attrGroupId = id || 0;
this.visible = true;
this.$nextTick(() => {
this.$refs["dataForm"].resetFields();
if (this.dataForm.attrGroupId) {
this.$http({
url: this.$http.adornUrl(
`/product/attrgroup/info/${this.dataForm.attrGroupId}`
),
method: "get",
params: this.$http.adornParams(),
}).then(({ data }) => {
if (data && data.code === 0) {
this.dataForm.attrGroupName = data.data.attrGroupName;
this.dataForm.sort = data.data.sort;
this.dataForm.descript = data.data.descript;
this.dataForm.icon = data.data.icon;
this.dataForm.catelogId = data.data.catelogId;
this.dataForm.catelogPath = data.data.catelogPath;
}
});
}
});
},
- 修改回显双向绑定属性
<el-form-item label="所属分类id" prop="catelogId">
<el-cascader v-model="dataForm.catelogPath" :options="categorys" :props="props"></el-cascader>
</el-form-item>
再次点击修改,回显成功。

- 当关闭修改页面时,删除catelogPath数据。防止下次点击新增,会带有上次修改的数据。
<el-dialog :title="!dataForm.attrGroupId ? '新增' : '修改'" :close-on-click-modal="false" :visible.sync="visible" @closed="dialogClose">
//关闭页面时回调函数
dialogClose(){
this.dataForm.catelogPath=[]
}
- 为新增属性分组添加搜索功能
filterable placeholder="试试搜索:蓝牙"
<el-form-item label="所属分类id" prop="catelogId">
<el-cascader v-model="dataForm.catelogPath" :options="categorys" :props="props" filterable placeholder="试试搜索:蓝牙"></el-cascader>
</el-form-item>
效果

4. 品牌功能完善
4.1 品牌管理全局查询功能

@RequestMapping("/list")
//@RequiresPermissions("product:brand:list")
public R list(@RequestParam Map<String, Object> params){
PageUtils page = brandService.queryPage(params);
return R.ok().put("page", page);
}
@Override
public PageUtils queryPage(Map<String, Object> params) {
//1.获取全局搜索关键字
String key = (String) params.get("key");
QueryWrapper<BrandEntity> wrapper = new QueryWrapper<BrandEntity>();
//有关键字,先拼接关键字
if(!StringUtils.isEmpty(key)){
//select * from pms_brand where brand_id=key or name like %key%
wrapper.eq("brand_id",key).or().like("name",key);
}
IPage<BrandEntity> page = this.page(
new Query<BrandEntity>().getPage(params),
wrapper
);
return new PageUtils(page);
}
4.2 品牌管理数据总数展示

- 使用mybatis-plus分页插件。需要引入一个inteceptor类
- https://baomidou.com/pages/8f40ae/
@Configuration
@EnableTransactionManagement //开启事务功能,标注事务注解就能生效
@MapperScan("com.atguigu.gulimall.product.dao")
public class MybatisConfig {
// 旧版 引入分页插件
@Bean
public PaginationInterceptor paginationInterceptor() {
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
// 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求 默认false
paginationInterceptor.setOverflow(true);
// 设置最大单页限制数量,默认 500 条,-1 不受限制
paginationInterceptor.setLimit(500);
// 开启 count 的 join 优化,只针对部分 left join
paginationInterceptor.setCountSqlParser(new JsqlParserCountOptimize(true));
return paginationInterceptor;
}
// 最新版
// @Bean
// public MybatisPlusInterceptor mybatisPlusInterceptor() {
// MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.H2));
// return interceptor;
// }
}
重启项目测试

5.品牌管理关联分类功能

品牌和分类是多对多关系。
- 比如:小米品牌关联多个分类,如手机,家用电器等
- 一个分类也对应多个品牌。比如手机分类对应小米,苹果等品牌。
5.1 查询关联分类接口
点击关联分类,会先查询出该品牌关联的所有分类。
根据接口文档进行开发

controller层
@GetMapping("/catelog/list")
//@RequiresPermissions("product:categorybrandrelation:list")
public R catelogList(@RequestParam("brandId") Long brandId){ //前端带一个请求参数brandId
List<CategoryBrandRelationEntity> data=categoryBrandRelationService.queryByBrandId(brandId);
return R.ok().put("data", data);
}
service层
- 根据品牌id查询关联数据。返回所有和品牌id相同的关联数据并返回。
@Override
public List<CategoryBrandRelationEntity> queryByBrandId(Long brandId) {
List<CategoryBrandRelationEntity> list = this.list(new QueryWrapper<CategoryBrandRelationEntity>().eq("brand_id", brandId));
return list;
}
5.2 新增关联分类接口
接口文档

- 前端传递参数只有品牌id和分类id。没有传入品牌名和分类名。但是数据库需要添加这两个字段。用来给前端展示。
- 如果使用品牌id或分类id去品牌表或分类表进行查询,每次都做表关联查询,会对数据库的性能有较大的影响。
- 所以在设计表的时候,为关联表添加了两个冗余字段,添加关系id的时候去查询一次品牌名或分类名。之后可以直接从关联表获取品牌名或分类名。

报错
- TypeError: Cannot read property ‘publish’ of undefined"
- 解决:https://blog.csdn.net/a1150499208/article/details/115726913
controller层
@RequestMapping("/save")
//@RequiresPermissions("product:categorybrandrelation:save")
public R save(@RequestBody CategoryBrandRelationEntity categoryBrandRelation){
categoryBrandRelationService.saveDetail(categoryBrandRelation);
return R.ok();
}
service层
- 用@Resource代替@AutoWired
@Service("categoryBrandRelationService")
public class CategoryBrandRelationServiceImpl extends ServiceImpl<CategoryBrandRelationDao, CategoryBrandRelationEntity> implements CategoryBrandRelationService {
@Resource //这里如果有@AutoWired时会出现注入不成功,用不了该组件
private CategoryServiceImpl categoryService;
@Resource
private BrandServiceImpl brandService;
@Override
public void saveDetail(CategoryBrandRelationEntity categoryBrandRelation) {
//获取品牌名和分类名
Long brandId = categoryBrandRelation.getBrandId();
Long catelogId = categoryBrandRelation.getCatelogId();
BrandEntity brandEntity = brandService.getById(brandId);
CategoryEntity categoryEntity = categoryService.getById(catelogId);
categoryBrandRelation.setBrandName(brandEntity.getName());
categoryBrandRelation.setCatelogName(categoryEntity.getName());
//保存数据库
this.save(categoryBrandRelation);
}
}
5.3 保证关联数据一致性
如:品牌分类关联表中保存有品牌名和品牌id,如果修改品牌表中的品牌id或品牌名,关联表中的数据也应该同步修改。
保证冗余字段的数据一致性。
5.3.1 品牌表修改方法
controller
@RequestMapping("/update")
// @RequiresPermissions("product:brand:update")
public R update(@Validated({UpdateGroup.class}) @RequestBody BrandEntity brand){
brandService.updateDetail(brand);
return R.ok();
}
Service
- @Transactional //修改两个表内容,开启事务。
@Autowired
private CategoryBrandRelationService categoryBrandRelationService;
@Override
@Transactional //修改两个表内容,开启事务。
public void updateDetail(BrandEntity brand) {
//先修改品牌表自身数据
this.updateById(brand);
//调用关联表方法,修改关联表内容 注入service组件
categoryBrandRelationService.updataBrand(brand.getBrandId(),brand.getName());
}
5.3.1 关联表修改方法
service
@Override
public void updataBrand(Long brandId, String name) {
CategoryBrandRelationEntity entity = new CategoryBrandRelationEntity();
entity.setBrandId(brandId);
entity.setBrandName(name);
this.update(entity,new UpdateWrapper<CategoryBrandRelationEntity>().eq("brand_id",brandId));
}
-----修改分类关联和上面方法一致-----

浙公网安备 33010602011771号