SpringBoot项目Excel模板下载功能详解

项目背景

"知分"系统是一款基于SpringBoot和Vue架构开发的B/S模式在线成绩管理平台,旨在解决传统成绩通知方式(如纸质成绩单、家长群通知)存在的效率低下、易出错、查询不便且不易保存等问题。

该系统作为连接家长与教师的数字化桥梁,具有以下核心价值:

  • 对于家长:通过微信小程序即可随时随地、安全便捷地查询子女的最新考试成绩与排名,促进家校沟通
  • 对于教师/学校:提供高效的Excel一键式成绩录入工具,自动完成分数统计与排名分析,通过可视化图表直观展示教学成果,极大减轻成绩管理工作负担

该系统实现了成绩管理的数字化、自动化与智能化,显著提升了家校互动的体验与效率。

本文将重点讲解成绩录入功能中Excel模板下载的实现细节。

功能展示

点击"录入成绩"按钮后会出现弹窗:

该功能支持上传xls和xlsx文件,并提供模板下载功能。模板会自动加载对应班级学生和考试名称,教师只需填写各科分数即可。

前端实现详解

1. 按钮触发逻辑

前端页面使用Element UI组件,点击"录入成绩"按钮触发相应事件:

html 复制代码
<div class="card-actions">
  <el-button 
    type="primary" 
    size="small" 
    @click="handleImportScore(classItem.id)"
    class="action-btn"
  >
    <i class="el-icon-upload"></i>
    录入成绩
  </el-button>
  <el-button 
    type="default" 
    size="small" 
    @click="handleViewDetails(classItem.id)"
    class="action-btn"
  >
    <i class="el-icon-view"></i>
    查看详情
  </el-button>
</div>

点击"录入成绩"按钮后,会触发handleImportScore方法并将班级ID作为参数传递:

javascript 复制代码
// 处理导入成绩
handleImportScore(classId) {
  this.currentClassId = classId
  this.scoreDialogVisible = true
  this.examName = ''
  this.fileList = []
  this.selectedFile = null
}

此方法主要完成以下工作:

  • 设置当前班级ID
  • 打开成绩录入弹窗
  • 重置相关表单数据

2. 模板下载功能

弹窗中的模板下载区域代码如下:

html 复制代码
<div class="upload-tips">
  <el-button 
    type="primary" 
    size="small" 
    @click="downloadExcelTemplate"
    class="download-template-btn"
  >
    <i class="el-icon-download"></i>
    点击下载样本
  </el-button>
  <p style="margin-top: 10px; color: #909399; font-size: 12px;">
    下载后填写分数,再上传即可
  </p>
</div>

点击下载按钮后,触发downloadExcelTemplate方法:

javascript 复制代码
// 下载Excel模板
downloadExcelTemplate() {
  // 条件校验
  if (!this.currentClassId) {
    this.$message.error('未选择班级')
    return
  }
  
  if (!this.selectedExamId) {
    this.$message.warning('请先选择考试')
    return
  }
  
  try {
    // 显示加载提示
    this.$message({
      message: '正在生成模板,请稍候',
      type: 'info',
      duration: 0
    })
    
    // 获取班级和考试信息用于构建文件名
    const exam = this.homeData.exams.find(e => e.id === this.selectedExamId)
    const className = this.homeData.classes.find(c => c.id === this.currentClassId)?.name || '未知班级'
    const examName = exam?.name || '未知考试'
    
    // 设置token到请求头,防止被权限拦截
    const token = this.$store.state.user.token
    if (!token) {
      this.$message.warning('登录状态失效,请重新登录')
      setTimeout(() => {
        this.$router.push('/login')
      }, 1000)
      return
    }
    
    // 使用XHR方式下载文件,确保携带token
    const xhr = new XMLHttpRequest()
    const requestUrl = `/teacher/home/downloadTemplate/${this.currentClassId}/${this.selectedExamId}`
    xhr.open('GET', requestUrl, true)
    xhr.setRequestHeader('Authorization', 'Bearer ' + token)
    xhr.responseType = 'blob'
    
    // 传递className和examName到回调函数中
    const downloadContext = {
      className: className,
      examName: examName,
      component: this
    }
    
    xhr.onload = function() {
      // 处理返回结果
      if (xhr.status === 200) {
        // 关闭所有消息提示
        downloadContext.component.$message.closeAll()
        downloadContext.component.$message.success('模板下载成功')
        
        try {
          // 设置文件名
          let finalFileName = `${downloadContext.className}成绩导入表${downloadContext.examName}.xlsx`
          const contentDisposition = xhr.getResponseHeader('Content-Disposition')
          
          // 根据响应类型设置正确的MIME类型
          let mimeType = 'application/octet-stream'
          const contentType = xhr.getResponseHeader('Content-Type')
          if (contentType) {
            mimeType = contentType
          }
          
          // 创建下载链接
          const blob = new Blob([xhr.response], { type: mimeType })
          const url = URL.createObjectURL(blob)
          
          // 创建a标签并模拟点击下载
          const downloadLink = document.createElement('a')
          downloadLink.href = url
          downloadLink.setAttribute('download', finalFileName)
          downloadLink.style.display = 'none'
          document.body.appendChild(downloadLink)
          downloadLink.click()
          
          // 延迟移除,确保点击事件完成
          setTimeout(() => {
            document.body.removeChild(downloadLink)
            URL.revokeObjectURL(url)
          }, 100)
        } catch (downloadError) {
          console.error('下载文件处理失败:', downloadError)
          downloadContext.component.$message.error('文件下载处理失败')
        }
      } else {
        console.error('请求失败,状态码:', xhr.status)
        downloadContext.component.$message.closeAll()
        downloadContext.component.$message.error('生成模板失败:请稍后重试')
      }
    }
    
    xhr.onerror = function(e) {
      console.error('网络错误:', e)
      this.$message.closeAll()
      this.$message.error('生成模板失败:网络错误')
    }
    
    xhr.send()
  } catch (error) {
    console.error('生成模板失败:', error)
    this.$message.closeAll()
    this.$message.error('生成模板失败:' + (error.message || '请稍后重试'))
  }
}

该方法的关键实现点包括:

  1. 进行前置条件校验(班级和考试选择)
  2. 获取token并设置到请求头中
  3. 使用XMLHttpRequest发起请求,设置responseType为blob
  4. 处理响应并创建下载链接
  5. 添加异常处理机制,确保用户体验

后端实现详解

1. Controller层实现

后端使用SpringBoot框架,Controller层代码如下:

java 复制代码
package com.eduscore.controller;

import com.eduscore.domain.Student;
import com.eduscore.service.ITeacherHomeService;
import com.eduscore.common.core.controller.BaseController;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/teacher/home")
public class TeacherHomeController extends BaseController {

    @Autowired
    private ITeacherHomeService teacherHomeService;
    
    /​**​
     * 下载Excel成绩导入模板
     */
    @GetMapping("/downloadTemplate/{classId}/{examId}")
    public void downloadExcelTemplate(@PathVariable Integer classId, 
                                     @PathVariable Integer examId, 
                                     HttpServletResponse response) {
        try {
            // 设置响应头
            response.setContentType("text/csv;charset=utf-8");
            response.setHeader("Content-Disposition", "attachment;filename=" + 
                URLEncoder.encode("成绩导入模板.csv", "UTF-8"));
            
            // 获取班级学生列表
            List<Student> students = teacherHomeService.getClassStudents(classId);
            // 获取班级考试信息
            Map<String, Object> examInfo = teacherHomeService.getClassExamInfo(classId, examId);
            // 获取考试科目
            List<String> subjectNames = (List<String>) examInfo.get("subjectNames");
            
            // 生成CSV内容
            StringBuilder csvContent = new StringBuilder();
            // 添加标题行
            csvContent.append("学生姓名,班级");
            for (String subject : subjectNames) {
                csvContent.append(",").append(subject);
            }
            csvContent.append(",").append("考试名称");
            csvContent.append("\n");
            
            // 添加学生数据行
            String className = (String) examInfo.get("className");
            String examName = (String) examInfo.get("examName");
            for (Student student : students) {
                csvContent.append(student.getName()).append(",").append(className);
                // 为每个科目添加空成绩列
                for (int i = 0; i < subjectNames.size(); i++) {
                    csvContent.append(",");
                }
                csvContent.append(",").append(examName);
                csvContent.append("\n");
            }
            
            // 输出CSV文件
            try (OutputStream out = response.getOutputStream()) {
                // 添加BOM头,确保Excel能正确识别UTF-8编码
                out.write(0xEF);
                out.write(0xBB);
                out.write(0xBF);
                out.write(csvContent.toString().getBytes(StandardCharsets.UTF_8));
                out.flush();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

2. Service层实现

Service层负责业务逻辑处理:

java 复制代码
package com.eduscore.service.impl;

import com.eduscore.domain.*;
import com.eduscore.mapper.*;
import com.eduscore.service.ITeacherHomeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.HashMap;

@Service
public class TeacherHomeServiceImpl implements ITeacherHomeService {
    @Autowired
    private TeacherMapper teacherMapper;

    @Autowired
    private SchoolMapper schoolMapper;

    @Autowired
    private StudentMapper studentMapper;

    @Autowired
    private ClassMapper classMapper;

    @Autowired
    private ExamMapper examMapper;

    @Autowired
    private ScoreMapper scoreMapper;

    @Autowired
    private SubjectMapper subjectMapper;
  
    @Override
    public Map<String, Object> getClassExamInfo(Integer classId, Integer examId) {
        // 获取班级信息
        Class clazz = classMapper.selectClassById(classId);
        // 获取考试信息
        Exam exam = examMapper.selectExamById(examId);
        // 获取该班级该考试的所有科目
        List<Subject> subjects = subjectMapper.selectSubjectList(new Subject());
        
        Map<String, Object> result = new HashMap<>();
        result.put("className", clazz != null ? clazz.getName() : "");
        result.put("examName", exam != null ? exam.getName() : "");
        
        List<String> subjectNames = subjects.stream()
                .map(Subject::getName)
                .collect(Collectors.toList());
        result.put("subjectNames", subjectNames);
        
        return result;
    }
}

实现原理总结

  1. 前端触发:用户点击下载模板按钮,前端进行条件校验并携带认证信息发起请求
  2. 后端处理:Controller接收请求,调用Service层获取班级、考试和科目信息
  3. 模板生成:后端动态生成CSV格式的模板文件,包含学生列表和科目信息
  4. 文件返回:设置正确的响应头和信息,将文件流返回给前端
  5. 前端下载:前端接收文件流,创建下载链接并触发浏览器下载

生成的模板示例

通过以上实现,教师可以下载预先填充了班级学生信息和考试科目的模板,只需填写分数即可完成成绩录入,极大提高了工作效率。

下一篇文章将详细讲解成绩录入功能的实现细节,包括文件上传、数据解析和批量导入等关键技术点。

相关推荐
程序员蜗牛2 小时前
你写代码会复用公共SQL么?
后端
猿究院-陆昱泽2 小时前
Redis 主从同步:原理、配置与实战优化
redis·后端·java-ee·intellij-idea
std860212 小时前
JavaScript性能优化实战技术文章大纲
java
老葱头蒸鸡3 小时前
(23)ASP.NET Core2.2 EF关系数据库建模
后端·asp.net
他们叫我技术总监3 小时前
帆软Report11多语言开发避坑:法语特殊引号导致SQL报错的解决方案
java·数据库·sql
程序员三明治3 小时前
二分查找思路详解,包含二分算法的变种,针对不同题的做法
java·数据结构·算法·二分查找
枣伊吕波3 小时前
五十三、bean的管理-bean的获取、bean的作用域、第三方bean
java·开发语言
xiaoningaijishu3 小时前
MATLAB中的Excel文件操作:从入门到精通
其他·算法·matlab·excel
啦工作呢3 小时前
Sass:CSS 预处理器
开发语言·后端·rust