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]

 

 

二. 创建界面

该页面可以将其分为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>

三. 列表接口编写:

为了防止数据太多, 导致前端卡顿 ,我们采用:懒加载方式输出

后台数据库查询过多, 可以采用缓存解决 ,,,  (以后再来改😂)

(只显示第一列,  点一下显示下一类)

业务分析:

可以看到这个表也有父类id  ,我们可以联想到菜单的列表也是这样的,所以我们菜单的列表分类又能用了

所以的一个接口就是返回 parentid = 0 的列表

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

这个属性绑定一个有三个参数的方法,测试一下 

row里包含这个id

我们就可以把id传到后端,结果用返回给列表

后端,接收到id,先查所有子类,  ,然后把所有查出来的数据再次查询有没有子类,

但是这样一来我们就要循环调用数据库,肯定会对性能产生很大的影响

所以我特意查了一下rudis和mysql的性能:Redis的性能为什么比Mysql要快?_哔哩哔哩_bilibili

数据库支持事务和安全性,Redis实现快速读写

所以我的想法是第一次查列表的时候,直接查出所有的,然后作为json保存到redis,key为用户的token,   在请求拦截器中, 检查用户没有登录的时候,一块把他删除

后续只读redis就不用都mysql了 , 查子组件的时候就可以json转对象,遍历查询了

二. 一级菜单:

1.后端

  1. 查询所有数据库,返回对象
  2. 转为json ,存储到redis
  3. 找出parentid=0的对象
  4. 找出这个对象是否有的子类
  5. 生产列表

首先创建三件套 : 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)

vue3中使用ts-CSDN博客

TS如何定义和使用对象数组_百度知道 (baidu.com)

es6中三个点是什么意思-前端问答-PHP中文网

安装:

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

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

占位

posted @ 2023-12-14 09:34  哲_心  阅读(26)  评论(0)    收藏  举报