From ea1524ea671156ae8dbf4537fab1c4165572561c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A5=9E=E7=A0=81-=E6=96=B9=E6=99=93=E8=BE=89?= Date: Mon, 2 Feb 2026 20:11:56 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E5=AD=A6=E7=94=9F?= =?UTF-8?q?=E6=89=B9=E9=87=8F=E5=AF=BC=E5=85=A5=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 新增 POST /business/student/import 接口 2. 实现导入业务逻辑: - 校验必填字段(姓名、学号、会员手机号、学校、年级、班级) - 根据名称匹配学校、年级、班级 - 根据手机号查找或创建会员(不存在则自动创建家长账号) - 检查学号唯一性 - 返回导入结果(成功数、失败数、失败明细) --- .../controller/PgStudentController.java | 19 ++ .../student/service/IPgStudentService.java | 9 + .../service/impl/PgStudentServiceImpl.java | 217 ++++++++++++++++++ 3 files changed, 245 insertions(+) diff --git a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/student/controller/PgStudentController.java b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/student/controller/PgStudentController.java index c89c4f2..67a4759 100644 --- a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/student/controller/PgStudentController.java +++ b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/student/controller/PgStudentController.java @@ -19,9 +19,12 @@ import org.dromara.pangu.student.domain.vo.StudentVo; import org.dromara.pangu.student.service.IPgStudentService; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; /** * 学生管理 @@ -124,4 +127,20 @@ public class PgStudentController extends BaseController { ExcelUtil.exportExcel(sampleData, "学生导入模板", StudentImportDto.class, response); } + + /** + * 批量导入学生 + */ + @SaCheckPermission("business:student:import") + @Log(title = "学生管理", businessType = BusinessType.IMPORT) + @PostMapping("/import") + public R> importData(MultipartFile file) throws Exception { + // 解析 Excel 数据 + List dataList = ExcelUtil.importExcel(file.getInputStream(), StudentImportDto.class); + + // 调用服务层处理导入 + Map result = studentService.importStudents(dataList); + + return R.ok(result); + } } diff --git a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/student/service/IPgStudentService.java b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/student/service/IPgStudentService.java index 689a80e..d106851 100644 --- a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/student/service/IPgStudentService.java +++ b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/student/service/IPgStudentService.java @@ -3,9 +3,11 @@ package org.dromara.pangu.student.service; import org.dromara.common.mybatis.core.page.PageQuery; import org.dromara.common.mybatis.core.page.TableDataInfo; import org.dromara.pangu.student.domain.PgStudent; +import org.dromara.pangu.student.domain.dto.StudentImportDto; import org.dromara.pangu.student.domain.vo.StudentVo; import java.util.List; +import java.util.Map; /** * 学生 Service 接口 @@ -44,4 +46,11 @@ public interface IPgStudentService { * 解绑学生 */ int unbindStudent(Long studentId); + + /** + * 批量导入学生 + * @param dataList Excel 导入的数据列表 + * @return 导入结果 {successCount, failCount, failList} + */ + Map importStudents(List dataList); } diff --git a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/student/service/impl/PgStudentServiceImpl.java b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/student/service/impl/PgStudentServiceImpl.java index 72b765d..924611a 100644 --- a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/student/service/impl/PgStudentServiceImpl.java +++ b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/student/service/impl/PgStudentServiceImpl.java @@ -2,10 +2,12 @@ package org.dromara.pangu.student.service.impl; import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.BCrypt; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.dromara.common.mybatis.core.page.PageQuery; import org.dromara.common.mybatis.core.page.TableDataInfo; import org.dromara.pangu.base.domain.PgClass; @@ -23,11 +25,14 @@ import org.dromara.pangu.school.mapper.PgSchoolMapper; import org.dromara.pangu.base.domain.PgSubject; import org.dromara.pangu.base.mapper.PgSubjectMapper; import org.dromara.pangu.student.domain.PgStudent; +import org.dromara.pangu.student.domain.dto.StudentImportDto; import org.dromara.pangu.student.domain.vo.StudentVo; import org.dromara.pangu.student.mapper.PgStudentMapper; import org.dromara.pangu.student.service.IPgStudentService; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import java.text.SimpleDateFormat; import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; @@ -37,6 +42,7 @@ import java.util.stream.Collectors; * * @author pangu */ +@Slf4j @RequiredArgsConstructor @Service public class PgStudentServiceImpl implements IPgStudentService { @@ -274,4 +280,215 @@ public class PgStudentServiceImpl implements IPgStudentService { .set(PgStudent::getMemberId, null) ); } + + @Override + @Transactional(rollbackFor = Exception.class) + public Map importStudents(List dataList) { + int successCount = 0; + List> failList = new ArrayList<>(); + + // 预先查询所有学校,方便匹配 + List allSchools = schoolMapper.selectList(new LambdaQueryWrapper<>()); + Map schoolNameMap = allSchools.stream() + .collect(Collectors.toMap(PgSchool::getSchoolName, Function.identity(), (a, b) -> a)); + + // 预先查询所有年级 + List allGrades = gradeMapper.selectList(new LambdaQueryWrapper<>()); + Map gradeNameMap = allGrades.stream() + .collect(Collectors.toMap(PgGrade::getGradeName, Function.identity(), (a, b) -> a)); + + // 预先查询所有班级 + List allClasses = classMapper.selectList(new LambdaQueryWrapper<>()); + Map classNameMap = allClasses.stream() + .collect(Collectors.toMap(PgClass::getClassName, Function.identity(), (a, b) -> a)); + + for (int i = 0; i < dataList.size(); i++) { + StudentImportDto dto = dataList.get(i); + int rowNum = i + 2; // Excel 行号从2开始(1是表头) + + try { + // 1. 校验必填字段 + String error = validateImportRow(dto); + if (error != null) { + failList.add(createFailItem(rowNum, error)); + continue; + } + + // 2. 查找学校 + PgSchool school = schoolNameMap.get(dto.getSchoolName().trim()); + if (school == null) { + failList.add(createFailItem(rowNum, "学校\"" + dto.getSchoolName() + "\"不存在")); + continue; + } + + // 3. 查找年级(学校年级关联表) + PgGrade baseGrade = gradeNameMap.get(dto.getGradeName().trim()); + if (baseGrade == null) { + failList.add(createFailItem(rowNum, "年级\"" + dto.getGradeName() + "\"不存在")); + continue; + } + PgSchoolGrade schoolGrade = schoolGradeMapper.selectOne( + new LambdaQueryWrapper() + .eq(PgSchoolGrade::getSchoolId, school.getSchoolId()) + .eq(PgSchoolGrade::getGradeId, baseGrade.getGradeId()) + ); + if (schoolGrade == null) { + failList.add(createFailItem(rowNum, "学校\"" + dto.getSchoolName() + "\"下没有年级\"" + dto.getGradeName() + "\"")); + continue; + } + + // 4. 查找班级(学校班级关联表) + PgClass baseClass = classNameMap.get(dto.getClassName().trim()); + if (baseClass == null) { + failList.add(createFailItem(rowNum, "班级\"" + dto.getClassName() + "\"不存在")); + continue; + } + PgSchoolClass schoolClass = schoolClassMapper.selectOne( + new LambdaQueryWrapper() + .eq(PgSchoolClass::getSchoolGradeId, schoolGrade.getId()) + .eq(PgSchoolClass::getClassId, baseClass.getClassId()) + ); + if (schoolClass == null) { + failList.add(createFailItem(rowNum, "年级\"" + dto.getGradeName() + "\"下没有班级\"" + dto.getClassName() + "\"")); + continue; + } + + // 5. 查找或创建会员 + Long memberId = findOrCreateMember(dto.getMemberPhone().trim()); + + // 6. 检查学号是否重复 + if (StrUtil.isNotBlank(dto.getStudentNo())) { + PgStudent existStudent = baseMapper.selectOne( + new LambdaQueryWrapper() + .eq(PgStudent::getStudentNo, dto.getStudentNo().trim()) + ); + if (existStudent != null) { + failList.add(createFailItem(rowNum, "学号\"" + dto.getStudentNo() + "\"已存在")); + continue; + } + } + + // 7. 创建学生 + PgStudent student = new PgStudent(); + student.setStudentName(dto.getStudentName().trim()); + student.setStudentNo(StrUtil.isNotBlank(dto.getStudentNo()) ? dto.getStudentNo().trim() : null); + student.setGender(convertGender(dto.getGender())); + student.setBirthday(parseBirthday(dto.getBirthday())); + student.setRegionPath(dto.getRegionPath()); + student.setSchoolId(school.getSchoolId()); + student.setSchoolGradeId(schoolGrade.getId()); + student.setSchoolClassId(schoolClass.getId()); + student.setMemberId(memberId); + student.setStatus("0"); // 默认正常 + + baseMapper.insert(student); + successCount++; + + } catch (Exception e) { + log.error("导入第{}行数据失败", rowNum, e); + failList.add(createFailItem(rowNum, "系统错误:" + e.getMessage())); + } + } + + Map result = new HashMap<>(); + result.put("successCount", successCount); + result.put("failCount", failList.size()); + result.put("failList", failList); + return result; + } + + /** + * 校验导入行数据 + */ + private String validateImportRow(StudentImportDto dto) { + if (StrUtil.isBlank(dto.getStudentName())) { + return "姓名不能为空"; + } + if (StrUtil.isBlank(dto.getStudentNo())) { + return "学号不能为空"; + } + if (StrUtil.isBlank(dto.getMemberPhone())) { + return "会员手机号不能为空"; + } + if (!dto.getMemberPhone().matches("^1[3-9]\\d{9}$")) { + return "会员手机号格式不正确"; + } + if (StrUtil.isBlank(dto.getSchoolName())) { + return "学校不能为空"; + } + if (StrUtil.isBlank(dto.getGradeName())) { + return "年级不能为空"; + } + if (StrUtil.isBlank(dto.getClassName())) { + return "班级不能为空"; + } + return null; + } + + /** + * 创建失败项 + */ + private Map createFailItem(int row, String reason) { + Map item = new HashMap<>(); + item.put("row", row); + item.put("reason", reason); + return item; + } + + /** + * 查找或创建会员 + */ + private Long findOrCreateMember(String phone) { + // 先查找已有会员 + PgMember member = memberMapper.selectOne( + new LambdaQueryWrapper() + .eq(PgMember::getPhone, phone) + ); + if (member != null) { + return member.getMemberId(); + } + + // 不存在则创建新会员(身份为家长,初始密码123456) + PgMember newMember = new PgMember(); + newMember.setPhone(phone); + newMember.setNickname("家长" + phone.substring(7)); // 默认昵称 + newMember.setIdentityType("1"); // 家长 + newMember.setPassword(BCrypt.hashpw("123456")); // 初始密码 + newMember.setStatus("0"); // 正常 + newMember.setRegisterSource("4"); // 批量导入 + memberMapper.insert(newMember); + + return newMember.getMemberId(); + } + + /** + * 转换性别 + */ + private String convertGender(String gender) { + if (StrUtil.isBlank(gender)) { + return "0"; // 未知 + } + if ("男".equals(gender.trim())) { + return "1"; + } + if ("女".equals(gender.trim())) { + return "2"; + } + return "0"; // 未知 + } + + /** + * 解析出生日期 + */ + private Date parseBirthday(String birthday) { + if (StrUtil.isBlank(birthday)) { + return null; + } + try { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + return sdf.parse(birthday.trim()); + } catch (Exception e) { + return null; + } + } }