feat: 实现学生批量导入功能

1. 新增 POST /business/student/import 接口
2. 实现导入业务逻辑:
   - 校验必填字段(姓名、学号、会员手机号、学校、年级、班级)
   - 根据名称匹配学校、年级、班级
   - 根据手机号查找或创建会员(不存在则自动创建家长账号)
   - 检查学号唯一性
   - 返回导入结果(成功数、失败数、失败明细)
This commit is contained in:
神码-方晓辉 2026-02-02 20:11:56 +08:00
parent eb7ef037f0
commit ea1524ea67
3 changed files with 245 additions and 0 deletions

View File

@ -19,9 +19,12 @@ import org.dromara.pangu.student.domain.vo.StudentVo;
import org.dromara.pangu.student.service.IPgStudentService; import org.dromara.pangu.student.service.IPgStudentService;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* 学生管理 * 学生管理
@ -124,4 +127,20 @@ public class PgStudentController extends BaseController {
ExcelUtil.exportExcel(sampleData, "学生导入模板", StudentImportDto.class, response); ExcelUtil.exportExcel(sampleData, "学生导入模板", StudentImportDto.class, response);
} }
/**
* 批量导入学生
*/
@SaCheckPermission("business:student:import")
@Log(title = "学生管理", businessType = BusinessType.IMPORT)
@PostMapping("/import")
public R<Map<String, Object>> importData(MultipartFile file) throws Exception {
// 解析 Excel 数据
List<StudentImportDto> dataList = ExcelUtil.importExcel(file.getInputStream(), StudentImportDto.class);
// 调用服务层处理导入
Map<String, Object> result = studentService.importStudents(dataList);
return R.ok(result);
}
} }

View File

@ -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.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo; import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.pangu.student.domain.PgStudent; 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.domain.vo.StudentVo;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* 学生 Service 接口 * 学生 Service 接口
@ -44,4 +46,11 @@ public interface IPgStudentService {
* 解绑学生 * 解绑学生
*/ */
int unbindStudent(Long studentId); int unbindStudent(Long studentId);
/**
* 批量导入学生
* @param dataList Excel 导入的数据列表
* @return 导入结果 {successCount, failCount, failList}
*/
Map<String, Object> importStudents(List<StudentImportDto> dataList);
} }

View File

@ -2,10 +2,12 @@ package org.dromara.pangu.student.service.impl;
import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil; 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.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.mybatis.core.page.PageQuery; import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo; import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.pangu.base.domain.PgClass; 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.domain.PgSubject;
import org.dromara.pangu.base.mapper.PgSubjectMapper; import org.dromara.pangu.base.mapper.PgSubjectMapper;
import org.dromara.pangu.student.domain.PgStudent; 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.domain.vo.StudentVo;
import org.dromara.pangu.student.mapper.PgStudentMapper; import org.dromara.pangu.student.mapper.PgStudentMapper;
import org.dromara.pangu.student.service.IPgStudentService; import org.dromara.pangu.student.service.IPgStudentService;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.text.SimpleDateFormat;
import java.util.*; import java.util.*;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -37,6 +42,7 @@ import java.util.stream.Collectors;
* *
* @author pangu * @author pangu
*/ */
@Slf4j
@RequiredArgsConstructor @RequiredArgsConstructor
@Service @Service
public class PgStudentServiceImpl implements IPgStudentService { public class PgStudentServiceImpl implements IPgStudentService {
@ -274,4 +280,215 @@ public class PgStudentServiceImpl implements IPgStudentService {
.set(PgStudent::getMemberId, null) .set(PgStudent::getMemberId, null)
); );
} }
@Override
@Transactional(rollbackFor = Exception.class)
public Map<String, Object> importStudents(List<StudentImportDto> dataList) {
int successCount = 0;
List<Map<String, Object>> failList = new ArrayList<>();
// 预先查询所有学校方便匹配
List<PgSchool> allSchools = schoolMapper.selectList(new LambdaQueryWrapper<>());
Map<String, PgSchool> schoolNameMap = allSchools.stream()
.collect(Collectors.toMap(PgSchool::getSchoolName, Function.identity(), (a, b) -> a));
// 预先查询所有年级
List<PgGrade> allGrades = gradeMapper.selectList(new LambdaQueryWrapper<>());
Map<String, PgGrade> gradeNameMap = allGrades.stream()
.collect(Collectors.toMap(PgGrade::getGradeName, Function.identity(), (a, b) -> a));
// 预先查询所有班级
List<PgClass> allClasses = classMapper.selectList(new LambdaQueryWrapper<>());
Map<String, PgClass> 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<PgSchoolGrade>()
.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<PgSchoolClass>()
.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<PgStudent>()
.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<String, Object> 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<String, Object> createFailItem(int row, String reason) {
Map<String, Object> item = new HashMap<>();
item.put("row", row);
item.put("reason", reason);
return item;
}
/**
* 查找或创建会员
*/
private Long findOrCreateMember(String phone) {
// 先查找已有会员
PgMember member = memberMapper.selectOne(
new LambdaQueryWrapper<PgMember>()
.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;
}
}
} }