feat: 实现学生批量导入功能
1. 新增 POST /business/student/import 接口 2. 实现导入业务逻辑: - 校验必填字段(姓名、学号、会员手机号、学校、年级、班级) - 根据名称匹配学校、年级、班级 - 根据手机号查找或创建会员(不存在则自动创建家长账号) - 检查学号唯一性 - 返回导入结果(成功数、失败数、失败明细)
This commit is contained in:
parent
eb7ef037f0
commit
ea1524ea67
|
|
@ -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<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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<String, Object> importStudents(List<StudentImportDto> dataList);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue