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 20753c1..457690c 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 @@ -12,6 +12,7 @@ import org.dromara.pangu.school.domain.PgSchool; import org.dromara.pangu.school.domain.vo.SchoolTreeNode; import org.dromara.pangu.school.service.IPgSchoolService; import org.dromara.pangu.student.domain.PgStudent; +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.*; @@ -34,7 +35,7 @@ public class PgStudentController extends BaseController { @SaCheckPermission("business:student:list") @GetMapping("/list") - public TableDataInfo list(PgStudent student, PageQuery pageQuery) { + public TableDataInfo list(PgStudent student, PageQuery pageQuery) { return studentService.selectPageList(student, pageQuery); } @@ -48,7 +49,7 @@ public class PgStudentController extends BaseController { @SaCheckPermission("business:student:query") @GetMapping("/{studentId}") - public R getInfo(@PathVariable Long studentId) { + public R getInfo(@PathVariable Long studentId) { return R.ok(studentService.selectById(studentId)); } diff --git a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/student/domain/vo/StudentVo.java b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/student/domain/vo/StudentVo.java new file mode 100644 index 0000000..1f17187 --- /dev/null +++ b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/student/domain/vo/StudentVo.java @@ -0,0 +1,77 @@ +package org.dromara.pangu.student.domain.vo; + +import lombok.Data; + +import java.util.Date; + +/** + * 学生视图对象(包含关联数据) + * + * @author pangu + */ +@Data +public class StudentVo { + + private Long studentId; + + private String studentName; + + private String studentNo; + + /** + * 性别(0未知 1男 2女) + */ + private String gender; + + private Date birthday; + + private Long regionId; + + private String regionPath; + + private Long schoolId; + + /** + * 学校名称 + */ + private String schoolName; + + private Long schoolGradeId; + + /** + * 年级名称 + */ + private String gradeName; + + private Long schoolClassId; + + /** + * 班级名称 + */ + private String className; + + private Long subjectId; + + /** + * 学科名称 + */ + private String subjectName; + + private Long memberId; + + /** + * 会员昵称 + */ + private String memberNickname; + + /** + * 会员手机号 + */ + private String memberPhone; + + private String status; + + private Date createTime; + + private String remark; +} 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 3cd0363..689a80e 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,6 +3,7 @@ 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.vo.StudentVo; import java.util.List; @@ -12,9 +13,9 @@ import java.util.List; * @author pangu */ public interface IPgStudentService { - TableDataInfo selectPageList(PgStudent student, PageQuery pageQuery); + TableDataInfo selectPageList(PgStudent student, PageQuery pageQuery); List selectList(PgStudent student); - PgStudent selectById(Long studentId); + StudentVo selectById(Long studentId); int insert(PgStudent student); int update(PgStudent student); int deleteByIds(Long[] studentIds); 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 d4754f1..72b765d 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 @@ -1,18 +1,36 @@ package org.dromara.pangu.student.service.impl; +import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.util.StrUtil; 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 org.dromara.common.mybatis.core.page.PageQuery; import org.dromara.common.mybatis.core.page.TableDataInfo; +import org.dromara.pangu.base.domain.PgClass; +import org.dromara.pangu.base.domain.PgGrade; +import org.dromara.pangu.base.mapper.PgClassMapper; +import org.dromara.pangu.base.mapper.PgGradeMapper; +import org.dromara.pangu.member.domain.PgMember; +import org.dromara.pangu.member.mapper.PgMemberMapper; +import org.dromara.pangu.school.domain.PgSchool; +import org.dromara.pangu.school.domain.PgSchoolClass; +import org.dromara.pangu.school.domain.PgSchoolGrade; +import org.dromara.pangu.school.mapper.PgSchoolClassMapper; +import org.dromara.pangu.school.mapper.PgSchoolGradeMapper; +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.vo.StudentVo; import org.dromara.pangu.student.mapper.PgStudentMapper; import org.dromara.pangu.student.service.IPgStudentService; import org.springframework.stereotype.Service; -import java.util.Arrays; -import java.util.List; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; /** * 学生 Service 实现 @@ -24,12 +42,25 @@ import java.util.List; public class PgStudentServiceImpl implements IPgStudentService { private final PgStudentMapper baseMapper; + private final PgSchoolMapper schoolMapper; + private final PgSchoolGradeMapper schoolGradeMapper; + private final PgSchoolClassMapper schoolClassMapper; + private final PgGradeMapper gradeMapper; + private final PgClassMapper classMapper; + private final PgSubjectMapper subjectMapper; + private final PgMemberMapper memberMapper; @Override - public TableDataInfo selectPageList(PgStudent student, PageQuery pageQuery) { + public TableDataInfo selectPageList(PgStudent student, PageQuery pageQuery) { LambdaQueryWrapper lqw = buildQueryWrapper(student); Page page = baseMapper.selectPage(pageQuery.build(), lqw); - return TableDataInfo.build(page); + + // 转换为 VO 并填充关联数据 + List voList = convertToVoList(page.getRecords()); + + Page voPage = new Page<>(page.getCurrent(), page.getSize(), page.getTotal()); + voPage.setRecords(voList); + return TableDataInfo.build(voPage); } @Override @@ -38,8 +69,12 @@ public class PgStudentServiceImpl implements IPgStudentService { } @Override - public PgStudent selectById(Long studentId) { - return baseMapper.selectById(studentId); + public StudentVo selectById(Long studentId) { + PgStudent student = baseMapper.selectById(studentId); + if (student == null) { + return null; + } + return convertToVo(student); } @Override @@ -61,6 +96,7 @@ public class PgStudentServiceImpl implements IPgStudentService { LambdaQueryWrapper lqw = new LambdaQueryWrapper<>(); lqw.like(StrUtil.isNotBlank(student.getStudentName()), PgStudent::getStudentName, student.getStudentName()); lqw.like(StrUtil.isNotBlank(student.getStudentNo()), PgStudent::getStudentNo, student.getStudentNo()); + lqw.eq(StrUtil.isNotBlank(student.getGender()), PgStudent::getGender, student.getGender()); lqw.eq(student.getSchoolId() != null, PgStudent::getSchoolId, student.getSchoolId()); lqw.eq(student.getSchoolGradeId() != null, PgStudent::getSchoolGradeId, student.getSchoolGradeId()); lqw.eq(student.getSchoolClassId() != null, PgStudent::getSchoolClassId, student.getSchoolClassId()); @@ -69,20 +105,130 @@ public class PgStudentServiceImpl implements IPgStudentService { return lqw; } + /** + * 批量转换为 VO 并填充关联数据 + */ + private List convertToVoList(List students) { + if (students == null || students.isEmpty()) { + return Collections.emptyList(); + } + + // 收集所有需要查询的 ID + Set schoolIds = new HashSet<>(); + Set schoolGradeIds = new HashSet<>(); + Set schoolClassIds = new HashSet<>(); + Set subjectIds = new HashSet<>(); + Set memberIds = new HashSet<>(); + + for (PgStudent s : students) { + if (s.getSchoolId() != null) schoolIds.add(s.getSchoolId()); + if (s.getSchoolGradeId() != null) schoolGradeIds.add(s.getSchoolGradeId()); + if (s.getSchoolClassId() != null) schoolClassIds.add(s.getSchoolClassId()); + if (s.getSubjectId() != null) subjectIds.add(s.getSubjectId()); + if (s.getMemberId() != null) memberIds.add(s.getMemberId()); + } + + // 批量查询关联数据 + Map schoolMap = schoolIds.isEmpty() ? Collections.emptyMap() : + schoolMapper.selectByIds(schoolIds).stream() + .collect(Collectors.toMap(PgSchool::getSchoolId, Function.identity())); + + Map schoolGradeMap = schoolGradeIds.isEmpty() ? Collections.emptyMap() : + schoolGradeMapper.selectByIds(schoolGradeIds).stream() + .collect(Collectors.toMap(PgSchoolGrade::getId, Function.identity())); + + Map schoolClassMap = schoolClassIds.isEmpty() ? Collections.emptyMap() : + schoolClassMapper.selectByIds(schoolClassIds).stream() + .collect(Collectors.toMap(PgSchoolClass::getId, Function.identity())); + + Map subjectMap = subjectIds.isEmpty() ? Collections.emptyMap() : + subjectMapper.selectByIds(subjectIds).stream() + .collect(Collectors.toMap(PgSubject::getSubjectId, Function.identity())); + + Map memberMap = memberIds.isEmpty() ? Collections.emptyMap() : + memberMapper.selectByIds(memberIds).stream() + .collect(Collectors.toMap(PgMember::getMemberId, Function.identity())); + + // 收集基础年级和班级 ID + Set baseGradeIds = schoolGradeMap.values().stream() + .map(PgSchoolGrade::getGradeId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + Set baseClassIds = schoolClassMap.values().stream() + .map(PgSchoolClass::getClassId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + + // 查询基础年级和班级名称 + Map gradeNameMap = baseGradeIds.isEmpty() ? Collections.emptyMap() : + gradeMapper.selectByIds(baseGradeIds).stream() + .collect(Collectors.toMap(PgGrade::getGradeId, PgGrade::getGradeName)); + + Map classNameMap = baseClassIds.isEmpty() ? Collections.emptyMap() : + classMapper.selectByIds(baseClassIds).stream() + .collect(Collectors.toMap(PgClass::getClassId, PgClass::getClassName)); + + // 转换为 VO + List voList = new ArrayList<>(); + for (PgStudent s : students) { + StudentVo vo = new StudentVo(); + BeanUtil.copyProperties(s, vo); + + // 填充学校名称 + PgSchool school = schoolMap.get(s.getSchoolId()); + if (school != null) { + vo.setSchoolName(school.getSchoolName()); + } + + // 填充年级名称 + PgSchoolGrade schoolGrade = schoolGradeMap.get(s.getSchoolGradeId()); + if (schoolGrade != null && schoolGrade.getGradeId() != null) { + vo.setGradeName(gradeNameMap.get(schoolGrade.getGradeId())); + } + + // 填充班级名称 + PgSchoolClass schoolClass = schoolClassMap.get(s.getSchoolClassId()); + if (schoolClass != null && schoolClass.getClassId() != null) { + vo.setClassName(classNameMap.get(schoolClass.getClassId())); + } + + // 填充学科名称 + PgSubject subject = subjectMap.get(s.getSubjectId()); + if (subject != null) { + vo.setSubjectName(subject.getSubjectName()); + } + + // 填充会员信息 + PgMember member = memberMap.get(s.getMemberId()); + if (member != null) { + vo.setMemberNickname(member.getNickname()); + vo.setMemberPhone(member.getPhone()); + } + + voList.add(vo); + } + + return voList; + } + + /** + * 单个转换为 VO + */ + private StudentVo convertToVo(PgStudent student) { + List voList = convertToVoList(Collections.singletonList(student)); + return voList.isEmpty() ? null : voList.get(0); + } + @Override public TableDataInfo selectAvailableStudents(String studentName, String studentNo, Long memberId, Long schoolId, PageQuery pageQuery) { LambdaQueryWrapper lqw = new LambdaQueryWrapper<>(); - // 姓名模糊查询 lqw.like(StrUtil.isNotBlank(studentName), PgStudent::getStudentName, studentName); - // 学号模糊查询 lqw.like(StrUtil.isNotBlank(studentNo), PgStudent::getStudentNo, studentNo); - // 可绑定条件:未被绑定 或 已绑定当前会员 lqw.and(wrapper -> wrapper .isNull(PgStudent::getMemberId) .or() .eq(memberId != null, PgStudent::getMemberId, memberId) ); - // 教师身份限制:只能选本校学生 lqw.eq(schoolId != null, PgStudent::getSchoolId, schoolId); lqw.orderByDesc(PgStudent::getCreateTime); @@ -122,9 +268,8 @@ public class PgStudentServiceImpl implements IPgStudentService { if (studentId == null) { return 0; } - // 使用原生SQL更新memberId为null return baseMapper.update(null, - new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper() + new LambdaUpdateWrapper() .eq(PgStudent::getStudentId, studentId) .set(PgStudent::getMemberId, null) ); diff --git a/frontend/ruoyi-ui/src/views/business/student/components/ImportDialog.vue b/frontend/ruoyi-ui/src/views/business/student/components/ImportDialog.vue index 0322f85..b00b547 100644 --- a/frontend/ruoyi-ui/src/views/business/student/components/ImportDialog.vue +++ b/frontend/ruoyi-ui/src/views/business/student/components/ImportDialog.vue @@ -2,7 +2,7 @@ @@ -16,13 +16,16 @@
1. 请先下载导入模板,按模板格式填写数据
2. 支持 xlsx、xls 格式文件,单次最多导入500条
- 3. 必填字段:姓名、学校、年级、班级 + 3. 必填字段:姓名、学号、用户手机号、区域、学校、年级、班级
+ 4. 若用户手机号已存在则挂载到已有用户,否则自动创建家长账号
- 下载模板 + + 下载模板 +
- +
将文件拖到此处,或点击上传
+ +
+ +
+ {{ uploadProgress === 100 ? '上传完成,正在处理数据...' : '正在上传...' }} +
+
+
+
失败明细:
- - + +
@@ -70,40 +83,45 @@ * 学生批量导入弹窗 * @author pangu */ -import { Download, Upload } from '@element-plus/icons-vue' +import { Download, UploadFilled } from '@element-plus/icons-vue' import { ElMessage } from 'element-plus' import { computed, ref } from 'vue' +import { getToken } from '@/utils/auth' -const props = defineProps({ - modelValue: { - type: Boolean, - default: false - } -}) - -const emit = defineEmits(['update:modelValue', 'success']) - -const visible = computed({ - get: () => props.modelValue, - set: (val) => emit('update:modelValue', val) -}) +const emit = defineEmits(['success']) +const visible = ref(false) const uploadRef = ref(null) const importResult = ref(null) +const downloadLoading = ref(false) +const uploading = ref(false) +const uploadProgress = ref(0) -// 上传地址 -const uploadUrl = '/business/student/import' +// 上传地址(使用完整路径) +const uploadUrl = computed(() => { + return import.meta.env.VITE_APP_BASE_API + '/business/student/import' +}) // 上传请求头 const uploadHeaders = computed(() => { - const token = localStorage.getItem('token') + const token = getToken() return token ? { Authorization: 'Bearer ' + token } : {} }) // 下载模板 -const handleDownloadTemplate = () => { - // 实际应该调用后端接口下载模板 - ElMessage.info('模板下载功能需要对接后端下载接口') +const handleDownloadTemplate = async () => { + downloadLoading.value = true + try { + // 使用 window.open 直接下载 + const baseApi = import.meta.env.VITE_APP_BASE_API || '' + const token = getToken() + const url = `${baseApi}/business/student/template?token=${encodeURIComponent(token)}` + window.open(url, '_blank') + } catch (e) { + ElMessage.error('下载模板失败') + } finally { + downloadLoading.value = false + } } // 上传前校验 @@ -119,15 +137,25 @@ const beforeUpload = (file) => { return false } importResult.value = null + uploading.value = true + uploadProgress.value = 0 return true } +// 上传进度 +const handleProgress = (event) => { + uploadProgress.value = Math.round(event.percent) +} + // 上传成功 const handleSuccess = (response) => { + uploading.value = false + uploadProgress.value = 0 + if (response.code === 200) { importResult.value = response.data if (response.data.failCount === 0) { - ElMessage.success('导入成功') + ElMessage.success(`导入成功,共导入 ${response.data.successCount} 条数据`) emit('success') } else { ElMessage.warning('部分数据导入失败,请查看失败原因') @@ -138,9 +166,31 @@ const handleSuccess = (response) => { } // 上传失败 -const handleError = () => { +const handleError = (error) => { + uploading.value = false + uploadProgress.value = 0 + console.error('上传失败:', error) ElMessage.error('文件上传失败,请重试') } + +// 关闭弹窗 +const handleClose = () => { + visible.value = false + // 清理状态 + importResult.value = null + uploading.value = false + uploadProgress.value = 0 +} + +// 打开弹窗 +const open = () => { + visible.value = true + importResult.value = null + uploading.value = false + uploadProgress.value = 0 +} + +defineExpose({ open })