学生通讯录

项目简介

这是一个前后端分离的项目,基本技术栈是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

猜你喜欢

转载自blog.csdn.net/qq_46331393/article/details/113073818