Web课程设计之学生成绩管理系统

一、实验题目

学生成绩管理系统

二、实验目的

Web应用技术课程设计是计算机软件工程专业一个综合性实践的学习编程环节,是学习完《Internet与Web编程》课程后进行的一次全面的综合练习。其目的在于能促进我们复习和巩固计算机软件设计知识,加深对软件设计方法、软件设计技术和设计思想的理解,并能运用所学软件设计知识和web工程技术进行综合软件设计,通过本课程设计,我能掌握软件设计的方法和面向对象设计的基本技术,能提高我在软件开发方面的综合应用能力。

通过本课程设计,我进一步掌握了前端框架vue、vue桌面端组件库elementui、后端框架ssm的应用和学习,认识到注解开发有其便捷之处,以及mvc整体架构的一个设计运作流程,很好地把所学的框架知识运用到实践当中。

关于本系统的设计,笔者出于对以下几点的考虑,最终敲定设计的课题为学生成绩管理系统:

学校均存在可以对学生信息进行增删改查、对学生成绩进行录入查看、导出表格、分析个人成绩和班级成绩并展示数据的教务系统,教务系统成为了老师了解关注学生和掌握本班级的学习情况的一个重要平台,因此学生成绩管理系统有其十分重要的应用价值,而系统本身的使用为避免有人恶意操作又需要指定人员的登录后才能使用,本次实验对学生成绩管理系统的设计正是考虑到以上几点,思考到学生成绩管理系统的必要价值和功能,进行近一步开发设计,本系统有其拓展功能,但仍有不足之处,以下是本次系统设计过程的详细分析。

三、总体设计

3.1 实验要求:

1、添加学生功能:姓名、学号、性别、出生年月日。(学号自动生成且唯一)。

2、添加学生成绩功能:每个人都有数学、Java、英语、体育四门课,可分课程输入成绩。

3、根据学生学号查找学生成绩功能:在界面上显示姓名、学号和成绩,学号不存在的能给出提示信息。

4、根据学生姓名(支持模糊匹配)查找学生成绩功能:并在界面上显示姓名、学号和成绩,如果有多个相同姓名学生存在,一起显示出来,姓名不存在的给出提示信息。

5、支持对单个学生各科成绩画出柱状分布图。

6、学生信息的修改与删除功能:不能修改学号。

7、生成学生学习情况报表功能:报表包含学号、姓名、各科目成绩及对应的该科目班级平均值,总成绩以及班级总成绩平均值,并将该排序结果输出至excel文件。

系统功能概念模型:

在这里插入图片描述

3.2 本系统所运用到的技术栈:

项目管理工具:Maven

版本控制系统:Git

前端技术:html、css、javascript、vue、elementui、Echarts

前后端通信技术:axios

Web应用服务器:Tomcat

数据库技术:MySql

后端技术:Spring、SpringMVC、Mybatis、JavaBean、EasyExcel

3.3 本系统所运用的编程工具

代码编写:IntelliJ IDEA 2020.1

数据库图形化管理工具:Navicat for MySQL

数据库建模工具:PowerDesigner

Web应用服务器:Tomcat7.0

项目管理工具:Maven

代码托管服务:gitee

3.4 核心技术介绍

Vue:Vue.js是一套构建用户界面的渐进式框架。与其他重量级框架不同的是,Vue 采用自底向上增量开发的设计。Vue的核心库只关注视图层。

Axios:axios是一个基于Promise 用于浏览器和nodejs的 HTTP client。

Spring:Spring是一个轻量级Java开发框架,目的是为了解决企业级应用开发的业务逻辑层和其他各层的耦合问题。

SpringMVC:SpringMVC是Spring框架的一个模块,是基于mvc的webframework模块。mvc是一种Web端服务的架构模式,即model-view-controller。

MyBatis:MyBatis是一款优秀的持久层框架,它支持自定义SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和JavaPOJO为数据库中的记录。

Echarts:一个纯Javascript的图表库,可以流畅的运行在PC和移动设备上,兼容当前绝大部分浏览器。提供直观,生动,可交互,可高度个性化定制的数据可视化图表。

EasyExcel:EasyExcel是一个基于Java的简单、省内存的读写Excel的开源项目。在尽可能节约内存的情况下支持读写百兆的Excel。

3.5 模块介绍

数据库概念数据模型设计:

在这里插入图片描述

数据库表设计:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

登录模块:
在这里插入图片描述

首页模块:
在这里插入图片描述
学生信息查询模块:

在这里插入图片描述

学生成绩查询模块:

在这里插入图片描述
学生成绩分析模块:
在这里插入图片描述

新建学生信息、录入学生成绩、查看学生成绩、编辑学生信息等模态框模块:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3.6 设计步骤

1、创建Maven工程、添加公共静态文件、编写web.xml等核心配置文件

2、编写ssm配置文件,使用Mybatis逆向工程生成mapper文件和实体类,修改mapper的配置文件,添加查询带有成绩的学生信息的方法。

3、引入vue和elementui组件库文件,设计前端页面的整体框架,使用PageHelper编写后端Controller控制层分页代码,能正常返回json数据。

4、实现分页查询学生信息并展现在前端页面的功能。

5、实现新增学生信息和删除学生信息的功能。

6、实现修改学生信息的功能,点击编辑按钮后实现后台数据回显到编辑模态框中,并能进行数据校验给出提示信息。

7、实现录入学生成绩,并能在学生信息查询页面查看学生个人成绩的功能。并实现查询学生成绩页面,在此页面可以修改学生成绩。

8、实现分析学生成绩和班级各科成绩的平均分并生成学生成绩柱状图的共功能,并将更新班级各科平均分的代码封装成函数,完善了可能牵扯到班级平均分发生改变的操作代码,用上了avg_grade数据库。

9、美化前端界面,并实现了登录注册的功能,设计拦截器使得用户不登录无法访问系统。

10、实现在学生成绩分析页面展示查询到的学生所在班级的学生学习情况,该班级同学总成绩平均分根据各阶段的分数标准划分了优秀、良好、中等、及格、不及格等5个成绩等级,并将各个成绩等级的学生人数分布通过饼状图的形式展现在前端页面。

11、实现退出系统的功能,一旦点击退出系统则消除网站的session数据,刷新当前页面进入登录界面,用户必须登录后才能继续访问系统。

12、实现将附带有学生成绩的学生信息导出到excel文件中。

四、详细设计

4.1 项目资源目录介绍:

在这里插入图片描述

在这里插入图片描述

4.2 关键代码介绍

4.2.1 创建Maven工程,导入相关jar包依赖

①ssm框架相关jar包依赖:spring-webmvc、spring-jdbc、spring-aspects、mybatis、mybatis-spring

②数据库连接驱动相关jar包依赖:c3p0、mysql

③服务器插件依赖:tomcat7-maven-plugin

④其他jar包依赖:jstl、servlet-api、junit

⑤后续需要用到的jar包依赖:mybatis-generator-core(mybatis逆向工程)、pagehelper(分页插件)、spring-test、jackson-databind(支持返回json字符串)、hibernate-validator(JSR303数据校验支持)、log4j(log4j日志文件)、easyexcel(导出excel表格)。

4.2.2 编写spring、springmvc、mybatis配置文件

①编写web.xml文件

编写启动spring容器的代码并创建applicationContext.xml文件(主要配置和业务逻辑相关的)。配置springmvc的前端控制器,并创建dispatcherServlet-servlet.xml文件。配置字符编码过滤器。

②配置springmvc

在dispatcherServlet-servlet.xml中配置springmvc(包含网站跳转逻辑的控制)。

<!--    扫描控制器组件-->
    <context:component-scan base-package="com.shenshang" use-default-filters="false">
        <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
    </context:component-scan>

③配置spring

在applicationContext.xml中配置spring,这里主要配置和业务逻辑有关的。配置数据源、配置和mybatis的整合、配置扫描器、配置事务管理器、开启基于注解的事务。

④配置mybatis

在官方文档中复制相应的配置模板、进行全局配置文件的编写,包括驼峰命名规则和使用typeAliases标签为Java类型起别名。

4.2.3 使用mybatis逆向工程,修改mapper配置文件

MyBatis Generator:简称MBG,是一个专门为MyBatis框架使用者定制的代码生成器,可以快速的根据表生成对应的映射文件,接口,以及bean类。支持基本的增删改查,以及QBC风格的条件查询。但是表连接、存储过程等这些复杂sql的定义需要我们手工编写。

使用PowerDesigner设计数据库概念模型E-R图,并导出为sql语句文件,在navicat中运行生成自动创建好外键关系的表。

参照mybati官网中使用mybatis逆向工程的实例,在主工程中创建mbg.xml文件按,将实例代码复制到mbg.xml中,修改mbg.xml文件,配置数据库连接,指定JavgBean生成的位置,指定sql映射mapper文件生成的位置,指定dao接口生成位置,指定每个表的生成策略。在mybatis官网找到Running MyBatis Generator中的Java Program,在测试文件夹test中创建MBGTest.java并运行此段代码,逆向工程生成指定文件。

由于复杂的连接查询sql语句需要我们手工编写,因此修改mapper文件,编写连接表的sql语句,使得之后在编写业务逻辑方法时能直接调用方法,查询到带有成绩的单个学生信息,带有成绩的学生列表。修改完mapper文件后,在测试文件夹test中创建MapperTest.java创建接口测试实现类中的操作数据库方法,观察是否有bug出现,进一步修改完善代码。

4.2.4 引入静态文件和组件库文件,测试返回json数据。

在webapp包中创建plugins和static包,在其中引入vue、elementui组件库和jquery以及axios等组件库文件。在Controller包中创建stuInfoController.java文件,使用PageHelper分页插件,并测试能否返回json数据。

@ResponseBody
@RequestMapping("/findPage")
public Msg getPageInfoWithJson(@RequestParam(value = "pn",defaultValue = "1")Integer pn){
    
    
    PageHelper.startPage(pn,5);
    List<Student> studentList = stuInfoService.getStudentList();
    PageInfo pageInfo = new PageInfo(studentList,5);
    return Msg.success().add("pageInfo",pageInfo);
}

4.2.5 实现分页查询学生信息并展现在前端页面的功能

编写前端页面代码,参照elementui官方文档,进行添加组件,在输入框组件标签内部使用v-model实现数据的双向绑定,例如查询输入框,在输入框内输入内容,前端v-model数据绑定过后,通过axios发送post请求,Controller层利用@ResquestBody注解进行参数映射,拿到前端参数,创建Service接口和Service实现类,创建Service实体类对象并调用实体类内部方法操作数据库,查询到学生信息列表,并将信息和数据总数封装成PageResult返回Controller层,进而返回前端,前端拿到后端数据,进行前端参数赋值后,同样通过数据双向绑定展示到前端页面。

获取学生信息的Service接口实现类方法getStudentList():

public PageResult getStudentList(Integer currentPage, Integer pageSize, String queryString){
    
    
//        截取查询条件
    String[] queryList = queryString.split("-");
//        正则表达式判断字符串是否为数字
    Pattern pattern = Pattern.compile("^[-\\+]?[\\d]*$");
    StudentExample studentExample = new StudentExample();
    StudentExample.Criteria criteria = studentExample.createCriteria();
    for (int i = 0; i < queryList.length; i++) {
    
    
        if (pattern.matcher(queryList[i]).matches() && !queryList[i].equals("")&& Integer.parseInt(queryList[i] <= 7)){
    
    //字符串是数字且不为空且不小于7
            int classId = Integer.parseInt(queryList[i]);//将该字符串转换成数字
            criteria = criteria.andClassidEqualTo(classId);//将班级id拼接到查询条件中
        }else if (!queryList[i].equals("")){
    
    
            criteria = criteria.andNameLike("%"+queryList[i]+"%");//将姓名拼接到查询条件
        }
    }
        PageHelper.startPage(currentPage,pageSize);
//        Page<Student> page = (Page<Student>) studentMapper.selectByExample(studentExample);
    Page<Student> page = (Page<Student>) studentMapper.selectByExampleWithGrade(studentExample);
    return new PageResult(page.getTotal(),page.getResult());
}

前端发送axios请求拿到后端数据并进行参数赋值:

//分页查询学生信息
searchBtn(){
    
    
    // 使得查询按钮点下时都是从第一页开始显示数据
    this.pagination.currentPage = 1;
    //分页参数
    let param = {
    
    
        currentPage:this.pagination.currentPage,
        pageSize:this.pagination.pageSize,
        queryString:this.queryString.queryName+'-'+this.queryString.classSelected
    };
    // axios请求后台获取数据
    axios.post("/stuInfo/findPage",param).then((response)=>{
    
    
        // 为模型数据赋值,通过vue的双向数据绑定输出到页面
        this.tableData = response.data.rows;
        this.pagination.total = response.data.total;
    })
},

注意:创建PageResult实体类的目的:封装好分页结果类,能将查询数据和分页条信息一起返回到前端。

4.2.6 实现新增学生信息的功能

整体逻辑和查询逻辑类似,需要注意的是,点击新增按钮后弹出模态框内部要进行表单重置,否则,表单仍然保留上次输入后遗留的数据和校验信息,给用户体验感不好。再者,点击新增模态框内部的确定按钮后,发送axios请求,而前端传入的参数包括学生姓名,性别,班级,生日等信息,一定要注意的是如果要使用@RequestBody注解把json数据自动转换成对应的实体类,前端传入的参数必须要和实体类中对应的属性名相同,否则后端接收到的参数为空值。在新增学生信息的Service实现类方法体内部将性别和生日转换成数据库中统一的数据类型后,通过studentMapper调用insertSelective()方法,实现插入数据库数据。

新增学生信息的Service接口实现类方法add():

public void add(Student student) {
    
    
//        随机生成学号
    Random random = new Random();
    int sId = random.nextInt(5000) + 7;//学号
    student.setId(sId);
//        把参数中学生性别进行转换
    if (student.getSex().equals("1")){
    
    
        student.setSex("男");
    }else{
    
    
        student.setSex("女");
    }
//        设置学生生日格式为"xxx年xx月xx日"
    String birthday = student.getBirthday();
    String[] birthdaySplitList = birthday.split("-");
    student.setBirthday(birthdaySplitList[0]+"年"+birthdaySplitList[1]+"月"+birthdaySplitList[2]+"日");
//        调用函数新增学生信息
    studentMapper.insertSelective(student);
}

需要注意的是前端需要进行表单校验,表单校验不通过点击确定按钮则给出提示信息,且无法发送axios请求,当输入的表单数据均满足要求后点击确定按钮,则发送axios请求调用后端方法。

前端新增学生信息模态框表单校验规则:

rules:{
    
    //学生信息表单校验规则
    stuName:[
        {
    
     required: true, message: '请输入姓名', trigger: 'blur' },
        {
    
     min :2,max :5,message: '输入姓名长度在2到5个字符' },
    ],
    stuSex:[
        {
    
     required: true, message: '性别必须选择', trigger: 'blur'}
    ],
    stuClass:[
        {
    
     required: true, message: '班级必须选择', trigger: 'blur'}
    ],
    stuBorn: [
        {
    
     required: true, message: '请输入出生日期', trigger: 'blur' },
    ]
},

前端重置表单数据和校验信息方法:

// 重置表单数据和表单校验信息
resetForm(){
    
    
    // 把模态框表单中数据清空
    this.modalBox.form = {
    
    
        // stuId: '',
        stuName: '',
        stuSex: '1',
        stuClass: '',
        stuBorn:''
    };
    // 清除上次校验效果
    this.$nextTick(()=>{
    
    //Dom没有加载完成,导致获取不到refs元素
        // 方法作用是当数据被修改后使用这个方法,会回调获取更新后的dom再渲染出来
        this.$refs['modalBox.form'].clearValidate(['stuName','stuSex','stuClass','stuBorn']);
    })
},

点击确定按钮校验表单输入信息是否满足校验规则,不满足则给出提示信息:表单数据校验失败;满足则发送axios请求,关键代码后续类似操作会给出。

4.2.7 实现删除学生信息的功能

首先需要注意的是如果成绩表和学生表两表之间的外键关系没有建立正确,当进行删除学生信息时是会出bug的。点击删除按钮时,要弹出提示信息提示用户是否要删除学号为xxx的学生信息,当用户点击确定时,删除指定学号的学生信息,当用户点击取消时,不进行任何操作。另外,当用户点击确定按钮时,发送axios请求是只穿一个参数:学号,笔者在写删除功能时有个地方卡了很久。前台传输学号参数,后台同样使用@RequestBody注解接收参数,结果后台拿到的数据多了一个等号,百思不得其解,笔者以为是数据绑定出了问题,筛查后发现并不是绑定的问题,搜集过大量资料后发现是前后端数据传输类型不同的原因。搜集过大量资料后发现是因为前端发送axios请求时,默认的请求头headers内部的Content-Type是application/x-www-form-urlencoded;charset=UTF-8,这是一种键值对的数据结构,传输过程中把json当作key,而value当作空值,所以传输到后端会多出等号。需要在发送axios请求时设置请求头headers信息,并把后端接收的数据编码和前端设置统一就可以解决。

前端代码点击删除按钮执行的方法代码:

// 删除按钮,点击删除单行数据
handleDelete(row) {
    
    
    this.$confirm('确定删除学号为'+row.id+'的学生信息吗?', '提示', {
    
    
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
    }).then(() => {
    
    
       // 发送ajax请求,请求后台删除指定学号的学生信息
        axios.post("stuInfo/delete",row.id,{
    
    
            headers: {
    
    
                'Content-Type':'application/json'
            }
        }).then((response)=>{
    
    
            if (response.data.flag){
    
    
                // 删除成功
                this.$message({
    
    
                    message: response.data.message,
                    type: 'success'
                })
            }else{
    
    
                // 删除失败
                this.$message.error(response.data.message);
            }
        }).finally(()=>{
    
    
            // 重新获取删除后的分页数据
            this.searchBtn();
        })
    });
},

后端Controller层接收参数时,也要指定接收数据的格式:

@RequestMapping(value = "/delete",produces = "application/json;charset=UTF-8")//指定接收数据的格式
public Result deleteStuInfo(@RequestBody String id){
    
    
    try {
    
    
        int sid = Integer.parseInt(id);
        stuInfoService.deleteById(sid);
    }catch (Exception e){
    
    
        e.printStackTrace();
        return new Result(false , MessageConstant.DELETE_STU_FAIL);
    }
    return new Result(true , MessageConstant.DELETE_STU_SUCCESS);
}

然后在Service接口实现类中使用studentMapper实体类调用deleteByPrimaryKey()方法将指定学号的学生信息从数据库中删除即可。

4.2.8 实现修改学生信息的功能

与其他功能不相同的地方是,当点击编辑按钮时,弹出编辑模态框,需要在编辑模态框表单出回显出该学生的信息,回显时要注意前后端数据存储格式不同的,要对数据格式进行转换,同样也要对编辑模态框表单中的数据进行校验,格式不正确的给出校验提示信息,当校验通过时才能修改成功,并更新数据库中指定学号学生的信息。前端点击编辑按钮,改变dialogFormVisibleEdit的值为true,以实现模态框的显现,后发送axios请求后台数据,要注意此时仍然是传递了学生学号这一个参数,仍然要设置请求头headers信息,并把后端接收的数据编码和前端设置统一。

前端点击编辑按钮后执行的回显数据方法代码:

// 表格中的编辑按钮
handleUpdate(row){
    
    
    // alert(row.id);
    // 发送请求获取目前行学生信息
    axios.post("/stuInfo/findById",row.id,{
    
    
        headers:{
    
    
            'Content-Type':'application/json'
        }
    }).then((response)=>{
    
    
        if (response.data.flag){
    
    
            // 显示出编辑模态框
            this.modalBox.dialogFormVisibleEdit = true;
            // 将数据回显到编辑模态框的表单中
            this.modalBox.editForm.stuId = response.data.data.id;
            this.modalBox.editForm.stuName = response.data.data.name;
            this.modalBox.editForm.stuSex = response.data.data.sex == "男"? '1':'0';
            this.modalBox.editForm.stuClass = response.data.data.classid;
            this.modalBox.editForm.stuBorn = response.data.data.birthday;
        }else{
    
    
            this.$message.error(response.data.message);
        }
    })
},

因为前端获取或接收和数据库中存储的数据格式不同,要注意后端Service接口实现类中findById()方法内部要对查询到的数据库中指定学号的学生信息数据进行格式转换,进而返回到前端展示:

public Student findById(Integer id) {
    
    
    Student student = studentMapper.selectByPrimaryKey(id);
//            把xx年xx月xx日格式的字符串转变成xx-xx-xx格式的字符串,便于在前端赋值给日期选择器
//            2014年01月10日
    String year = student.getBirthday().substring(0,4);
    String mouth = student.getBirthday().substring(5,7);
    String day = student.getBirthday().substring(8,10);
//            重新设置student类的birthday属性遵循xx-xx-xx的格式
    student.setBirthday(year+"-"+mouth+"-"+day);
    return student;
}

当点击编辑模态框中的确定按钮时,首先需要对表单数据进行校验,任何一个输入框输入信息不满足校验规则都无法进行提交,给出提示信息,即使不对学生信息进行任何修改,那么后台还是会执行一遍update方法,只不过将学生信息更新为原来信息而已(即不做任何修改),前端表单校验方法代码在此处贴出来,其他要用到校验代码的均与此处类似:

//编辑模态框中的确定更新按钮
handleEdit() {
    
    
    let param = {
    
    
        id:this.modalBox.editForm.stuId,
        name:this.modalBox.editForm.stuName,
        sex:this.modalBox.editForm.stuSex,
        classid:this.modalBox.editForm.stuClass,
        birthday: this.modalBox.editForm.stuBorn
    }
    // 校验表单数据
    this.$refs['modalBox.editForm'].validate((valid)=>{
    
    
        if (valid){
    
    
            // 校验成功,发送axios请求
            axios.post("stuInfo/update",param).then((response)=>{
    
    
                // 隐藏编辑模态框
                this.modalBox.dialogFormVisibleEdit = false;
                if (response.data.flag){
    
    
                    // 更新数据成功
                    this.$message({
    
    
                        message: response.data.message,
                        type: 'success'
                    });
                }else{
    
    
                    // 更新数据失败
                    this.$message.error(response.data.message);
                }
            }).finally(()=>{
    
    
                // 展示更新后的表格数据
                this.searchBtn();
            })
        }else{
    
    
            // 校验失败
            this.$message.error('表单校验数据失败');
            return false;
        }
    })
},

4.2.9 实现录入学生成绩且能在学生信息管理页面查看学生成绩

实现此功能要注意的是前端页面的表格中有操作这一栏,操作一栏有录入和查看两个按钮,在学生尚未录入成绩时,录入按钮是可以点击的,而查看按钮是无法点击的,学生成绩已经录入后,则查看按钮是可以点击查看该学生成绩的,而录入按钮设置成无法点击,避免重复录入或者查看成绩为空。实现此操作主要是依据数据库s_stu表中的flag值,当学生成绩未录入时(新建学生信息后),flag值为null,此时录入按钮检测到flag值为null时,则设置为可点击状态,查看按钮设置为不可点击状态,当学生成绩已经录入后,将flag值置为1,当录入按钮检测到flag值为1时,录入按钮设置为不可点击状态,查看按钮设置成可点击状态。前端两个按钮的代码如下:

<el-button slot="reference" type="text" size="small" :disabled="scope.row.flag == 1? false:true">查看</el-button>
<el-button type="text" size="small" @click="gradeInput(scope.row)" :disabled="scope.row.flag == 1? true:false">录入</el-button>

录入按钮点击后显示出录入成绩的模态框,重置表单数据和校验信息,并且将获取的改行的学生学号赋值给表单中的stuId,方便之后数据传参。当点击录入模态框中的确定按钮时,要对表单输入内容进行校验,当输入成绩满足校验规则时才能发送axios请求,在Service接口实现类中将指定学号的学生的flag值置为1,然后将该学生的成绩插入到s_grade表中。

后端Service接口实现类的方法代码:

public void inputGrade(Grade grade) {
    
    
//        计算出该学生的总成绩
    Integer sum = grade.getMath()+grade.getJava()+grade.getEnglish()+grade.getPe();
    grade.setGradenum(sum);
//        把指定学号的学生信息的flag值置为1,表示该学生的成绩已经录入了
    Student student = studentMapper.selectByPrimaryKey(grade.getId());
    student.setFlag(1);
//        将数据更新到数据库中
    studentMapper.updateByPrimaryKeySelective(student);
    gradeMapper.insertSelective(grade);
}

录入学生成绩后,前端的查看按钮将会设置为可点击状态,此时点击查看按钮会弹出消息提示框,展示学生个人成绩。逻辑实现主要是通过前端代码使用elementui中的标签实现,由于展示分页数据的后端实现类方法使用的查询方法是查询到带有成绩的学生列表,因此在前端查看学生成绩时无需再请求后端数据,直接将前端的参数值展示出来即可。

前端查看成绩的代码:

<el-table-column label="成绩" width="185">
    <template slot-scope="scope">
        <el-popover placement="right" width="200" trigger="click">
            <el-tag>学号:{
    
    {
    
    scope.row.id}}</el-tag>
            <el-tag type="success">姓名:{
    
    {
    
    scope.row.name}}</el-tag>
            <p><el-tag type="warning">数学成绩:{
    
    {
    
    scope.row.grade.math}}</el-tag></p>
            <p><el-tag type="warning">java成绩:{
    
    {
    
    scope.row.grade.java}}</el-tag></p>
            <p><el-tag type="warning">英语成绩:{
    
    {
    
    scope.row.grade.english}}</el-tag></p>
            <p><el-tag type="warning">体育成绩:{
    
    {
    
    scope.row.grade.pe}}</el-tag></p>
            <p><el-tag type="danger">总成绩:{
    
    {
    
    scope.row.grade.gradenum}}</el-tag></p>
            <el-button slot="reference" type="text" size="small" :disabled="scope.row.flag == 1? false:true">查看</el-button>
        </el-popover>
        <el-button type="text" size="small" @click="gradeInput(scope.row)" :disabled="scope.row.flag == 1? true:false">录入</el-button>
    </template>
</el-table-column>

4.2.10 实现查询学生成绩并能修改指定学生成绩的功能

创建gradeSearch.html文件,添加前端组件,此处的查询功能和学生信息的查询功能类似,前后端均使用了查询条件拼接,可根据学号、姓名、班级查询学生成绩,前端将查询条件进行拼接,中间使用"-"符号进行连接,当数据传到后端时,要使用split()方法进行字符串切割,后遍历切割后生成的字符串数组,将学号和班级的字符串转换成数字,后拼接到sql语句当中操作数据库。此处在学号和班级号进行sql语句拼接时做了处理,由于班级号和学号均可以转换为数字,所以要判断此字符串数组单元值是学号还是班级号,此处的约定为:学号一定大于7,班级号一定不大于7,由此进行判断。

后端Service接口实现类方法的代码:

public PageResult getStudentList(Integer currentPage, Integer pageSize, String queryString){
    
    
//        截取查询条件
    String[] queryList = queryString.split("-");
//        正则表达式判断字符串是否为数字
    Pattern pattern = Pattern.compile("^[-\\+]?[\\d]*$");
    StudentExample studentExample = new StudentExample();
    StudentExample.Criteria criteria = studentExample.createCriteria();
    for (int i = 0; i < queryList.length; i++) {
    
    
        if (pattern.matcher(queryList[i]).matches() && !queryList[i].equals("")){
    
    //字符串是数字且不为空
            int tmp = Integer.parseInt(queryList[i]);
            if (tmp > 7){
    
    //说明tmp是学号
                int stuId = tmp;//把中间值赋值给学号
                criteria = criteria.andIdEqualTo(stuId);//把学号拼接到查询条件中
            }else{
    
    //说明tmp是班级号
                int classId = Integer.parseInt(queryList[i]);//将该字符串转换成数字
                criteria = criteria.andClassidEqualTo(classId);//将班级id拼接到查询条件中
            }
        }else if (!queryList[i].equals("")){
    
    
            criteria = criteria.andNameEqualTo(queryList[i]);//将姓名拼接到查询条件
        }
    }
    PageHelper.startPage(currentPage,pageSize);
    Page<Student> page = (Page<Student>) studentMapper.selectByExampleWithGrade(studentExample);
    return new PageResult(page.getTotal(),page.getResult());
}

在gradeSearch.html的前端页面中,可以修改指定学生的各科成绩,点击编辑按钮弹出编辑成绩模态框,弹出模态框之前需要对表单数据进行清空和重置表单校验信息,同样需要对表单数据进行回显校验,符合指定校验规则的数据可被传送到后端,不符合校验规则的表单数据无法提交,点击确定按钮后给出提示信息。此处功能需要注意的是编辑按钮同样通过判断s_stu表中的flag值来决定编辑按钮是否可点击,将没录入成绩(flag值为null)的学生的成绩编辑按钮设置为不可点击状态。

后端Service接口实现类方法将指定学生成绩进行更新:

public void update(Grade grade) {
    
    
    int sum = grade.getMath()+grade.getJava()+grade.getEnglish()+grade.getPe();
    grade.setGradenum(sum);
    gradeMapper.updateByPrimaryKey(grade);
//        根据学生学号查询出该学生所在班级
    Integer sid = grade.getId();
    Student student = studentMapper.selectByPrimaryKey(sid);
    Integer classid = student.getClassid();
    updateAvgClassGrade.updateAvgGrade(classid);
}

4.2.11 实现分析学生成绩和班级各科成绩平均分并生成柱状图

创建gradeAnalysis.html文件,添加前端组件,此页面仅仅实现了通过指定学号分析学生成绩的功能,因为能保证学号不重复,如果有重名现象则无法查询分析出重名学生的成绩柱状图,那是因为通过姓名查询出重名学生这将导致柱状图数量不确定且前端页面会被撑开,所以只能通过查询学号来分析学生成绩并生成相应柱状图,此处使用的是Javascript的图标库Echarts。点击查询按钮后,如果输入框内输入学号为空,则不执行任何操作;若输入内容不为空,则发送axios请求,请求参数为输入的学号,此时需要注意的是,仍然要设置请求头headers信息,并把后端接收的数据编码和前端设置统一。学号传到后端后,后端查询出指定学号带有成绩的单个学生信息并返回前端,前端若拿到的数据为空,则说明查询的学号不存在,则将idIsNull赋值为true,并给出学号不存在的提示信息,表明后续不再发送请求获取该学生所在班级的各科平均成绩(因为学生不存在,班级肯定更不存在了);若前端拿到数据不为空,则对前端双向绑定的参数进行赋值,以生成柱形图中的柱形,然后再一次发送axios请求获取该学生所在班级的各科平均成绩(idIsNull值为false的情况下,表明该学号存在),传入参数为学生所在班级号,后端拿到参数后,创建并注入实体类avgGradeMapper,通过avgGradeMapper实体类调用selectByPrimaryKey()方法查询出avg_grade表中指定班级的各科平均成绩,并将查询结果返回给前端,前端对双向绑定数据进行赋值以生成柱形图中的折线。此处代码曾有完善更新,初代版本此处是通过学生班级号查询出此班级所有的带有成绩的学生列表,计算出班级各科平均值和总成绩平均值再返回前端,后思考到当新建学生信息并录入学生成绩时、当修改某学生成绩时、当删除某学生信息时均可能导致班级的各科平均值和总成绩平均值发生变化,并且初代版本的代码导致avg_grade表根本没用上,于是笔者将计算班级各科平均值和总成绩平均值的代码单独封装成方法,在可能影响到班级成绩的Service接口实现类操作方法中均实现了调用此方法计算出班级成绩并更新avg_grade表,此时就可以直接通过avgGradeMapper调用查询方法查询avg_grade表中的值直接返回Controller层进而返回前端了。

后端封装的计算班级成绩的方法代码:

public void updateAvgGrade(int classId) {
    
    
    //更新该学生班级的各科平均成绩
    Integer mathSum = 0,javaSum = 0,englishSum = 0,peSum = 0,gradeNumSum = 0;
    StudentExample studentExample = new StudentExample();
    StudentExample.Criteria criteria = studentExample.createCriteria();
    criteria.andClassidEqualTo(classId);
    List<Student> students = studentMapper.selectByExampleWithGrade(studentExample);
//        循环遍历这个班级的每个学生,算出各科平均分
    for(Student stu : students) {
    
    //因为存在没录入成绩的学生,所以不能直接累加
        if (stu.getGrade().getMath() != null) mathSum += stu.getGrade().getMath();
        if (stu.getGrade().getJava() != null) javaSum += stu.getGrade().getJava();
        if (stu.getGrade().getEnglish() != null) englishSum += stu.getGrade().getEnglish();
        if (stu.getGrade().getPe() != null) peSum += stu.getGrade().getPe();
        if (stu.getGrade().getGradenum() != null) gradeNumSum += stu.getGrade().getGradenum();
    }
    int studentSize = students.size();//班级的学生总数
    float math = (float)mathSum / studentSize;//班级的数学成绩平均分
    float java = (float)javaSum / studentSize;//班级的java成绩平均分
    float english = (float)englishSum / studentSize;//班级的英语成绩平均分
    float pe = (float)peSum / studentSize;//班级的体育成绩平均分
    float gradeNum = (float)gradeNumSum / studentSize;//班级的总成绩平均分
//        把班级平均数封装成对象
    avgGrade gradeAvgClass = new avgGrade(classId,math,java,english,pe,gradeNum);
    avgGradeMapper.updateByPrimaryKey(gradeAvgClass);
}

要注意的是当输入的学号不为空且输入学号存在时,前端将连续发送两个axios请求,需要区分的连续发送和同时发送,必须先获取到要查询学生的班级号,才能发送第二个axios请求,因为第二个axios请求就是以该学生所在的班级号为参数的。其他涉及到一些输入值为空、学号不存在的情况上文已经讨论,此处不再赘述。

前端点击查询按钮后执行的方法代码:

gradeAnalysis(){
    
    
    if (this.queryStr.queryId == undefined || this.queryStr.queryId == null || this.queryStr.queryId == '') return false;
    axios.post("stuAnalysis/searchById",this.queryStr.queryId,{
    
    //发送请求根据学生学号查询其成绩
        headers:{
    
    
            'Content-Type':'application/json'
        }
    }).then((response)=>{
    
    
        this.showOutPutBtn = true;
        if (response.data.flag){
    
    
            if (response.data.data == null){
    
    
                this.idIsNull = true;//该学号不存在,设置标志值以至于判断是否发送第二个请求
                this.$message.error('学号不存在');
                // 清空柱状图数据
                this.resetCharts();
                this.initCharts();//初始化图表
                return false;//终止请求
            }else{
    
    
                this.queryResult.stuId = response.data.data.id + "";
                this.queryResult.stuName = response.data.data.name + "";
                this.queryResult.stuClass = response.data.data.classid + "";
                this.queryResult.gradeMath = response.data.data.grade.math + "";
                this.queryResult.gradeJava = response.data.data.grade.java + "";
                this.queryResult.gradeEnglish = response.data.data.grade.english + "";
                this.queryResult.gradePE = response.data.data.grade.pe + "";
                this.queryResult.gradeNum = response.data.data.grade.gradenum + "";
                this.$message({
    
    
                    message: response.data.message,
                    type: 'success',
                })
            }
        }else{
    
    
            this.$message.error(response.data.message);
        }
    }).then(()=>{
    
    
        if (this.idIsNull == true){
    
    //如果学号为空
            this.idIsNull = false;//重新赋值,方便下次查询
        }else{
    
    
            // 发送axios请求查询该学生所在班级的各科平均分
            axios.post("stuAnalysis/avgByClassId",this.queryResult.stuClass,{
    
    
                headers:{
    
    
                    'Content-Type':'application/json'
                }
            }).then((response)=>{
    
    
                this.showOutPutBtn = !response.data.flag;
                this.queryResult.avgMath = response.data.data.avgMath + "";
                this.queryResult.avgJava = response.data.data.avgJava + "";
                this.queryResult.avgPE = response.data.data.avgPe + "";
                this.queryResult.avgEnglish = response.data.data.avgEnglish + "";
                this.queryResult.avgGradeNum = response.data.data.avgNumber + "";
            }).finally(()=>{
    
    
                this.initCharts();//初始化柱状图表
                this.initPieCharts();//初始化饼状图表
            })
        }
    })
},

另外,此处前端代码使用到了Javascirpt的图标库Echarts,此技术在本报告中只做简单介绍,本报告中也是涉及到Echarts的基本用法,因此不做过深阐述。前端在拿到学生各科成绩和学生所在班级的平均成绩后将双向绑定的数据进行赋值,最后执行初始化柱状图表,在initCharts()方法体内部,首先获取柱状图指定盒子div并初始化myChart数据,后指定柱状图表的配置项和数据,然后使用刚刚指定好的配置项和数据显示到图表。

前端初始化柱状图表的initCharts()方法代码:

initCharts(){
    
    
    this.myChart = echarts.init(document.getElementById('chartBox'));

    // 指定柱状图表的配置项和数据
    let option = {
    
    
        title: {
    
    
            text: this.queryResult.stuClass + '班的' + this.queryResult.stuName + '成绩分析柱状图',
        },
        tooltip: {
    
    },
        legend: {
    
    
            data: ['分数','班级平均分'],
        },
        xAxis: {
    
    
            data: ['数学', 'Java', '英语', '体育'],
        },
        yAxis: [
            {
    
    
                name: '分值',
                min: 0,
                max: 100,
            },
        ],
        series: [
            {
    
    
                name: '分数',
                type: 'bar',
                data: [
                    this.queryResult.gradeMath,
                    this.queryResult.gradeJava,
                    this.queryResult.gradeEnglish,
                    this.queryResult.gradePE,
                ],
                itemStyle: {
    
    
                    normal: {
    
    
                        color: '#4ad2ff',
                    }
                }
            },
            {
    
    
                name: '班级平均分',
                type: 'line',
                data: [
                    this.queryResult.avgMath,
                    this.queryResult.avgJava,
                    this.queryResult.avgEnglish,
                    this.queryResult.avgPE,
                ],
                itemStyle: {
    
    
                    normal: {
    
    
                        color: '#ffa500',
                    }
                }
            }
        ]
    };
    // 使用刚指定的配置项和数据显示图表
    this.myChart.setOption(option);
},

4.2.12 美化前端界面,利用拦截器实现登录注册功能

前期笔者把精力主要放在前后端业务逻辑的实现上,对于页面元素没有进行过度美化,只是添加了必要的组件。然后笔者就页面美化做了更新,并通过拦截器实现了登录注册功能。前端页面美化,笔者主要使用了logo神器网站生成了项目logo,使用colorspace网页生成较为美观的渐变色,添加组件使得网页右上角有用户名头像,点击后弹出下拉菜单。找到登录注册的美观的前端代码,进行导入和修改,换上了笔者喜欢的主题,由此登录注册的前端页面,网页整体的色调,美观界面均初步完成。

在login.html文件中,点击登录按钮执行登录方法,发送axios请求,传入的参数是用户输入的用户名,在后端Service接口的实现类方法中通过loginUserMapper调用selectByPrimaryKey()方法查找数据库中该用户名所对应的密码,若查找为空,则说明用户名或者密码不存在,若查找到的密码与用户输入密码不匹配,说明用户输入密码错误,否则,密码正确登录系统,登录系统即通过window.open(‘http://localhost:81/main.html’,‘_self’);语句在当前窗口打开main.html页面即可。

前端点击登录按钮执行的登录方法代码:

login(){
    
    
   let param = {
    
    
      username: this.user.userName,
      password: this.user.passWord,
   }
   axios.post("/login/checkPwd" , param).then((response)=>{
    
    
      if (response.data.flag){
    
    //登录成功
         this.$message({
    
    
            message: response.data.message,
            type: 'success'
         });
         location.reload();//刷新当前页面
      }else {
    
    //账号或密码错误
         this.$message.error( response.data.message );
      }
   })
},

实现至此,仅仅是登录成功后实现了简单的页面跳转,实际上可以直接越过登录注册页面,在未登录系统时,直接输入main.html网址访问系统,这样此时的登录注册功能完全失去了意义,因此有必要使用拦截器对登录注册功能进一步完善。

拦截器作用:当用户未登录系统时,想要跳过登录注册页面而直接访问系统,即使用户输入main.html的具体网址,仍然无法访问main.html的资源,点击访问后系统会自动跳转到登录注册页面。创建interceptor包,在包内新建LoginInterceptor.java文件,此拦截器实体类需要实现HandlerInterceptor接口,实现接口方法preHandle(),此方法会在需要拦截的页面访问之前执行,在方法体内部通过request获取session域,若session域中的"user"所对应的值为空,说明用户未登录,此时对除登录注册页面、一些静态资源之外的所有页面进行拦截;若获取的session域中的"user"所对应的值不为空,说明用户已经登录过了,系统中存有用户登录的session值,此时对页面不进行拦截。此时需要在登录的Controller层方法中判断登录成功后,通过setAttribute()方法将user以键值对的形式存入到session域当中,表明用户已经登录。

后端的拦截器方法代码:

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    
    
    HttpSession session = request.getSession();
    if (session.getAttribute("user") != null){
    
    
        //用户已登录不拦截
        return true;
    }else {
    
    
//            拦截后进入登录页面
        response.sendRedirect(request.getContextPath() + "/login.html");
        return false;
    }
}

另外,需要在dispatcherServlet-servlet.xml(springmvc的配置文件)中进行拦截器配置,指定需要拦截的文件和不需要拦截的文件,以及进行拦截器实体类映射,只有在配置文件中配置过拦截器后,拦截器才能真正起作用。

dispatcherServlet-servlet.xml文件中有关拦截器的配置:

<mvc:interceptors>
    <mvc:interceptor>
<!--            拦截所有mvc控制器-->
        <mvc:mapping path="/**"/>
        <mvc:exclude-mapping path="/login.html"/>
        <mvc:exclude-mapping path="/static/**"/>
        <mvc:exclude-mapping path="/plugins/**"/>
        <mvc:exclude-mapping path="/login/**"/>
        <bean class="com.shenshang.manager.interceptor.LoginInterceptor"></bean>
    </mvc:interceptor>
</mvc:interceptors>

4.2.13 实现分析班级学生成绩等级划分的饼状图功能

笔者认为单单一个包括学生个人成绩和班级各科平均分的柱状图无法展现出班级整体学生成绩水平的梯度,同时也想更加熟练一下Echarts的使用,由此拓展实现了能展示查询到学生所在班级的学生学习情况的饼状图,饼状图作用:该班级同学总成绩平均分根据各阶段的分数标准划分了优秀、良好、中等、及格、不及格等5个成绩等级,并将各个成绩等级的学生人数分布通过饼状图的形式展现在前端页面。前端的业务逻辑和生成柱状图的业务逻辑类似,此处不再赘述,前端代码仍然包括发送axios请求后拿到后台数据,然后进行赋值,并初始化饼状图展现在页面,前端唯一需要注意的点是当点击查询按钮时,分析出饼状图和分析出柱状图的方法是同时执行的,在前端查询按钮处@click="gradeAnalysis(),classAnalysis()"两个同时执行的方法之间用逗号连接。classAnalysis()方法中发送axios请求时传的参数为输入的学生学号。后端Service接口实现类方法中拿到前端学生学号后,首先查询出此学号对应的单个学生信息,获取到此学生所在的班级,通过班级号查询出此班级所有的带有成绩的学生列表,定义并初始化各个等级的计数器后,遍历学生列表,计算出每个学生的总成绩平均分,根据学生总成绩平均分判断学生处于哪个成绩等级区间,将相应成绩等级区间的计数器累加一,最后将得到的计数器的值通过构造器实例化对象并返回到前端。前端拿到数据后赋值给双向绑定的数据,通过饼状图的形式展现出来。

后端Service接口实现类方法代码:

public GradeRank rankById(String id) {
    
    
    Integer excellent = 0, good = 0, middle = 0, passed = 0, unPass = 0;
    int sid = Integer.parseInt(id);
    Student student = studentMapper.selectByPrimaryKey(sid);
    if (student == null) return null;
    Integer classid = student.getClassid();
    StudentExample studentExample = new StudentExample();
    StudentExample.Criteria criteria = studentExample.createCriteria();
    criteria.andClassidEqualTo(classid);
    List<Student> students = studentMapper.selectByExampleWithGrade(studentExample);
    for (Student stu : students) {
    
    
        if (stu.getFlag() == null) continue;
        float avgGradeNum = (float)stu.getGrade().getGradenum() / 4;//学生总分平均分
        if (avgGradeNum >= 60 && avgGradeNum < 70) passed++;//及格人数自增1
        else if(avgGradeNum >= 70 && avgGradeNum < 80) middle++;//中等人数自增1
        else if(avgGradeNum >=80 && avgGradeNum < 90) good++;//良好人数自增1
        else if(avgGradeNum >= 90 && avgGradeNum <= 100) excellent++;//优秀人数自增1
        else unPass++;//不及格人数自增1
    }
    return new GradeRank(excellent , good , middle , passed , unPass);
}

4.2.14 实现退出系统的功能

用户一旦点击退出系统则消除网站的session域中的数据,并刷新当前页面,由于拦截器检测到目前session域中已无用户的登录数据,刷新页面后会拦截main.html页面转而进入到登录注册页面,用户需要登录注册后才能访问系统资源。当用户点击退出系统按钮后,前端执行handleComment()方法,此方法是elementui中下拉框标签的内置方法,当点击过此方法时,获取点击的标签中的command值,在方法体内部判断当获取的command值等于"b"时(退出系统对应的command值为"b"),则发送axios请求,在退出系统的Controller层使用session.removeAttribute()语句将session域中存有的user登录信息清除。清除成功后,前端方法体内将利用window.open(‘http://localhost:81/login.html’);语句进行跳转到登录页面,可以测试进行输入main.html的网址,发现访问系统内部资源时,仍然会自动跳转到login.html,拦截器生效。

退出系统的Controller层方法体代码:

@RequestMapping("/exitSystem")
public Result exitSystem(@RequestBody Integer id , HttpSession session){
    
    
    try{
    
    
        session.removeAttribute("user");
        return new Result( true , MessageConstant.EXIT_SYSTEM_SUCCESS );
    }catch (Exception e){
    
    
        e.printStackTrace();
        return new Result( false , MessageConstant.EXIT_SYSTEM_FAIL );
    }
}

4.2.15 实现将附带成绩的学生信息导出到excel文件

首先需要导入EasyExcel的相关依赖到pom.xml文件中。点击导出报表按钮后,前端发送axios请求,传入的参数是查询结果集合,Controller层接收参数是有和前端传入数据类型对应相同的对象属性的实体类OutPutExcel,实体类中的每个属性前添加属性@ExcelProperty,此属性中value值对应生成的Excel表格中表头名称,且注解当中的index值用来指定表头名称的出现先后顺序,index小的在前,index大的在后。Controller层中通过stuAnalysisService调用接口实体类方法,在Service接口实体类方法中指定导出excel文件的路径,将接收的参数outPutExcel存放入数组当中,然后将数组写入到excel文件中去。

后端Service接口实现类方法的代码:

public void pushExcel(OutPutExcel outPutExcel) {
    
    
//        指定导出excel文件的路径
    String fileName = filename;//此处的filename要换成指定的文件路径
//        存放对象的数组
    List<OutPutExcel> gradeCharts = new ArrayList<OutPutExcel>();
    gradeCharts.add(outPutExcel);
//        将数组写入到excel表格中去
    EasyExcel.write(fileName , OutPutExcel.class).sheet("个人成绩报表").doWrite(gradeCharts);
}

此处前端代码需要注意的是,导出报表的按钮只有在查询到结果后才会设置为可点击状态,如果没查询出来信息,那么导出报表的按钮将是不可点击状态。

五、实验结果与分析

最终项目成型,学生成绩管理系统目前未发现任何bug,项目实现了任务指导书中的所有基本功能,另外,笔者拓展了很多功能作为此项目的加分项。但此项目也有不足之处存在,此处进行一并分析。

项目功能创新拓展点:

①新增学生信息的模态框表单中使用了单选框、多选框、日期选择器等多种类型的输入框,与一些只使用可以输入的输入框的表单相比较,此种类型能更好地约束用户的输入。例如日期选择器的使用,elementui的内置日期选择器类型的输入框会随着日历变更可以选择的时间,笔者在前端进行处理,使得大于当天的日期无法被选中。日期选择器将会随着系统日期的变更而变更,减少了复杂繁多的后台校验代码。

②学生信息查询页面的表格当中含有操作一栏,在此处管理员可以对学生成绩进行录入和查看,录入和查看按钮也根据学生成绩是否已录入而设立了可点击状态和不可点击状态,避免管理员进行重复录入学生信息,也避免管理员查看学生成绩时显示空白提示框。录入按钮设立在此处与传统的输入姓名录入成绩不同,主要是方便管理员查看哪些同学还没有录入成绩,并且在录入成绩后可以直接在学生信息页面查看学生成绩,方便管理员核对成绩是否有误。而实现至此,学生成绩查询页面看似多余(因为通过学生信息查询页面已经可以获取学生成绩了),但是在成绩查询页面可以一并查看多个学生的成绩,并且输入班级作为查询条件后,还可以查看某个班级的多个学生的成绩,可以直观地对不同学生的成绩进行比对,并且学生成绩查询页面还有可以修改某学生成绩的功能,对于成绩有误的学生,在此页面可以直接修改该学生成绩,由此可见,学生成绩查询页面并不多余,反而两个页面相互影响,相互关联。

③学生信息查询页面和学生成绩查询页面的编辑按钮点击后,均弹出类似的模态框,而此模态框均进行了数据回显,因为考虑到没有数据回显的话,会导致修改时无原先信息的参考数据,容易造成修改失误。

④学生信息查询页面和学生成绩查询页面均把班级号作为查询条件之一,此目的是可以通过输入班级号,查询指定班级的所有学生信息和指定班级的所有学生成绩。这一点任务指导书上未作要求,笔者觉得这样可以给用户选择把查询范围由专业范围缩小到班级范围,所以进行了此功能的拓展。

⑤在学生成绩分析页面,笔者将导出报表此按钮进行了处理,只有在后台有返回数据的前提下,导出报表按钮才是可点击状态,很好地避免了在查询不到输入学号的学生时导出按钮点击无反应、点击报错以及生成的excel表格中某表头列数值为空的情况。

⑥在学生成绩分析页面,后台查询到指定学号的带有成绩的学生信息后,前端获取到该学生的班级号,将此班级号作为第二次axios请求的参数传递,以此在后台再计算出班级的各科平均成绩传递到前端,通过折线图的方式展现出来,而折线图和柱形图在一张图上,方便管理员将学生和其所在班级的各科平均成绩水平进行对比,可以很方便地找出学生拉低班级平均分的科目。

⑦在学生成绩分析页面,当输入学号并点击查询按钮后,会同时执行分析学生成绩柱状图的方法和分析班级成绩饼状图的方法,之所以考虑制作饼状图是想到柱状图中难以反映出班级的学生整体水平如何,无法判断出班级学生梯度(即无法知道是学习好的学生多或是少,学习中等的学生多还是少,不及格的学生又有多少等数据),于是实现饼状图的功能可以很好地反映出班级学生的梯度情况,可以反馈给管理员班级大致整体的学习氛围。

⑧实现了登录注册功能,这一功能在任务指导书上并未做要求,笔者认为一个好的系统,登录注册功能是必不可少的,如果未进行登录的用户输入网址即可访问系统资源,那系统就失去了最起码的保密性和安全性。登录注册是系统安全的第一道防线。

⑨实现了退出系统的功能,这一点任务指导书中也未做要求,但退出系统的实现可以方便用户进行重新登录,或者切换账号登录,如若不实现退出系统的功能,则下一次拦截器生效只能是清除浏览器缓存或重启服务器,这样给用户体验不好,用户的账号保密性也得不到保证。

项目功能的不足之处:

①在学生成绩分析页面,当查询出学生成绩分析柱状图和班级成绩饼状图后,此时可以点击导出报表按钮,点击按钮后会给出提示信息“导出学生成绩报表成功”,而excel文件保存路径是在项目中就已经指定好的。这一点给用户体验不是很好,如果可以让用户指定文件保存路径就更好了。

②在登录注册页面,当单击了注册按钮后,注册是简单的输入用户名和新密码,符合校验规则的用户名和密码会被插入到数据库表中,没有再次输入新密码的输入框,这样会导致用户可能注册账号时密码摁错了一位,导致登录时登录不上的问题。其实实现再次输入密码的逻辑特别简单,只要完成后台校验并给出前端提示信息就可以了。

③系统没有实现修改密码的功能,笔者认为此功能和项目中的编辑按钮实现逻辑极为相似,觉得没有再写的必要,所以没有实现修改密码功能,其实如果想写的更好一些,完全可以让用户输入注册过的邮箱或手机号,在注册账号和修改密码时均发到用户邮箱里邮件或发送手机短信,这样安全性更高。

④在建数据库表时,登录表s_login只有简简单单的用户名和密码等信息,太少了,导致个人中心没办法做,因为有关用户的可以回显的数据实在太少了(只有账号和密码),而且账号和密码也没必要回显,其实当时在建表时可以多增添一些管理员可以选择输入的信息(比如头像、性别、年龄、个性签名等信息),这样可以把个人中心这一功能做出来,还可以修改头像什么的。

⑤即使笔者已经尽可能地美化前端页面,但感觉前端页面还是不太美观。另外可能引入的elementui版本太低,导致现行版本的elementui组件库中的部分icon图标无法使用。

以上是笔者回溯整个项目后,对本次课程设计的项目一个自我点评,项目有不少创新点,也有一些不足的地方,但任务指导书上所有的基本功能均已实现。之后有心有想法后再实现更多拓展功能和创新点,再完善不足之处。

六、小结与心得体会

本次课程设计,笔者通过vue+ssm技术实现了简单的前后端分离项目——学生成绩管理系统。此系统虽是较为传统的老牌项目,但项目实现意义不小,另外能很好地锻炼学生对于数据库数据的增删改查业务逻辑实现的思维,能很好地巩固所学的ssm框架知识,能很好地体会前后端分离的好处。也能使学生体会到前后端框架对比于直接写javascript、jsp、jquery、servlet等要方便很多,感受到使用框架的便捷之处。

可以看出,笔者在本次课程设计过程中实现了一些任务指导书上没有强制要求的拓展功能,笔者认为任务指导书只是给学生们一个实现大致功能的思路,更重要地是要求学生站在用户的角度去思考怎样完善更新系统。而本系统也有一些不完备的地方,这也是搞项目常有的事情,项目是不断打磨出来的。课程设计的结束不代表项目的结束,以后想法常有则系统常更新。

另外,之前笔者热衷于通过看网课视频实现demo来巩固新知识点的学习,但往往发现成效甚微,在跟着老师一步步做项目的过程中,当时对于某个知识点可能是明明白白的,但项目做完两周后,可能对于项目又感到十分生疏,这说明项目中使用到的知识点自己并没有掌握。而此次课程设计完全是由自己独立完成,我认识到只有自己一行一行敲出来的代码,知识点才能在脑中停留较长的时间,一味地热衷于跟随网课老师的脚步,反而学得快忘得快,自己敲代码搞项目可能过程漫长,但这个过程是发现问题纠正问题的过程,这也是快速进步的过程,所以之后对于新技术的学习,会更多地将所学转换到做项目解决实际问题当中,demo尽力自己完成,实在不能理解,没有思路的地方可以查看类似源码学习,源码还看不懂的可以借助网课理解。

在此次课程设计中,笔者确实对于vue和ssm框架的使用更加熟练了,对于框架实现的底层逻辑有稍许了解,但仍不够深入,笔者认为框架的学习应该深入,把ssm框架可以当作一个模板框架去学习,就业之后,公司可能有自己的技术框架,在十分熟练掌握ssm框架的底层逻辑的前提下,迁移到学习公司框架时才能更加游刃有余。

以上为本次课程设计的总结和心得体会,落笔有感,以后勤思考多练手,多看重动手实践能力,牢记:纸上得来终觉浅,绝知此事要躬行。

猜你喜欢

转载自blog.csdn.net/qq_54162207/article/details/125219175