2026-01-31 17:55:58 +08:00
|
|
|
|
# 学生管理模块 - 后端技术方案
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
| 文档信息 | 内容 |
|
|
|
|
|
|
|---------|------|
|
|
|
|
|
|
| **文档版本** | 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;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 学生实体
|
|
|
|
|
|
*
|
2026-01-31 23:14:11 +08:00
|
|
|
|
* @author pangu
|
2026-01-31 17:55:58 +08:00
|
|
|
|
*/
|
|
|
|
|
|
@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;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 学生传输对象
|
|
|
|
|
|
*
|
2026-01-31 23:14:11 +08:00
|
|
|
|
* @author pangu
|
2026-01-31 17:55:58 +08:00
|
|
|
|
*/
|
|
|
|
|
|
@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;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 学生查询条件
|
|
|
|
|
|
*
|
2026-01-31 23:14:11 +08:00
|
|
|
|
* @author pangu
|
2026-01-31 17:55:58 +08:00
|
|
|
|
*/
|
|
|
|
|
|
@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;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 学生视图对象
|
|
|
|
|
|
*
|
2026-01-31 23:14:11 +08:00
|
|
|
|
* @author pangu
|
2026-01-31 17:55:58 +08:00
|
|
|
|
*/
|
|
|
|
|
|
@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解析)
|
|
|
|
|
|
*
|
2026-01-31 23:14:11 +08:00
|
|
|
|
* @author pangu
|
2026-01-31 17:55:58 +08:00
|
|
|
|
*/
|
|
|
|
|
|
@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;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 导入结果视图对象
|
|
|
|
|
|
*
|
2026-01-31 23:14:11 +08:00
|
|
|
|
* @author pangu
|
2026-01-31 17:55:58 +08:00
|
|
|
|
*/
|
|
|
|
|
|
@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;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 学生管理控制器
|
|
|
|
|
|
*
|
2026-01-31 23:14:11 +08:00
|
|
|
|
* @author pangu
|
2026-01-31 17:55:58 +08:00
|
|
|
|
*/
|
|
|
|
|
|
@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;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 学生服务接口
|
|
|
|
|
|
*
|
2026-01-31 23:14:11 +08:00
|
|
|
|
* @author pangu
|
2026-01-31 17:55:58 +08:00
|
|
|
|
*/
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 学生服务实现
|
|
|
|
|
|
*
|
2026-01-31 23:14:11 +08:00
|
|
|
|
* @author pangu
|
2026-01-31 17:55:58 +08:00
|
|
|
|
*/
|
|
|
|
|
|
@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.*;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 学生服务测试
|
|
|
|
|
|
*
|
2026-01-31 23:14:11 +08:00
|
|
|
|
* @author pangu
|
2026-01-31 17:55:58 +08:00
|
|
|
|
*/
|
|
|
|
|
|
@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.*;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 学生控制器测试
|
|
|
|
|
|
*
|
2026-01-31 23:14:11 +08:00
|
|
|
|
* @author pangu
|
2026-01-31 17:55:58 +08:00
|
|
|
|
*/
|
|
|
|
|
|
@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% |
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
*文档结束*
|