1430 lines
42 KiB
Markdown
1430 lines
42 KiB
Markdown
# 学生管理模块 - 后端技术方案
|
||
|
||
---
|
||
|
||
| 文档信息 | 内容 |
|
||
|---------|------|
|
||
| **文档版本** | 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<Long> 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<FailItem> 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<StudentVO> 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<StudentMapper, Student> 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<StudentVO> 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<StudentImportDTO> 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<StudentImportDTO> importList = new ArrayList<>();
|
||
|
||
// 解析Excel
|
||
EasyExcel.read(file.getInputStream(), StudentImportDTO.class,
|
||
new AnalysisEventListener<StudentImportDTO>() {
|
||
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<Student> 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<StudentVO> selectStudentList(StudentQueryDTO query) {
|
||
return studentMapper.selectStudentVOList(query);
|
||
}
|
||
```
|
||
|
||
### 8.2 Mapper SQL数据权限
|
||
|
||
```xml
|
||
<!-- StudentMapper.xml -->
|
||
<select id="selectStudentVOList" resultType="StudentVO">
|
||
SELECT
|
||
s.student_id,
|
||
s.student_name,
|
||
s.student_no,
|
||
s.gender,
|
||
s.birthday,
|
||
s.region_path,
|
||
sch.school_name,
|
||
g.grade_name,
|
||
c.class_name,
|
||
sub.subject_name,
|
||
s.member_id,
|
||
m.nickname AS member_nickname,
|
||
CONCAT(LEFT(m.phone, 3), '****', RIGHT(m.phone, 4)) AS member_phone,
|
||
s.create_time
|
||
FROM pg_student s
|
||
LEFT JOIN pg_school sch ON s.school_id = sch.school_id
|
||
LEFT JOIN pg_school_grade sg ON s.school_grade_id = sg.id
|
||
LEFT JOIN pg_grade g ON sg.grade_id = g.grade_id
|
||
LEFT JOIN pg_school_class sc ON s.school_class_id = sc.id
|
||
LEFT JOIN pg_class c ON sc.class_id = c.class_id
|
||
LEFT JOIN pg_subject sub ON s.subject_id = sub.subject_id
|
||
LEFT JOIN pg_member m ON s.member_id = m.member_id
|
||
WHERE s.del_flag = '0'
|
||
<if test="studentName != null and studentName != ''">
|
||
AND s.student_name LIKE CONCAT('%', #{studentName}, '%')
|
||
</if>
|
||
<if test="studentNo != null and studentNo != ''">
|
||
AND s.student_no = #{studentNo}
|
||
</if>
|
||
<if test="gender != null and gender != ''">
|
||
AND s.gender = #{gender}
|
||
</if>
|
||
<if test="schoolId != null">
|
||
AND s.school_id = #{schoolId}
|
||
</if>
|
||
<if test="schoolGradeId != null">
|
||
AND s.school_grade_id = #{schoolGradeId}
|
||
</if>
|
||
<if test="schoolClassId != null">
|
||
AND s.school_class_id = #{schoolClassId}
|
||
</if>
|
||
<if test="subjectId != null">
|
||
AND s.subject_id = #{subjectId}
|
||
</if>
|
||
<if test="memberPhone != null and memberPhone != ''">
|
||
AND m.phone LIKE CONCAT('%', #{memberPhone}, '%')
|
||
</if>
|
||
<!-- 数据权限过滤 -->
|
||
${params.dataScope}
|
||
ORDER BY s.create_time DESC
|
||
</select>
|
||
```
|
||
|
||
### 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<StudentVO> 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% |
|
||
|
||
---
|
||
|
||
*文档结束*
|