_126- 基于vue-element-admin搭建前端项目
vue-element-admin
vue-element-admin是基于element-ui 的一套后台管理系统集成方案。
GitHub地址: https://github.com/PanJiaChen/vue-element-admin
项目在线预览: https://panjiachen.gitee.io/vue-element-admin
vue-admin-template
简介
vueAdmin-template是基于vue-element-admin的一套后台管理系统基础模板(最少精简版),可作为模板进行二次开发。
GitHub地址: https://github.com/PanJiaChen/vue-admin-template
根据用户角色来动态生成侧边栏的分支:https://github.com/PanJiaChen/vue-admin-template/tree/permission-control
安装和运行
解压压缩包 vue-admin-template-permission-control.zip
# 解压压缩包vue-admin-template-permission-control.zip
# 重命名为srb-admin
# 进入目录
cd srb-admin
# 安装依赖
npm install
# 启动。执行后,浏览器自动弹出并访问http://localhost:9528/
npm run dev
_127-前端页面的修改
前端配置
禁用ESLint语法检查
vue.config.js 第30行处禁用ESLint语法检查
lintOnSave: false,
添加prettier格式化配置
在vue项目根目录下新建一个文件.prettierrc
{
"semi": false,
"singleQuote": true,
"htmlWhitespaceSensitivity": "ignore"
}
修改页面标题
src/settings.js 第3行处修改页面标题
国际化设置
src/main.js 第7行处修改语言
import locale from 'element-ui/lib/locale/lang/zh-CN' // lang i18n
测试平台语言的修改
下拉菜单修改
登录页修改
src/views/login/index.vue
修改页面标题、登录按钮等
_128-项目中路由的定义
组件定义
创建vue组件
在src/views文件夹下创建以下文件夹和文件
core/integral-grade/list.vue
<template>
<div class="app-container">
积分等级列表
</div>
</template>
<script>
export default {
}
</script>
<style scoped>
</style>
core/integral-grade/form.vue
<template>
<div class="app-container">
积分等级表单
</div>
</template>
<script>
export default {
}
</script>
<style scoped>
</style>
路由定义
修改 src/router/index.js 文件,重新定义constantRoutes,拷贝到 dashboard路由节点 下面
注意:每个路由的name不能相同
{
path: '/core/integral-grade',
component: Layout,
redirect: '/core/integral-grade/list',
name: 'coreIntegralGrade',
meta: { title: '积分等级管理', icon: 'el-icon-s-marketing' },
alwaysShow: true,
children: [
{
path: 'list',
name: 'coreIntegralGradeList',
component: () => import('@/views/core/integral-grade/list'),
meta: { title: '积分等级列表' }
},
{
path: 'create',
name: 'coreIntegralGradeCreate',
component: () => import('@/views/core/integral-grade/form'),
meta: { title: '新增积分等级' }
},
{
path: 'edit/:id',
name: 'coreIntegralGradeEdit',
component: () => import('@/views/core/integral-grade/form'),
meta: { title: '编辑积分等级' },
hidden: true
}
]
},
改一下其他菜单的 title
dashboard 改成首页
Example 改成 例子
Form 改成表单
Table 改成 表格
Tree 改成 树
_129-其他路由节点的说明
无内容
_130-全栈开发流程说明
下图是开发过程中涉及到和几个核心的模块,我们已经完成了路由的配置和页面组件的创建,接下来我们需要进一步完善页面组件的模板部分,以及脚本等部分的开发,然后创建前后端对接需要的api模块,最后通过api模块向后端接口发起调用。
_131-将接口服务器地址由mockserver切换到nginx
nginx反向代理配置
目前,应用程序的前后端基本架构如下:srb-admin是前端程序,直接调用后端的srb-core微服务
为了能够让前端程序能够同时对接多个后端服务,我们可以使用多种解决方案,例如nginx反向代理、微服务网关等。这里我们先使用nginx作为前后端中间的反向代理层,架构如下
nginx的配置
把压缩包复制到一个没有中文的路劲下面,解压
修改配置
server {
listen 80;
server_name localhost;
location ~ /core/ {
proxy_pass http://localhost:8110;
}
location ~ /sms/ {
proxy_pass http://localhost:8120;
}
location ~ /oss/ {
proxy_pass http://localhost:8130;
}
}
启动 nginx
输入 start nginx.exe ,回车就好了
nginx的命令
start nginx #启动
nginx -s stop #停止
nginx -s reload #重新加载配置
前端的配置: .env.development
# base api: 配置到后台的 nginx 服务器
VUE_APP_BASE_API = 'http://localhost'
注意: vue 改配置文件需要重启才能生效
_132-将登录接口改成mockserver的地址
mock-server
VUE_APP_BASE_API的修改会影响到平台模拟登录功能的mock数据,因此需要修改mock-server的地址
修改 mock/mock-server.js 文件 第37行
url: new RegExp(`/dev-api${url}`),
修改 src/api/user.js中的接口调用,为每一个远程调用添加配置
baseURL: '/dev-api',
_133-后端接口地址的修改的总结
无内容
_134-前端api模块的定义
定义api模块
创建文件 src/api/core/integral-grade.js
// @ 符号在vue.config.js 中配置, 表示 'src' 路径的别名
// 引入 axios 的初始化模块
import request from '@/utils/request'
// 导出默认模块
export default {
// 定义模块成员
// 成员方法:获取积分等级列表
list() {
// 调用 axios 的初始化模块,发送远程 ajax 请求
return request({
url: '/admin/core/integralGrade/list',
method: 'get'
})
}
}
_135-list组件中调用api
定义页面组件脚本
src/views/core/integral-grade/list.vue
<script>
// 引入 api 模块
import integralGradeApi from '@/api/core/integral-grade'
export default {
// 定义数据模型
data() {
return {
list: [] // 数据列表
}
},
// 页面渲染成功后获取数据
created() {
this.fetchData()
},
// 定义方法
methods: {
fetchData() {
// 调用api
integralGradeApi.list().then(response => {
this.list = response.data.list
})
}
}
}
</script>
现在可以打开浏览器,查看是否成功访问后端拿到数据
修改一下 获取 list 接口
@ApiOperation("积分等级列表")
@GetMapping("/list")
public R listAll(){
log.info("hi i'm helen");
//log.warn("warning!!!");
//log.error("it's a error");
List<IntegralGrade> list = integralGradeService.list();
return R.ok().data("list", list).message("获取列表成功");
}
_136-axios的响应拦截器的拦和放行条断
axios响应拦截器修改
src/utils/request.js 中 将第49行的
if (res.code !== 20000) {
修改成
if (res.code !== 0 && res.code !== 20000) {
因为我们的后端接口统一结果判断0为成功的响应结果,而mock数据判断20000位正确的结果
_137-积分等级列表的页面渲染
定义页面组件模板
<template>
<div class="app-container">
<!-- 表格 -->
<!-- stripe 是否为斑马纹 table -->
<el-table :data="list" border stripe>
<el-table-column type="index" width="50" />
<el-table-column prop="borrowAmount" label="借款额度" />
<el-table-column prop="integralStart" label="积分区间开始" />
<el-table-column prop="integralEnd" label="积分区间结束" />
</el-table>
</div>
</template>
_138-响应码的兼容性判断的说明
无内容
_139-常见错误说明
无内容
_140-删除功能的实现和断点调试
定义api
src/api/core/integral-grade.js
removeById(id) {
return request({
url: `/admin/core/integralGrade/remove/${id}`,
method: 'delete'
})
}
页面组件模板
src/views/core/integral-grade/list.vue,在table组件中添加删除列
<el-table-column label="操作" width="200" align="center">
<template slot-scope="scope">
<el-button
type="danger"
size="mini"
icon="el-icon-delete"
@click="removeById(scope.row.id)"
>
删除
</el-button>
</template>
</el-table-column>
_141-删除功能确认
删除数据脚本
// 根据id删除数据
removeById(id) {
this.$confirm('此操作将永久删除该记录, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
integralGradeApi.removeById(id).then(response => {
this.$message.success(response.message)
this.fetchData()
})
}).catch(() => {
this.$message({
type: 'info',
message: '取消删除'
})
})
}
_142-删除功能流程优化
then() 与 then() 用串联的方式写,不要用嵌套
removeById(id) {
this.$confirm('此操作将永久删除该记录, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
return integralGradeApi.removeById(id)
}).then(response => {
this.$message.success(response.message)
this.fetchData()
})
.catch(() => {
this.$message({
type: 'info',
message: '取消删除'
})
})
}
_143-保存功能脚本的编写
定义api
src/api/core/integral-grade.js
save(integralGrade) {
return request({
url: '/admin/core/integralGrade/save',
method: 'post',
data: integralGrade
})
}
定义页面data
src/views/core/integral-grade/form.vue,完善data定义
<script>
export default {
data() {
return {
integralGrade: {}, // 初始化数据
saveBtnDisabled: false // 保存按钮是否禁用,防止表单重复提交
}
}
}
</script>
新增数据脚本
src/views/core/integral-grade/form.vue,引入api
import integralGradeApi from '@/api/core/integral-grade'
定义保存方法
methods: {
saveOrUpdate() {
// 禁用保存按钮
this.saveBtnDisabled = true
this.saveData()
},
// 新增数据
saveData() {
// debugger
integralGradeApi.save(this.integralGrade).then(response => {
this.$message({
type: 'success',
message: response.message
})
this.$router.push('/core/integral-grade/list')
})
}
}
_144-保存功能的表单实现和脚本优化
页面组件模板
src/views/core/integral-grade/form.vue,完善template
<template>
<div class="app-container">
<!-- 输入表单 -->
<el-form label-width="120px">
<el-form-item label="借款额度">
<el-input-number v-model="integralGrade.borrowAmount" :min="0" />
</el-form-item>
<el-form-item label="积分区间开始">
<el-input-number v-model="integralGrade.integralStart" :min="0" />
</el-form-item>
<el-form-item label="积分区间结束">
<el-input-number v-model="integralGrade.integralEnd" :min="0" />
</el-form-item>
<el-form-item>
<el-button
:disabled="saveBtnDisabled"
type="primary"
@click="saveOrUpdate()"
>
保存
</el-button>
</el-form-item>
</el-form>
</div>
</template>
_145-表单数据回显
列表页编辑按钮
src/views/core/integral-grade/list.vue,表格“操作”列中增加“修改”按钮
<router-link :to="'/core/integral-grade/edit/' + scope.row.id" style="margin-right:5px;" >
<el-button type="primary" size="mini" icon="el-icon-edit">
修改
</el-button>
</router-link>
定义api
src/api/core/integral-grade.js
getById(id) {
return request({
url: `/admin/core/integralGrade/get/${id}`,
method: 'get'
})
}
获取数据脚本
src/views/core/integral-grade/form.vue,methods中定义回显方法
// 根据id查询记录
fetchDataById(id) {
integralGradeApi.getById(id).then(response => {
this.integralGrade = response.data.record
})
}
页面渲染成功后获取数据
因为已在路由中定义如下内容:path: ‘edit/:id’,因此可以使用 this.$route.params.id 获取路由中的id
//页面渲染成功
created() {
// 当路由中存在 id 属性的时候,就是回显表单,需要调用回显数据的接口
if (this.$route.params.id) {
this.fetchDataById(this.$route.params.id)
}
},
_146-表单数据更新
定义api
src/api/core/integral-grade.js
updateById(integralGrade) {
return request({
url: '/admin/core/integralGrade/update',
method: 'put',
data: integralGrade
})
}
更新数据脚本
// 根据id更新记录
updateData() {
// 数据的获取
integralGradeApi.updateById(this.integralGrade).then(response => {
this.$message({
type: 'success',
message: response.message
})
this.$router.push('/core/integral-grade/list')
})
}
完善saveOrUpdate方法
saveOrUpdate() {
// 禁用保存按钮
this.saveBtnDisabled = true
if (!this.integralGrade.id) {
this.saveData()
} else {
this.updateData()
}
},
_147-组件的概念
什么是组件
组件(Component)是 Vue.js 最强大的功能之一。
组件可以扩展 HTML 元素,封装可重用的代码。
组件系统让我们可以用独立可复用的小组件来构建大型应用,几乎任意类型的应用界面都可以抽象为一个组件树:
_148-组件学习的目标
无内容
_149-前端程序的入口html-index.html
项目组件分析
程序入口
• 入口html:public/index.html
• 入口js脚本:src/main.js
• 顶层组件:src/App.vue
• 路由:src/router/index.js
main.js 中引入了App.vue和 router/index.js,根据路由配置,App.vue中的路由出口会显示相应的页面组件的内容
_150-前端程序的入口脚本-main.js
入口脚本
引入顶层组件模块和路由模块
_151-前端程序的顶层组件-App.vue
顶层组件
路由出口的位置
路由配置
路由出口的位置显示的页面组件
登录页组件关系
_152-前端程序的顶层组件-App.vue炭入的404
无内容
_153-前端程序的布局组件-Layout
路由
src/router/index.js:这个组件应用了Layout布局文件
布局
src/layout/index.vue:侧边栏、导航栏、主内容区
主内容区
src/layout/components/AppMain.vue:Layout的路由出口(主内容区)
_154-前端程序的嵌套路由和嵌套路由出口
积分区间列表页面组件
_155-Excel导入导出的开发场景
Excel导入导出的应用场景
数据导入
减轻录入工作量
数据导出
统计信息归档
数据传输
异构系统之间数据传输
_156-EasyExcel介绍
EasyExcel简介
常见excel分析框架:POI、EasyExcel
官方网站
https://github.com/alibaba/easyexcel
快速开始:https://www.yuque.com/easyexcel/doc/easyexcel
_157-EasyExcel的优点和工作原理
EasyExcel特点
- Java领域解析、生成Excel比较有名的框架有Apache poi、jxl等。但他们都存在一个严重的问题就是非常的耗内存。如果你的系统并发量不大的话可能还行,但是一旦并发上来后一定会OOM或者JVM频繁的full gc。
- EasyExcel是阿里巴巴开源的一个excel处理框架,以使用简单、节省内存著称。EasyExcel能大大减少占用内存的主要原因是在解析Excel时没有将文件数据一次性全部加载到内存中,而是从磁盘上一行行读取数据,逐个解析。
- EasyExcel采用一行一行的解析模式,并将一行的解析结果以观察者的模式通知处理(AnalysisEventListener)。
_158-最简单的EasyExcel写
现在主要测试学习,所以新建一个 maven 项目
创建项目
创建一个普通的maven项目
项目名:alibaba-easyexcel
pom中引入xml相关依赖
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>2.1.7</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.5</version>
</dependency>
<dependency>
<groupId>org.apache.xmlbeans</groupId>
<artifactId>xmlbeans</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
</dependencies>
创建基类目录 com.atguigu.easyexcel
在 com.atguigu.easyexcel 下建立 dto 目录
dto 目录新建 ExcelStudentDTO类
package com.atguigu.easyexcel.dto;
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;
import java.util.Date;
@Data
public class ExcelStudentDTO {
@ExcelProperty("姓名")
private String name;
@ExcelProperty("生日")
private Date birthday;
@ExcelProperty("薪资")
private Double salary;
}
在 test 目录下 新建 com.atguigu.easyexcel 基类目录
然后再新建 ExcelWriteTest 测试类
package com.atguigu.easyexcel;
import com.alibaba.excel.EasyExcel;
import com.atguigu.easyexcel.dto.ExcelStudentDTO;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
public class ExcelWriteTest {
@Test
public void simpleWriteXlsx() {
String fileName = "d:/excel/simpleWrite.xlsx"; //需要提前新建目录
// 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
EasyExcel.write(fileName, ExcelStudentDTO.class).sheet("模板").doWrite(data());
}
//辅助方法
private List<ExcelStudentDTO> data(){
List<ExcelStudentDTO> list = new ArrayList<ExcelStudentDTO>();
//算上标题,做多可写65536行
//超出:java.lang.IllegalArgumentException: Invalid row number (65536) outside allowable range (0..65535)
for (int i = 0; i < 65535; i++) {
ExcelStudentDTO data = new ExcelStudentDTO();
data.setName("Helen" + i);
data.setBirthday(new Date());
data.setSalary(123456.1234);
list.add(data);
}
return list;
}
}
_159-不同版本的写
不同版本的写 .XLS 文件
@Test
public void simpleWriteXls() {
String fileName = "d:/excel/simpleWrite.xls";
// 如果这里想使用03 则 传入excelType参数即可
EasyExcel.write(fileName, ExcelStudentDTO.class).excelType(ExcelTypeEnum.XLS).sheet("模板").doWrite(data());
}
_160-不同版本的写
写入大数据量
xls 版本的Excel最多一次可写0 …65535行
xlsx 版本的Excel最多一次可写0…1048575行
_161-Excel的标题设置
实体类 @ExcelProperty(“xxx”) 注解设置
_162- 最简单的读
参考文档
https://www.yuque.com/easyexcel/doc/read
创建监听器
在 com.atguigu.easyexcel 下面 新建 listener 包
listener 包 新建 ExcelStudentDTOListener 类
package com.atguigu.easyexcel.listener;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.atguigu.easyexcel.dto.ExcelStudentDTO;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class ExcelStudentDTOListener extends AnalysisEventListener<ExcelStudentDTO> {
/**
* 这个每一条数据解析都会来调用
*/
public void invoke(ExcelStudentDTO data, AnalysisContext context) {
log.info("解析到一条数据:{}", data);
}
/**
* 所有数据解析完成了 都会来调用
*/
public void doAfterAllAnalysed(AnalysisContext context) {
log.info("所有数据解析完成!");
}
}
测试用例
新建 ExcelReadTest 测试类
package com.atguigu.easyexcel;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.support.ExcelTypeEnum;
import com.atguigu.easyexcel.dto.ExcelStudentDTO;
import com.atguigu.easyexcel.listener.ExcelStudentDTOListener;
import org.junit.Test;
public class ExcelReadTest {
/**
* 最简单的读
*/
@Test
public void simpleReadXlsx() {
String fileName = "d:/excel/simpleWrite.xlsx";
// 这里默认读取第一个sheet
// 这里 需要指定读用哪个class去读,然后读取第一个sheet 文件流会自动关闭
EasyExcel.read(fileName, ExcelStudentDTO.class, new ExcelStudentDTOListener()).sheet().doRead();
}
@Test
public void simpleReadXls() {
String fileName = "d:/excel/simpleWrite.xls";
EasyExcel.read(fileName, ExcelStudentDTO.class, new ExcelStudentDTOListener()).excelType(ExcelTypeEnum.XLS).sheet().doRead();
}
}
_163-数据字典表的设计
需求
什么是数据字典
何为数据字典?数据字典负责管理系统常用的分类数据或者一些固定数据,例如:省市区三级联动数据、民族数据、行业数据、学历数据等,数据字典帮助我们方便的获取和适用这些通用数据。
数据字典的设计
- parent_id:上级id,通过id与parent_id构建上下级关系,例如:我们要获取所有行业数据,那么只需要查询parent_id=20000的数据
- name:名称,例如:填写用户信息,我们要select标签选择民族,“汉族”就是数据字典的名称
- value:值,例如:填写用户信息,我们要select标签选择民族,“1”(汉族的标识)就是数据字典的值
- dict_code:编码,编码是我们自定义的,全局唯一,例如:我们要获取行业数据,我们可以通过parent_id获取,但是parent_id是不确定的,所以我们可以根据编码来获取行业数据
_164-Excel数据读取的接口实现
添加依赖
servive-core 服务模块中添加如下依赖:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
</dependency>
<dependency>
<groupId>org.apache.xmlbeans</groupId>
<artifactId>xmlbeans</artifactId>
</dependency>
创建Excel实体类
com.atguigu.srb.core.pojo 下新建 dto 包
dto 包 下新建 ExcelDictDTO 类
package com.atguigu.srb.core.pojo.dto;
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;
@Data
public class ExcelDictDTO {
@ExcelProperty("id")
private Long id;
@ExcelProperty("上级id")
private Long parentId;
@ExcelProperty("名称")
private String name;
@ExcelProperty("值")
private Integer value;
@ExcelProperty("编码")
private String dictCode;
}
创建监听器
com.atguigu.srb.core 下新建 listener 包
listener 包 新建 ExcelDictDTOListener 类
package com.atguigu.srb.core.listener;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.atguigu.srb.core.pojo.dto.ExcelDictDTO;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
//@AllArgsConstructor //全参
@NoArgsConstructor //无参
public class ExcelDictDTOListener extends AnalysisEventListener<ExcelDictDTO> {
@Override
public void invoke(ExcelDictDTO data, AnalysisContext context) {
log.info("解析到一条记录: {}", data);
}
/**
* 所有数据解析完成了 都会来调用
*/
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
log.info("所有数据解析完成!");
}
}
Service层创建监听器实例
接口 DictService 添加方法
package com.atguigu.srb.core.service;
import com.atguigu.srb.core.pojo.entity.Dict;
import com.baomidou.mybatisplus.extension.service.IService;
import java.io.InputStream;
/**
* <p>
* 数据字典 服务类
* </p>
*
* @author Bliss
* @since 2022-07-14
*/
public interface DictService extends IService<Dict> {
void importData(InputStream inputStream);
}
实现:DictServiceImpl
注意:此处添加了事务处理,默认情况下rollbackFor = RuntimeException.class
package com.atguigu.srb.core.service.impl;
import com.alibaba.excel.EasyExcel;
import com.atguigu.srb.core.listener.ExcelDictDTOListener;
import com.atguigu.srb.core.pojo.dto.ExcelDictDTO;
import com.atguigu.srb.core.pojo.entity.Dict;
import com.atguigu.srb.core.mapper.DictMapper;
import com.atguigu.srb.core.service.DictService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.io.InputStream;
/**
* <p>
* 数据字典 服务实现类
* </p>
*
* @author Bliss
* @since 2022-07-14
*/
@Service
@Slf4j
public class DictServiceImpl extends ServiceImpl<DictMapper, Dict> implements DictService {
@Transactional(rollbackFor = {Exception.class})
@Override
public void importData(InputStream inputStream) {
// 这里 需要指定读用哪个class去读,然后读取第一个sheet 文件流会自动关闭
EasyExcel.read(inputStream, ExcelDictDTO.class, new ExcelDictDTOListener(baseMapper)).sheet().doRead();
log.info("log.info("Excel 导入成功");
}
}
Controller层接收客户端上传
package com.atguigu.srb.core.controller.admin;
import com.atguigu.common.exception.BusinessException;
import com.atguigu.common.result.R;
import com.atguigu.common.result.ResponseEnum;
import com.atguigu.srb.core.service.DictService;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.io.InputStream;
/**
* @auth Baiqing Wu
* @create 2022-07-24 12:09
* @className com.atguigu.srb.core.controller.admin.AdminDictController
* @versions 1.0
*/
public class AdminDictController {
@Resource
private DictService dictService;
@ApiOperation("Excel批量导入数据字典")
@PostMapping("/import")
public R batchImport(
@ApiParam(value = "Excel文件", required = true)
@RequestParam("file") MultipartFile file) {
try {
InputStream inputStream = file.getInputStream();
dictService.importData(inputStream);
return R.ok().message("批量导入成功");
} catch (Exception e) {
//UPLOAD_ERROR(-103, "文件上传错误"),
throw new BusinessException(ResponseEnum.UPLOAD_ERROR, e);
}
}
}
_165-Excel数据的批量保存方案
改造 ExcelDictDTOListener
package com.atguigu.srb.core.listener;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.atguigu.srb.core.mapper.DictMapper;
import com.atguigu.srb.core.pojo.dto.ExcelDictDTO;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
@Slf4j
//@AllArgsConstructor //全参
@NoArgsConstructor //无参
public class ExcelDictDTOListener extends AnalysisEventListener<ExcelDictDTO> {
/**
* 每隔5条存储数据库,实际使用中可以3000条,然后清理list ,方便内存回收
*/
private static final int BATCH_COUNT = 5;
List<ExcelDictDTO> list = new ArrayList();
private DictMapper dictMapper;
//传入mapper对象
public ExcelDictDTOListener(DictMapper dictMapper) {
this.dictMapper = dictMapper;
}
/**
*遍历每一行的记录
* @param data
* @param context
*/
@Override
public void invoke(ExcelDictDTO data, AnalysisContext context) {
log.info("解析到一条记录: {}", data);
// 将数据存入数据列表
list.add(data);
// 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOM
if (list.size() >= BATCH_COUNT) {
saveData();
// 存储完成清理 list
list.clear();
}
}
/**
* 所有数据解析完成了 都会来调用
*/
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
// 当最后剩余的数据记录数不足 BATCH_COUNT 时,我们最终一次性存储剩余数据
saveData();
log.info("所有数据解析完成!");
}
/**
* 加上存储数据库
*/
private void saveData() {
log.info("{}条数据,开始存储数据库!", list.size());
dictMapper.insertBatch(list); //批量插入
log.info("存储数据库成功!");
}
}
_166-Excel数据导入的mapper实现
Mapper层批量插入
接口:DictMapper
DictMapper 类加入批量插入方法
void insertBatch(List<ExcelDictDTO> list);
xml:DictMapper.xml 加入
<insert id="insertBatch">
insert into dict (
id ,
parent_id ,
name ,
value ,
dict_code
) values
<foreach collection="list" item="item" index="index" separator=",">
(
#{item.id} ,
#{item.parentId} ,
#{item.name} ,
#{item.value} ,
#{item.dictCode}
)
</foreach>
</insert>
添加mapper发布配置
注意:因为maven工程在默认情况下src/main/java目录下的所有资源文件是不发布到target目录下的,因此我们需要在pom.xml中添加xml配置文件发布配置
service-core 服务模块 的 pom.xml 里面加入下面代码
<build>
<!-- 项目打包时会将java目录中的*.xml文件也进行打包 -->
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
</build>
现在重新启动程序
先把 dict 字典表的数据清空,然后再去调用导入的接口把 数据字典 excel 文件导入进去
到这里数据导入成功,程序正常
_167-前端路由的配置和页面的创建
创建页面组件
创建 src/views/core/dict/list.vue
<template>
<div class="app-container">
</div>
</template>
<script>
export default {
}
</script>
配置路由
{
path: '/core',
component: Layout,
redirect: '/core/dict/list',
name: 'coreDict',
meta: { title: '系统设置', icon: 'el-icon-setting' },
alwaysShow: true,
children: [
{
path: 'dict/list',
name: '数据字典',
component: () => import('@/views/core/dict/list'),
meta: { title: '数据字典' }
}
]
},
实现数据导入
在 src/views/core/dict/list.vue 加入代码
<template>
<div class="app-container">
<!-- 导入按钮 -->
<div style="margin-bottom: 10px;">
<el-button
type="primary"
size="mini"
icon="el-icon-download"
@click="dialogVisible = true"
>
导入Excel
</el-button>
</div>
<!-- dialog -->
<el-dialog title="数据字典导入" :visible.sync="dialogVisible" width="30%">
<el-form>
<el-form-item label="请选择Excel文件">
<el-upload
:auto-upload="true"
:multiple="false"
:limit="1"
:on-exceed="fileUploadExceed"
:on-success="fileUploadSuccess"
:on-error="fileUploadError"
:action="BASE_API + '/admin/core/dict/import'"
name="file"
accept="application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
>
<el-button size="small" type="primary">点击上传</el-button>
</el-upload>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">
取消
</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
export default {
// 定义数据
data() {
return {
dialogVisible: false // 文件上传对话框是否显示
}
}
}
</script>
_168-前端文件上传组件的整合
加入数据定义
BASE_API: process.env.VUE_APP_BASE_API // 获取后端接口地址
_169-上传组件的属性详解
添加方法
methods: {
// 上传多于一个文件时
fileUploadExceed() {
this.$message.warning('只能选取一个文件')
},
// 上传成功回调
fileUploadSuccess(response) {
if (response.code === 0) {
this.$message.success('数据导入成功')
this.dialogVisible = false
} else {
this.$message.error(response.message)
}
},
// 上传失败回调
fileUploadError(_error) {
this.$message.error('数据导入失败')
}
}
_170-Excel数据导出的前端实现和接口定义
前端添加 导出按钮
<!-- Excel导出按钮 -->
<el-button
type="primary"
size="mini"
icon="el-icon-upload2"
@click="exportData"
>导出Excel</el-button>
添加导出方法:
//Excel数据导出
exportData() {
window.location.href = this.BASE_API + '/admin/core/dict/export'
}
后端接口
Controller层接收客户端请求
当前完整controller
package com.atguigu.srb.core.controller.admin;
import com.alibaba.excel.EasyExcel;
import com.atguigu.common.exception.BusinessException;
import com.atguigu.common.result.R;
import com.atguigu.common.result.ResponseEnum;
import com.atguigu.srb.core.pojo.dto.ExcelDictDTO;
import com.atguigu.srb.core.service.DictService;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLEncoder;
/**
* @auth Baiqing Wu
* @create 2022-07-24 12:09
* @className com.atguigu.srb.core.controller.admin.AdminDictController
* @versions 1.0
*/
public class AdminDictController {
@Resource
private DictService dictService;
@ApiOperation("Excel批量导入数据字典")
@PostMapping("/import")
public R batchImport(
@ApiParam(value = "Excel文件", required = true)
@RequestParam("file") MultipartFile file) {
try {
InputStream inputStream = file.getInputStream();
dictService.importData(inputStream);
return R.ok().message("批量导入成功");
} catch (Exception e) {
//UPLOAD_ERROR(-103, "文件上传错误"),
throw new BusinessException(ResponseEnum.UPLOAD_ERROR, e);
}
}
@ApiOperation("Excel数据的导出")
@GetMapping("/export")
public void export(HttpServletResponse response){
try {
// 这里注意 有同学反应使用swagger 会导致各种问题,请直接用浏览器或者用postman
response.setContentType("application/vnd.ms-excel");
response.setCharacterEncoding("utf-8");
// 这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系
String fileName = URLEncoder.encode("mydict", "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
EasyExcel.write(response.getOutputStream(), ExcelDictDTO.class).sheet("数据字典").doWrite(dictService.listDictData());
} catch (IOException e) {
//EXPORT_DATA_ERROR(104, "数据导出失败"),
throw new BusinessException(ResponseEnum.EXPORT_DATA_ERROR, e);
}
}
}
_171-Excel数据导出的业务实现和测试
Service层解析Excel数据
接口:DictService
List<ExcelDictDTO> listDictData();
实现:DictServiceImpl
@Override
public List<ExcelDictDTO> listDictData() {
List<Dict> dictList = baseMapper.selectList(null);
//创建ExcelDictDTO列表,将Dict列表转换成ExcelDictDTO列表
ArrayList<ExcelDictDTO> excelDictDTOList = new ArrayList<>(dictList.size());
dictList.forEach(dict -> {
ExcelDictDTO excelDictDTO = new ExcelDictDTO();
BeanUtils.copyProperties(dict, excelDictDTO);
excelDictDTOList.add(excelDictDTO);
});
return excelDictDTOList;
}
_172-嵌套表格数据展示的方案分析
_173-获取数据字典列表接口的定义
实体类添加属性
Dict中添加属性
@ApiModelProperty(value = "是否包含子节点")
@TableField(exist = false)//在数据库表中忽略此列
private boolean hasChildren;
Service层实现数据查询
接口:DictService
List<Dict> listByParentId(Long parentId);
实现:DictServiceImpl
@Override
public List<Dict> listByParentId(Long parentId) {
List<Dict> dictList = baseMapper.selectList(new QueryWrapper<Dict>().eq("parent_id", parentId));
dictList.forEach(dict -> {
//如果有子节点,则是非叶子节点
boolean hasChildren = this.hasChildren(dict.getId());
dict.setHasChildren(hasChildren);
});
return dictList;
}
/**
* 判断该节点是否有子节点
*/
private boolean hasChildren(Long id) {
QueryWrapper<Dict> queryWrapper = new QueryWrapper<Dict>().eq("parent_id", id);
Integer count = baseMapper.selectCount(queryWrapper);
if(count.intValue() > 0) {
return true;
}
return false;
}
Controller层接收前端请求
在 AdminDictController 加入
@ApiOperation("根据上级id获取子节点数据列表")
@GetMapping("/listByParentId/{parentId}")
public R listByParentId(
@ApiParam(value = "上级节点id", required = true)
@PathVariable Long parentId) {
List<Dict> dictList = dictService.listByParentId(parentId);
return R.ok().data("list", dictList);
}
_174-前端调用接口进行数据绑定
api
创建 src/api/core/dict.js
import request from '@/utils/request'
export default {
listByParentId(parentId) {
return request({
url: `/admin/core/dict/listByParentId/${parentId}`,
method: 'get'
})
}
}
组件脚本
在list.vue定义data
list: [], //数据字典列表
生命周期函数
created() {
this.fetchData()
},
获取数据的方法
import dictApi from '@/api/core/dict'
// 调用api层获取数据库中的数据
fetchData() {
dictApi.listByParentId(1).then(response => {
this.list = response.data.list
})
},
//延迟加载子节点
getChildren(row, treeNode, resolve) {
dictApi.listByParentId(row.id).then(response => {
//负责将子节点数据展示在展开的列表中
resolve(response.data.list)
})
},
_175-嵌套表格的展示
组件模板
<el-table :data="list" border row-key="id" lazy :load="load">
<el-table-column label="名称" align="left" prop="name" />
<el-table-column label="编码" prop="dictCode" />
<el-table-column label="值" align="left" prop="value" />
</el-table>
加载二级节点
// 加载二级节点
load(tree, treeNode, resolve) {
// 获取数据
dictApi.listByParentId(tree.id).then(response => {
resolve(response.data.list)
})
}
流程优化
数据导入后刷新页面的数据列表
//上传成功回调
fileUploadSuccess(response) {
if (response.code === 0) {
this.$message.success('数据导入成功')
this.dialogVisible = false
this.fetchData()
} else {
this.$message.error(response.message)
}
},
_176-项目中引入redis
场景
由于数据字典的变化不是很频繁,而且系统对数据字典的访问较频繁,所以我们有必要把数据字典的数据存入缓存,减少数据库压力和提高访问速度。这里,我们使用Redis作为系统的分布式缓存中间件。
RedisTemplate
在Spring Boot项目中中,默认集成Spring Data Redis,Spring Data Redis针对Redis提供了非常方便的操作模版RedisTemplate,并且可以进行连接池自动管理。
项目中集成Redis
service-base模块中添加redis依赖,Spring Boot 2.0以上默认通过commons-pool2连接池连接Redis
<!-- spring boot redis缓存引入 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 缓存连接池-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- redis 存储 json序列化 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
添加Redis连接配置
service-core 的 application.yml 中 的 spring节点下添加如下配置:
#spring:
redis:
host: 192.168.100.100
port: 6379
database: 0
password: 123456 #默认为空
timeout: 3000ms #最大等待时间,超时则抛出异常,否则请求一直等待
lettuce:
pool:
max-active: 20 #最大连接数,负值表示没有限制,默认8
max-wait: -1 #最大阻塞等待时间,负值表示没限制,默认-1
max-idle: 8 #最大空闲连接,默认8
min-idle: 0 #最小空闲连接,默认0
启动Redis服务
远程连接Linux服务器
#启动服务
cd /usr/local/redis-5.0.7
bin/redis-server redis.conf
_177-redis存值测试
测试RedisTemplate
存值测试
test中创建测试类RedisTemplateTests
package com.atguigu.srb.core;
import com.atguigu.srb.core.mapper.DictMapper;
import com.atguigu.srb.core.pojo.entity.Dict;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.test.context.junit4.SpringRunner;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
/**
* @auth Baiqing Wu
* @create 2022-07-26 22:18
* @className com.atguigu.srb.core.RedisTemplateTests
* @versions 1.0
*/
@SpringBootTest
@RunWith(SpringRunner.class)
public class RedisTemplateTests {
@Resource
private RedisTemplate redisTemplate;
@Resource
private DictMapper dictMapper;
@Test
public void saveDict(){
Dict dict = dictMapper.selectById(1);
//向数据库中存储string类型的键值对, 过期时间5分钟
redisTemplate.opsForValue().set("dict", dict, 5, TimeUnit.MINUTES);
}
}
发现RedisTemplate默认使用了JDK的序列化方式存储了key和value
_178-redisTemplate配置文件-解决对象序列化存储的问题
Redis配置文件
service-base 中添加RedisConfig,我们可以在这个配置文件中配置Redis序列化方案
package com.atguigu.srb.base.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* @auth Baiqing Wu
* @create 2022-07-26 22:19
* @className com.atguigu.srb.base.config.RedisConfig
* @versions 1.0
*/
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
//首先解决key的序列化方式
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(stringRedisSerializer);
//解决value的序列化方式
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
//序列化时将类的数据类型存入json,以便反序列化的时候转换成正确的类型
ObjectMapper objectMapper = new ObjectMapper();
//objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
// 解决jackson2无法反序列化LocalDateTime的问题
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
objectMapper.registerModule(new JavaTimeModule());
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
return redisTemplate;
}
}
再次测试,key使用了字符串存储,value使用了json存储
_179-redisTemplate取值测试
取值测试
@Test
public void getDict(){
Dict dict = (Dict)redisTemplate.opsForValue().get("dict");
System.out.println(dict);
}
_180-数据字典中整合redis
将数据字典存入Redis
DictServiceImpl
注意:当redis服务器宕机时,我们不要抛出异常,要正常的执行后面的流程,使业务可以正常的运行
引入 redis
@Resource
private RedisTemplate redisTemplate;
修改 listByParentId 方法
@Override
public List<Dict> listByParentId(Long parentId) {
//先查询redis中是否存在数据列表
List<Dict> dictList = null;
try {
dictList = (List<Dict>)redisTemplate.opsForValue().get("srb:core:dictList:" + parentId);
if(dictList != null){
log.info("从redis中取值");
return dictList;
}
} catch (Exception e) {
log.error("redis服务器异常:" + ExceptionUtils.getStackTrace(e));//此处不抛出异常,继续执行后面的代码
}
log.info("从数据库中取值");
dictList = baseMapper.selectList(new QueryWrapper<Dict>().eq("parent_id", parentId));
dictList.forEach(dict -> {
//如果有子节点,则是非叶子节点
boolean hasChildren = this.hasChildren(dict.getId());
dict.setHasChildren(hasChildren);
});
//将数据存入redis
try {
redisTemplate.opsForValue().set("srb:core:dictList:" + parentId, dictList, 5, TimeUnit.MINUTES);
log.info("数据存入redis");
} catch (Exception e) {
log.error("redis服务器异常:" + ExceptionUtils.getStackTrace(e));//此处不抛出异常,继续执行后面的代码
}
return dictList;
}