当前位置: 首页 > news >正文

SpringBoot项目Excel成绩录入功能详解:从文件上传到数据入库的全流程解析

功能概述

本文接上文Excel模板下载功能,详细讲解成绩录入的完整流程。教师下载模板并填写成绩后,可通过上传功能将Excel中的数据批量导入系统,实现高效的成绩管理。

前端实现解析

1. 文件上传组件

前端使用Element UI的上传组件实现文件选择功能:

<el-uploadclass="upload-demo"dragaction="#":auto-upload="false":on-change="handleFileChange":on-remove="handleFileRemove":file-list="fileList"accept=".xlsx,.xls"
><i class="el-icon-upload"></i><div class="el-upload__text">将Excel文件拖到此处,或<em>点击上传</em></div><div class="el-upload__tip" slot="tip">请上传xlsx/xls格式的Excel文件,且不超过10MB</div>
</el-upload>

组件参数说明​:

  • drag: 启用拖拽上传功能
  • auto-upload="false": 禁止自动上传,需要手动触发
  • on-change: 文件选择变化时的回调函数
  • accept: 限制只能选择Excel文件

2. 文件处理逻辑

当用户选择文件后,触发handleFileChange函数:

// 处理文件选择
async handleFileChange(file) {// 文件类型校验 - 只允许Excel格式const isExcel = file.name.endsWith('.xlsx') || file.name.endsWith('.xls')if (!isExcel) {this.$message.error('只能上传Excel文件!')return false}// 文件大小校验 - 不超过10MBconst isLt10M = file.size / 1024 / 1024 < 10if (!isLt10M) {this.$message.error('文件大小不能超过10MB!')return false}this.selectedFile = file// 解析Excel文件获取考试名称try {const workbook = await this.readExcelFile(file.raw)const firstSheetName = workbook.SheetNames[0]const worksheet = workbook.Sheets[firstSheetName]// 获取第一行标题行,找到考试名称列const range = XLSX.utils.decode_range(worksheet['!ref'])let examNameColumn = -1// 查找考试名称列for (let C = range.s.c; C <= range.e.c; C++) {const cellAddress = { c: C, r: 0 }const cellRef = XLSX.utils.encode_cell(cellAddress)if (worksheet[cellRef] && worksheet[cellRef].v === '考试名称') {examNameColumn = Cbreak}}// 如果找到了考试名称列,获取第一行数据中的考试名称if (examNameColumn >= 0) {// 取第二行(数据行的第一行)的考试名称const examNameCellAddress = { c: examNameColumn, r: 1 }const examNameCellRef = XLSX.utils.encode_cell(examNameCellAddress)if (worksheet[examNameCellRef]) {const templateExamName = worksheet[examNameCellRef].v// 查找对应的examIdconst matchingExam = this.homeData.exams.find(e => e.name === templateExamName)if (matchingExam) {this.selectedExamId = matchingExam.idthis.isExamDisabled = true // 禁用考试选择器this.$message.success(`已自动设置考试: ${templateExamName}`)} else {this.$message.warning(`模板中的考试名称 "${templateExamName}" 不存在,请手动选择考试`)}}}} catch (error) {console.error('解析Excel文件失败:', error)this.$message.error('解析Excel文件失败,请检查文件格式')}return true
}

效果展示:

代码解析​:

  1. 文件验证​:检查文件类型和大小,确保符合要求
  2. Excel解析​:使用XLSX库读取Excel文件内容
  3. 自动识别考试​:从Excel中提取考试名称并自动匹配系统内的考试
  4. 异常处理​:捕获解析过程中可能出现的错误

3. Excel文件读取工具函数

// 读取Excel文件
readExcelFile(file) {return new Promise((resolve, reject) => {const reader = new FileReader()reader.onload = e => {try {const data = new Uint8Array(e.target.result)const workbook = XLSX.read(data, { type: 'array' })resolve(workbook)} catch (error) {reject(error)}}reader.onerror = rejectreader.readAsArrayBuffer(file)})
}

功能说明​:使用FileReader API将文件读取为ArrayBuffer,然后通过XLSX库解析为workbook对象。

4. 提交成绩数据

用户点击提交按钮后,触发成绩上传流程:

<div slot="footer" class="dialog-footer"><el-button @click="scoreDialogVisible = false">取消</el-button><el-button type="primary" @click="handleSubmitScore" :loading="uploadLoading">{{ uploadLoading ? '上传中...' : '提交' }}</el-button>
</div>

对应的处理函数:

// 提交成绩
async handleSubmitScore() {if (!this.selectedFile) {this.$message.warning('请选择Excel文件')return}if (!this.selectedExamId) {this.$message.warning('请选择考试')return}if (!this.currentClassId) {this.$message.error('未选择班级')return}try {await this.$confirm(`确定要导入 ${this.selectedFile.name} 的成绩数据吗?`, '确认导入', {confirmButtonText: '确定',cancelButtonText: '取消',type: 'warning'})this.uploadLoading = true// 根据selectedExamId获取考试名称const exam = this.homeData.exams.find(e => e.id === this.selectedExamId)const formData = new FormData()formData.append('file', this.selectedFile.raw)formData.append('examName', exam ? exam.name : '')formData.append('examId', this.selectedExamId)formData.append('classId', this.currentClassId)formData.append('teacherId', this.teacherId)await uploadScores(formData)this.$message.success('成绩导入成功!')this.scoreDialogVisible = false// 完全刷新页面,而不仅仅是重新加载数据window.location.reload()} catch (error) {if (error !== 'cancel') {console.error('导入失败:', error)this.$message.error('成绩导入失败:' + (error.message || '请检查文件格式'))}} finally {this.uploadLoading = false}
}

流程说明​:

  1. 参数校验​:确保文件、考试和班级信息已填写
  2. 用户确认​:弹出确认对话框,防止误操作
  3. 构建表单数据​:将文件和相关参数封装为FormData
  4. 调用API​:发送上传请求
  5. 处理结果​:成功则刷新页面,失败则提示错误信息

5. API请求封装

// 上传成绩文件
export function uploadScores(data) {return request({url: '/teacher/score/upload',method: 'post',data: data,headers: {'Content-Type': 'multipart/form-data'}})
}

后端实现解析

1. Controller层 - 接收上传请求

package com.eduscore.controller;import com.eduscore.common.core.controller.BaseController;
import com.eduscore.common.core.domain.AjaxResult;
import com.eduscore.domain.Exam;
import com.eduscore.domain.Score;
import com.eduscore.domain.Student;
import com.eduscore.domain.Subject;
import com.eduscore.mapper.ExamMapper;
import com.eduscore.mapper.ScoreMapper;
import com.eduscore.mapper.StudentMapper;
import com.eduscore.mapper.SubjectMapper;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;import java.io.InputStream;
import java.util.*;
import java.util.stream.Collectors;@RestController
@RequestMapping("/teacher/score")
public class ScoreController extends BaseController {@Autowiredprivate ScoreMapper scoreMapper;@Autowiredprivate StudentMapper studentMapper;@Autowiredprivate SubjectMapper subjectMapper;@Autowiredprivate ExamMapper examMapper;/​**​* 上传成绩文件*/@PostMapping("/upload")public AjaxResult uploadScoreFile(@RequestParam("file") MultipartFile file,@RequestParam("examId") Integer examId,@RequestParam("classId") Integer classId,@RequestParam("teacherId") Integer teacherId) {try {// 验证文件if (file.isEmpty()) {return AjaxResult.error("上传文件不能为空");}// 获取文件类型String fileName = file.getOriginalFilename();if (fileName == null || (!fileName.endsWith(".xlsx") && !fileName.endsWith(".xls"))) {return AjaxResult.error("请上传Excel格式的文件");}// 解析Excel文件List<Score> scoreList = parseExcelFile(file.getInputStream(), examId, classId);// 批量插入成绩数据if (!scoreList.isEmpty()) {// 先删除该考试该班级的所有成绩记录scoreMapper.deleteScoresByExamAndClass(examId, classId);// 批量插入新成绩scoreMapper.batchInsertScores(scoreList);}return AjaxResult.success("成绩导入成功");} catch (Exception e) {e.printStackTrace();return AjaxResult.error("成绩导入失败:" + e.getMessage());}}// 其他方法将在下面详细解析...
}

代码解析​:

  • @PostMapping("/upload"): 定义POST请求接口
  • @RequestParam: 接收前端传递的参数
  • 文件验证:检查文件是否为空和格式是否正确
  • 业务逻辑:解析Excel并处理成绩数据

2. Excel解析核心方法

/​**​* 解析Excel文件,提取成绩数据*/
private List<Score> parseExcelFile(InputStream inputStream, Integer examId, Integer classId) throws Exception {List<Score> scoreList = new ArrayList<>();try (Workbook workbook = new XSSFWorkbook(inputStream)) {Sheet sheet = workbook.getSheetAt(0);if (sheet == null) {throw new Exception("Excel文件中没有工作表");}// 获取第一行作为标题行Row titleRow = sheet.getRow(0);if (titleRow == null) {throw new Exception("Excel文件中没有标题行");}// 确定各列的索引Map<String, Integer> columnIndexMap = new HashMap<>();for (int i = 0; i < titleRow.getLastCellNum(); i++) {Cell cell = titleRow.getCell(i);if (cell != null) {String columnName = cell.getStringCellValue().trim();columnIndexMap.put(columnName, i);}}// 验证必要的列是否存在if (!columnIndexMap.containsKey("学生姓名") || !columnIndexMap.containsKey("班级")) {throw new Exception("Excel文件缺少必要的列:学生姓名、班级");}// 获取所有科目列表List<Subject> allSubjects = subjectMapper.selectSubjectList(new Subject());Map<String, Integer> subjectNameToIdMap = allSubjects.stream().collect(Collectors.toMap(Subject::getName, Subject::getId));// 获取该班级的所有学生List<Student> students = studentMapper.selectStudentsByClass(classId);Map<String, Integer> studentNameToIdMap = students.stream().collect(Collectors.toMap(Student::getName, Student::getId));// 解析数据行int rowNum = 0;// 记录每个学生的总分Map<Integer, Float> studentTotalScores = new HashMap<>();// 记录每个学生的科目成绩Map<Integer, Map<Integer, Float>> studentSubjectScores = new HashMap<>();for (Row row : sheet) {rowNum++;// 跳过标题行if (rowNum == 1) {continue;}try {// 获取学生姓名Cell studentNameCell = row.getCell(columnIndexMap.get("学生姓名"));if (studentNameCell == null) {continue;}String studentName = getStringCellValue(studentNameCell).trim();if (studentName.isEmpty()) {continue;}// 获取学生IDInteger studentId = studentNameToIdMap.get(studentName);if (studentId == null) {throw new Exception("未找到学生:" + studentName);}// 初始化学生的科目成绩记录studentSubjectScores.putIfAbsent(studentId, new HashMap<>());// 遍历所有可能的科目列for (Map.Entry<String, Integer> entry : columnIndexMap.entrySet()) {String columnName = entry.getKey();Integer columnIndex = entry.getValue();// 跳过非科目列if (columnName.equals("学生姓名") || columnName.equals("班级") || columnName.equals("考试名称")) {continue;}// 检查是否为科目列if (subjectNameToIdMap.containsKey(columnName)) {Integer subjectId = subjectNameToIdMap.get(columnName);Cell scoreCell = row.getCell(columnIndex);Float scoreValue = getFloatCellValue(scoreCell);if (scoreValue != null) {studentSubjectScores.get(studentId).put(subjectId, scoreValue);// 累加总分studentTotalScores.put(studentId, studentTotalScores.getOrDefault(studentId, 0f) + scoreValue);}}}} catch (Exception e) {throw new Exception("第" + rowNum + "行数据解析错误:" + e.getMessage());}}// 构建Score对象列表for (Map.Entry<Integer, Map<Integer, Float>> studentEntry : studentSubjectScores.entrySet()) {Integer studentId = studentEntry.getKey();Float totalScore = studentTotalScores.getOrDefault(studentId, 0f);for (Map.Entry<Integer, Float> subjectEntry : studentEntry.getValue().entrySet()) {Integer subjectId = subjectEntry.getKey();Float scoreValue = subjectEntry.getValue();Score score = new Score();score.setExamId(examId);score.setStudentId(studentId);score.setSubjectId(subjectId);score.setScore(scoreValue);score.setTotalScore(totalScore);scoreList.add(score);}}}return scoreList;
}

解析流程​:

  1. 读取Excel文件​:使用Apache POI库解析Excel
  2. 解析标题行​:确定各数据列的索引位置
  3. 数据验证​:检查必要列是否存在
  4. 获取映射关系​:建立科目名称到ID、学生姓名到ID的映射
  5. 遍历数据行​:逐行解析成绩数据
  6. 计算总分​:累加每个学生的各科成绩
  7. 构建成绩对象​:创建Score对象并添加到结果列表

3. 工具方法

/​**​* 获取单元格的字符串值*/
private String getStringCellValue(Cell cell) {if (cell == null) {return "";}switch (cell.getCellType()) {case STRING:return cell.getStringCellValue();case NUMERIC:// 如果是整数,格式化为整数字符串if (Math.floor(cell.getNumericCellValue()) == cell.getNumericCellValue()) {return String.valueOf((long) cell.getNumericCellValue());}return String.valueOf(cell.getNumericCellValue());case BOOLEAN:return String.valueOf(cell.getBooleanCellValue());default:return "";}
}/​**​* 获取单元格的浮点数值*/
private Float getFloatCellValue(Cell cell) {if (cell == null) {return null;}try {switch (cell.getCellType()) {case NUMERIC:return (float) cell.getNumericCellValue();case STRING:String value = cell.getStringCellValue().trim();if (!value.isEmpty()) {return Float.parseFloat(value);}return null;default:return null;}} catch (NumberFormatException e) {return null;}
}

功能说明​:这两个工具方法负责处理Excel单元格数据转换,确保不同类型的数据都能正确转换为需要的格式。

数据库操作解析

1. 先删除后插入策略

// 先删除该考试该班级的所有成绩记录
scoreMapper.deleteScoresByExamAndClass(examId, classId);
// 批量插入新成绩
scoreMapper.batchInsertScores(scoreList);

设计思路​:采用"先删除后插入"的策略,确保数据的唯一性和一致性。这种方式避免了复杂的数据更新逻辑,简化了实现过程。

2. MyBatis批量插入

通常在Mapper中使用批量插入来提高性能:

<insert id="batchInsertScores" parameterType="java.util.List">INSERT INTO score (exam_id, student_id, subject_id, score, total_score)VALUES<foreach collection="list" item="item" separator=",">(#{item.examId}, #{item.studentId}, #{item.subjectId}, #{item.score}, #{item.totalScore})</foreach>
</insert>

实现原理总结

  1. 前端上传​:用户选择Excel文件,前端进行基本验证
  2. 文件解析​:前端初步解析文件内容,自动识别考试信息
  3. 数据提交​:将文件和相关参数封装为FormData提交到后端
  4. 后端处理​:接收文件流,使用POI库解析Excel内容
  5. 数据转换​:将Excel中的学生姓名、科目名称转换为对应的数据库ID
  6. 数据存储​:先删除原有成绩,再批量插入新成绩
  7. 结果反馈​:向前端返回操作结果

技术亮点

  1. 自动识别机制​:系统能够自动从Excel中识别考试信息,减少用户操作
  2. 数据验证​:多层数据验证确保数据的完整性和准确性
  3. 批量处理​:使用批量插入提高数据库操作性能
  4. 异常处理​:完善的异常处理机制,提供友好的错误提示
  5. 用户体验​:提供进度提示和结果反馈,增强用户体验

本文详细解析了成绩录入功能的实现原理和代码细节,希望能帮助读者理解整个流程,并在实际开发中借鉴相关技术方案。

http://www.dtcms.com/a/415504.html

相关文章:

  • Excel批量导入到数据库的方法
  • 开发网站定制珠海柏泰教育官方网站建设
  • 出版物级标记语言解析
  • 集合划分:用元素交换法破解等和难题
  • 培训建设网站深圳建站公司
  • 网站规划与建设规划书wordpress个性时光轴主题
  • 个人网站备案幕布网易梦幻西游手游官方网站下载
  • 数据集 - Al-Maktabah-Al-Shamilah (伊斯兰典籍全集)
  • uC/OS-III 队列(Queue)操作
  • 速度即排名:90分以下=谷歌流量流失?
  • 企业网站空间选择什么网站可以找人做设计
  • 网站建设如何快速增加用户中山企业网站建设公司
  • 工业4.0下的边缘存储设计:数据就地处理,响应更快更安全
  • 做情诗网站私有云 搭建wordpress
  • 上海定制app开发公司杭州seo关键词优化公司
  • Photoshop - Photoshop 根据需要以最佳格式保存照片
  • 11-Redis 集合类型深度指南:从去重特性到集合运算场景落地
  • 【Redis】超级超市的仓库管理系统
  • 个人网站建设模板视频链接生成器在线
  • 网站建设 推广薪资公司网站开发工具
  • 深圳seo网站推广报价电器网站建设策划书
  • 做360网站优化快wordpress5.1下载
  • 深度学习复现:CIFAR-10 数据集任务的实现(测试集)
  • 【Spring 1】Spring IoC:颠覆传统编程的控制反转艺术
  • 如何为网站做面包屑导航网站必须要备案吗
  • AI 动画视频创作:技巧升级与行业未来趋势
  • 数字化转型:概念性名词浅谈(第五十三讲)
  • 制作网站参考案例wordpress推介联盟
  • 当遇到人生低谷期,该怎么度过?别装坚强,熬过去才是真本事
  • 电商网站开发报价单濮阳网站建设陈帅