6.商品管理
本章节需要先准备一个用户拥有所有的菜单权限,, 便于开发
-------分类管理---------
一. 创建路由:
在前端项目中创建对应的页面,以及配置对应的异步路由
- 1 在src/views下创建文件夹product
- 2 在product文件夹下创建文件 category.vue

- 3 在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: '分类管理',
},
},
],
},
]
- 4 在src/router/index.js中添加异步路由,如下所示:
import product from './modules/product'
// 动态菜单
export const asyncRoutes = [...system,...product]
二. 创建界面
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>

三. 列表接口编写:
为了防止数据太多, 导致前端卡顿 ,我们采用:懒加载方式输出
后台数据库查询过多, 可以采用缓存解决 ,,, (以后再来改😂)
(只显示第一列, 点一下显示下一类)

业务分析:
可以看到这个表也有父类id ,我们可以联想到菜单的列表也是这样的,所以我们菜单的列表分类又能用了
所以的一个接口就是返回 parentid = 0 的列表

官方说为true,才会有
这个展开箭头, 所有我们需要自己判断有没有子类, 然后返回true

这个属性绑定一个有三个参数的方法,测试一下
row里包含这个id

我们就可以把id传到后端,结果用
返回给列表
后端,接收到id,先查所有子类, ,然后把所有查出来的数据再次查询有没有子类,
但是这样一来我们就要循环调用数据库,肯定会对性能产生很大的影响
所以我特意查了一下rudis和mysql的性能:Redis的性能为什么比Mysql要快?_哔哩哔哩_bilibili
数据库支持事务和安全性,Redis实现快速读写
所以我的想法是第一次查列表的时候,直接查出所有的,然后作为json保存到redis,key为用户的token, 在请求拦截器中, 检查用户没有登录的时候,一块把他删除
后续只读redis就不用都mysql了 , 查子组件的时候就可以json转对象,遍历查询了
二. 一级菜单:
1.后端
- 查询所有数据库,返回对象
- 转为json ,存储到redis
- 找出parentid=0的对象
- 找出这个对象是否有的子类
- 生产列表
首先创建三件套 : controller . Service . Mybatis
1.controller 因为我们需要取到token所有参数里需要有HttpServletRequest
String token = request.getHeader("token"); //从请求头里读取token
查看代码
package com.zhexin.manage.controller.Product;
import com.zhexin.manage.service.Product.categoryService;
import com.zhexin.model.entity.product.Category;
import com.zhexin.model.vo.common.Result;
import com.zhexin.model.vo.common.ResultCodeEnum;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@Tag(name = "商品管理") //swagger文档注释
@RestController //注册到bean
@RequestMapping("/admin/product/category") //注册访问路径
public class categoryController {
@Autowired
private categoryService categoryService;
@Operation(summary = "获取第一轮列表")
@GetMapping("/getList/{id}")
public Result getList(HttpServletRequest request , @PathVariable("id") Long id) {
//返回第一列的所有数据
List<Category> jg = categoryService.getList(request , id);
return Result.build(jg, ResultCodeEnum.SUCCESS);
}
}
2. 实现Service
套了两个循环
第一代:
package com.zhexin.manage.service.Product.Impl;
import com.alibaba.fastjson2.JSON;
import com.zhexin.common.service.exception.zhexinException;
import com.zhexin.manage.config.zxRedisEnum;
import com.zhexin.manage.mapper.Product.categoryMapper;
import com.zhexin.manage.service.Product.categoryService;
import com.zhexin.model.entity.product.Category;
import com.zhexin.model.vo.common.ResultCodeEnum;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
@Service
public class categoryServiceImpl implements categoryService {
@Autowired
private categoryMapper categoryMapper;
@Autowired
private RedisTemplate<String,String> redisTemplate;
@Override
public List<Category> getList(HttpServletRequest request) {
//初始化返回结果
List<Category> jg = new ArrayList<>();
// 查询所有数据库,返回对象
List<Category> qblist = categoryMapper.getList();
if (qblist == null || qblist.isEmpty()){
throw new zhexinException(ResultCodeEnum.JGWK);
}
// 存储到redis
//取到token
String token = request.getHeader("token"); //从请求头里读取token
//转json
String qblistjsonString = JSON.toJSONString(qblist);
//存入redis
redisTemplate.opsForValue().set(zxRedisEnum.SPLB+token , qblistjsonString , 60 , TimeUnit.SECONDS);
redisTemplate.persist(zxRedisEnum.SPLB+token); //设置为永不过期
// 找出parentid=0的对象
qblist.forEach(category ->{
//第一层放入
if (category.getParentId() == 0) {
// 找出这个对象是否有的子类
//初始化字节点
boolean hasChildren = false;
//查是否有子节点:
zxx: for (Category zi : qblist) {
if (zi.getParentId().equals(category.getId())) {
hasChildren = true;
break zxx; //只要检查到一次就跳出循环
}
}
//设置字节点,给list
category.setHasChildren(hasChildren);
jg.add(category);
}
});
// 生产列表
return jg;
}
}
第二代:
package com.zhexin.manage.service.Product.Impl;
import cn.hutool.core.util.StrUtil;
import com.alibaba.excel.EasyExcel;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.zhexin.common.service.exception.zhexinException;
import com.zhexin.manage.config.zxRedisEnum;
import com.zhexin.manage.mapper.Product.categoryMapper;
import com.zhexin.manage.service.Product.categoryService;
import com.zhexin.model.entity.product.Category;
import com.zhexin.model.vo.common.ResultCodeEnum;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
@Service
public class categoryServiceImpl implements categoryService {
@Autowired
private categoryMapper categoryMapper;
@Autowired
private RedisTemplate<String,String> redisTemplate;
@Override
public List<Category> getList(HttpServletRequest request , Long id) {
//初始化返回结果
List<Category> jg = new ArrayList<>();;
//查询所有数据库,返回对象
List<Category> qblist = null;
//取到token
String token = request.getHeader("token"); //从请求头里读取token
//查询redis中有没有库存
String redisyzm = redisTemplate.opsForValue().get(zxRedisEnum.SPLB.getRedisID()+token);
if ( id == 0 || StrUtil.isEmpty(redisyzm)){
//没有就查数据库
qblist = categoryMapper.getList();
if (qblist == null || qblist.isEmpty()){
//删除旧的商品列表:
redisTemplate.delete(zxRedisEnum.SPLB.getRedisID() + token);
throw new zhexinException(ResultCodeEnum.JGWK);
}
//删除旧的商品列表:
redisTemplate.delete(zxRedisEnum.SPLB.getRedisID() + token);
// 查完就存,存储到redis
//转json
String qblistjsonString = JSON.toJSONString(qblist);
//存入redis
redisTemplate.opsForValue().set(zxRedisEnum.SPLB.getRedisID()+token , qblistjsonString , 60 , TimeUnit.SECONDS);
redisTemplate.persist(zxRedisEnum.SPLB.getRedisID()+token); //设置为永不过期
}else {
//有就查redis
JSONArray objects = JSON.parseArray(redisyzm);
qblist = objects.toList(Category.class);
}
//格式化结果
//找出parentid=id的对象
List<Category> finalQblist = qblist;
qblist.forEach(category ->{
//通过父类的id查本层, 为0则为第一层 ,,所有数据库中id不能有0的否则会混为一起
//TODO 这里使用 == 会一部分数据判断不出来,, 具体原因有待测试
if (Objects.equals(category.getParentId(), id)) {
// 找出这个对象是否有的子类
//初始化字节点
boolean hasChildren = false;
//查是否有子节点:
zxx: for (Category zi : finalQblist) {
if (zi.getParentId().equals(category.getId())) {
hasChildren = true;
break zxx; //只要检查到一次就跳出循环
}
}
//设置字节点,给list
category.setHasChildren(hasChildren);
jg.add(category);
}
});
// 生产列表
return jg;
}
//下载
//TODO 后期可以再加上 宽高 、 把分类给写到不同的模板里
@Override
public void exportData(HttpServletRequest request , HttpServletResponse response) {
try {
// 设置响应结果类型
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
// 这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系
String fileName = null;
fileName = URLEncoder.encode("分类数据", "UTF-8");
response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx");
// response.setHeader("Access-Control-Expose-Headers", "Content-Disposition");
//TODO 这里频繁调用token,可以把token放到SysUser里, 方便线程变量读取
String token = request.getHeader("token"); //从请求头里读取token
//查询redis中有没有库存
String redisyzm = redisTemplate.opsForValue().get(zxRedisEnum.SPLB.getRedisID()+token);
JSONArray objects = JSON.parseArray(redisyzm);
List<Category> qblist = objects.toList(Category.class);
//传入文件名 , 写入模板名 , 写入数据List 类型要和.class相同
EasyExcel.write(response.getOutputStream(), Category.class).sheet("商品分类").doWrite(qblist);
} catch (Exception e) {
throw new zhexinException(ResultCodeEnum.DATA_ERROR);
}
}
@Override
public void importData(MultipartFile file) {
try {
//创建监听器对象,传递mapper对象
ExcelListener excelListener = new ExcelListener(categoryMapper);
//调用read方法读取excel数据
EasyExcel.read(file.getInputStream(), //输入流
Category.class, //映射
excelListener).sheet().doRead(); //监听
// ExcelCommonException
} catch (IOException e) {
throw new zhexinException(ResultCodeEnum.DATA_ERROR);
}
}
}
3. Mapper
<?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.zhexin.manage.mapper.Product.categoryMapper">
<!-- 用于select查询公用抽取的列 -->
<sql id="columns">
id,name,image_url,parent_id,status,order_num,create_time,update_time,is_deleted
</sql>
<select id="getList" resultType="com.zhexin.model.entity.product.Category">
-- 查询结果映射
select <include refid="columns" />
from category
-- 查询所有
where is_deleted = 0
-- 通过id排序
order by id desc
</select>
</mapper>
4.修改拦截器, 删除redis
private void exit(HttpServletResponse response, String token) {
Result result = Result.build(null, ResultCodeEnum.LOGIN_AUTH); //创建返回格式
//初始化发送通道
//设置编码
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html; charset=utf-8");
try (PrintWriter writer = response.getWriter()) { //用完自动关闭通道
//删除旧的商品列表:
redisTemplate.delete(zxRedisEnum.SPLB.getRedisID()+token);
//获取发送通道
writer.print(JSON.toJSONString(result)); //发送通道只能发送文本,转为文本
writer.flush(); //发射
} catch (IOException e) {
e.printStackTrace();
}
}
因为我们需要存储
2.前端:
1.api接口
import request from '@/utils/request'
const api = "/zx/admin/product/category"
// 第一层列表接口
export const getList = id => {
return request({
url: `${api}/getList/${id}`,
method: 'get',
})
}
2.接口调用:
引入刚才的接口 import { getList } from '@/api/category';
然后修改这里
<script setup>
import { ref ,onMounted ,getCurrentInstance } from 'vue';
import { getList } from '@/api/category';
const { proxy: ctx } = getCurrentInstance() // 可以把ctx当成vue2中的this
// 定义list属性模型
const list = ref([])
onMounted(async () => {
//取列表, 第一层为0
const data = await getList(0)
ctx.list = data.data
})
// 加载数据的方法
const fetchData = async (row, treeNode, resolve) => {
console.log(row);
// 向后端发送请求获取数据
const data = await getList(row.id)
// 返回数据
resolve(data.data)
}
</script>
结果,相当完美:

四. 导出接口编写:
利用阿里巴巴的EasyExcel模块 , 实现 数据和Excel 互相转换
官方文档: EasyExcel官方文档 - 基于Java的Excel处理工具 | Easy Excel (alibaba.com)
官网已经有栗子了: 
1. 首先我们编写一个操作这个模块的 学习测试类
引入包,我们已经在实体包中引入了, 又因为我们其他包引入了实体包,所有不用再引入了
<!--excel表格文件解析-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.1.0</version>
</dependency>
(我没有用Test)

1. 写入
导出肯定是写入,我们读取数据库然后,写入到表格
所有我们先从写入开始吧 :
这是官方给的例子:
package com.zhexin.common.uilt.Service;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.annotation.ExcelIgnore;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.util.ListUtils;
import com.alibaba.excel.write.metadata.WriteSheet;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import java.util.Date;
import java.util.List;
public class AlibabaEasyExcelUtil {
public static void main(String[] args) {
new AlibabaEasyExcelUtil().simpleWrite();
}
private List<DemoData> data() {
List<DemoData> list = ListUtils.newArrayList();
for (int i = 0; i < 10; i++) {
DemoData data = new DemoData();
data.setString("字符串" + i);
data.setDate(new Date());
data.setDoubleData(0.56);
list.add(data);
}
return list;
}
/**
* 最简单的写
* <p>
* 1. 创建excel对应的实体对象 参照{@link DemoData}
* <p>
* 2. 直接写即可
*/
public void simpleWrite() {
// 注意 simpleWrite在数据量不大的情况下可以使用(5000以内,具体也要看实际情况),数据量大参照 重复多次写入
// 写法1 JDK8+
// since: 3.0.0-beta1
String fileName = "C:\\Users\\zhexi\\Desktop\\"+"simpleWrite" + System.currentTimeMillis() + ".xlsx";
// 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
// 如果这里想使用03 则 传入excelType参数即可
EasyExcel.write(fileName, DemoData.class)
.sheet("模板")
.doWrite(() -> {
// 分页查询数据
return data();
});
// 写法2
fileName = "C:\\Users\\zhexi\\Desktop\\"+"simpleWrite" + System.currentTimeMillis() + ".xlsx";
// 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
// 如果这里想使用03 则 传入excelType参数即可
EasyExcel.write(fileName, DemoData.class).sheet("模板").doWrite(data());
// 写法3
fileName = "C:\\Users\\zhexi\\Desktop\\"+"simpleWrite" + System.currentTimeMillis() + ".xlsx";
// 这里 需要指定写用哪个class去写
try (ExcelWriter excelWriter = EasyExcel.write(fileName, DemoData.class).build()) {
WriteSheet writeSheet = EasyExcel.writerSheet("模板").build();
excelWriter.write(data(), writeSheet);
}
}
}
@Getter
@Setter
@EqualsAndHashCode
class DemoData {
@ExcelProperty("字符串标题")
private String string;
@ExcelProperty("日期标题")
private Date date;
@ExcelProperty("数字标题")
private Double doubleData;
/**
* 忽略这个字段
*/
@ExcelIgnore
private String ignore;
}
成功在我桌面上留下了三个文件

打开也有结果:

最终简化代码:
fileName =fileName +".xlsx"; //输出目录
//传入文件名 , 写入模板名 , 写入数据List 类型要和.class相同
EasyExcel.write(fileName, DemoData.class).sheet("模板").doWrite(data());
对应关系:

2.读取:
// 这里 需要指定读用哪个class去读,然后读取第一个sheet 文件流会自动关闭
EasyExcel.read(fileName, DemoData.class, new DemoDataListener()).sheet().doRead();
class DemoDataListener implements ReadListener<DemoData> {
// 这个每一条数据解析都会来调用
@Override
public void invoke(DemoData demoData, AnalysisContext analysisContext) {
System.out.println(demoData);
}
//解析完成
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
}
}
2.向前端输出表格
1.后端
先输出到桌面看一下:
//下载
@Override
public void exportData(HttpServletRequest request , HttpServletResponse response) {
String fileName ="C:\\Users\\zhexi\\Desktop\\fileName" +".xlsx"; //输出目录
//TODO 这里频繁调用token,可以把token放到SysUser里, 方便线程变量读取
String token = request.getHeader("token"); //从请求头里读取token
//查询redis中有没有库存
String redisyzm = redisTemplate.opsForValue().get(zxRedisEnum.SPLB.getRedisID()+token);
JSONArray objects = JSON.parseArray(redisyzm);
List<Category> qblist = objects.toList(Category.class);
//传入文件名 , 写入模板名 , 写入数据List 类型要和.class相同
EasyExcel.write(fileName, Category.class).sheet("模板").doWrite(qblist);
}
发现时间和标题没有输出来,看官网 ,
时间其实所有的在上面:


标题改一下, 另一个实体类也一样,就不写了
@Data
public class BaseEntity implements Serializable {
//表格的列的名称 , 和先后顺序
@ExcelProperty(value = "ID", index = 0)
@Schema(description = "唯一标识")
private Long id;
@ExcelProperty("创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Schema(description = "创建时间")
private Date createTime;
@ExcelProperty("修改时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") //json时间
@Schema(description = "修改时间")
private Date updateTime;
@ExcelProperty("是否删除")
@Schema(description = "是否删除")
private Integer isDeleted;
}
可以了:

官网的美化:

最后我们通过输出流发送出去:(HttpServletResponse.getOutputStream())
下载请求头:文件下载响应头的设置_文件下载设置响应头-CSDN博客
//下载
//TODO 后期可以再加上 宽高 、 把分类给写到不同的模板里
@Override
public void exportData(HttpServletRequest request , HttpServletResponse response) {
try {
// 设置响应结果类型
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
// 这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系
String fileName = null;
fileName = URLEncoder.encode("分类数据", "UTF-8");
response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx");
// response.setHeader("Access-Control-Expose-Headers", "Content-Disposition");
//TODO 这里频繁调用token,可以把token放到SysUser里, 方便线程变量读取
String token = request.getHeader("token"); //从请求头里读取token
//查询redis中有没有库存
String redisyzm = redisTemplate.opsForValue().get(zxRedisEnum.SPLB.getRedisID()+token);
JSONArray objects = JSON.parseArray(redisyzm);
List<Category> qblist = objects.toList(Category.class);
//传入文件名 , 写入模板名 , 写入数据List 类型要和.class相同
EasyExcel.write(response.getOutputStream(), Category.class).sheet("商品分类").doWrite(qblist);
} catch (Exception e) {
throw new zhexinException(ResultCodeEnum.DATA_ERROR);
}
}
2.前端对接:
category.js
在src/api文件夹下创建一个category.js文件,文件的内容如下所示:
// 导出方法
export const ExportCategoryData = () => {
return request({
url: `${api_name}/exportData`,
method: 'get',
responseType: 'blob' // // 这里指定响应类型为blob类型,二进制数据类型,用于表示大量的二进制数据
})
}
category.vue
-
导出按钮绑定事件
-
添加导出方法
<div class="tools-div">
<el-button type="success" size="small" @click="exportData">导出</el-button>
<el-button type="primary" size="small" >导入</el-button>
</div>
<script setup>
import { FindCategoryByParentId , ExportCategoryData} from '@/api/category.js'
const exportData = () => {
// 调用 ExportCategoryData() 方法获取导出数据
ExportCategoryData().then(res => {
// 创建 Blob 对象,用于包含二进制数据
const blob = new Blob([res]);
// 创建 a 标签元素,并将 Blob 对象转换成 URL
const link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
// 设置下载文件的名称
link.download = '分类数据.xlsx';
// 模拟点击下载链接
link.click();
})
}
</script>
需要程序重新运行

3.前端输入表格
1,后端
1。读取表格
2.正确删除原来数据
3.存入
参考: MyBatis<foreach>标签的用法及多种循环方式_<foreach-CSDN博客
service
@Override
public void importData(MultipartFile file) {
try {
//创建监听器对象,传递mapper对象
ExcelListener excelListener = new ExcelListener(categoryMapper);
//调用read方法读取excel数据
EasyExcel.read(file.getInputStream(), //输入流
Category.class, //映射
excelListener).sheet().doRead(); //监听
// ExcelCommonException
} catch (IOException e) {
throw new zhexinException(ResultCodeEnum.DATA_ERROR);
}
}
controller
@Operation(summary = "上传表格")
@PostMapping("importData")
public Result importData(MultipartFile file) {
categoryService.importData(file);
return Result.build(null , ResultCodeEnum.SUCCESS) ;
}
映射器:
package com.zhexin.manage.service.Product.Impl;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.excel.util.ListUtils;
import com.zhexin.manage.mapper.Product.categoryMapper;
import com.zhexin.model.entity.product.Category;
import java.util.List;
public class ExcelListener extends AnalysisEventListener<Category> {
/**
* 每隔5条存储数据库,实际使用中可以100条,然后清理list ,方便内存回收
*/
private static final int BATCH_COUNT = 100;
/**
* 缓存的数据
*/
private List cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
//官方说这个类无法注册为bean ,所以只能传入 ,获取mapper对象
private categoryMapper categoryMapper;
public ExcelListener(categoryMapper categoryMapper) {
this.categoryMapper = categoryMapper;
}
// 每解析一行数据就会调用一次该方法
@Override
public void invoke(Category data, AnalysisContext analysisContext) {
if (analysisContext.readRowHolder().getRowIndex() == 1){
//删除数据库所有旧数据://便于存入新数据,防止出现主键重复的现象
categoryMapper.deleteList();
}
// CategoryExcelVo data = (CategoryExcelVo)o;
cachedDataList.add(data);
// 达到BATCH_COUNT (5条数据)了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOM
if (cachedDataList.size() >= BATCH_COUNT) {
saveData();
// 刷新缓存容器
cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
// excel解析完毕以后需要执行的代码
// 这里也要保存数据,确保最后遗留的数据也存储到数据库
saveData();
}
private void saveData() {
if (!(cachedDataList == null || cachedDataList.isEmpty())) {
//不能抛异常,因为最后一个很有可能是空
categoryMapper.batchInsert(cachedDataList);
}
}
}
2.前端:
<template>
<div class="tools-div">
<el-button type="success" size="small" @click="exportData">导出</el-button>
<el-button type="primary" size="small" @click="importData">导入</el-button>
</div>
<el-dialog v-model="dialogImportVisible" title="导入-(⚠️⚠️⚠️此操作会删除之前存在的所有数据⚠️⚠️⚠️)" width="30%">
<el-form label-width="120px">
<el-form-item label="分类文件">
<el-upload
class="upload-demo"
action="/zx/admin/product/category/importData"
:on-success="onUploadSuccess"
:headers="headers"
>
<el-button type="primary">上传</el-button>
</el-upload>
</el-form-item>
</el-form>
</el-dialog>
<!---懒加载的树形表格-->
<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 ,onMounted ,getCurrentInstance } from 'vue';
import { getList , ExportCategoryData } from '@/api/category';
import { useApp } from '@/pinia/modules/app'
const { proxy: ctx } = getCurrentInstance() // 可以把ctx当成vue2中的this
// 定义list属性模型
const list = ref([])
onMounted(async () => {
//取列表, 第一层为0
const data = await getList(0)
ctx.list = data.data
})
// 加载数据的方法
const fetchData = async (row, treeNode, resolve) => {
console.log(row);
// 向后端发送请求获取数据
const data = await getList(row.id)
// 返回数据
resolve(data.data)
}
const exportData = () => {
// 调用 ExportCategoryData() 方法获取导出数据
ExportCategoryData().then(res => {
// 创建 Blob 对象,用于包含二进制数据
const blob = new Blob([res]);
// 创建 a 标签元素,并将 Blob 对象转换成 URL
const link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
// 设置下载文件的名称
link.download = '分类数据.xlsx';
// 模拟点击下载链接
link.click();
})
}
// 文件上传相关变量以及方法定义
const dialogImportVisible = ref(false)
const headers = {
token: useApp().authorization.token // 从pinia中获取token,在进行文件上传的时候将token设置到请求头中
}
const importData = () => {
dialogImportVisible.value = true
}
// 上传文件成功以后要执行方法
const onUploadSuccess = async (response, file) => {
ctx.$message.success('操作成功')
dialogImportVisible.value = false
const data = await getList(0)
ctx.list = data.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>
4.总结
本目录的重难点已经完成了 , 其他的前面都是实现过,大同小异
主要技术:
- Excel的和 数据库互转
- 文件上传与下载
- 思路还是很复杂的,,建议好好看
有兴趣的可以继续实现:
- 单个列表的 删改 操作
- 搜索
- 添加
- 修改分类
- 部分下载 (思路:查出来的数据,对父类id进行过滤, 看是否为传进的id, 生成新的list)
- 上传到部分(思路:没有子标签的, 显示本按钮 , 上传Excel到父类id)
-------品牌管理---------
一.页面实现:
在前端项目中创建对应的页面,以及配置对应的异步路由
在src/views/product目录下,创建brand.vue和category.vue文件
该页面可以将其分为3部分:
1、添加按钮
2、数据表格
3、分页组件
brand.vue 代码实现如下所示:
<template>
<div class="tools-div">
<el-button type="success" size="small">添 加</el-button>
</div>
<el-table :data="list" style="width: 100%">
<el-table-column prop="name" label="品牌名称" />
<el-table-column prop="logo" label="品牌图标" #default="scope">
<img :src="scope.row.logo" width="50" />
</el-table-column>
<el-table-column prop="createTime" label="创建时间" />
<el-table-column label="操作" align="center" width="200" >
<el-button type="primary" size="small">
修改
</el-button>
<el-button type="danger" size="small">
删除
</el-button>
</el-table-column>
</el-table>
<el-pagination
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next"
:total="total"
/>
</template>
<script setup>
import { ref } from 'vue'
// 定义表格数据模型
const list = ref([
{"id":1 , "name":"华为" , "logo":"http://139.198.127.41:9000/sph/20230506/华为.png"} ,
{"id":2 , "name":"小米" , "logo":"http://139.198.127.41:9000/sph/20230506/小米.png"} ,
])
// 分页条数据模型
const total = ref(0)
</script>
<style scoped>
.tools-div {
margin: 10px 0;
padding: 10px;
border: 1px solid #ebeef5;
border-radius: 3px;
background-color: #fff;
}
</style>
在src/router/modules文件夹下创建product.js路由文件
const Layout = () => import('@/layout/index.vue')
const category = () => import('@/views/product/category.vue')
const brand = () => import('@/views/product/brand.vue')
export default [
{
path: '/product',
component: Layout,
name: 'product',
meta: {
title: '商品管理',
},
icon: 'Histogram',
children: [
{
path: '/category',
name: 'category',
component: category,
meta: {
title: '分类管理',
},
},
{
path: '/brand',
name: 'brand',
component: brand,
meta: {
title: '品牌管理',
},
},
],
},
]
剩下的就是实现增删改查,不多说了,只写个查放这里吧:
这里我们顺便学习一下ts(会有些爆红,不影响) :
Ts定义基本数据类型-腾讯云开发者社区-腾讯云 (tencent.com)
TS如何定义和使用对象数组_百度知道 (baidu.com)
安装:
npm install -g typescript
tsc -v


不出这个, 运行不了:tsc : 无法加载文件 C:\Users\Administrator\AppData\Roaming\npm\tsc.ps1,因为在此系统上禁止运行脚本-CSDN博客
写了个v3框架: 把vue3变成了vue2😂😂😂
可以 不看
<script>
// 接口引入区
import { ref ,reactive ,onMounted ,defineComponent , toRefs , getCurrentInstance} from 'vue'
// 接口暴露到外面
//再外面写 , 用js引入的写法, 可以单独写成一个js文件 与<script setup>是一样的,这样写就要再内部定义setup
export default defineComponent({
//vue引入这个export ,直接运行setup
setup(){
// 外部--- <vue属性区>
const { proxy: ctx } = getCurrentInstance() // 可以把ctx当成vue2中的this
//载入函数
onMounted(async () => {
})
//抛到外面的区域---<属性区>
const thiss =reactive({
})
//抛到外面的区域--- <方法区>
const model =reactive({
})
return {
//说白了就是把衣服脱了,不管是大括号([])、花括号({}),统统不在话下,全部脱掉脱掉!
...toRefs(thiss), //toRefs抛出ff, 并实实时同步状态,而且...展开为对象
...toRefs(model),
}
},
})
</script>
1.后端:
这里主要用到的就是, 之前写用户头像的minio方法,我我看经常使用于是把他提取成了一个方法:
其他的还是增删改查。。。。。。
package com.zhexin.common.uilt.Service;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.UUID;
import io.minio.BucketExistsArgs;
import io.minio.MakeBucketArgs;
import io.minio.MinioClient;
import io.minio.PutObjectArgs;
import io.minio.errors.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Date;
@Service
public class MinioUtil {
@Autowired
private MinioProperties minioProperties;
//springMVC中的文件上传工具MultipartFile
public String sc(MultipartFile multipartFile , String wjjname) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {
//创建连接
MinioClient minioClient =
MinioClient.builder()
.endpoint(minioProperties.getUrl())
.credentials(minioProperties.getZh(), minioProperties.getMm())
.build();
// 判断是否存在容器,否则创建
boolean found = minioClient.bucketExists(BucketExistsArgs.builder().bucket(minioProperties.getRqname()).build());
if (!found) {
// 创建容器
minioClient.makeBucket(MakeBucketArgs.builder().bucket(minioProperties.getRqname()).build());
}
// 设置存储对象名称
String dateDir = DateUtil.format(new Date(), "yyyyMMdd");
String uuid = UUID.randomUUID().toString().replace("-", "");
//20230801/文件夹名称/443e1e772bef482c95be28704bec58a901.jpg
String fileName = dateDir+"/"+wjjname +"/"+uuid+multipartFile.getOriginalFilename();
//把文件写入容器
PutObjectArgs putObjectArgs = PutObjectArgs.builder()
.bucket(minioProperties.getRqname())
.object(fileName)
.stream(multipartFile.getInputStream(),multipartFile.getSize(),-1)
.build();
//发送
minioClient.putObject(putObjectArgs) ;
//要在Minio里设置这个桶的权限为public 才能访问到图片,如果访问地址得不到图片的话
//http://127.0.0.1:9000/usertx/20231212/文件夹名称/8f4cf52b13f74d8a87511e147c67cbedv2-4c534c96dc62e9f541473ea2f5e73fd5_r.png
return minioProperties.getUrl() + "/" + minioProperties.getRqname() + "/" + fileName ;
}
/*//调用方法:
@Autowired
private sysMenuService sysMenuService;
String jg = "";
if (ObjectUtil.isEmpty(file)) {
throw new zhexinException(ResultCodeEnum.CSCW);
}
try {
jg=minioUtil.sc(file, "文件夹名称");
} catch (Exception e) {
throw new zhexinException(ResultCodeEnum.TPSCSB);
}
return jg;
*/
}
2.前端:
api:
import request from '@/utils/request'
const api = "/zx/admin/product/brand"
// get列表
export const getList = (pageNum,pageSize) => {
return request({
url: `${api}/getList/${pageNum}/${pageSize}`,
method: 'get',
})
}
// 添加和修改 id = 0: 添加
export const newbran = datas => {
return request({
url: `${api}/newbran`,
method: 'post',
data : datas
})
}
// 删除
export const deletebran = id => {
return request({
url: `${api}/deletebran/${id}`,
method: 'delete',
})
}
因为用了ts所以编译器有些爆红,不影响
<template>
<div class="tools-div">
<el-button type="success" size="small" @click="saveOrUpdate">添 加</el-button>
</div>
<el-dialog v-model="dialogVisible" title="添加或修改" width="30%">
<el-form label-width="120px">
<el-form-item label="品牌名称">
<el-input v-model="label.name"/>
</el-form-item>
<el-form-item label="品牌图标">
<el-upload
class="avatar-uploader"
action="/zx/admin/product/brand/newImage"
:show-file-list="false"
:on-success="handleAvatarSuccess"
:headers="headers"
>
<img v-if="label.logo" :src="label.logo" class="avatar" />
<el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
</el-upload>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="newANDupdata">提交</el-button>
<el-button @click="dialogVisible = false">取消</el-button>
</el-form-item>
</el-form>
</el-dialog>
<el-table :data="list" style="width: 100%">
<el-table-column prop="name" label="品牌名称" />
<el-table-column prop="logo" label="品牌图标" #default="scope">
<img :src="scope.row.logo" width="50" />
</el-table-column>
<el-table-column prop="createTime" label="创建时间" />
<el-table-column label="操作" align="center" width="200" #default="scope">
<el-button type="primary" size="small" @click="editShow(scope.row)">
修改
</el-button>
<el-button type="danger" size="small" @click="deletebrans(scope.row)" >
删除
</el-button>
</el-table-column>
</el-table>
<!--分页条 @是绑定按钮的方法 , v-model是绑定参数-->
<!--监听分页,换页,和字符-->
<el-pagination :page-sizes="[10, 2, 20, 50, 100]" layout="total, sizes, prev, pager, next"
:total="pagination.total"
@size-change="getList"
@current-change="getList"
v-model:current-page="pagination.pageNum"
v-model:page-size="pagination.pageSize" />
</template>
<script lang="ts">
//引入区
import { getList , newbran, deletebran } from '@/api/brand'
import { ref ,reactive ,onMounted ,defineComponent , toRefs,getCurrentInstance } from 'vue'
import { useApp } from '@/pinia/modules/app'
// 接口暴露到外面
//再外面写 , 用js引入的写法, 可以单独写成一个js文件 与<script setup>是一样的,这样写就要再内部定义setup
export default defineComponent({
//vue引入这个export ,直接运行setup
setup(){
// 外部--- <vue属性区>
// 定义表格数据模型ts写法, 再vue3里让怎么写 , 但是ts不让: // list : Array<{id:Number , name:String , logo:String}>[],
interface labelin{
id:Number ,
name:String ,
logo:String
}
const label : labelin ={
id:0 ,
name :"",
logo:""
}
const list : Array<labelin> = []
const { proxy: ctx } = getCurrentInstance() // 可以把ctx当成vue2中的this
//载入函数
onMounted(async () => {
model.getList()
})
//抛到外面的区域---<属性区>
const thiss =reactive({
pagination: {
//当前页码
pageNum: 1,
//当前显示页码
pageSize: 10,
// 分页条总记录数
total: 0,
},
//列表定义到外面
list:list,
label:label,
dialogVisible:false,
headers :{
token: useApp().authorization.token // 从pinia中获取token,在进行文件上传的时候将token设置到请求头中
}
})
//抛到外面的区域--- <方法区>
const model =reactive({
//查询列表
async getList() {
const { code, data, message } = await getList(thiss.pagination.pageNum, thiss.pagination.pageSize)
if (code == 200) {
thiss.list = data.list
thiss.pagination.pageNum = data.pageNum
thiss.pagination.pageSize = data.pageSize
thiss.pagination.total = data.total
ctx.$message.success(message)
} else {
ctx.$message.error(message)
}
},
//图片上传成功回调
handleAvatarSuccess(response, uploadFile) {
thiss.label.logo = response.data
},
//添加
saveOrUpdate() {
thiss.label = {
id: 0,
name: "",
logo: ""
}
thiss.dialogVisible = true
},
//修改
editShow(data) {
thiss.label = data
thiss.dialogVisible = true
},
//提交
async newANDupdata() {
const { code, data, message } = await newbran(thiss.label)
if (code == 200) {
model.getList()
ctx.$message.success(message)
thiss.dialogVisible = false
} else {
ctx.$message.error(message)
}
},
//删除
async deletebrans(datas) {
const { code, data, message } = await deletebran(datas.id)
if (code == 200) {
model.getList()
ctx.$message.success(message)
} else {
ctx.$message.error(message)
}
},
})
return {
//说白了就是把衣服脱了,不管是大括号([])、花括号({}),统统不在话下,全部脱掉脱掉!
...toRefs(thiss), //toRefs抛出ff, 并实实时同步状态,而且...展开为对象
...toRefs(model),
}
},
})
</script>
<style scoped>
.tools-div {
margin: 10px 0;
padding: 10px;
border: 1px solid #ebeef5;
border-radius: 3px;
background-color: #fff;
}
.avatar-uploader .avatar {
width: 100px;
height: 100px;
display: block;
}
</style>
-------分配品牌分类--------
查询列表的,数据库是精髓:(多表查询) SQL中inner、left、right三种表连接_inner left-CSDN博客
<select id="GetCategoryBrandPageList" resultType="com.zhexin.model.entity.product.CategoryBrand">
SELECT
b.id brandId,
bc.category_id categoryId,
b.name brandName,
c.name categoryName,
b.logo,
b.create_time,
b.update_time,
b.is_deleted
FROM
brand AS b
INNER JOIN category_brand AS bc ON b.id = bc.brand_id
INNER JOIN category AS c ON c.id = bc.category_id
<where>
-- b.is_deleted =0
<if test="brandId != null and brandId != '' ">
AND bc.brand_id = #{brandId}
</if>
<if test="categoryId != null and categoryId != '' ">
AND bc.category_id = #{categoryId}
</if>
</where>
</select>
注解Sql:

vue3关键字 typeof: 【Vue3+Ts project】typeof 作用 、components 全局组件添加类型-CSDN博客
spu:
两个手机,一个x21 是 x23
这就是两个不同的spu

sku
多个参数组合, 为一个sku
也就是说:参数的各种组合,组合成各种sku

占位

浙公网安备 33010602011771号