07 尚品甄选-后台系统-分类和品牌管理
1 分类管理
分类管理就是对商品的分类数据进行维护。常见的分类数据:电脑办公、玩具乐器、家居家装、汽车用品...
1.1 菜单添加
首先在系统中添加分类管理的菜单,具体步骤如下所示:
1、在后台管理系统中通过系统管理的菜单管理添加分类管理的相关菜单,如下所示:

2、给系统管理员角色分配分类管理菜单访问权限:

3、在前端项目中创建对应的页面,以及配置对应的异步路由
- 在src/views下创建文件夹product
- 在product文件夹下创建文件 category.vue
- 在src/router/modules文件夹下创建product.js路由文件,文件内容如下所示:
const Layout = () => import('@/layout/index.vue')
const category = () => import('@/views/product/category.vue')
export default [
{
path: '/product',
component: Layout,
name: 'product',
meta: {
title: '商品管理',
},
icon: 'Histogram',
children: [
{
path: '/category',
name: 'category',
component: category,
meta: {
title: '分类管理',
},
},
],
},
]
- 在src/router/index.js中添加异步路由,如下所示:
import product from './modules/product'
// 动态菜单
export const asyncRoutes = [...system,...product]
1.2 表结构介绍
分类数据所对应的表结构如下所示:
CREATE TABLE `category` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '分类id',
`name` varchar(50) DEFAULT NULL COMMENT '分类名称',
`image_url` varchar(200) DEFAULT NULL,
`parent_id` bigint DEFAULT NULL COMMENT '父分类id',
`status` tinyint DEFAULT NULL COMMENT '是否显示[0-不显示,1显示]',
`order_num` int DEFAULT NULL COMMENT '排序',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`is_deleted` tinyint NOT NULL DEFAULT '0' COMMENT '删除标记(0:不可用 1:可用)',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=704 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='商品分类';
注意:分类数据是具有层级结构的,因此在进行数据展示的时候可以考虑使用树形表格进行展示。在咱们的尚品甄选项目中关于分类的数据只支持三级。
1.3 页面制作
对比如下页面结构,使用Element Plus制作出对应的页面,数据可以暂时使用假数据。

该页面可以将其分为2部分:
1、导入导出按钮
2、分类列表展示【树形表格】
category.vue代码实现如下所示:
<template>
<div class="tools-div">
<el-button type="success" size="small" >导出</el-button>
<el-button type="primary" size="small" >导入</el-button>
</div>
<!---懒加载的树形表格-->
<el-table
:data="list"
style="width: 100%"
row-key="id"
border
lazy
:load="fetchData"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
>
<el-table-column prop="name" label="分类名称" />
<el-table-column prop="imageUrl" label="图标" #default="scope">
<img :src="scope.row.imageUrl" width="50" />
</el-table-column>
<el-table-column prop="orderNum" label="排序" />
<el-table-column prop="status" label="状态" #default="scope">
{{ scope.row.status == 1 ? '正常' : '停用' }}
</el-table-column>
<el-table-column prop="createTime" label="创建时间" />
</el-table>
</template>
<script setup>
import { ref } from 'vue';
// 定义list属性模型
const list = ref([
{"id":1 , "name":"数码" , "orderNum":"1" , "status":1 , "createTime":"2023-05-22" , "hasChildren": true},
{"id":2 , "name":"手机" , "orderNum":"1", "status":1, "createTime":"2023-05-22"},
])
// 加载数据的方法
const fetchData = (row, treeNode, resolve) => {
// 向后端发送请求获取数据
const data = [
{"id":3 , "name":"智能设备" , "orderNum":"1" , "status":1 , "createTime":"2023-05-22" },
{"id":4 , "name":"电子教育" , "orderNum":"2" , "status":1 , "createTime":"2023-05-22" },
]
// 返回数据
resolve(data)
}
</script>
<style scoped>
.search-div {
margin-bottom: 10px;
padding: 10px;
border: 1px solid #ebeef5;
border-radius: 3px;
background-color: #fff;
}
.tools-div {
margin: 10px 0;
padding: 10px;
border: 1px solid #ebeef5;
border-radius: 3px;
background-color: #fff;
}
</style>
1.4 列表查询
1.4.1 需求分析
当页面初始化完毕以后,此时就需要从请求后端接口查询所有的一级分类数据,一级分类数据的parent_id为0。当用户点击某一个分类前的小箭头,那么此时就需要查询该分类下所对应的所有的子分类数据。对应的sql语句如下所示:
select * from category where parent_id = 0 ;
1.4.2 后端接口
Category
定义一个与数据库表相对应的实体类:
// com.atguigu.spzx.model.entity.product
@Data
public class Category extends BaseEntity {
private String name;
private String imageUrl;
private Long parentId;
private Integer status;
private Integer orderNum;
private Boolean hasChildren;
private List<Category> children;
}
CategoryController
表现层代码实现:
// com.atguigu.spzx.manager.controller
@RestController
@RequestMapping(value="/admin/product/category")
public class CategoryController {
@Autowired
private CategoryService categoryService;
@Operation(summary = "根据parentId获取下级节点")
@GetMapping(value = "/findByParentId/{parentId}")
public Result<List<Category>> findByParentId(@PathVariable Long parentId) {
List<Category> list = categoryService.findByParentId(parentId);
return Result.build(list , ResultCodeEnum.SUCCESS) ;
}
}
CategoryService
业务层代码实现:
// com.atguigu.spzx.manager.service.impl
@Service
public class CategoryServiceImpl implements CategoryService {
@Autowired
private CategoryMapper categoryMapper ;
@Override
public List<Category> findByParentId(Long parentId) {
// 根据分类id查询它下面的所有的子分类数据
List<Category> categoryList = categoryMapper.selectByParentId(parentId);
if(!CollectionUtils.isEmpty(categoryList)) {
// 遍历分类的集合,获取每一个分类数据
categoryList.forEach(item -> {
// 查询该分类下子分类的数量
int count = categoryMapper.countByParentId(item.getId());
if(count > 0) {
item.setHasChildren(true);
} else {
item.setHasChildren(false);
}
});
}
return categoryList;
}
}
CategoryMapper
持久层代码实现:
@Mapper
public interface CategoryMapper {
public abstract List<Category> selectByParentId(Long parentId);
public abstract int countByParentId(Long id);
}
CategoryMapper.xml
映射文件中添加如下的sql语句:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.atguigu.spzx.mapper.CategoryMapper">
<resultMap id="categoryMap" type="com.atguigu.spzx.model.entity.product.Category" autoMapping="true">
</resultMap>
<!-- 用于select查询公用抽取的列 -->
<sql id="columns">
id,name,image_url,parent_id,status,order_num,create_time,update_time,is_deleted
</sql>
<select id="selectByParentId" resultMap="categoryMap">
select <include refid="columns" />
from category
where parent_id = #{parentId}
and is_deleted = 0
order by id desc
</select>
<select id="countByParentId" resultType="Integer">
select count(id)
from category
where parent_id = #{parentId}
and is_deleted = 0
</select>
</mapper>
1.4.3 前端对接
category.js
在src/api文件夹下创建一个category.js文件,文件的内容如下所示:
import request from '@/utils/request'
const api_name = '/admin/product/category'
// 根据parentId获取下级节点
export const FindCategoryByParentId = parentId => {
return request({
url: `${api_name}/findByParentId/${parentId}`,
method: 'get',
})
}
category.vue
修改category.vue文件,内容如下所示:
<script setup>
import { ref , onMounted} from 'vue';
import { FindCategoryByParentId } from '@/api/category.js'
import { ElMessage, ElMessageBox } from 'element-plus'
// 定义list属性模型
const list = ref([])
// 页面初始化完毕以后请求后端接口查询数据
onMounted(async () => {
const {code , data , message} = await FindCategoryByParentId(0)
list.value = data ;
})
// 加载数据的方法
const fetchData = async (row, treeNode, resolve) => {
// 向后端发送请求获取数据
const {code , data , message} = await FindCategoryByParentId(row.id)
// 返回数据
resolve(data)
}
</script>
2 EasyExcel
2.1 数据导入导出意义
后台管理系统是管理、处理企业业务数据的重要工具,在这样的系统中,数据的导入和导出功能是非常重要的,其主要意义包括以下几个方面:
1、提高数据操作效率:手动逐条添加或修改数据不仅费时费力,而且容易出错,此时就可以将大量数据从Excel等表格软件中导入到系统中时,通过数据导入功能,可以直接将表格中的数据批量导入到系统中,提高了数据操作的效率。
2、实现数据备份与迁移:通过数据导出功能,管理员可以 将系统中的数据导出为 Excel 或其他格式的文件,以实现数据备份,避免数据丢失。同时,也可以将导出的数据文件用于数据迁移或其他用途。
3、方便企业内部协作:不同部门可能会使用不同的系统或工具进行数据处理,在这种情况下,通过数据导入和导出功能,可以方便地转换和共享数据,促进企业内部协作。
2.2 EasyExcel简介
官网地址:https://easyexcel.opensource.alibaba.com/

EasyExcel 的主要特点如下:
1、高性能:EasyExcel 采用了异步导入导出的方式,并且底层使用 NIO 技术实现,使得其在导入导出大数据量时的性能非常高效。
2、易于使用:EasyExcel 提供了简单易用的 API,用户可以通过少量的代码即可实现复杂的 Excel 导入导出操作。
3、增强的功能“EasyExcel 支持多种格式的 Excel 文件导入导出,同时还提供了诸如合并单元格、数据校验、自定义样式等增强的功能。
4、可扩展性好:EasyExcel 具有良好的扩展性,用户可以通过自定义 Converter 对自定义类型进行转换,或者通过继承 EasyExcelListener 来自定义监听器实现更加灵活的需求。
2.3 入门案例
2.3.1 读取Excel数据
需求:对资料中的excel数据进行解析,将其存储到对应的List集合中,并遍历List集合
步骤:
1、在spzx-model的pom.xml文件中添加如下依赖:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.1.0</version>
</dependency>
2、定义一个实体类来封装每一行的数据,如下所示:
// com.atguigu.spzx.model.vo.product
@Data
public class CategoryExcelVo {
@ExcelProperty(value = "id" ,index = 0)
private Long id;
@ExcelProperty(value = "名称" ,index = 1)
private String name;
@ExcelProperty(value = "图片url" ,index = 2)
private String imageUrl ;
@ExcelProperty(value = "上级id" ,index = 3)
private Long parentId;
@ExcelProperty(value = "状态" ,index = 4)
private Integer status;
@ExcelProperty(value = "排序" ,index = 5)
private Integer orderNum;
}
3、定义一个监听器,监听解析到的数据,如下所示:
public class ExcelListener<T> extends AnalysisEventListener<T> {
//可以通过实例获取该值
private List<T> datas = new ArrayList<>();
@Override
public void invoke(T o, AnalysisContext analysisContext) { // 每解析一行数据就会调用一次该方法
datas.add(o);//数据存储到list,供批量处理,或后续自己业务逻辑处理。
}
public List<T> getDatas() {
return datas;
}
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
// excel解析完毕以后需要执行的代码
}
}
4、编写测试方法
public class EasyExcelTest {
public static void main(String[] args) {
readDateToExcel();
}
//读取方法
public static void readDateToExcel() {
String fileName = "D://分类数据.xlsx" ;
// 创建一个监听器对象
ExcelListener<CategoryExcelVo> excelListener = new ExcelListener<>();
EasyExcel.read(fileName, CategoryExcelVo.class, excelListener).sheet().doRead(); // 解析excel表格
List<CategoryExcelVo> excelVoList = excelListener.getDatas(); //获取解析到的数据
excelVoList.forEach(s -> System.out.println(s) ); // 进行遍历操作
}
}