Quellcode durchsuchen

批量导入创建账号

lzzz vor 5 Monaten
Ursprung
Commit
79b898384d

+ 4 - 1
application.yaml

@@ -100,4 +100,7 @@ springdoc:
   swagger-ui:
     enabled: true
     path: /doc/swagger/swagger-ui.html
-    packagesToScan: com.njuzr.eaibackend.controller
+    packagesToScan: com.njuzr.eaibackend.controller
+portal:
+  base-url: http://localhost:8080
+  batch-register-path: /api/user/register/batch/internal

+ 12 - 4
src/main/java/com/njuzr/eaibackend/controller/ClassController.java

@@ -4,12 +4,12 @@ import com.njuzr.eaibackend.dto.ClassDTO;
 import com.njuzr.eaibackend.exception.MyException;
 import com.njuzr.eaibackend.po.Class;
 import com.njuzr.eaibackend.service.ClassService;
+import com.njuzr.eaibackend.vo.BatchStudentImportResultVO;
 import com.njuzr.eaibackend.vo.ClassVO;
 import com.njuzr.eaibackend.vo.StudentInfoVO;
-import lombok.Data;
 import lombok.extern.slf4j.Slf4j;
-import org.apache.ibatis.annotations.Delete;
 import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.util.StringUtils;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.multipart.MultipartFile;
 
@@ -85,10 +85,18 @@ public class ClassController {
     @PreAuthorize("hasRole('ADMIN') or hasRole('TEACHER')")
     public MyResponse batchAddStudents(
             @PathVariable Long classId,
-            @RequestParam("file") MultipartFile file) {
+            @RequestParam("file") MultipartFile file,
+            @RequestHeader(value = "Authorization", required = false) String authorization) {
 
         try {
-            String result = classService.batchAddStudents(classId, file);
+            if (!StringUtils.hasText(authorization)) {
+                return MyResponse.error(401, "缺少Authorization令牌");
+            }
+            String token = authorization.startsWith("Bearer ") ? authorization.substring(7).trim() : authorization.trim();
+            if (!StringUtils.hasText(token)) {
+                return MyResponse.error(401, "无效的Authorization令牌");
+            }
+            BatchStudentImportResultVO result = classService.batchAddStudents(classId, file, token);
             return MyResponse.success(result);
         } catch (MyException e) {
             return MyResponse.error(e.getErrCode(), e.getMessage());

+ 2 - 1
src/main/java/com/njuzr/eaibackend/service/ClassService.java

@@ -2,6 +2,7 @@ package com.njuzr.eaibackend.service;
 
 import com.njuzr.eaibackend.dto.ClassDTO;
 import com.njuzr.eaibackend.po.Class;
+import com.njuzr.eaibackend.vo.BatchStudentImportResultVO;
 import com.njuzr.eaibackend.vo.StudentInfoVO;
 import com.njuzr.eaibackend.vo.ClassVO;
 import org.springframework.web.multipart.MultipartFile;
@@ -22,7 +23,7 @@ public interface ClassService {
      */
     String deleteClassWithCheck(Long classId);
 
-    String batchAddStudents(Long classId, MultipartFile file);
+    BatchStudentImportResultVO batchAddStudents(Long classId, MultipartFile file, String accessToken);
     /**
      * 通过学号添加学生到班级
      * @param classId 班级ID

+ 355 - 125
src/main/java/com/njuzr/eaibackend/service/impl/ClassServiceImpl.java

@@ -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;
+        }
+    }
+
 
 }

+ 5 - 1
src/main/resources/application-dev.yaml

@@ -75,4 +75,8 @@ springdoc:
   swagger-ui:
     enabled: true
     path: /doc/swagger/swagger-ui.html
-    packagesToScan: com.njuzr.eaibackend.controller
+    packagesToScan: com.njuzr.eaibackend.controller
+
+portal:
+  base-url: http://localhost:8080
+  batch-register-path: /api/user/register/batch/internal

+ 4 - 0
src/main/resources/application-prod.yaml

@@ -75,3 +75,7 @@ springdoc:
     enabled: false
     path: /doc/swagger/swagger-ui.html
     packagesToScan: com.njuzr.eaibackend.controller
+
+portal:
+  base-url: http://localhost:8080
+  batch-register-path: /api/user/register/batch/internal

+ 2 - 2
src/main/resources/mapper/UserMapper.xml

@@ -38,9 +38,9 @@
     </update>
 
     <insert id="batchInsert" parameterType="list">
-        INSERT INTO users (name, password, official_number, official_email, role, create_time) VALUES
+        INSERT INTO users (name, password, official_number, official_email, role, create_time, phone, pid) VALUES
         <foreach collection="list" item="user" index="index" separator=",">
-            (#{user.name}, #{user.password}, #{user.officialNumber}, #{user.officialEmail}, #{user.role}, #{user.createTime})
+            (#{user.name}, #{user.password}, #{user.officialNumber}, #{user.officialEmail}, #{user.role}, #{user.createTime}, #{user.phone}, #{user.pid})
         </foreach>
     </insert>