# 学生管理模块 - 后端技术方案 --- | 文档信息 | 内容 | |---------|------| | **文档版本** | V1.0 | | **项目名称** | 盘古用户平台(Pangu User Platform) | | **模块名称** | 学生管理模块 - 后端 | | **编写团队** | pangu | | **创建日期** | 2026-01-31 | | **审核状态** | 待审核 | --- ## 目录 1. [技术栈说明](#1-技术栈说明) 2. [模块结构](#2-模块结构) 3. [数据库设计](#3-数据库设计) 4. [实体设计](#4-实体设计) 5. [接口设计](#5-接口设计) 6. [服务层设计](#6-服务层设计) 7. [批量导入设计](#7-批量导入设计) 8. [数据权限控制](#8-数据权限控制) 9. [异常处理](#9-异常处理) 10. [单元测试](#10-单元测试) --- ## 1. 技术栈说明 | 技术 | 版本 | 用途 | |------|------|------| | Spring Boot | 3.3.x | 应用框架 | | Spring Security | 6.x | 安全框架 | | MyBatis Plus | 3.5.x | ORM框架 | | EasyExcel | 4.x | Excel处理 | | Hutool | 5.x | 工具库 | | Lombok | 1.18.x | 代码简化 | | Validation | 3.x | 参数校验 | | JDK | 17+ | 运行环境 | --- ## 2. 模块结构 ``` pangu-system/ ├── src/main/java/com/pangu/system/ │ ├── controller/ │ │ └── StudentController.java # 学生管理控制器 │ ├── domain/ │ │ ├── Student.java # 学生实体 │ │ ├── dto/ │ │ │ ├── StudentDTO.java # 学生传输对象 │ │ │ ├── StudentQueryDTO.java # 学生查询条件 │ │ │ └── StudentImportDTO.java # 学生导入对象 │ │ └── vo/ │ │ ├── StudentVO.java # 学生视图对象 │ │ ├── StudentDetailVO.java # 学生详情视图对象 │ │ └── ImportResultVO.java # 导入结果视图对象 │ ├── mapper/ │ │ └── StudentMapper.java # 学生Mapper接口 │ ├── service/ │ │ ├── IStudentService.java # 学生服务接口 │ │ └── impl/ │ │ └── StudentServiceImpl.java # 学生服务实现 │ └── listener/ │ └── StudentImportListener.java # Excel导入监听器 └── src/main/resources/ └── mapper/ └── StudentMapper.xml # MyBatis映射文件 ``` --- ## 3. 数据库设计 ### 3.1 学生表(pg_student) ```sql CREATE TABLE `pg_student` ( `student_id` bigint NOT NULL AUTO_INCREMENT COMMENT '学生ID', `student_name` varchar(50) NOT NULL COMMENT '学生姓名', `student_no` varchar(32) DEFAULT NULL COMMENT '学号', `gender` char(1) DEFAULT '0' COMMENT '性别(0未知 1男 2女)', `birthday` date DEFAULT NULL COMMENT '出生年月', `region_id` bigint NOT NULL COMMENT '所属区域ID', `region_path` varchar(200) DEFAULT NULL COMMENT '区域路径', `school_id` bigint NOT NULL COMMENT '所属学校ID', `school_grade_id` bigint NOT NULL COMMENT '所属学校年级ID', `school_class_id` bigint NOT NULL COMMENT '所属学校班级ID', `subject_id` bigint DEFAULT NULL COMMENT '学科ID', `member_id` bigint NOT NULL COMMENT '归属会员ID', `status` char(1) DEFAULT '0' COMMENT '状态(0正常 1停用)', `create_by` varchar(64) DEFAULT '' COMMENT '创建者', `create_time` datetime DEFAULT NULL COMMENT '创建时间', `update_by` varchar(64) DEFAULT '' COMMENT '更新者', `update_time` datetime DEFAULT NULL COMMENT '更新时间', `del_flag` char(1) DEFAULT '0' COMMENT '删除标志(0存在 1删除)', `remark` varchar(500) DEFAULT NULL COMMENT '备注', PRIMARY KEY (`student_id`), UNIQUE KEY `uk_student_no` (`student_no`), KEY `idx_member_id` (`member_id`), KEY `idx_school_id` (`school_id`), KEY `idx_school_class_id` (`school_class_id`), KEY `idx_student_name` (`student_name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学生表'; ``` ### 3.2 初始化数据 ```sql -- 学生示例数据 INSERT INTO pg_student (student_id, student_name, student_no, gender, birthday, region_id, region_path, school_id, school_grade_id, school_class_id, member_id, status, create_time) VALUES (1, '张小明', 'STU20260001', '1', '2015-03-15', 111, '湖北省-武汉市-武昌区', 1, 1, 1, 1, '0', NOW()), (2, '张小红', 'STU20260002', '2', '2017-06-20', 111, '湖北省-武汉市-武昌区', 3, 4, 1, 1, '0', NOW()), (3, '李明明', 'STU20260003', '1', '2015-09-10', 111, '湖北省-武汉市-武昌区', 1, 1, 2, 2, '0', NOW()); ``` --- ## 4. 实体设计 ### 4.1 Student.java - 学生实体 ```java package com.pangu.system.domain; import com.baomidou.mybatisplus.annotation.*; import com.fasterxml.jackson.annotation.JsonFormat; import lombok.Data; import java.io.Serializable; import java.time.LocalDate; import java.time.LocalDateTime; /** * 学生实体 * * @author pangu */ @Data @TableName("pg_student") public class Student implements Serializable { private static final long serialVersionUID = 1L; /** 学生ID */ @TableId(type = IdType.AUTO) private Long studentId; /** 学生姓名 */ private String studentName; /** 学号 */ private String studentNo; /** 性别(0未知 1男 2女) */ private String gender; /** 出生年月 */ @JsonFormat(pattern = "yyyy-MM") private LocalDate birthday; /** 所属区域ID */ private Long regionId; /** 区域路径 */ private String regionPath; /** 所属学校ID */ private Long schoolId; /** 所属学校年级ID */ private Long schoolGradeId; /** 所属学校班级ID */ private Long schoolClassId; /** 学科ID */ private Long subjectId; /** 归属会员ID */ private Long memberId; /** 状态(0正常 1停用) */ private String status; /** 创建者 */ @TableField(fill = FieldFill.INSERT) private String createBy; /** 创建时间 */ @TableField(fill = FieldFill.INSERT) @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime createTime; /** 更新者 */ @TableField(fill = FieldFill.INSERT_UPDATE) private String updateBy; /** 更新时间 */ @TableField(fill = FieldFill.INSERT_UPDATE) @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime updateTime; /** 删除标志(0存在 1删除) */ @TableLogic private String delFlag; /** 备注 */ private String remark; } ``` ### 4.2 StudentDTO.java - 学生传输对象 ```java package com.pangu.system.domain.dto; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; import lombok.Data; import java.time.LocalDate; import java.util.List; /** * 学生传输对象 * * @author pangu */ @Data public class StudentDTO { /** 学生ID(编辑时必填) */ private Long studentId; /** 学生姓名 */ @NotBlank(message = "学生姓名不能为空") @Size(max = 50, message = "学生姓名最大50个字符") private String studentName; /** 学号 */ @Size(max = 32, message = "学号最大32个字符") private String studentNo; /** 性别(0未知 1男 2女) */ private String gender; /** 出生年月 */ private LocalDate birthday; /** 区域ID数组(级联选择器) */ private List regionIds; /** 所属区域ID */ @NotNull(message = "请选择区域") private Long regionId; /** 所属学校ID */ @NotNull(message = "请选择学校") private Long schoolId; /** 所属学校年级ID */ @NotNull(message = "请选择年级") private Long schoolGradeId; /** 所属学校班级ID */ @NotNull(message = "请选择班级") private Long schoolClassId; /** 学科ID */ private Long subjectId; /** 归属会员ID */ @NotNull(message = "请选择归属用户") private Long memberId; /** 备注 */ private String remark; } ``` ### 4.3 StudentQueryDTO.java - 学生查询条件 ```java package com.pangu.system.domain.dto; import lombok.Data; /** * 学生查询条件 * * @author pangu */ @Data public class StudentQueryDTO { /** 学生姓名(模糊查询) */ private String studentName; /** 学号 */ private String studentNo; /** 性别 */ private String gender; /** 学校ID */ private Long schoolId; /** 学校年级ID */ private Long schoolGradeId; /** 学校班级ID */ private Long schoolClassId; /** 学科ID */ private Long subjectId; /** 归属用户手机号 */ private String memberPhone; /** 页码 */ private Integer pageNum = 1; /** 每页条数 */ private Integer pageSize = 10; } ``` ### 4.4 StudentVO.java - 学生视图对象 ```java package com.pangu.system.domain.vo; import com.fasterxml.jackson.annotation.JsonFormat; import lombok.Data; import java.time.LocalDate; import java.time.LocalDateTime; /** * 学生视图对象 * * @author pangu */ @Data public class StudentVO { /** 学生ID */ private Long studentId; /** 学生姓名 */ private String studentName; /** 学号 */ private String studentNo; /** 性别(0未知 1男 2女) */ private String gender; /** 出生年月 */ @JsonFormat(pattern = "yyyy-MM") private LocalDate birthday; /** 区域路径 */ private String regionPath; /** 学校名称 */ private String schoolName; /** 年级名称 */ private String gradeName; /** 班级名称 */ private String className; /** 学科名称 */ private String subjectName; /** 归属会员ID */ private Long memberId; /** 会员昵称 */ private String memberNickname; /** 会员手机号(脱敏) */ private String memberPhone; /** 创建时间 */ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime createTime; } ``` ### 4.5 StudentImportDTO.java - 学生导入对象 ```java package com.pangu.system.domain.dto; import com.alibaba.excel.annotation.ExcelProperty; import lombok.Data; /** * 学生导入对象(Excel解析) * * @author pangu */ @Data public class StudentImportDTO { @ExcelProperty(value = "姓名", index = 0) private String studentName; @ExcelProperty(value = "学号", index = 1) private String studentNo; @ExcelProperty(value = "用户手机号", index = 2) private String memberPhone; @ExcelProperty(value = "区域", index = 3) private String regionPath; @ExcelProperty(value = "学校", index = 4) private String schoolName; @ExcelProperty(value = "年级", index = 5) private String gradeName; @ExcelProperty(value = "班级", index = 6) private String className; @ExcelProperty(value = "性别", index = 7) private String gender; @ExcelProperty(value = "出生年月", index = 8) private String birthday; /** 行号(用于错误提示) */ private Integer rowIndex; } ``` ### 4.6 ImportResultVO.java - 导入结果视图对象 ```java package com.pangu.system.domain.vo; import lombok.Data; import java.util.ArrayList; import java.util.List; /** * 导入结果视图对象 * * @author pangu */ @Data public class ImportResultVO { /** 成功数量 */ private int successCount = 0; /** 失败数量 */ private int failCount = 0; /** 失败列表 */ private List failList = new ArrayList<>(); /** * 添加失败记录 */ public void addFail(int row, String reason) { this.failCount++; this.failList.add(new FailItem(row, reason)); } /** * 增加成功数量 */ public void addSuccess() { this.successCount++; } /** * 失败项 */ @Data public static class FailItem { private int row; private String reason; public FailItem(int row, String reason) { this.row = row; this.reason = reason; } } } ``` --- ## 5. 接口设计 ### 5.1 StudentController.java ```java package com.pangu.system.controller; import com.pangu.common.core.controller.BaseController; import com.pangu.common.core.domain.AjaxResult; import com.pangu.common.core.page.TableDataInfo; import com.pangu.system.domain.dto.StudentDTO; import com.pangu.system.domain.dto.StudentQueryDTO; import com.pangu.system.service.IStudentService; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; /** * 学生管理控制器 * * @author pangu */ @RestController @RequestMapping("/api/student") @RequiredArgsConstructor @Validated public class StudentController extends BaseController { private final IStudentService studentService; /** * 查询学生列表 */ @PreAuthorize("@ss.hasPermi('system:student:list')") @GetMapping("/list") public TableDataInfo list(StudentQueryDTO query) { startPage(); return getDataTable(studentService.selectStudentList(query)); } /** * 获取学生详情 */ @PreAuthorize("@ss.hasPermi('system:student:query')") @GetMapping("/{studentId}") public AjaxResult getInfo(@PathVariable Long studentId) { return success(studentService.selectStudentById(studentId)); } /** * 新增学生 */ @PreAuthorize("@ss.hasPermi('system:student:add')") @PostMapping public AjaxResult add(@Valid @RequestBody StudentDTO dto) { return toAjax(studentService.insertStudent(dto)); } /** * 修改学生 */ @PreAuthorize("@ss.hasPermi('system:student:edit')") @PutMapping public AjaxResult edit(@Valid @RequestBody StudentDTO dto) { return toAjax(studentService.updateStudent(dto)); } /** * 删除学生 */ @PreAuthorize("@ss.hasPermi('system:student:remove')") @DeleteMapping("/{studentId}") public AjaxResult remove(@PathVariable Long studentId) { return toAjax(studentService.deleteStudentById(studentId)); } /** * 下载导入模板 */ @PreAuthorize("@ss.hasPermi('system:student:import')") @GetMapping("/template") public void downloadTemplate(HttpServletResponse response) { studentService.downloadTemplate(response); } /** * 批量导入学生 */ @PreAuthorize("@ss.hasPermi('system:student:import')") @PostMapping("/import") public AjaxResult importData(MultipartFile file) { return success(studentService.importStudents(file)); } } ``` ### 5.2 接口权限配置 | 接口 | 权限标识 | 说明 | |------|----------|------| | GET /api/student/list | system:student:list | 查询学生列表 | | GET /api/student/{id} | system:student:query | 查询学生详情 | | POST /api/student | system:student:add | 新增学生 | | PUT /api/student | system:student:edit | 修改学生 | | DELETE /api/student/{id} | system:student:remove | 删除学生 | | GET /api/student/template | system:student:import | 下载导入模板 | | POST /api/student/import | system:student:import | 批量导入 | --- ## 6. 服务层设计 ### 6.1 IStudentService.java - 服务接口 ```java package com.pangu.system.service; import com.pangu.system.domain.dto.StudentDTO; import com.pangu.system.domain.dto.StudentQueryDTO; import com.pangu.system.domain.vo.ImportResultVO; import com.pangu.system.domain.vo.StudentDetailVO; import com.pangu.system.domain.vo.StudentVO; import jakarta.servlet.http.HttpServletResponse; import org.springframework.web.multipart.MultipartFile; import java.util.List; /** * 学生服务接口 * * @author pangu */ public interface IStudentService { /** * 查询学生列表 * * @param query 查询条件 * @return 学生列表 */ List selectStudentList(StudentQueryDTO query); /** * 根据ID查询学生详情 * * @param studentId 学生ID * @return 学生详情 */ StudentDetailVO selectStudentById(Long studentId); /** * 新增学生 * * @param dto 学生数据 * @return 影响行数 */ int insertStudent(StudentDTO dto); /** * 修改学生 * * @param dto 学生数据 * @return 影响行数 */ int updateStudent(StudentDTO dto); /** * 删除学生 * * @param studentId 学生ID * @return 影响行数 */ int deleteStudentById(Long studentId); /** * 下载导入模板 * * @param response HTTP响应 */ void downloadTemplate(HttpServletResponse response); /** * 批量导入学生 * * @param file Excel文件 * @return 导入结果 */ ImportResultVO importStudents(MultipartFile file); /** * 校验学号唯一性 * * @param studentNo 学号 * @param studentId 学生ID(编辑时排除自己) * @return 是否唯一 */ boolean checkStudentNoUnique(String studentNo, Long studentId); } ``` ### 6.2 StudentServiceImpl.java - 服务实现 ```java package com.pangu.system.service.impl; import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.util.StrUtil; import com.alibaba.excel.EasyExcel; import com.alibaba.excel.context.AnalysisContext; import com.alibaba.excel.event.AnalysisEventListener; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.pangu.common.core.exception.ServiceException; import com.pangu.common.security.utils.SecurityUtils; import com.pangu.system.domain.Member; import com.pangu.system.domain.Student; import com.pangu.system.domain.dto.StudentDTO; import com.pangu.system.domain.dto.StudentImportDTO; import com.pangu.system.domain.dto.StudentQueryDTO; import com.pangu.system.domain.vo.ImportResultVO; import com.pangu.system.domain.vo.StudentDetailVO; import com.pangu.system.domain.vo.StudentVO; import com.pangu.system.mapper.StudentMapper; import com.pangu.system.service.*; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.List; /** * 学生服务实现 * * @author pangu */ @Slf4j @Service @RequiredArgsConstructor public class StudentServiceImpl extends ServiceImpl implements IStudentService { private final StudentMapper studentMapper; private final IMemberService memberService; private final ISchoolService schoolService; private final IRegionService regionService; private final IGradeService gradeService; private final IClassService classService; /** * 查询学生列表 */ @Override public List selectStudentList(StudentQueryDTO query) { return studentMapper.selectStudentVOList(query); } /** * 根据ID查询学生详情 */ @Override public StudentDetailVO selectStudentById(Long studentId) { return studentMapper.selectStudentDetailById(studentId); } /** * 新增学生 */ @Override @Transactional(rollbackFor = Exception.class) public int insertStudent(StudentDTO dto) { // 校验学号唯一性 if (StrUtil.isNotBlank(dto.getStudentNo()) && !checkStudentNoUnique(dto.getStudentNo(), null)) { throw new ServiceException("学号已存在"); } // 构建区域路径 String regionPath = regionService.buildRegionPath(dto.getRegionId()); Student student = new Student(); BeanUtil.copyProperties(dto, student); student.setRegionPath(regionPath); student.setCreateBy(SecurityUtils.getUsername()); return studentMapper.insert(student); } /** * 修改学生 */ @Override @Transactional(rollbackFor = Exception.class) public int updateStudent(StudentDTO dto) { // 校验学生存在 Student existStudent = studentMapper.selectById(dto.getStudentId()); if (existStudent == null) { throw new ServiceException("学生不存在"); } // 校验学号唯一性 if (StrUtil.isNotBlank(dto.getStudentNo()) && !checkStudentNoUnique(dto.getStudentNo(), dto.getStudentId())) { throw new ServiceException("学号已存在"); } // 构建区域路径 String regionPath = regionService.buildRegionPath(dto.getRegionId()); Student student = new Student(); BeanUtil.copyProperties(dto, student); student.setRegionPath(regionPath); student.setUpdateBy(SecurityUtils.getUsername()); return studentMapper.updateById(student); } /** * 删除学生 */ @Override @Transactional(rollbackFor = Exception.class) public int deleteStudentById(Long studentId) { return studentMapper.deleteById(studentId); } /** * 下载导入模板 */ @Override public void downloadTemplate(HttpServletResponse response) { try { response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setCharacterEncoding("utf-8"); String fileName = URLEncoder.encode("学生导入模板", StandardCharsets.UTF_8).replace("\\+", "%20"); response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx"); // 生成模板数据 List templateData = new ArrayList<>(); StudentImportDTO example = new StudentImportDTO(); example.setStudentName("张小明"); example.setStudentNo("STU20260001"); example.setMemberPhone("13812345678"); example.setRegionPath("湖北省-武汉市-武昌区"); example.setSchoolName("武汉市第一中学"); example.setGradeName("七年级"); example.setClassName("1班"); example.setGender("男"); example.setBirthday("2015-03"); templateData.add(example); EasyExcel.write(response.getOutputStream(), StudentImportDTO.class) .sheet("学生信息") .doWrite(templateData); } catch (IOException e) { log.error("下载模板失败", e); throw new ServiceException("下载模板失败"); } } /** * 批量导入学生 */ @Override @Transactional(rollbackFor = Exception.class) public ImportResultVO importStudents(MultipartFile file) { ImportResultVO result = new ImportResultVO(); try { List importList = new ArrayList<>(); // 解析Excel EasyExcel.read(file.getInputStream(), StudentImportDTO.class, new AnalysisEventListener() { private int rowIndex = 1; @Override public void invoke(StudentImportDTO data, AnalysisContext context) { data.setRowIndex(++rowIndex); importList.add(data); } @Override public void doAfterAllAnalysed(AnalysisContext context) { log.info("解析完成,共{}条数据", importList.size()); } }).sheet().doRead(); // 校验数据量 if (importList.size() > 1000) { throw new ServiceException("单次导入数据量不能超过1000条"); } // 逐条处理 for (StudentImportDTO dto : importList) { try { processImportRow(dto); result.addSuccess(); } catch (Exception e) { result.addFail(dto.getRowIndex(), e.getMessage()); } } } catch (IOException e) { log.error("导入失败", e); throw new ServiceException("文件解析失败"); } return result; } /** * 处理导入的单行数据 */ private void processImportRow(StudentImportDTO dto) { // 1. 校验必填字段 validateImportData(dto); // 2. 校验学号唯一性 if (StrUtil.isNotBlank(dto.getStudentNo()) && !checkStudentNoUnique(dto.getStudentNo(), null)) { throw new ServiceException("学号已存在"); } // 3. 解析区域 Long regionId = regionService.getRegionIdByPath(dto.getRegionPath()); if (regionId == null) { throw new ServiceException("区域信息不存在"); } // 4. 解析学校 Long schoolId = schoolService.getSchoolIdByName(dto.getSchoolName(), regionId); if (schoolId == null) { throw new ServiceException("学校信息不存在"); } // 5. 解析年级 Long schoolGradeId = schoolService.getSchoolGradeId(schoolId, dto.getGradeName()); if (schoolGradeId == null) { throw new ServiceException("年级信息不存在"); } // 6. 解析班级 Long schoolClassId = schoolService.getSchoolClassId(schoolGradeId, dto.getClassName()); if (schoolClassId == null) { throw new ServiceException("班级信息不存在"); } // 7. 处理会员 Long memberId = getOrCreateMember(dto.getMemberPhone()); // 8. 保存学生 Student student = new Student(); student.setStudentName(dto.getStudentName()); student.setStudentNo(dto.getStudentNo()); student.setGender(parseGender(dto.getGender())); student.setBirthday(parseBirthday(dto.getBirthday())); student.setRegionId(regionId); student.setRegionPath(dto.getRegionPath()); student.setSchoolId(schoolId); student.setSchoolGradeId(schoolGradeId); student.setSchoolClassId(schoolClassId); student.setMemberId(memberId); student.setCreateBy(SecurityUtils.getUsername()); studentMapper.insert(student); } /** * 校验导入数据 */ private void validateImportData(StudentImportDTO dto) { if (StrUtil.isBlank(dto.getStudentName())) { throw new ServiceException("姓名不能为空"); } if (StrUtil.isBlank(dto.getStudentNo())) { throw new ServiceException("学号不能为空"); } if (StrUtil.isBlank(dto.getMemberPhone())) { throw new ServiceException("用户手机号不能为空"); } if (StrUtil.isBlank(dto.getRegionPath())) { throw new ServiceException("区域不能为空"); } if (StrUtil.isBlank(dto.getSchoolName())) { throw new ServiceException("学校不能为空"); } if (StrUtil.isBlank(dto.getGradeName())) { throw new ServiceException("年级不能为空"); } if (StrUtil.isBlank(dto.getClassName())) { throw new ServiceException("班级不能为空"); } } /** * 获取或创建会员 */ private Long getOrCreateMember(String phone) { // 查询已有会员 Member member = memberService.selectMemberByPhone(phone); if (member != null) { return member.getMemberId(); } // 创建新会员(身份:家长,初始密码:123456) return memberService.createMemberForImport(phone, "123456"); } /** * 解析性别 */ private String parseGender(String gender) { if ("男".equals(gender)) return "1"; if ("女".equals(gender)) return "2"; return "0"; } /** * 解析出生日期 */ private LocalDate parseBirthday(String birthday) { if (StrUtil.isBlank(birthday)) return null; try { return LocalDate.parse(birthday + "-01", DateTimeFormatter.ofPattern("yyyy-MM-dd")); } catch (Exception e) { return null; } } /** * 校验学号唯一性 */ @Override public boolean checkStudentNoUnique(String studentNo, Long studentId) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(Student::getStudentNo, studentNo); if (studentId != null) { wrapper.ne(Student::getStudentId, studentId); } return count(wrapper) == 0; } } ``` --- ## 7. 批量导入设计 ### 7.1 导入流程图 ``` ┌─────────────────────────────────────────────────────────────────┐ │ 批量导入处理流程 │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ 接收文件 ──► 校验文件类型 ──► 解析Excel ──► 校验数据量 │ │ │ │ │ ▼ │ │ ┌─────────────────────┐ │ │ │ 逐行处理数据 │ │ │ └──────────┬──────────┘ │ │ │ │ │ ┌────────────────────────────────────────┼────────────────┐│ │ ▼ ▼ ▼│ │ 校验必填字段 校验业务数据 处理会员关联│ │ │ │ ││ │ ▼ ▼ ▼│ │ 字段格式校验 区域/学校匹配 查询或创建会员│ │ │ │ ││ │ ▼ ▼ ▼│ │ 学号唯一性校验 年级/班级匹配 返回会员ID │ │ │ │ ││ │ └────────────────────────────────────────┼────────────────┘│ │ │ │ │ ▼ │ │ ┌─────────────┐ │ │ │ 保存学生 │ │ │ └──────┬──────┘ │ │ │ │ │ ┌─────────────────────┼─────────────────┐│ │ ▼ ▼ ▼│ │ 成功 失败 异常捕获│ │ 计数+1 记录行号和原因 记录错误 │ │ │ │ ││ │ └─────────────────────┴─────────────────┘│ │ │ │ │ ▼ │ │ 返回导入结果 │ │ │ └─────────────────────────────────────────────────────────────────┘ ``` ### 7.2 会员自动创建规则 | 场景 | 处理方式 | |------|----------| | 手机号已存在 | 直接关联到已有会员 | | 手机号不存在 | 自动创建家长会员 | **自动创建会员的属性:** | 属性 | 值 | |------|------| | 手机号 | 导入数据中的手机号 | | 昵称 | 自动生成(用户+后4位手机号) | | 身份类型 | 家长(1) | | 初始密码 | 123456(BCrypt加密) | | 注册来源 | 批量导入(4) | | 状态 | 正常(0) | ### 7.3 错误处理策略 | 错误类型 | 处理方式 | 是否继续处理后续数据 | |---------|----------|:--------------------:| | 文件格式错误 | 立即返回错误 | 否 | | 数据量超限 | 立即返回错误 | 否 | | 必填字段为空 | 记录错误,跳过当前行 | 是 | | 学号重复 | 记录错误,跳过当前行 | 是 | | 区域/学校不匹配 | 记录错误,跳过当前行 | 是 | | 数据库异常 | 记录错误,跳过当前行 | 是 | --- ## 8. 数据权限控制 ### 8.1 数据权限注解 ```java /** * 数据权限注解 */ @DataScope(deptAlias = "s", userAlias = "u") public List selectStudentList(StudentQueryDTO query) { return studentMapper.selectStudentVOList(query); } ``` ### 8.2 Mapper SQL数据权限 ```xml ``` ### 8.3 数据权限SQL生成规则 | 角色 | 生成的SQL条件 | |------|---------------| | 超级管理员 | 无附加条件 | | 分公司用户 | `AND s.region_id IN (用户所属区域及子区域)` | | 学校用户 | `AND s.school_id = 用户所属学校ID` | --- ## 9. 异常处理 ### 9.1 业务异常定义 ```java /** * 学生业务异常 */ public class StudentException extends ServiceException { public StudentException(String message) { super(message); } public static StudentException studentNotFound() { return new StudentException("学生不存在"); } public static StudentException studentNoExists() { return new StudentException("学号已存在"); } public static StudentException importFailed(String reason) { return new StudentException("导入失败:" + reason); } } ``` ### 9.2 全局异常处理 ```java /** * 全局异常处理器 */ @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(ServiceException.class) public AjaxResult handleServiceException(ServiceException e) { log.error("业务异常:{}", e.getMessage()); return AjaxResult.error(e.getMessage()); } @ExceptionHandler(MethodArgumentNotValidException.class) public AjaxResult handleValidationException(MethodArgumentNotValidException e) { String message = e.getBindingResult().getFieldErrors().stream() .map(FieldError::getDefaultMessage) .findFirst() .orElse("参数校验失败"); return AjaxResult.error(message); } } ``` --- ## 10. 单元测试 ### 10.1 服务层测试 ```java package com.pangu.system.service; import com.pangu.system.domain.dto.StudentDTO; import com.pangu.system.domain.dto.StudentQueryDTO; import com.pangu.system.domain.vo.StudentVO; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.transaction.annotation.Transactional; import java.util.List; import static org.junit.jupiter.api.Assertions.*; /** * 学生服务测试 * * @author pangu */ @SpringBootTest @Transactional class StudentServiceTest { @Autowired private IStudentService studentService; @Test void testSelectStudentList() { StudentQueryDTO query = new StudentQueryDTO(); query.setPageNum(1); query.setPageSize(10); List list = studentService.selectStudentList(query); assertNotNull(list); } @Test void testInsertStudent() { StudentDTO dto = new StudentDTO(); dto.setStudentName("测试学生"); dto.setStudentNo("TEST001"); dto.setRegionId(111L); dto.setSchoolId(1L); dto.setSchoolGradeId(1L); dto.setSchoolClassId(1L); dto.setMemberId(1L); int result = studentService.insertStudent(dto); assertEquals(1, result); } @Test void testCheckStudentNoUnique_Unique() { boolean unique = studentService.checkStudentNoUnique("UNIQUE001", null); assertTrue(unique); } @Test void testCheckStudentNoUnique_Exists() { // 假设STU20260001已存在 boolean unique = studentService.checkStudentNoUnique("STU20260001", null); assertFalse(unique); } } ``` ### 10.2 控制器测试 ```java package com.pangu.system.controller; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; /** * 学生控制器测试 * * @author pangu */ @SpringBootTest @AutoConfigureMockMvc class StudentControllerTest { @Autowired private MockMvc mockMvc; @Test void testList() throws Exception { mockMvc.perform(get("/api/student/list") .param("pageNum", "1") .param("pageSize", "10") .header("Authorization", "Bearer xxx")) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)); } @Test void testGetInfo() throws Exception { mockMvc.perform(get("/api/student/1") .header("Authorization", "Bearer xxx")) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)); } @Test void testAdd() throws Exception { String json = """ { "studentName": "测试学生", "studentNo": "TEST001", "regionId": 111, "schoolId": 1, "schoolGradeId": 1, "schoolClassId": 1, "memberId": 1 } """; mockMvc.perform(post("/api/student") .contentType(MediaType.APPLICATION_JSON) .content(json) .header("Authorization", "Bearer xxx")) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)); } } ``` ### 10.3 测试覆盖率要求 | 模块 | 覆盖率要求 | |------|-----------| | Service层 | ≥ 80% | | Controller层 | ≥ 70% | | 工具类 | ≥ 90% | --- *文档结束*