|
|
@@ -3,20 +3,28 @@ package com.njuzr.eaibackend.service.impl;
|
|
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
|
|
import com.njuzr.eaibackend.dto.ClassDTO;
|
|
|
import com.njuzr.eaibackend.enums.ClassStudentState;
|
|
|
+import com.njuzr.eaibackend.enums.Role;
|
|
|
import com.njuzr.eaibackend.exception.MyException;
|
|
|
import com.njuzr.eaibackend.mapper.*;
|
|
|
import com.njuzr.eaibackend.po.Class;
|
|
|
import com.njuzr.eaibackend.po.ClassStudent;
|
|
|
+import com.njuzr.eaibackend.po.Enrollment;
|
|
|
import com.njuzr.eaibackend.po.User;
|
|
|
import com.njuzr.eaibackend.service.ClassService;
|
|
|
+import com.njuzr.eaibackend.utils.WebClientUtil;
|
|
|
+import com.njuzr.eaibackend.vo.BatchStudentImportDetailVO;
|
|
|
+import com.njuzr.eaibackend.vo.BatchStudentImportResultVO;
|
|
|
import com.njuzr.eaibackend.vo.StudentInfoVO;
|
|
|
import com.njuzr.eaibackend.vo.ClassVO;
|
|
|
import com.opencsv.CSVReader;
|
|
|
import com.opencsv.exceptions.CsvValidationException;
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.springframework.beans.factory.annotation.Value;
|
|
|
import org.apache.poi.ss.usermodel.*;
|
|
|
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
|
|
import org.springframework.http.HttpStatus;
|
|
|
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
|
|
+import org.springframework.security.crypto.password.PasswordEncoder;
|
|
|
import org.springframework.stereotype.Service;
|
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
|
import org.springframework.web.multipart.MultipartFile;
|
|
|
@@ -25,8 +33,14 @@ import java.io.IOException;
|
|
|
import java.io.InputStreamReader;
|
|
|
import java.math.BigDecimal;
|
|
|
import java.util.ArrayList;
|
|
|
+import java.util.Date;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.HashSet;
|
|
|
import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
import java.util.Random;
|
|
|
+import java.util.Set;
|
|
|
+import java.util.regex.Pattern;
|
|
|
import java.util.stream.Collectors;
|
|
|
import org.springframework.beans.BeanUtils;
|
|
|
|
|
|
@@ -37,12 +51,26 @@ public class ClassServiceImpl implements ClassService {
|
|
|
private final ClassStudentMapper classStudentMapper;
|
|
|
private final UserMapper userMapper;
|
|
|
private final CourseStudentMapper courseStudentMapper;
|
|
|
+ private final WebClientUtil webClientUtil;
|
|
|
|
|
|
- public ClassServiceImpl(ClassMapper classMapper, ClassStudentMapper classStudentMapper, UserMapper userMapper, CourseStudentMapper courseStudentMapper) {
|
|
|
+ @Value("${portal.base-url:http://localhost:8080}")
|
|
|
+ private String portalBaseUrl;
|
|
|
+
|
|
|
+ @Value("${portal.batch-register-path:/api/user/register/batch/internal}")
|
|
|
+ private String portalBatchRegisterPath;
|
|
|
+
|
|
|
+ private final PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
|
|
|
+
|
|
|
+ public ClassServiceImpl(ClassMapper classMapper,
|
|
|
+ ClassStudentMapper classStudentMapper,
|
|
|
+ UserMapper userMapper,
|
|
|
+ CourseStudentMapper courseStudentMapper,
|
|
|
+ WebClientUtil webClientUtil) {
|
|
|
this.classMapper = classMapper;
|
|
|
this.classStudentMapper = classStudentMapper;
|
|
|
this.userMapper = userMapper;
|
|
|
this.courseStudentMapper = courseStudentMapper;
|
|
|
+ this.webClientUtil = webClientUtil;
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
@@ -158,7 +186,7 @@ public class ClassServiceImpl implements ClassService {
|
|
|
|
|
|
@Override
|
|
|
@Transactional
|
|
|
- public String batchAddStudents(Long classId, MultipartFile file) {
|
|
|
+ public BatchStudentImportResultVO batchAddStudents(Long classId, MultipartFile file, String accessToken) {
|
|
|
// 检查班级是否存在
|
|
|
Class cls = classMapper.selectById(classId);
|
|
|
if (cls == null) {
|
|
|
@@ -166,14 +194,14 @@ public class ClassServiceImpl implements ClassService {
|
|
|
}
|
|
|
|
|
|
// 解析文件
|
|
|
- List<ClassStudent> students;
|
|
|
+ List<StudentImportRow> rows;
|
|
|
String fileName = file.getOriginalFilename();
|
|
|
|
|
|
try {
|
|
|
if (fileName != null && (fileName.endsWith(".xls") || fileName.endsWith(".xlsx"))) {
|
|
|
- students = parseExcelFile(file, classId);
|
|
|
+ rows = parseExcelFile(file);
|
|
|
} else if (fileName != null && fileName.endsWith(".csv")) {
|
|
|
- students = parseCsvFile(file, classId);
|
|
|
+ rows = parseCsvFile(file);
|
|
|
} else {
|
|
|
throw new MyException(400, "不支持的文件类型");
|
|
|
}
|
|
|
@@ -182,17 +210,219 @@ public class ClassServiceImpl implements ClassService {
|
|
|
throw new MyException(500, "解析学生文件失败: " + e.getMessage());
|
|
|
}
|
|
|
|
|
|
- if (students.isEmpty()) {
|
|
|
- return "未找到有效学生数据";
|
|
|
+ BatchStudentImportResultVO result = new BatchStudentImportResultVO();
|
|
|
+ result.setTotal(rows.size());
|
|
|
+ result.setSuccessCount(0);
|
|
|
+ result.setSkippedCount(0);
|
|
|
+ result.setFailedCount(0);
|
|
|
+
|
|
|
+ if (rows.isEmpty()) {
|
|
|
+ return result;
|
|
|
}
|
|
|
|
|
|
- // 批量插入学生
|
|
|
- int insertedCount = classStudentMapper.batchInsert(students);
|
|
|
+ Map<Integer, Map<String, Object>> portalResultByRow = registerUsersInPortal(rows, accessToken);
|
|
|
+ Set<String> fileOfficialNumbers = new HashSet<>();
|
|
|
+ Set<String> filePhones = new HashSet<>();
|
|
|
+
|
|
|
+ List<User> usersToInsert = new ArrayList<>();
|
|
|
+ List<ClassStudent> studentsToInsert = new ArrayList<>();
|
|
|
+
|
|
|
+ for (StudentImportRow row : rows) {
|
|
|
+ BatchStudentImportDetailVO detail = new BatchStudentImportDetailVO();
|
|
|
+ detail.setRowIndex(row.getRowIndex());
|
|
|
+ detail.setOfficialNumber(row.getOfficialNumber());
|
|
|
+ detail.setStuName(row.getStuName());
|
|
|
+ detail.setPhone(row.getPhone());
|
|
|
+
|
|
|
+ String validationError = validateRow(row, fileOfficialNumbers, filePhones);
|
|
|
+ if (validationError != null) {
|
|
|
+ detail.setStatus("FAILED");
|
|
|
+ detail.setMessage(validationError);
|
|
|
+ result.getDetails().add(detail);
|
|
|
+ result.setFailedCount(result.getFailedCount() + 1);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
|
|
|
- // 更新班级人数
|
|
|
- classMapper.increaseStuNumber(classId, insertedCount);
|
|
|
+ Map<String, Object> portalRow = portalResultByRow.get(row.getRowIndex());
|
|
|
+ if (portalRow == null || !"SUCCESS".equals(String.valueOf(portalRow.get("status")))) {
|
|
|
+ detail.setStatus("SKIPPED");
|
|
|
+ detail.setMessage(portalRow == null ? "Portal未返回该行处理结果" : String.valueOf(portalRow.get("message")));
|
|
|
+ result.getDetails().add(detail);
|
|
|
+ result.setSkippedCount(result.getSkippedCount() + 1);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ User existsByNumber = userMapper.selectByOfficialNumber(row.getOfficialNumber());
|
|
|
+ if (existsByNumber != null) {
|
|
|
+ detail.setStatus("SKIPPED");
|
|
|
+ detail.setMessage("EAI中学号已存在,已跳过");
|
|
|
+ result.getDetails().add(detail);
|
|
|
+ result.setSkippedCount(result.getSkippedCount() + 1);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ User existsByPhone = userMapper.selectByPhone(row.getPhone());
|
|
|
+ if (existsByPhone != null) {
|
|
|
+ detail.setStatus("SKIPPED");
|
|
|
+ detail.setMessage("EAI中手机号已存在,已跳过");
|
|
|
+ result.getDetails().add(detail);
|
|
|
+ result.setSkippedCount(result.getSkippedCount() + 1);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ QueryWrapper<ClassStudent> classStudentWrapper = new QueryWrapper<>();
|
|
|
+ classStudentWrapper.eq("class_id", classId)
|
|
|
+ .eq("official_number", row.getOfficialNumber());
|
|
|
+ if (classStudentMapper.selectCount(classStudentWrapper) > 0) {
|
|
|
+ detail.setStatus("SKIPPED");
|
|
|
+ detail.setMessage("学生已在班级中,已跳过");
|
|
|
+ result.getDetails().add(detail);
|
|
|
+ result.setSkippedCount(result.getSkippedCount() + 1);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ User user = new User();
|
|
|
+ user.setName(row.getStuName());
|
|
|
+ user.setOfficialNumber(row.getOfficialNumber());
|
|
|
+ user.setPhone(row.getPhone());
|
|
|
+ user.setRole(Role.STUDENT);
|
|
|
+ user.setCreateTime(new Date());
|
|
|
+ Object portalUserId = portalRow.get("userId");
|
|
|
+ if (portalUserId instanceof Number) {
|
|
|
+ user.setPid(String.valueOf(((Number) portalUserId).longValue()));
|
|
|
+ }
|
|
|
+ user.setPassword(passwordEncoder.encode(generateDefaultPassword(row.getOfficialNumber())));
|
|
|
+ usersToInsert.add(user);
|
|
|
+
|
|
|
+ ClassStudent student = new ClassStudent();
|
|
|
+ student.setClassId(classId);
|
|
|
+ student.setOfficialNumber(row.getOfficialNumber());
|
|
|
+ student.setStuName(row.getStuName());
|
|
|
+ student.setState(ClassStudentState.NOT_JOINED);
|
|
|
+ studentsToInsert.add(student);
|
|
|
+
|
|
|
+ detail.setStatus("SUCCESS");
|
|
|
+ detail.setMessage("导入成功");
|
|
|
+ result.getDetails().add(detail);
|
|
|
+ result.setSuccessCount(result.getSuccessCount() + 1);
|
|
|
+ fileOfficialNumbers.add(row.getOfficialNumber());
|
|
|
+ filePhones.add(row.getPhone());
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!usersToInsert.isEmpty()) {
|
|
|
+ userMapper.batchInsert(usersToInsert);
|
|
|
+ classStudentMapper.batchInsert(studentsToInsert);
|
|
|
+ classMapper.increaseStuNumber(classId, studentsToInsert.size());
|
|
|
+
|
|
|
+ List<String> importedOfficialNumbers = studentsToInsert.stream()
|
|
|
+ .map(ClassStudent::getOfficialNumber)
|
|
|
+ .collect(Collectors.toList());
|
|
|
+ List<User> importedUsers = userMapper.selectUserIdByOfficialNumbers(importedOfficialNumbers);
|
|
|
+ List<Enrollment> enrollments = new ArrayList<>();
|
|
|
+ for (User importedUser : importedUsers) {
|
|
|
+ Enrollment enrollment = new Enrollment();
|
|
|
+ enrollment.setCourseId(cls.getCourseId());
|
|
|
+ enrollment.setStudentId(importedUser.getId());
|
|
|
+ enrollments.add(enrollment);
|
|
|
+ }
|
|
|
+ for (Enrollment enrollment : enrollments) {
|
|
|
+ QueryWrapper<Enrollment> enrollmentWrapper = new QueryWrapper<>();
|
|
|
+ enrollmentWrapper.eq("course_id", enrollment.getCourseId())
|
|
|
+ .eq("student_id", enrollment.getStudentId());
|
|
|
+ if (!courseStudentMapper.exists(enrollmentWrapper)) {
|
|
|
+ courseStudentMapper.insert(enrollment);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- return "成功添加 " + insertedCount + " 名学生";
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ private Map<Integer, Map<String, Object>> registerUsersInPortal(List<StudentImportRow> rows, String accessToken) {
|
|
|
+ String endpoint = portalBaseUrl + portalBatchRegisterPath;
|
|
|
+ if (accessToken == null || accessToken.trim().isEmpty()) {
|
|
|
+ throw new MyException(401, "缺少访问令牌");
|
|
|
+ }
|
|
|
+ List<Map<String, Object>> users = new ArrayList<>();
|
|
|
+ for (StudentImportRow row : rows) {
|
|
|
+ Map<String, Object> user = new HashMap<>();
|
|
|
+ user.put("rowIndex", row.getRowIndex());
|
|
|
+ user.put("username", row.getOfficialNumber());
|
|
|
+ user.put("name", row.getStuName());
|
|
|
+ user.put("phone", row.getPhone());
|
|
|
+ user.put("role", "STUDENT");
|
|
|
+ users.add(user);
|
|
|
+ }
|
|
|
+ Map<String, Object> request = new HashMap<>();
|
|
|
+ request.put("users", users);
|
|
|
+
|
|
|
+ Map<String, Object> response;
|
|
|
+ try {
|
|
|
+ response = webClientUtil.post(endpoint, request, Map.class);
|
|
|
+ } catch (Exception e) {
|
|
|
+ throw new MyException(502, "调用Portal批量注册失败:" + e.getMessage());
|
|
|
+ }
|
|
|
+
|
|
|
+ if (response == null || !response.containsKey("code")) {
|
|
|
+ throw new MyException(502, "Portal批量注册返回格式异常");
|
|
|
+ }
|
|
|
+ Number code = (Number) response.get("code");
|
|
|
+ if (code == null || code.intValue() != 0) {
|
|
|
+ throw new MyException(502, "Portal批量注册失败:" + String.valueOf(response.get("message")));
|
|
|
+ }
|
|
|
+
|
|
|
+ Object dataObj = response.get("data");
|
|
|
+ if (!(dataObj instanceof Map)) {
|
|
|
+ throw new MyException(502, "Portal批量注册返回数据为空");
|
|
|
+ }
|
|
|
+ Map<String, Object> data = (Map<String, Object>) dataObj;
|
|
|
+ Object detailsObj = data.get("details");
|
|
|
+ if (!(detailsObj instanceof List)) {
|
|
|
+ throw new MyException(502, "Portal批量注册结果缺少details字段");
|
|
|
+ }
|
|
|
+
|
|
|
+ Map<Integer, Map<String, Object>> resultByRow = new HashMap<>();
|
|
|
+ for (Object detailObj : (List<?>) detailsObj) {
|
|
|
+ if (!(detailObj instanceof Map)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ Map<String, Object> detail = (Map<String, Object>) detailObj;
|
|
|
+ Number rowIndex = (Number) detail.get("rowIndex");
|
|
|
+ if (rowIndex != null) {
|
|
|
+ resultByRow.put(rowIndex.intValue(), detail);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return resultByRow;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String validateRow(StudentImportRow row, Set<String> officialNumberSet, Set<String> phoneSet) {
|
|
|
+ if (row.getStuName() == null || row.getStuName().isEmpty()
|
|
|
+ || row.getOfficialNumber() == null || row.getOfficialNumber().isEmpty()
|
|
|
+ || row.getPhone() == null || row.getPhone().isEmpty()) {
|
|
|
+ return "必填字段缺失:学号/姓名/手机号不能为空";
|
|
|
+ }
|
|
|
+ if (!Pattern.matches("^\\d{11}$", row.getPhone())) {
|
|
|
+ return "手机号格式错误,必须为11位数字";
|
|
|
+ }
|
|
|
+ if (officialNumberSet.contains(row.getOfficialNumber())) {
|
|
|
+ return "导入文件中学号重复";
|
|
|
+ }
|
|
|
+ if (phoneSet.contains(row.getPhone())) {
|
|
|
+ return "导入文件中手机号重复";
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String generateDefaultPassword(String officialNumber) {
|
|
|
+ if (officialNumber == null || officialNumber.length() < 6) {
|
|
|
+ throw new MyException(400, "学号长度不足6位,无法生成默认密码");
|
|
|
+ }
|
|
|
+ String suffix = officialNumber.substring(officialNumber.length() - 6);
|
|
|
+ if (!Pattern.matches("^\\d{6}$", suffix)) {
|
|
|
+ throw new MyException(400, "学号后6位必须是数字");
|
|
|
+ }
|
|
|
+ return "Stu" + suffix;
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
@@ -323,158 +553,158 @@ public class ClassServiceImpl implements ClassService {
|
|
|
return classStudentMapper.findClassByStudentIdAndCourseId(studentId, courseId);
|
|
|
}
|
|
|
|
|
|
- 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;
|
|
|
+ private List<StudentImportRow> parseExcelFile(MultipartFile file) throws IOException {
|
|
|
+ List<StudentImportRow> rows = new ArrayList<>();
|
|
|
+ try (Workbook workbook = new XSSFWorkbook(file.getInputStream())) {
|
|
|
+ Sheet sheet = workbook.getSheetAt(0);
|
|
|
|
|
|
- ClassStudent student = new ClassStudent();
|
|
|
+ Row headerRow = sheet.getRow(0);
|
|
|
+ int nameCol = findColumnIndex(headerRow, "学生姓名", "姓名");
|
|
|
+ int numberCol = findColumnIndex(headerRow, "学生学号", "学号");
|
|
|
+ int phoneCol = findColumnIndex(headerRow, "手机号", "手机号码");
|
|
|
|
|
|
- // 获取学生姓名
|
|
|
- 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();
|
|
|
- }
|
|
|
+ if (nameCol == -1 || numberCol == -1 || phoneCol == -1) {
|
|
|
+ throw new MyException(400, "文件缺少必要列:学生姓名 / 学生学号 / 手机号");
|
|
|
}
|
|
|
- student.setOfficialNumber(officialNumber);
|
|
|
-
|
|
|
- student.setClassId(classId);
|
|
|
- student.setState(ClassStudentState.NOT_JOINED); // 默认状态
|
|
|
|
|
|
- // 验证必要字段
|
|
|
- if (student.getStuName().isEmpty() || student.getOfficialNumber().isEmpty()) {
|
|
|
- log.warn("跳过无效学生数据行: {}", i + 1);
|
|
|
- continue;
|
|
|
+ for (int i = 1; i <= sheet.getLastRowNum(); i++) {
|
|
|
+ Row row = sheet.getRow(i);
|
|
|
+ if (row == null) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ StudentImportRow importRow = new StudentImportRow();
|
|
|
+ importRow.setRowIndex(i + 1);
|
|
|
+ importRow.setStuName(getCellStringValue(row.getCell(nameCol)));
|
|
|
+ importRow.setOfficialNumber(getCellStringValue(row.getCell(numberCol)).replace(",", ""));
|
|
|
+ importRow.setPhone(getCellStringValue(row.getCell(phoneCol)).replace(",", ""));
|
|
|
+ rows.add(importRow);
|
|
|
}
|
|
|
-
|
|
|
- students.add(student);
|
|
|
}
|
|
|
-
|
|
|
- return students;
|
|
|
+ return rows;
|
|
|
}
|
|
|
- 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, "文件缺少必要列:学生姓名 或 学生学号");
|
|
|
- }
|
|
|
+ private List<StudentImportRow> parseCsvFile(MultipartFile file)
|
|
|
+ throws IOException, CsvValidationException {
|
|
|
|
|
|
- // 处理数据行
|
|
|
- String[] nextRecord;
|
|
|
- while ((nextRecord = csvReader.readNext()) != null) {
|
|
|
- if (nextRecord.length < Math.max(nameCol, numberCol) + 1) {
|
|
|
- log.warn("跳过无效数据行: {}", String.join(",", nextRecord));
|
|
|
- continue;
|
|
|
+ List<StudentImportRow> rows = new ArrayList<>();
|
|
|
+ try (CSVReader csvReader = new CSVReader(new InputStreamReader(file.getInputStream()))) {
|
|
|
+ String[] header = csvReader.readNext();
|
|
|
+ if (header == null) {
|
|
|
+ throw new MyException(400, "CSV文件为空");
|
|
|
}
|
|
|
|
|
|
- ClassStudent student = new ClassStudent();
|
|
|
- student.setStuName(nextRecord[nameCol].trim());
|
|
|
+ int nameCol = findColumnIndex(header, "学生姓名", "姓名");
|
|
|
+ int numberCol = findColumnIndex(header, "学生学号", "学号");
|
|
|
+ int phoneCol = findColumnIndex(header, "手机号", "手机号码");
|
|
|
|
|
|
- // 处理可能包含逗号的数值
|
|
|
- String officialNumber = nextRecord[numberCol].trim();
|
|
|
- if (officialNumber.contains(",")) {
|
|
|
- officialNumber = officialNumber.replace(",", "");
|
|
|
+ if (nameCol == -1 || numberCol == -1 || phoneCol == -1) {
|
|
|
+ throw new MyException(400, "文件缺少必要列:学生姓名 / 学生学号 / 手机号");
|
|
|
}
|
|
|
- student.setOfficialNumber(officialNumber);
|
|
|
|
|
|
- student.setClassId(classId);
|
|
|
- student.setState(ClassStudentState.NOT_JOINED); // 默认状态
|
|
|
+ String[] nextRecord;
|
|
|
+ int rowNum = 1;
|
|
|
+ while ((nextRecord = csvReader.readNext()) != null) {
|
|
|
+ rowNum++;
|
|
|
+ int maxIndex = Math.max(Math.max(nameCol, numberCol), phoneCol);
|
|
|
+ if (nextRecord.length < maxIndex + 1) {
|
|
|
+ StudentImportRow importRow = new StudentImportRow();
|
|
|
+ importRow.setRowIndex(rowNum);
|
|
|
+ importRow.setStuName("");
|
|
|
+ importRow.setOfficialNumber("");
|
|
|
+ importRow.setPhone("");
|
|
|
+ rows.add(importRow);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
|
|
|
- // 验证必要字段
|
|
|
- if (student.getStuName().isEmpty() || student.getOfficialNumber().isEmpty()) {
|
|
|
- log.warn("跳过无效学生数据: {}", String.join(",", nextRecord));
|
|
|
- continue;
|
|
|
+ StudentImportRow importRow = new StudentImportRow();
|
|
|
+ importRow.setRowIndex(rowNum);
|
|
|
+ importRow.setStuName(nextRecord[nameCol] == null ? "" : nextRecord[nameCol].trim());
|
|
|
+ importRow.setOfficialNumber(nextRecord[numberCol] == null ? "" : nextRecord[numberCol].trim().replace(",", ""));
|
|
|
+ importRow.setPhone(nextRecord[phoneCol] == null ? "" : nextRecord[phoneCol].trim().replace(",", ""));
|
|
|
+ rows.add(importRow);
|
|
|
}
|
|
|
-
|
|
|
- students.add(student);
|
|
|
}
|
|
|
-
|
|
|
- return students;
|
|
|
+ return rows;
|
|
|
}
|
|
|
- private int findColumnIndex(Row headerRow, String columnName) {
|
|
|
+
|
|
|
+ private int findColumnIndex(Row headerRow, String... columnNames) {
|
|
|
+ if (headerRow == null) {
|
|
|
+ return -1;
|
|
|
+ }
|
|
|
for (Cell cell : headerRow) {
|
|
|
- if (cell.getStringCellValue().trim().equalsIgnoreCase(columnName)) {
|
|
|
- return cell.getColumnIndex();
|
|
|
+ String value = getCellStringValue(cell);
|
|
|
+ for (String columnName : columnNames) {
|
|
|
+ if (value.equalsIgnoreCase(columnName)) {
|
|
|
+ return cell.getColumnIndex();
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
return -1;
|
|
|
}
|
|
|
|
|
|
- private int findColumnIndex(String[] header, String columnName) {
|
|
|
+ private int findColumnIndex(String[] header, String... columnNames) {
|
|
|
for (int i = 0; i < header.length; i++) {
|
|
|
- if (header[i].trim().equalsIgnoreCase(columnName)) {
|
|
|
- return i;
|
|
|
+ String value = header[i] == null ? "" : header[i].trim();
|
|
|
+ for (String columnName : columnNames) {
|
|
|
+ if (value.equalsIgnoreCase(columnName)) {
|
|
|
+ return i;
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
return -1;
|
|
|
}
|
|
|
|
|
|
private String getCellStringValue(Cell cell) {
|
|
|
- if (cell == null) return "";
|
|
|
-
|
|
|
+ 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;
|
|
|
}
|
|
|
|
|
|
+ private static class StudentImportRow {
|
|
|
+ private Integer rowIndex;
|
|
|
+ private String officialNumber;
|
|
|
+ private String stuName;
|
|
|
+ private String phone;
|
|
|
+
|
|
|
+ public Integer getRowIndex() {
|
|
|
+ return rowIndex;
|
|
|
+ }
|
|
|
+
|
|
|
+ public void setRowIndex(Integer rowIndex) {
|
|
|
+ this.rowIndex = rowIndex;
|
|
|
+ }
|
|
|
+
|
|
|
+ public String getOfficialNumber() {
|
|
|
+ return officialNumber;
|
|
|
+ }
|
|
|
+
|
|
|
+ public void setOfficialNumber(String officialNumber) {
|
|
|
+ this.officialNumber = officialNumber;
|
|
|
+ }
|
|
|
+
|
|
|
+ public String getStuName() {
|
|
|
+ return stuName;
|
|
|
+ }
|
|
|
+
|
|
|
+ public void setStuName(String stuName) {
|
|
|
+ this.stuName = stuName;
|
|
|
+ }
|
|
|
+
|
|
|
+ public String getPhone() {
|
|
|
+ return phone;
|
|
|
+ }
|
|
|
+
|
|
|
+ public void setPhone(String phone) {
|
|
|
+ this.phone = phone;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
|
|
|
}
|