|
@@ -2,24 +2,44 @@ package com.njuzr.eaibackend.service.impl;
|
|
|
|
|
|
|
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
|
|
import com.njuzr.eaibackend.dto.ClassDTO;
|
|
import com.njuzr.eaibackend.dto.ClassDTO;
|
|
|
|
|
+import com.njuzr.eaibackend.enums.ClassStudentState;
|
|
|
import com.njuzr.eaibackend.exception.MyException;
|
|
import com.njuzr.eaibackend.exception.MyException;
|
|
|
import com.njuzr.eaibackend.mapper.ClassMapper;
|
|
import com.njuzr.eaibackend.mapper.ClassMapper;
|
|
|
|
|
+import com.njuzr.eaibackend.mapper.ClassStudentMapper;
|
|
|
|
|
+import com.njuzr.eaibackend.mapper.UserMapper;
|
|
|
import com.njuzr.eaibackend.po.Class;
|
|
import com.njuzr.eaibackend.po.Class;
|
|
|
|
|
+import com.njuzr.eaibackend.po.ClassStudent;
|
|
|
|
|
+import com.njuzr.eaibackend.po.User;
|
|
|
import com.njuzr.eaibackend.service.ClassService;
|
|
import com.njuzr.eaibackend.service.ClassService;
|
|
|
|
|
+import com.njuzr.eaibackend.vo.StudentInfoVO;
|
|
|
|
|
+import com.opencsv.CSVReader;
|
|
|
|
|
+import com.opencsv.exceptions.CsvValidationException;
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
|
|
+import org.apache.poi.ss.usermodel.*;
|
|
|
|
|
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
|
|
import org.springframework.http.HttpStatus;
|
|
import org.springframework.http.HttpStatus;
|
|
|
import org.springframework.stereotype.Service;
|
|
import org.springframework.stereotype.Service;
|
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
|
|
|
+import org.springframework.web.multipart.MultipartFile;
|
|
|
|
|
|
|
|
|
|
+import java.io.IOException;
|
|
|
|
|
+import java.io.InputStreamReader;
|
|
|
|
|
+import java.math.BigDecimal;
|
|
|
|
|
+import java.util.ArrayList;
|
|
|
import java.util.List;
|
|
import java.util.List;
|
|
|
|
|
+import java.util.stream.Collectors;
|
|
|
|
|
|
|
|
@Slf4j
|
|
@Slf4j
|
|
|
@Service
|
|
@Service
|
|
|
public class ClassServiceImpl implements ClassService {
|
|
public class ClassServiceImpl implements ClassService {
|
|
|
private final ClassMapper classMapper;
|
|
private final ClassMapper classMapper;
|
|
|
|
|
+ private final ClassStudentMapper classStudentMapper;
|
|
|
|
|
+ private final UserMapper userMapper;
|
|
|
|
|
|
|
|
- public ClassServiceImpl(ClassMapper classMapper) {
|
|
|
|
|
|
|
+ public ClassServiceImpl(ClassMapper classMapper, ClassStudentMapper classStudentMapper, UserMapper userMapper) {
|
|
|
this.classMapper = classMapper;
|
|
this.classMapper = classMapper;
|
|
|
|
|
+ this.classStudentMapper = classStudentMapper;
|
|
|
|
|
+ this.userMapper = userMapper;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
@Override
|
|
@@ -92,5 +112,313 @@ public class ClassServiceImpl implements ClassService {
|
|
|
return "删除成功";
|
|
return "删除成功";
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ @Override
|
|
|
|
|
+ @Transactional
|
|
|
|
|
+ public String batchAddStudents(Long classId, MultipartFile file) {
|
|
|
|
|
+ // 检查班级是否存在
|
|
|
|
|
+ Class cls = classMapper.selectById(classId);
|
|
|
|
|
+ if (cls == null) {
|
|
|
|
|
+ throw new MyException(404, "班级不存在");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 解析文件
|
|
|
|
|
+ List<ClassStudent> students;
|
|
|
|
|
+ String fileName = file.getOriginalFilename();
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (fileName != null && (fileName.endsWith(".xls") || fileName.endsWith(".xlsx"))) {
|
|
|
|
|
+ students = parseExcelFile(file, classId);
|
|
|
|
|
+ } else if (fileName != null && fileName.endsWith(".csv")) {
|
|
|
|
|
+ students = parseCsvFile(file, classId);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ throw new MyException(400, "不支持的文件类型");
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("解析学生文件失败", e);
|
|
|
|
|
+ throw new MyException(500, "解析学生文件失败: " + e.getMessage());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (students.isEmpty()) {
|
|
|
|
|
+ return "未找到有效学生数据";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 批量插入学生
|
|
|
|
|
+ int insertedCount = classStudentMapper.batchInsert(students);
|
|
|
|
|
+
|
|
|
|
|
+ // 更新班级人数
|
|
|
|
|
+ classMapper.increaseStuNumber(classId, insertedCount);
|
|
|
|
|
+
|
|
|
|
|
+ return "成功添加 " + insertedCount + " 名学生";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ @Transactional
|
|
|
|
|
+ public String addStudentByOfficialNumber(Long classId, String officialNumber) {
|
|
|
|
|
+ // 1. 检查班级是否存在
|
|
|
|
|
+ Class cls = classMapper.selectById(classId);
|
|
|
|
|
+ if (cls == null) {
|
|
|
|
|
+ throw new MyException(404, "班级不存在");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 根据学号查询用户
|
|
|
|
|
+ QueryWrapper<User> userWrapper = new QueryWrapper<>();
|
|
|
|
|
+ userWrapper.eq("official_number", officialNumber);
|
|
|
|
|
+ User user = userMapper.selectOne(userWrapper);
|
|
|
|
|
+
|
|
|
|
|
+ if (user == null) {
|
|
|
|
|
+ throw new MyException(404, "未找到学号为 " + officialNumber + " 的学生");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 检查是否已在班级中
|
|
|
|
|
+ QueryWrapper<ClassStudent> existWrapper = new QueryWrapper<>();
|
|
|
|
|
+ existWrapper.eq("class_id", classId)
|
|
|
|
|
+ .eq("official_number", officialNumber);
|
|
|
|
|
+
|
|
|
|
|
+ if (classStudentMapper.selectCount(existWrapper) > 0) {
|
|
|
|
|
+ throw new MyException(400, "该学生已在班级中");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 4. 创建学生记录
|
|
|
|
|
+ ClassStudent student = new ClassStudent();
|
|
|
|
|
+ student.setClassId(classId);
|
|
|
|
|
+ student.setOfficialNumber(officialNumber);
|
|
|
|
|
+ student.setStuName(user.getName()); // 从用户表获取姓名
|
|
|
|
|
+ student.setState(ClassStudentState.NOT_JOINED); // 默认状态
|
|
|
|
|
+
|
|
|
|
|
+ // 5. 插入记录
|
|
|
|
|
+ int insertResult = classStudentMapper.insert(student);
|
|
|
|
|
+ if (insertResult <= 0) {
|
|
|
|
|
+ throw new MyException(500, "添加学生失败");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 6. 更新班级人数
|
|
|
|
|
+ classMapper.increaseStuNumber(classId, 1);
|
|
|
|
|
+
|
|
|
|
|
+ return "成功添加学生: " + user.getName() + " (" + officialNumber + ")";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
|
|
+ public String batchDeleteStudents(Long classId, List<String> officialNumbers) {
|
|
|
|
|
+ // 1. 检查班级是否存在
|
|
|
|
|
+ Class cls = classMapper.selectById(classId);
|
|
|
|
|
+ if (cls == null) {
|
|
|
|
|
+ throw new MyException(404, "班级不存在");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 验证输入
|
|
|
|
|
+ if (officialNumbers == null || officialNumbers.isEmpty()) {
|
|
|
|
|
+ throw new MyException(400, "学号列表不能为空");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 检查学生是否在班级中
|
|
|
|
|
+ QueryWrapper<ClassStudent> wrapper = new QueryWrapper<>();
|
|
|
|
|
+ wrapper.eq("class_id", classId)
|
|
|
|
|
+ .in("official_number", officialNumbers);
|
|
|
|
|
+
|
|
|
|
|
+ List<ClassStudent> students = classStudentMapper.selectList(wrapper);
|
|
|
|
|
+ if (students.isEmpty()) {
|
|
|
|
|
+ throw new MyException(404, "没有找到匹配的学生");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 4. 获取实际存在的学号列表
|
|
|
|
|
+ List<String> existingNumbers = students.stream()
|
|
|
|
|
+ .map(ClassStudent::getOfficialNumber)
|
|
|
|
|
+ .collect(Collectors.toList());
|
|
|
|
|
+
|
|
|
|
|
+ // 5. 批量删除学生记录
|
|
|
|
|
+ int deleteCount = classStudentMapper.batchDeleteByClassIdAndNumbers(classId, existingNumbers);
|
|
|
|
|
+ if (deleteCount <= 0) {
|
|
|
|
|
+ throw new MyException(500, "删除学生失败");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 6. 更新班级人数
|
|
|
|
|
+ int updateCount = classMapper.decreaseStuNumber(classId, deleteCount);
|
|
|
|
|
+ if (updateCount <= 0) {
|
|
|
|
|
+ throw new MyException(500, "更新班级人数失败");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 7. 处理部分成功的情况
|
|
|
|
|
+ List<String> notFoundNumbers = officialNumbers.stream()
|
|
|
|
|
+ .filter(number -> !existingNumbers.contains(number))
|
|
|
|
|
+ .collect(Collectors.toList());
|
|
|
|
|
+
|
|
|
|
|
+ String successMsg = "成功删除 " + deleteCount + " 名学生";
|
|
|
|
|
+ if (!notFoundNumbers.isEmpty()) {
|
|
|
|
|
+ String notFoundMsg = ",但以下学号未在班级中找到: " + String.join(", ", notFoundNumbers);
|
|
|
|
|
+ successMsg += notFoundMsg;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return successMsg;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public List<StudentInfoVO> getStudentsByClassId(Long classId) {
|
|
|
|
|
+ return classStudentMapper.selectStudentsByClassId(classId);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public Class getClassById(Long classId) {
|
|
|
|
|
+ if (classId == null || classId <= 0) {
|
|
|
|
|
+ log.warn("无效的 classId: {}", classId);
|
|
|
|
|
+ throw new IllegalArgumentException("无效的班级ID");
|
|
|
|
|
+ }
|
|
|
|
|
+ Class courseClass = classMapper.selectById(classId);
|
|
|
|
|
+ return courseClass;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private List<ClassStudent> parseExcelFile(MultipartFile file, Long classId) throws IOException {
|
|
|
|
|
+ List<ClassStudent> students = new ArrayList<>();
|
|
|
|
|
+ Workbook workbook = new XSSFWorkbook(file.getInputStream());
|
|
|
|
|
+ Sheet sheet = workbook.getSheetAt(0);
|
|
|
|
|
+
|
|
|
|
|
+ // 查找列索引
|
|
|
|
|
+ Row headerRow = sheet.getRow(0);
|
|
|
|
|
+ int nameCol = findColumnIndex(headerRow, "学生姓名");
|
|
|
|
|
+ int numberCol = findColumnIndex(headerRow, "学生学号");
|
|
|
|
|
+
|
|
|
|
|
+ if (nameCol == -1 || numberCol == -1) {
|
|
|
|
|
+ throw new MyException(400, "文件缺少必要列:学生姓名 或 学生学号");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 创建数据格式化器
|
|
|
|
|
+ DataFormatter formatter = new DataFormatter();
|
|
|
|
|
+
|
|
|
|
|
+ // 处理数据行
|
|
|
|
|
+ for (int i = 1; i <= sheet.getLastRowNum(); i++) {
|
|
|
|
|
+ Row row = sheet.getRow(i);
|
|
|
|
|
+ if (row == null) continue;
|
|
|
|
|
+
|
|
|
|
|
+ ClassStudent student = new ClassStudent();
|
|
|
|
|
+
|
|
|
|
|
+ // 获取学生姓名
|
|
|
|
|
+ Cell nameCell = row.getCell(nameCol);
|
|
|
|
|
+ student.setStuName(nameCell != null ?
|
|
|
|
|
+ formatter.formatCellValue(nameCell).trim() : "");
|
|
|
|
|
+
|
|
|
|
|
+ // 获取学号(特殊处理大数字)
|
|
|
|
|
+ Cell numberCell = row.getCell(numberCol);
|
|
|
|
|
+ String officialNumber = "";
|
|
|
|
|
+ if (numberCell != null) {
|
|
|
|
|
+ if (numberCell.getCellType() == CellType.NUMERIC) {
|
|
|
|
|
+ // 处理科学计数法
|
|
|
|
|
+ double numericValue = numberCell.getNumericCellValue();
|
|
|
|
|
+ if (String.valueOf(numericValue).contains("E")) {
|
|
|
|
|
+ BigDecimal bigDecimal = BigDecimal.valueOf(numericValue);
|
|
|
|
|
+ officialNumber = bigDecimal.toPlainString();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 避免尾数出现 .0
|
|
|
|
|
+ if (numericValue % 1 == 0) {
|
|
|
|
|
+ officialNumber = String.valueOf((long) numericValue);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ officialNumber = String.valueOf(numericValue);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ officialNumber = formatter.formatCellValue(numberCell).trim();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ student.setOfficialNumber(officialNumber);
|
|
|
|
|
+
|
|
|
|
|
+ student.setClassId(classId);
|
|
|
|
|
+ student.setState(ClassStudentState.NOT_JOINED); // 默认状态
|
|
|
|
|
+
|
|
|
|
|
+ // 验证必要字段
|
|
|
|
|
+ if (student.getStuName().isEmpty() || student.getOfficialNumber().isEmpty()) {
|
|
|
|
|
+ log.warn("跳过无效学生数据行: {}", i + 1);
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ students.add(student);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return students;
|
|
|
|
|
+ }
|
|
|
|
|
+ private List<ClassStudent> parseCsvFile(MultipartFile file, Long classId)
|
|
|
|
|
+ throws IOException, CsvValidationException {
|
|
|
|
|
+
|
|
|
|
|
+ List<ClassStudent> students = new ArrayList<>();
|
|
|
|
|
+ CSVReader csvReader = new CSVReader(new InputStreamReader(file.getInputStream(), "UTF-8"));
|
|
|
|
|
+
|
|
|
|
|
+ // 读取表头
|
|
|
|
|
+ String[] header = csvReader.readNext();
|
|
|
|
|
+ if (header == null) {
|
|
|
|
|
+ throw new MyException(400, "CSV文件为空");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ int nameCol = findColumnIndex(header, "学生姓名");
|
|
|
|
|
+ int numberCol = findColumnIndex(header, "学生学号");
|
|
|
|
|
+
|
|
|
|
|
+ if (nameCol == -1 || numberCol == -1) {
|
|
|
|
|
+ throw new MyException(400, "文件缺少必要列:学生姓名 或 学生学号");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 处理数据行
|
|
|
|
|
+ String[] nextRecord;
|
|
|
|
|
+ while ((nextRecord = csvReader.readNext()) != null) {
|
|
|
|
|
+ if (nextRecord.length < Math.max(nameCol, numberCol) + 1) {
|
|
|
|
|
+ log.warn("跳过无效数据行: {}", String.join(",", nextRecord));
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ ClassStudent student = new ClassStudent();
|
|
|
|
|
+ student.setStuName(nextRecord[nameCol].trim());
|
|
|
|
|
+
|
|
|
|
|
+ // 处理可能包含逗号的数值
|
|
|
|
|
+ String officialNumber = nextRecord[numberCol].trim();
|
|
|
|
|
+ if (officialNumber.contains(",")) {
|
|
|
|
|
+ officialNumber = officialNumber.replace(",", "");
|
|
|
|
|
+ }
|
|
|
|
|
+ student.setOfficialNumber(officialNumber);
|
|
|
|
|
+
|
|
|
|
|
+ student.setClassId(classId);
|
|
|
|
|
+ student.setState(ClassStudentState.NOT_JOINED); // 默认状态
|
|
|
|
|
+
|
|
|
|
|
+ // 验证必要字段
|
|
|
|
|
+ if (student.getStuName().isEmpty() || student.getOfficialNumber().isEmpty()) {
|
|
|
|
|
+ log.warn("跳过无效学生数据: {}", String.join(",", nextRecord));
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ students.add(student);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return students;
|
|
|
|
|
+ }
|
|
|
|
|
+ private int findColumnIndex(Row headerRow, String columnName) {
|
|
|
|
|
+ for (Cell cell : headerRow) {
|
|
|
|
|
+ if (cell.getStringCellValue().trim().equalsIgnoreCase(columnName)) {
|
|
|
|
|
+ return cell.getColumnIndex();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return -1;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private int findColumnIndex(String[] header, String columnName) {
|
|
|
|
|
+ for (int i = 0; i < header.length; i++) {
|
|
|
|
|
+ if (header[i].trim().equalsIgnoreCase(columnName)) {
|
|
|
|
|
+ return i;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return -1;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String getCellStringValue(Cell cell) {
|
|
|
|
|
+ if (cell == null) return "";
|
|
|
|
|
+
|
|
|
|
|
+ DataFormatter formatter = new DataFormatter();
|
|
|
|
|
+
|
|
|
|
|
+ // 使用 DataFormatter 获取单元格的字符串表示
|
|
|
|
|
+ String cellValue = formatter.formatCellValue(cell).trim();
|
|
|
|
|
+
|
|
|
|
|
+ // 特殊处理科学计数法表示的大数字
|
|
|
|
|
+ if (cellValue.contains("E") && cell.getCellType() == CellType.NUMERIC) {
|
|
|
|
|
+ BigDecimal bigDecimal = BigDecimal.valueOf(cell.getNumericCellValue());
|
|
|
|
|
+ cellValue = bigDecimal.toPlainString();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return cellValue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
|
|
|
}
|
|
}
|