项目简介
这是一个前后端分离的项目,基本技术栈是SpringBoot+SpringDataJPA+EasyExcel+Vue+ElementUI。这里说明一下,Easy Excel是阿里巴巴一个开源项目,可以方便的将数据库数据导出为Excel,也可以根据提供的模板将Excel中的数据导入到数据库中。SpringDataJPA(Hibernate)使用起来比较方便,可以动态拼写sql语句,当然感兴趣的朋友也可以使用MyBatis,这里不再赘述。这个项目可以:
1、使用SpringDataJPA实现基本的增删改查,分页查询(每页10条数据,自动计算页数)
2、动态查询(给出的查询条件不确定个)
3、模糊查询(关键字查询,比如给我一个“王”字可以找出所有姓王的同学)
4、将数据库数据导出为Excel并下载到本地,也可以根据模板将本地Excel中的数据导入到数据库,学号已存在的信息不会导入数据库。
5、根据不同的专业查找信息
6、备份功能:本项目有设置回收站,当从Student表中删除数据后会保存在回收站中,还可以恢复或者彻底删除。
7、添加信息检查:如果学号已经存在,或者邮箱、手机不合法则不能插入信息,会弹出警告;生日自动计算,选择专业时选项是从数据库中查询出来的,而不是自己录入。
项目展示:
代码目录:
以下内容是项目说明,具体源码请移步GitHub
https://github.com/Nown1/contacts
跨域访问问题
这是一个前后端分离的项目,后端SpringBoot+SpringDataJPA+EasyExcel,服务端口是8181,需要在application.yml文件添加以下信息
spring:
datasource:
url: jdbc:mysql://localhost:3306/nown_test02?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
show-sql: true
properties:
hibernate:
format_sql: true
server:
port: 8181
前端服务端口是8080,当8080端口去向8181端口发送请求时便涉及到了跨域访问,因此需要添加配置类信息,代码如下:
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CorsConfig implements WebMvcConfigurer {
//重载WebMvcConfigurer的addCorsMappings方法
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
.allowCredentials(true)
.maxAge(3600)
.allowedHeaders("*");
}
}
基本思路应该是,前端向后端发送请求,后端Controller要给出响应,然后去调用Service类的方法,Service又需要去调用Dao层接口。关于学生的信息,我们不妨设置一个StudentController去响应请求,设置一个StudentService类去调用StudentDao的方法。SpringDataJPA最大的好处就是,我们不需要自己去写SQL语句,只要继承一些接口就可以实现增删改查功能。JpaRepository接口继承了PagingAndSortingRepository和QueryByExampleExecutor两个接口,能够实现增删改查。为了实现动态查询,需要再继承JpaSpecificationExecutor接口。这样StudentDao可以通过方法名来自动拼写sql,实现方法。比如通过id查找信息,方法名就是findById(Integer id)。如果是模糊查询,就要加一个关键字Like。比如通过名字模糊查询,方法名就是findByNameLike。代码如下:
StudentDao
import com.nown.contacts.entity.Student;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.repository.PagingAndSortingRepository;
import java.util.List;
public interface StudentDao extends JpaRepository<Student,Integer> ,
JpaSpecificationExecutor<Student> {
Student findStudentByNumber(String number);
List<Student> findStudentsByNameLike(String name);
}
StudentService:
import com.nown.contacts.entity.Student;
import com.nown.contacts.entity.StudentDaoSpec;
import com.nown.contacts.repository.StudentDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;
import java.util.Iterator;
import java.util.List;
@Service
public class StudentService {
@Autowired
private StudentDao studentDao;
public List<Student> findByParams(Student student){
List<Student> list = studentDao.findAll(
StudentDaoSpec.getSpec(
student.getNumber(),student.getName(),
student.getSex(),student.getAge(),student.getBirthday(),
student.getTel(),student.getAddress(),
student.getMail(),student.getMajor()
)
);
return list;
}
public Page<Student> findAll(PageRequest request) {
return studentDao.findAll(request);
}
public Student save(Student student) {
return studentDao.save(student);
}
public void save(List<Student> list){
Iterator<Student> iterator=list.iterator();
while (iterator.hasNext()){
Student student= iterator.next();
if (findByNumber(student.getNumber())==null){
save(student);
}else{
System.out.println("学号已存在,未添加到数据库:"+student);
}
}
}
public Student findById(Integer id) {
return studentDao.findById(id).get();
}
public void deleteById(Integer id) {
studentDao.deleteById(id);
}
public List<Student> findAll(){
return studentDao.findAll();
}
public Student findByNumber(String number){
return studentDao.findStudentByNumber(number);
}
public List<Student> findByNameLike(String name){
return studentDao.findStudentsByNameLike("%"+name+"%");
}
}
关于Vue
Vue是一个比较好用的前端框架,官网有教程,官网地址:https://cn.vuejs.org/v2/guide/
为了方便向后端发送请求,我安装了axios插件。为了尽量让视图美观,提高开发效率,我又安装了elementUI插件。当我们开发时可以找到想要的模板,直接把代码copy下来修改一下就可以使用。npm安装命令:
$ npm install axios
$ npm i element-ui -S
这里附上ElementUI地址:https://element.eleme.cn/#/zh-CN
vue我理解为是一个嵌套的视图。它有一个App.vue 的主页面。当我们要“跳转”到其他页面时,需要在App.vue开辟一个入口,使用<router-view>
标签。这样我们要访问的页面就会呈现在这个标签里面。
Vue目录:
为了解决页面跳转的路径问题,需要把我们写的所有视图导入,比如import AddStudent from '../views/AddStudent'
。在const routes里面添加路径,重点是path和component两个属性。在index.js添加如下信息:
import Vue from 'vue'
import VueRouter from 'vue-router'
import StudentManage from '../views/StudentManage'
import AddStudent from '../views/AddStudent'
import Index from '../views/Index'
import StudentUpdate from '../views/StudentUpdate'
import StudentSearch from '../views/StudentSearch.vue'
import List from '../views/List.vue'
import Dustbin from '../views/Dustbin.vue'
import Fuzzy from '../views/Fuzzy.vue'
Vue.use(VueRouter)
const routes = [
{
path: "/",
name: "通信录",
component: Index,
redirect: "/StudentManage",
},
{
path: "/StudentManage",
name: "信息查询",
component: StudentManage
},
{
path: "/AddStudent",
name: "添加信息",
component: AddStudent
},
{
path: "/StudentSearch",
name: "查找",
component: StudentSearch
},
{
path: '/update',
component: StudentUpdate
},
{
path: '/list',
component: List
},
{
path:'/dustbin',
component:Dustbin
},
{
path:'/Fuzzy',
component:Fuzzy
}
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
export default router
App.vue代码:
<template>
<div id="app">
<el-container style="height: 2000px; border: 1px solid #eee">
<el-aside width="200px" style="background-color: rgb(238, 241, 246)">
<el-menu router :default-openeds="['0']">
<el-submenu index="0">
<template slot="title"><i class="el-icon-notebook-1"></i>通信录</template>
<el-menu-item index="StudentManage"><i class="el-icon-refresh"></i>信息查询</el-menu-item>
<el-menu-item index="AddStudent"><i class="el-icon-edit"></i>添加信息</el-menu-item>
<el-menu-item index="StudentSearch"><i class="el-icon-search"></i>查找</el-menu-item>
<el-menu-item index="Fuzzy"><i class="el-icon-view"></i>模糊查询</el-menu-item>
<el-menu-item index="Dustbin"><i class="el-icon-delete"></i>回收站</el-menu-item>
</el-submenu>
<el-submenu index="1">
<template slot="title"><i class="el-icon-document"></i>文件</template>
<el-menu-item @click="exportWord"><i class="el-icon-download"></i>下载文件</el-menu-item>
<el-upload ref="upload" action :multiple="false" :auto-upload="false" :limit="1" :http-request="importWordConfirm">
<el-button slot="trigger" size="small" type="primary" style="margin-bottom: 10px;" plain>选取文件</el-button>
<el-button style="margin-left: 10px;" size="small" type="success" @click="submitUpload" plain>上传文件</el-button>
</el-upload>
</el-submenu>
<el-submenu index="2">
<template slot="title"><i class="el-icon-folder-opened"></i>分类</template>
<el-menu-item v-for="item in majors" @click="jump(item.name)"><i class="el-icon-collection-tag"></i>{{item.name}}</el-menu-item>
</el-submenu>
</el-menu>
</el-aside>
<el-main>
<router-view></router-view>
</el-main>
</el-container>
</div>
</template>
<style>
.el-header {
background-color: #B3C0D1;
color: #333;
line-height: 60px;
}
.el-aside {
color: #333;
}
</style>
<script>
export default {
data() {
return {
form: {
id:null,
name: null,
number: null,
sex: null,
age: null,
birthday: null,
tel: null,
address: null,
mail: null,
major: null,
},
fileList: [{
id: 0,
name: ''
}],
majors:[]
};
},
methods: {
exportWord() {
//将数据库数据导出为Excel并下载
this.$axios.get('http://localhost:8181/file/download', {
responseType: 'blob'
}).then(res => {
let blob = new Blob([res.data], {
type: 'application/ms-excel;charset=utf-8'
});
let downloadElement = document.createElement('a');
let href = window.URL.createObjectURL(blob); //创建下载的链接
downloadElement.href = href;
downloadElement.download = '学生信息.xlsx'; //下载后文件名
document.body.appendChild(downloadElement);
downloadElement.click(); //点击下载
document.body.removeChild(downloadElement); //下载完成移除元素
window.URL.revokeObjectURL(href); //释放掉blob对象
})
},
submitUpload() {
this.$refs.upload.submit();
},
importWordConfirm(item) {
//上传Excel文件并导入数据库
const _this=this;
const fileObj = item.file
const formData = new FormData()
formData.append('file', fileObj)
this.$axios.post('http://localhost:8181/file/upload/', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
}).then(res => {
_this.$alert('上传成功!', '消息', {
confirmButtonText: '确定',
callback: action => {
_this.$router.push({
path:"/StudentManage"
})
}
})
})
},
jump(x){
const _this=this;
this.form.major=x
this.$router.push({
path:'/list',
query:{
data:_this.form
}
})
}
},
created() {
const _this = this
axios.get('http://localhost:8181/major/findAll').then(function(resp) {
console.log(resp)
_this.majors = resp.data
})
}
};
</script>
下面我们根据功能顺序依次介绍实现方法:
分页查询
当前端向后端发送分页查询请求时,需要有页数和每页多少条的参数,所以这里有一个findAll方法:
@GetMapping("/findAll/{page}/{size}")
public Page<Student> findAll(@PathVariable("page") Integer page, @PathVariable("size") Integer size){
//当我们分页查询时需要一个PageRequest类型的参数
//调用它的of方法,第一个参数表示页码,第二个表示这一页多少条数据
PageRequest request = PageRequest.of(page,size);
return studentService.findAll(request);
}
前端Vue的话,要实现分页查询,添加了page方法:
page(currentPage) {
const _this = this
axios.get('http://localhost:8181/student/findAll/' + (currentPage - 1) + '/10').then(function (resp) {
console.log(resp)
_this.tableData = resp.data.content
_this.pageSize = resp.data.size
_this.total = resp.data.totalElements
})
}
为了访问8080端口时就加载第一页的数据,要在created方法里面添加如下代码:
created() {
const _this = this
//注意下面的this指针,可能不在是指全局的this指针,而是axios的指针,因此设置了一个_this的临时变量
axios.get('http://localhost:8181/student/findAll/0/10').then(function (resp) {
console.log(resp)
_this.tableData = resp.data.content
_this.pageSize = resp.data.size
_this.total = resp.data.totalElements
})
}
模糊查询
只要在StudentController添加findLike(String name)方法即可
@GetMapping("/findLike/{name}")
public List<Student> findLike(@PathVariable("name") String name){
System.out.println(name);
// return studentService.findByNameLike(name);
List<Student> list=studentService.findByNameLike(name);
Iterator<Student> iterator=list.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
return list;
}
vue的话要注意传过来的是一个List集合,要进行匹配
deleteStudent(row) {
const _this = this
axios.delete('http://localhost:8181/student/deleteById/' + row.id).then(function (resp) {
_this.$alert('删除成功!', '消息', {
confirmButtonText: '确定',
callback: action => {
window.location.reload()
}
})
})
},
editStudent(row) {
this.$router.push({
path: '/update',
query: {
id: row.id
}
})
},
queryStudents(){
const _this=this
axios.get('http://localhost:8181/student/findLike/'+_this.forml.name).then(function(resp){
console.log(resp)
_this.datas = resp.data
})
}
},
data(){
return{
forml:{
name:''
},
datas:[]
}
}
动态查询
为了实现动态查询,需要让StudentDao继承JpaSpecificationExecutor
接口。
public interface JpaSpecificationExecutor<T> {
Optional<T> findOne(@Nullable Specification<T> var1);
List<T> findAll(@Nullable Specification<T> var1);
Page<T> findAll(@Nullable Specification<T> var1, Pageable var2);
List<T> findAll(@Nullable Specification<T> var1, Sort var2);
long count(@Nullable Specification<T> var1);
}
由此可见,要实现findAll方法还要实现Specification接口。实现这个接口需要根据实体类Student类写一个数据元类Student_.java。
import javax.persistence.metamodel.SingularAttribute;
import javax.persistence.metamodel.StaticMetamodel;
@StaticMetamodel(Student.class)
public class Student_ {
public static volatile SingularAttribute<Student, Integer> id;// 用户ID,自增量
public static volatile SingularAttribute<Student, String> number;
public static volatile SingularAttribute<Student, String> name;
public static volatile SingularAttribute<Student, String> sex;
public static volatile SingularAttribute<Student, Integer> age;
public static volatile SingularAttribute<Student, String> birthday;
public static volatile SingularAttribute<Student, String> tel;
public static volatile SingularAttribute<Student, String> address;
public static volatile SingularAttribute<Student, String> mail;
public static volatile SingularAttribute<Student, String> major;
}
然后是写一个获取Specification接口的方法,用于动态拼写Sql。在我们动态查询时,传过来的是一些条件,如果是空则不需要and,如果不空就要把这个条件拼上。
import org.springframework.data.jpa.domain.Specification;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
public class StudentDaoSpec {
public static Specification<Student> getSpec(
final String number,
final String name,
final String sex,
final Integer age,
final String birthday,
final String tel,
final String address,
final String mail,
final String major) {
return new Specification<Student>() {
@SuppressWarnings("unused")
@Override
public Predicate toPredicate(Root<Student> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
Predicate p1 = null;
if (number!=null){
System.out.println("正在操作number!!!");
Predicate p2 = cb.equal(root.get(Student_.number), number);
if (p1 != null) {
p1 = cb.and(p1, p2);
} else {
p1 = p2;
}
}
if (name!=null) {
System.out.println("正在操作name!!!");
Predicate p2 = cb.equal(root.get(Student_.name), name);
if (p1 != null) {
p1 = cb.and(p1, p2);
} else {
p1 = p2;
}
}
if (sex!=null){
System.out.println("正在操作sex!!!");
Predicate p2 = cb.equal(root.get(Student_.sex), sex);
if (p1 != null) {
p1 = cb.and(p1, p2);
} else {
p1 = p2;
}
}
if (age != null) {
System.out.println("正在操作age!!!");
Predicate p2 = cb.equal(root.get(Student_.age), age);
if (p1 != null) {
p1 = cb.and(p1, p2);
} else {
p1 = p2;
}
}
if (birthday!=null) {
System.out.println("正在操作birthday!!!");
Predicate p2 = cb.equal(root.get(Student_.birthday), birthday);
if (p1 != null) {
p1 = cb.and(p1, p2);
} else {
p1 = p2;
}
}
if (tel!=null) {
System.out.println("正在操作tel!!!");
Predicate p2 = cb.equal(root.get(Student_.tel), tel);
if (p1 != null) {
p1 = cb.and(p1, p2);
} else {
p1 = p2;
}
}
if (address!=null) {
System.out.println("正在操作address!!!");
Predicate p2 = cb.equal(root.get(Student_.address), address);
if (p1 != null) {
p1 = cb.and(p1, p2);
} else {
p1 = p2;
}
}
if (mail!=null) {
System.out.println("正在操作mail!!!");
Predicate p2 = cb.equal(root.get(Student_.mail), mail);
if (p1 != null) {
p1 = cb.and(p1, p2);
} else {
p1 = p2;
}
}
if (major!=null) {
System.out.println("正在操作major!!!");
Predicate p2 = cb.equal(root.get(Student_.major), major);
if (p1 != null) {
p1 = cb.and(p1, p2);
} else {
p1 = p2;
}
}
return p1;
}
};
}
}
由此,我们动态查询的准备工作已经做好了。在前端要动态查询的时候,为了方便传参,我们让它把条件绑定到一个Student类的对象中,然后依次获取它的属性值。
StudentSearch.vue中的部分代码:
data() {
return {
form: {
id:null,
name: null,
number: null,
sex: null,
age: null,
birthday: null,
tel: null,
address: null,
mail: null,
major: null,
},
fileList: [{
id: 0,
name: ''
}
],
tableData: []
};
},
onSubmit(){
this.$router.push({
path:'/list',
query:{
data:this.form
}
})
},
List.vue中的部分代码:
data() {
return {
tableData:[]
};
},
created() {
const _this = this
axios.post('http://localhost:8181/student/search', _this.$route.query.data).then(function(resp) {
console.log(resp)
_this.tableData = resp.data
})
}
后端要响应的话需要一个search方法:
@PostMapping("/search")
public List<Student> search(@RequestBody Student student){
System.out.println(student);
List<Student> list=studentService.findByParams(student);
Iterator<Student> iterator=list.iterator();
// while (iterator.hasNext()){
// System.out.println("查找的数据是:"+iterator.next());
// }
return studentService.findByParams(student);
}
分类查询
既然我们已经写好了动态查询接口,我们就没有必要再写一个findByMajor方法了,直接动态查询吧。后端StudentController的search方法去处理请求即可。前端的话,分专业查询,我是把专业直接放在App.vue的左侧菜单栏的,只要在App.vue的created方法里面添加查询所有专业的方法就可以了。
<el-submenu index="2">
<template slot="title"><i class="el-icon-folder-opened"></i>分类</template>
<el-menu-item v-for="item in majors" @click="jump(item.name)"><i class="el-icon-collection-tag"></i>{{item.name}}</el-menu-item>
</el-submenu>
data() {
return {
form: {
id:null,
name: null,
number: null,
sex: null,
age: null,
birthday: null,
tel: null,
address: null,
mail: null,
major: null,
},
fileList: [{
id: 0,
name: ''
}],
majors:[]
};
},
jump(x){
const _this=this;
this.form.major=x
this.$router.push({
path:'/list',
query:{
data:_this.form
}
})
}
created() {
const _this = this
axios.get('http://localhost:8181/major/findAll').then(function(resp) {
console.log(resp)
_this.majors = resp.data
})
}
后端也很简单,只要写一个查询所有方法即可
@GetMapping("/findAll")
public List<Major> findAll(){
return majorDao.findAll();
}
添加信息
在添加数据时,我们要对学号、邮箱、手机号进行校验,同时计算年龄。要检查学号是否存在,因此要在StudentController方法里面添加一个findByNum方法。保存的话需要一个Save方法:
@GetMapping("/findByNum/{num}")
public String findByNum(@PathVariable("num") String num){
System.out.println(num);
Student student=studentService.findByNumber(num);
if (student!=null){
return "已存在";
}
return "不存在";
}
@PostMapping("/save")
public String save(@RequestBody Student student){
System.out.println(student);
if (studentService.findByNumber(student.getNumber())!=null){
return "学号已存在!";
}
Student result = studentService.save(student);
if(result != null){
return "保存成功!";
}
return "保存失败!";
}
Vue校验方法:
this.$refs[formName].validate((valid) => {
if (valid) {
axios.get('http://localhost:8181/student/findByNum/' + _this.form.number).then(function(resp) {
if (resp.data == "已存在") {
alert("学号已存在")
return false;
} else {
axios.post('http://localhost:8181/student/save', _this.form).then(function(resp) {
_this.$alert(resp.data, '消息', {
confirmButtonText: '确定',
callback: action => {
_this.$router.push('/StudentManage')
}
})
})
}
})
}
校验邮箱和电话号码我们可以使用正则表达式来完成,为了不让已填写的信息丢失,我们只要让方法return false即可(AddStudent.vue的部分代码):
const _this = this
var x = this.form.mail
var atpos = x.indexOf("@");
var dotpos = x.lastIndexOf(".");
if (atpos < 1 || dotpos < atpos + 2 || dotpos + 2 >= x.length) {
alert("不是一个有效的 e-mail 地址");
return false;
}
var myreg = /^[1][3,4,5,7,8][0-9]{9}$/;
x = this.form.tel
if (!myreg.test(x.toString())) {
alert("手机号不合法");
return false;
}
自动计算年龄:
var birth = this.form.birthday;
birth = Date.parse(birth.replace('/-/g', "/"));
if (birth) {
var year = 1000 * 60 * 60 * 24 * 365;
var now = new Date();
var birthday = new Date(birth);
var age = parseInt((now - birthday) / year);
}
// alert(age);
this.form.age = age;
我们传到后端的数据是要绑定成一个Student对象的,注意数据绑定(AddStudent.vue和StudentUpdate.vue的部分代码):
form: {
name: '',
number: '',
sex: '',
age: '',
birthday: '',
tel: '',
address: '',
mail: '',
major: '',
},
修改信息
我们在修改信息时,要跳到修改界面,需要根据当前学生的id获得学生信息,所以StudentController要写一个findById(Integer id)方法和修改的update方法:
@GetMapping("/findById/{id}")
public Student findById(@PathVariable("id") Integer id){
System.out.println(new Date()+";"+id);
return studentService.findById(id);
}
@PostMapping("/update")
public String update(@RequestBody Student student){
System.out.println(student);
Student result = studentService.save(student);
if(result != null){
return "修改成功!";
}else{
return "修改失败!";
}
}
在StudentUpdate.vue的created方法里面添加:
created() {
const _this = this
axios.get('http://localhost:8181/student/findById/' + this.$route.query.id).then(function(resp) {
console.log(resp)
_this.form = resp.data
})
// alert("this.id="+this.$route.query.id+";_this.id="+_this.$route.query.id)
//在选择学生专业的时候下拉选项需要加载所有专业,因此要向MajorController发送请求
axios.get('http://localhost:8181/major/findAll').then(function(resp) {
console.log(resp)
_this.tableData = resp.data
})
}
不能修改学号,但是可见,因此学号这个标签应该添加属性readOnly(StudentUpdate.vue部分代码):
<el-form-item label="学生学号" prop="number" >
<el-input v-model="form.number" style="width:160px" readOnly></el-input>
</el-form-item>
修改完成之后要提交:
vue部分代码:
onSubmit(formName) {
const _this = this
this.$refs[formName].validate((valid) => {
if (valid) {
axios.post('http://localhost:8181/student/update', _this.form).then(function(resp) {
_this.$alert(resp.data, '消息', {
confirmButtonText: '确定',
callback: action => {
_this.$router.push('/StudentManage')
}
})
})
}
});
},
删除信息
根据学生的id删除即可,注意删除的时候应该是先把这条信息存到dustbin这张表中,然后在student表中是删除。因此Student Controller的deleteById方法如下:
@DeleteMapping("/deleteById/{id}")
public void deleteById(@PathVariable("id") Integer id){
System.out.println("删除操作"+new Date());
dustbinDao.save(converter.toDustbin(studentService.findById(id)));
studentService.deleteById(id);
}
虽然dustbin和student表中的内容非常相似,但是毕竟是两个不同的类,所以这里又写了一个Converter类用于两者之间的互化:
import com.nown.contacts.entity.Dustbin;
import com.nown.contacts.entity.Student;
import org.springframework.stereotype.Component;
@Component
public class Converter {
public Student toStudent(Dustbin dustbin){
Student student=new Student();
student.setName(dustbin.getName());
student.setNumber(dustbin.getNumber());
student.setAddress(dustbin.getAddress());
student.setAge(dustbin.getAge());
student.setBirthday(dustbin.getBirthday());
student.setId(dustbin.getStuid());
student.setMail(dustbin.getMail());
student.setMajor(dustbin.getMajor());
student.setSex(dustbin.getSex());
student.setTel(dustbin.getTel());
return student;
}
public Dustbin toDustbin(Student student){
Dustbin dustbin=new Dustbin();
dustbin.setStuid(student.getId());
dustbin.setNumber(student.getNumber());
dustbin.setName(student.getName());
dustbin.setAddress(student.getAddress());
dustbin.setAge(student.getAge());
dustbin.setBirthday(student.getBirthday());
dustbin.setMail(student.getMail());
dustbin.setMajor(student.getMajor());
dustbin.setSex(student.getSex());
dustbin.setTel(student.getTel());
return dustbin;
}
}
vue的删除代码如下(Student Manage.vue部分代码):
deleteStudent(row) {
const _this = this
axios.delete('http://localhost:8181/student/deleteById/' + row.id).then(function (resp) {
_this.$alert('删除成功!', '消息', {
confirmButtonText: '确定',
callback: action => {
window.location.reload()
}
})
})
},
备份功能
以上已经说明了如何将删除的数据保存到dustbin表中,下面解决如何从回收站恢复。在访问Dustbin.vue时我们就要加载所有已删除的信息,因此后端DustbinController类中有findAll方法,并且是分页查询的。然后回收站主要负责恢复和彻底删除,分别对应着recover和delete方法。不管恢复还是彻底删除,我们都需要一个id属性用于确定信息。
DustbinController的部分代码:
@GetMapping("/findAll/{page}/{size}")
public Page<Dustbin> findAll(@PathVariable("page") Integer page, @PathVariable("size") Integer size){
PageRequest request = PageRequest.of(page,size);
return dustbinDao.findAll(request);
}
@DeleteMapping("/deleteById/{id}")
public void deleteById(@PathVariable("id") Integer id){
dustbinDao.deleteById(id);
}
@GetMapping("/recover/{id}")
public String recover(@PathVariable("id") Integer id){
Student result=studentService.save(converter.toStudent(dustbinDao.findById(id).get()));
if (result!=null){
dustbinDao.deleteById(id);
return "success";
}
return "failed";
}
Dustbin.vue中的部分代码:
methods: {
delete(row) {
const _this = this
axios.delete('http://localhost:8181/dustbin/deleteById/' + row.id).then(function (resp) {
_this.$alert('删除成功!', '消息', {
confirmButtonText: '确定',
callback: action => {
window.location.reload()
}
})
})
},
recover(row) {
const _this=this
axios.get('http://localhost:8181/dustbin/recover/' + row.id).then(function (resp) {
if (resp.data == 'success') {
_this.$alert('恢复成功!', '消息', {
confirmButtonText: '确定',
callback: action => {
_this.$router.push('/StudentManage')
}
})
} else {
alert('恢复失败!')
return false;
}
})
},
page(currentPage) {
const _this = this
axios.get('http://localhost:8181/dustbin/findAll/' + (currentPage - 1) + '/10').then(function (resp) {
console.log(resp)
_this.tableData = resp.data.content
_this.pageSize = resp.data.size
_this.total = resp.data.totalElements
})
}
},
data() {
return {
pageSize: 0,
total: 0,
tableData: []
}
},
created() {
const _this = this
axios.get('http://localhost:8181/dustbin/findAll/0/10').then(function (resp) {
console.log(resp)
_this.tableData = resp.data.content
_this.pageSize = resp.data.size
_this.total = resp.data.totalElements
})
}
关于EasyExcel
下面解决导出和导入的问题。关于处理Excel,我选用的时EasyExcel,感兴趣的朋友可以去对比一下Apache poi。
EasyExcel地址:https://github.com/alibaba/easyexcel
语雀地址:https://www.yuque.com/easyexcel/doc/easyexcel
首先要对实体类Student类进行补充,为其添加@ExcelProperty注解,后面两个参数value代表了表格的表头名称,index代表了序号。
package com.nown.contacts.entity;
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;
import javax.persistence.*;
@Data
@Entity
@Table(name = "student")
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@ExcelProperty(value = "序号",index = 0)
private Integer id;
@ExcelProperty(value = "学号",index = 1)
private String number;
@ExcelProperty(value = "姓名",index = 2)
private String name;
@ExcelProperty(value = "性别",index = 3)
private String sex;
@ExcelProperty(value = "年龄",index = 4)
private Integer age;
@ExcelProperty(value = "出生日期",index = 5)
private String birthday;
@ExcelProperty(value = "手机号码",index = 6)
private String tel;
@ExcelProperty(value = "家庭住址",index = 7)
private String address;
@ExcelProperty(value = "电子邮箱",index = 8)
private String mail;
@ExcelProperty(value = "专业",index = 9)
private String major;
}
然后是写一个监听器,用于在读取Excel时将读出的数据导入到数据库中:
package com.nown.contacts.util;
import java.util.ArrayList;
import java.util.List;
import com.nown.contacts.entity.Student;
import com.nown.contacts.service.StudentService;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import org.springframework.beans.factory.annotation.Autowired;
// 有个很重要的点 DemoDataListener 不能被spring管理,要每次读取excel都要new,然后里面用到spring可以构造方法传进去
public class ExcelListener extends AnalysisEventListener<Student> {
private static final int BATCH_COUNT = 1;
private List<Student> list;
private StudentService studentService;
@Autowired
public ExcelListener(StudentService studentService) {
this.studentService=studentService;
this.list = new ArrayList<Student>();
}
@Override
public void invoke(Student student, AnalysisContext context) {
list.add(student);
// 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOM
if (list.size() >= BATCH_COUNT) {
saveData();
// 存储完成清理 list
list.clear();
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
System.out.println("插入完成");
}
/**
* 加上存储数据库
*/
private void saveData() {
System.out.println("{}条数据,开始存储数据库!");
studentService.save(list);
System.out.println("存储数据库成功!");
}
}
这里注意,我们是把Excel表格中的数据读出后放到了一个List集合里面了,因此我们需要重载StudentService的save方法(之前的save方法参数是Student类,这个方法的参数是List集合),同时注意如果学号已存在则不能将信息插入数据库
public Student save(Student student) {
return studentDao.save(student);
}
public void save(List<Student> list){
Iterator<Student> iterator=list.iterator();
while (iterator.hasNext()){
Student student= iterator.next();
if (findByNumber(student.getNumber())==null){
save(student);
}else{
System.out.println("学号已存在,未添加到数据库:"+student);
}
}
}
然后是写一个操作类,Operator类,用于写doWrite和doRead方法。
package com.nown.contacts.util;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.nown.contacts.entity.Student;
import com.nown.contacts.service.StudentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.OutputStream;
import java.util.List;
import java.util.Map;
@Component
public class Operator {
@Autowired
StudentService studentService;
public void doWrite(OutputStream outputStream, List<Student> list){
EasyExcel.write(outputStream, Student.class).sheet("模板").doWrite(list);
}
public void doRead(MultipartFile file) throws IOException {
EasyExcel.read(file.getInputStream(), Student.class, new ExcelListener(studentService)).sheet().doRead();
}
}
为了方便Controller调用这些方法,我们再来写一个ExcelService类
package com.nown.contacts.service;
import com.nown.contacts.entity.Student;
import com.nown.contacts.util.Operator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.OutputStream;
import java.util.List;
@Service
public class ExcelService {
@Autowired
private Operator operator;
@Autowired
private StudentService studentService;
public void doWrite(OutputStream outputStream){
List<Student> list=studentService.findAll();
operator.doWrite(outputStream,list);
}
public void doRead(MultipartFile file) throws IOException {
operator.doRead(file);
}
}
这样我们在处理请求时,只要Controller去响应请求,调用Service的方法,然后Service再去调用operator的方法就可以了。
至此,使用EasyExcel导入导出Excel已经做好准备,下面开始添加功能:
导出Excel
为了响应上传下载请求,可以写一个FileController类。我们先来看一下如何导出Excel。可以获取Response的输出流,这样导出Excel:
@Autowired
private ExcelService service;
@GetMapping("download")
public void download(HttpServletResponse response) throws IOException {
// 这里注意 有同学反应使用swagger 会导致各种问题,请直接用浏览器或者用postman
response.setContentType("application/vnd.ms-excel");
response.setCharacterEncoding("utf-8");
// 这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系
String fileName = URLEncoder.encode("学生信息", "UTF-8").replaceAll("\\+", "%20");
System.out.println(fileName);
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
service.doWrite(response.getOutputStream());
}
前端Vue的话,要响应后端传过来的文件流(App.vue的部分代码):
exportWord() {
this.$axios.get('http://localhost:8181/file/download', {
responseType: 'blob'
}).then(res => {
let blob = new Blob([res.data], {
type: 'application/ms-excel;charset=utf-8'
});
let downloadElement = document.createElement('a');
let href = window.URL.createObjectURL(blob); //创建下载的链接
downloadElement.href = href;
downloadElement.download = '学生信息.xlsx'; //下载后文件名
document.body.appendChild(downloadElement);
downloadElement.click(); //点击下载
document.body.removeChild(downloadElement); //下载完成移除元素
window.URL.revokeObjectURL(href); //释放掉blob对象
})
},
Excel导入
首先我们前端要发送请求(App.vue的部分代码):
submitUpload() {
this.$refs.upload.submit();
},
importWordConfirm(item) {
const _this=this;
const fileObj = item.file
const formData = new FormData()
formData.append('file', fileObj)
this.$axios.post('http://localhost:8181/file/upload/', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
}).then(res => {
_this.$alert('上传成功!', '消息', {
confirmButtonText: '确定',
callback: action => {
_this.$router.push({
path:"/StudentManage"
})
}
})
})
},
后端要处理请求(FileController中部分代码):
@PostMapping("/upload")
@ResponseBody
public String upload(MultipartFile file) throws IOException {
service.doRead(file);
return "success";
}
注意:
在使用Excel导入信息到数据库时,相同学号的信息不会被导入,可以从控制台看到哪条信息没有被导入。从Excel导入信息时,模板和下载模板类似,但是要删除id那一列,否则相同id的数据会覆盖掉数据库原有的信息。
OK,整个项目已经介绍完毕,具体代码已经上传至我的GitHub,欢迎大家参考,给个Star呗!
Github地址:https://github.com/Nown1/contacts