# SMS-backend **Repository Path**: dreiluo/sms-backend ## Basic Information - **Project Name**: SMS-backend - **Description**: 钟祥司徒实训结课后端 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-09-15 - **Last Updated**: 2026-09-19 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 学生管理系统 · 后端(stu-backend) 基于 **Spring Boot 4 + MyBatis-Plus + MySQL 8** 的学生管理系统后端服务,提供教职工、学生、课程、选课、成绩五大模块的 REST 接口,采用 **JWT 无状态鉴权** 与 **三级角色权限**(教学秘书 / 教师 / 学生)。 > 配套前端仓库:[gitee.com/dreiluo/sms-frontend](https://gitee.com/dreiluo/sms-frontend) --- ## 目录 - [一、项目简介](#一项目简介) - [二、技术栈](#二技术栈) - [三、环境要求](#三环境要求) - [四、快速开始](#四快速开始) - [五、演示账号](#五演示账号) - [六、配置项说明](#六配置项说明) - [七、目录结构](#七目录结构) - [八、接口约定](#八接口约定) - [九、接口清单](#九接口清单) - [十、核心业务规则](#十核心业务规则) - [十一、数据库设计](#十一数据库设计) - [十二、设计取舍](#十二设计取舍) - [十三、常见问题](#十三常见问题) --- ## 一、项目简介 面向高校教务场景,覆盖三类角色的完整教学管理闭环: | 角色 | 角色码 | 主要功能 | | --- | --- | --- | | 教学秘书 | `ADMIN` | 教职工管理(编辑 / 冻结 / 重置密码)、学生管理(编辑 / 状态变更 / Excel 导入导出)、课程管理、成绩审核与发布、选课监控 | | 教师 | `TEACHER` | 创建 / 编辑课程、发布课程、录入成绩(暂存 / Excel 导入 / 提交) | | 学生 | `STUDENT` | 学业总览、选课中心、我的课表、成绩查询 | ### 核心业务闭环 ``` 课程: 创建 → UNPUBLISHED(可编辑) → 发布 → PUBLISHED(永久锁定) 成绩: DRAFT 录入中 → SUBMITTED 待审核 → APPROVED 审核通过 → PUBLISHED 已发布 ↑ ↓ └──── 审核不通过(退回并附原因) 选课: 学生选课 → 容量原子校验 → 扣减名额 → 写入选课记录 →(发布成绩时)累加学分 + 重算 GPA ``` --- ## 二、技术栈 | 分类 | 技术 | 版本 | 说明 | | --- | --- | --- | --- | | 语言 | Java | 21 | LTS | | 框架 | Spring Boot | 4.1.1 | 注意是 `spring-boot-starter-webmvc`(Boot 4 中 `-web` 已更名) | | 构建 | Maven | 3.9+ | `spring-boot-maven-plugin` | | ORM | MyBatis-Plus | 3.5.17 | 含 `mybatis-plus-jsqlparser`(分页插件依赖) | | 数据库 | MySQL | 8.0+ | 字符集 `utf8mb4` / 排序规则 `utf8mb4_0900_ai_ci` | | 连接池 | HikariCP | 随 Boot 提供 | 默认配置 | | 鉴权 | JJWT | 0.12.6 | `jjwt-api` + `jjwt-impl` + `jjwt-jackson` | | 密码 | Spring Security Crypto | 随 Boot 提供 | 仅使用 `BCryptPasswordEncoder`,未引入完整 Spring Security | | 参数校验 | Spring Boot Validation | 随 Boot 提供 | Jakarta Bean Validation(`@NotBlank` / `@NotNull` / `@Min`) | | Excel | EasyExcel | 4.0.3 | 学生 / 成绩批量导入导出 | | 接口文档 | Knife4j | 5.6.0 | `knife4j-openapi3-boot4-spring-boot-starter` | | 工具 | Lombok | 随 Boot 提供 | `@Data` 简化实体 | --- ## 三、环境要求 | 组件 | 版本要求 | 检查命令 | | --- | --- | --- | | JDK | 21 及以上 | `java -version` | | Maven | 3.9 及以上 | `mvn -v` | | MySQL | 8.0 及以上 | `mysql --version` | --- ## 四、快速开始 ### 1. 初始化数据库 仓库已内置自包含的初始化脚本 `sql/student.sql`,包含全部表结构与演示数据,**可直接在全新实例上执行**: ```bash mysql -uroot -p < sql/student.sql ``` 或使用 Navicat / DBeaver 打开该文件后整体运行。 > ⚠️ **注意**:脚本开头会执行 `DROP DATABASE IF EXISTS stu`,即 **先删库再重建**。重复执行会覆盖旧数据,请勿在生产库上运行。 脚本内置数据量:`staff` 20 行、`student` 75 行、`course` 15 行、`student_course` 164 行、`course_grade_audit` 14 行。 ### 2. 启动服务 **方式一:IDE(推荐)** 用 IntelliJ IDEA 打开 `stu-backend` 目录,运行 `com.sms.stubackend.StuBackendApplication` 即可。 **方式二:命令行** ```bash mvn spring-boot:run ``` 看到下面这行即为启动成功: ``` Tomcat started on port 9090 (http) with context path '/' Started StuBackendApplication in x.xxx seconds ``` ### 3. 验证 | 地址 | 说明 | | --- | --- | | | Knife4j 接口文档(可直接在线调试) | | | 登录接口 | 用演示账号登录一次,确认数据库连接与鉴权链路正常: ```bash curl -X POST http://localhost:9090/api/auth/login \ -H "Content-Type: application/json" \ -d '{"account":"admin","password":"123456"}' ``` ### 4. 打包部署 ```bash mvn clean package -DskipTests java -jar target/stu-backend-0.0.1-SNAPSHOT.jar ``` --- ## 五、演示账号 初始化脚本与 `DataInitializer` 的初始密码统一为 **`123456`**: | 角色 | 账号 | 说明 | | --- | --- | --- | | 教学秘书 | `admin` | 管理员,可冻结 / 重置他人密码 | | 教师 | `t001` ~ `t019` | 分别绑定不同院系 | | 学生 | `2024001` ~ `2024075` | 2024 级,含 GPA / 已修学分数据 | > **账号不提供新建入口**:教职工与学生账号只能通过数据库初始化 / Excel 导入产生,前端不提供「新增账号」功能。 --- ## 六、配置项说明 配置位于 `src/main/resources/application.properties`,全部支持 **环境变量覆盖**(生产环境请通过环境变量注入敏感值,勿提交真实密码): | 配置项 | 环境变量 | 默认值 | 说明 | | --- | --- | --- | --- | | `server.port` | `SERVER_PORT` | `9090` | 服务端口 | | `spring.datasource.username` | `DB_USERNAME` | `root` | 数据库账号 | | `spring.datasource.password` | `DB_PASSWORD` | `QiRuoLi` | 数据库密码(**上线前必须改**) | | `spring.datasource.url` | `DB_URL` | `jdbc:mysql://localhost:3306/stu?...` | 连接串 | | `jwt.secret` | `JWT_SECRET` | 内置开发密钥 | **长度须 ≥ 32 字节**,JJWT 会据此自动选择 HS256/384/512 | | `jwt.expire-hours` | `JWT_EXPIRE_HOURS` | `12` | Token 有效期(小时) | | `app.seed.enabled` | `APP_SEED_ENABLED` | `true` | 是否在库为空时自动写入种子数据 | | `app.cors.allowed-origins` | `CORS_ALLOWED_ORIGINS` | `*` | 允许跨域来源,**生产应收敛为域名白名单** | | `app.upload.path` | `UPLOAD_PATH` | `D:/pic/` | 图片上传落盘目录 | | `app.base-url` | `APP_BASE_URL` | 空 | 拼接上传图片 URL 的基础地址,留空则按请求自动推导 | | `app.login.max-attempts` | `LOGIN_MAX_ATTEMPTS` | `5` | 连续登录失败上限 | | `app.login.lock-minutes` | `LOGIN_LOCK_MINUTES` | `10` | 达到上限后的锁定时长(分钟) | | `spring.servlet.multipart.max-file-size` | — | `10MB` | 单文件上传上限 | 设置环境变量的示例: ```bash # Linux / macOS export DB_PASSWORD=your_password export JWT_SECRET=your-own-secret-key-at-least-32-bytes-long # Windows PowerShell $env:DB_PASSWORD = "your_password" $env:JWT_SECRET = "your-own-secret-key-at-least-32-bytes-long" ``` --- ## 七、目录结构 ``` stu-backend/ ├── pom.xml ├── sql/ │ └── student.sql # 自包含数据库初始化脚本(含表结构与演示数据) └── src/main/ ├── java/com/sms/stubackend/ │ ├── StuBackendApplication.java # 启动类 │ ├── config/ │ │ ├── CommonConfig.java # MyBatis-Plus 分页插件 │ │ ├── WebConfig.java # CORS / 拦截器注册 / 静态资源映射 │ │ └── DataInitializer.java # 空库时写入种子数据 │ ├── controller/ # 7 个 REST 控制器 │ ├── service/ # 业务层:选课、成绩、登录防暴破 │ ├── mapper/ # MyBatis-Plus Mapper(BaseMapper + 注解 SQL) │ ├── entity/ # 5 张表对应实体 │ ├── model/ # DTO / VO / 查询条件 / 统一返回体 │ ├── exception/ # 业务异常 + 全局异常处理器 │ ├── interceptor/AuthInterceptor.java # JWT 鉴权拦截器 │ ├── context/UserContext.java # ThreadLocal 保存当前登录人 │ └── util/ # JwtUtil / ExcelFileUtil └── resources/ └── application.properties ``` **分层职责**:`Controller` 只做参数接收与角色校验 → `Service` 承载事务与业务规则 → `Mapper` 负责数据访问。跨表写操作(选课扣减、成绩发布)统一放在 `Service` 中并用 `@Transactional` 保证原子性。 --- ## 八、接口约定 ### 8.1 统一返回体 所有接口返回 `Result`: ```json { "data": {}, "message": "success", "code": 202 } ``` | code | 含义 | 说明 | | --- | --- | --- | | `202` | 成功 | 业务正常返回 | | `505` | 业务异常 | 参数不合法、业务规则不满足(如「课程尚未发布,不能录入成绩」) | | `401` | 未登录 | Token 缺失 / 非法 / 过期,或账号被冻结、被删除 | | `403` | 无权限 | 角色不匹配(如学生调用了管理员接口) | | `500` | 服务器异常 | 未预期的异常,日志中会记录堆栈,返回给前端的是通用提示 | ### 8.2 鉴权方式 登录成功后返回 JWT,后续请求需在请求头携带: ``` Authorization: Bearer ``` - 拦截器 `AuthInterceptor` 拦截 `/api/**`,放行 `/api/auth/login` 与所有 `OPTIONS` 预检请求。 - **每个请求都会二次校验账号状态**:账号被冻结或被删除后,已签发的旧 Token 立即失效(无需等 Token 过期)。 - 角色校验在 Controller 层通过 `UserContext.requireRole(...)` 完成。 ### 8.3 分页 列表类接口统一接收 `PageQC` 子类作为查询条件,返回 `PageResult`: ```json { "data": { "records": [], "total": 0 }, "message": "success", "code": 202 } ``` 请求体中 `pageNum` / `pageSize` 由基类 `PageQC.safePageNum()` / `safePageSize()` 兜底,避免传入 0 或负数导致分页异常。 ### 8.4 登录防暴破 `LoginAttemptService` 记录连续失败次数,达到 `app.login.max-attempts`(默认 5 次)后锁定账号 `app.login.lock-minutes`(默认 10 分钟)。 --- ## 九、接口清单 共 **37** 个接口,按模块划分。角色列表示调用该接口所需角色。 ### 认证 `/api/auth` | 方法 | 路径 | 角色 | 说明 | | --- | --- | --- | --- | | POST | `/login` | 免登录 | 工号 / 学号 + 密码登录,返回 Token 与用户信息 | ### 教职工管理 `/api/staff` | 方法 | 路径 | 角色 | 说明 | | --- | --- | --- | --- | | POST | `/query` | ADMIN | 分页查询(关键词 / 角色 / 状态) | | PUT | `/modify` | ADMIN | 编辑姓名 / 院系 / 手机号(工号、角色、状态不可改) | | POST | `/resetPassword/{id}` | ADMIN | 重置密码为 `123456` | | POST | `/updateStatus/{id}` | ADMIN | 冻结 / 启用(管理员不可冻结) | | GET | `/me` | 登录用户 | 当前登录教职工信息 | | DELETE | `/delete/{id}` | ADMIN | 删除账号(有课程或审核记录时拒绝) | ### 学生管理 `/api/student` | 方法 | 路径 | 角色 | 说明 | | --- | --- | --- | --- | | POST | `/query` | ADMIN | 分页查询(关键词 / 状态 / 院系 / 专业 / 班级) | | PUT | `/modify` | ADMIN | 编辑学生信息 | | POST | `/resetPassword/{id}` | ADMIN | 重置密码为 `123456` | | POST | `/updateStatus/{id}` | ADMIN | 变更状态:`ENABLED` / `FROZEN` / `SUSPENDED` | | GET | `/me` | STUDENT | 当前登录学生信息 | | DELETE | `/delete/{id}` | ADMIN | 删除学生(有选课或成绩记录时拒绝) | | POST | `/import` | ADMIN | Excel 批量导入(`multipart/form-data`,字段名 `file`) | | GET | `/export` | ADMIN | Excel 导出全量学生 | ### 课程管理 `/api/course` | 方法 | 路径 | 角色 | 说明 | | --- | --- | --- | --- | | POST | `/query` | TEACHER / ADMIN | 分页查询(教师只看本人课程,教秘看全部) | | GET | `/selectCenter` | STUDENT | 选课中心,仅返回已发布课程并标记是否已选 | | GET | `/monitor` | ADMIN | 选课监控,返回全部课程与选课人数 | | POST | `/insert` | TEACHER / ADMIN | 新增课程,默认「未发布」 | | PUT | `/modify` | TEACHER 本人 / ADMIN | 编辑课程(**仅未发布状态可改**) | | POST | `/publish/{id}` | TEACHER 本人 / ADMIN | 发布课程(发布后永久锁定) | | GET | `/detail/{id}` | 登录用户 | 课程详情(含授课教师姓名) | ### 选课管理 `/api/selection` | 方法 | 路径 | 角色 | 说明 | | --- | --- | --- | --- | | POST | `/enroll/{courseId}` | STUDENT | 选课 | | POST | `/drop/{courseId}` | STUDENT | 退课 | | GET | `/myCourses` | STUDENT | 我的课表 | | POST | `/adminDrop` | ADMIN | 教秘手动退课,请求体 `{ "studentId": 1, "courseId": 1 }` | ### 成绩管理 `/api/grade` | 方法 | 路径 | 角色 | 说明 | | --- | --- | --- | --- | | GET | `/roster/{courseId}` | TEACHER 本人 / ADMIN | 查看选课名单(含各项成绩) | | POST | `/save/{courseId}` | TEACHER | 暂存成绩(草稿,支持清空) | | POST | `/import/{courseId}` | TEACHER | Excel 批量导入成绩 | | POST | `/submit/{courseId}` | TEACHER | 提交成绩,进入待审核 | | GET | `/auditStatus/{courseId}` | TEACHER | 查看审核状态与退回原因 | | GET | `/auditList` | ADMIN | 审核列表,`status` 默认 `SUBMITTED` | | POST | `/approve/{courseId}` | ADMIN | 审核通过 | | POST | `/reject/{courseId}` | ADMIN | 审核不通过(退回教师,须填原因) | | POST | `/publish/{courseId}` | ADMIN | 发布成绩(锁定 + 累加学分 + 重算 GPA) | | GET | `/myGrades` | STUDENT | 我的成绩(仅已发布) | ### 文件 `/api/file` | 方法 | 路径 | 角色 | 说明 | | --- | --- | --- | --- | | POST | `/uploadOne` | 登录用户 | 上传单张图片,返回可访问 URL | 上传的图片落盘到 `app.upload.path`,通过 `/static/**` 对外访问。 --- ## 十、核心业务规则 ### 10.1 课程状态机 - 新建课程固定为 `UNPUBLISHED`,此时教师可反复编辑。 - **发布是不可逆操作**:`POST /publish/{id}` 后将 `status` 置为 `PUBLISHED`,`modify` 接口会直接拒绝,避免已选课学生的课程信息被中途篡改。 - 上课时间 `schedule` 与上课地点 `location` 为 **必填**,实体上通过 `@NotBlank` 强制校验。 ### 10.2 成绩状态机 | 状态 | 含义 | 可执行操作 | | --- | --- | --- | | `DRAFT` | 录入中 | 教师暂存 / 导入 / 提交 | | `SUBMITTED` | 待审核 | 教秘通过 / 退回 | | `APPROVED` | 审核通过 | 教秘发布 | | `PUBLISHED` | 已发布 | 永久锁定 | 两条关键约束: 1. **课程未发布不允许录入成绩**:`GradeService.requirePublished()` 在暂存、导入、提交三条写入路径上统一拦截,返回「课程尚未发布,不能录入成绩」。查看名单等只读操作不受影响。 2. **非 `DRAFT` 状态不可修改**:`requireEditable()` 拦截,防止已提交的成绩被偷偷改动。 ### 10.3 成绩发布(原子事务) `GradeService.publish()` 在一个事务内完成: 1. 校验审核状态必须为 `APPROVED`; 2. 审核记录置为 `PUBLISHED` 并记录发布时间; 3. 遍历该课程选课记录,对 **总评 ≥ 60 分且标记不为「缺考」「舞弊」** 的记录累加学分; 4. 调用 `recomputeAllGpa()` 用一条 SQL 重算全校 GPA(避免逐生查询)。 `total_earned_credits` 与 `gpa` 是**冗余字段**,只在发布成绩时统一刷新,查询时无需实时聚合。 ### 10.4 选课并发控制 选课不采用「先查再改」的写法,而是一条带条件的原子 `UPDATE`: ```sql UPDATE course SET current_enrolled = current_enrolled + 1 WHERE id = ? AND current_enrolled < capacity AND status = 'PUBLISHED' ``` 根据受影响行数判断是否成功,从数据库层面杜绝超卖。未发布课程同样无法被选。 ### 10.5 登录安全 - 密码使用 **BCrypt(cost 10)** 加密存储,数据库中不存明文。 - Token 使用 JJWT 签发,密钥长度决定算法(默认密钥 52 字节 → HS384)。 - 连续失败达上限后锁定账号,防止暴力破解。 --- ## 十一、数据库设计 数据库名 `stu`,字符集 `utf8mb4`,共 5 张表。 ### staff — 教职工表 | 字段 | 类型 | 说明 | | --- | --- | --- | | `id` | bigint PK | 自增主键 | | `staff_no` | varchar(20) UNIQUE | 工号(登录账号) | | `password_hash` | varchar(255) | BCrypt 密码 | | `real_name` | varchar(50) | 姓名 | | `role_type` | varchar(20) | `ADMIN` \| `TEACHER` | | `status` | varchar(20) | `ENABLED` \| `FROZEN` | | `department` / `phone` | varchar | 院系 / 手机号 | ### student — 学生表 | 字段 | 类型 | 说明 | | --- | --- | --- | | `id` | bigint PK | 自增主键 | | `student_no` | varchar(20) UNIQUE | 学号(登录账号) | | `password_hash` | varchar(255) | BCrypt 密码 | | `real_name` / `gender` / `phone` | varchar | 基本信息 | | `department` / `major` / `class_name` | varchar | 院系 / 专业 / 班级 | | `enrollment_year` | int | 入学年份 | | `status` | varchar(20) | `ENABLED` \| `FROZEN` \| `SUSPENDED` | | `total_earned_credits` | decimal(8,2) | 已修总学分(冗余) | | `required_credits` | decimal(8,2) | 毕业要求学分 | | `gpa` | decimal(3,2) | 累计平均绩点(冗余) | ### course — 课程表 | 字段 | 类型 | 说明 | | --- | --- | --- | | `id` | bigint PK | 自增主键 | | `course_code` | varchar(20) UNIQUE | 课程编号 | | `course_name` | varchar(100) | 课程名称 | | `credit` / `hours` | decimal(5,2) / int | 学分 / 总学时 | | `cover_image_url` | varchar(500) | 封面图 URL | | `teacher_id` | bigint | 授课教师(关联 `staff.id`) | | `schedule` / `location` | varchar | 上课时间 / 地点(**业务必填**) | | `capacity` / `current_enrolled` | int | 容量上限 / 已选人数 | | `status` | varchar(20) | `UNPUBLISHED` \| `PUBLISHED` | ### student_course — 学生选课表 | 字段 | 类型 | 说明 | | --- | --- | --- | | `id` | bigint PK | 自增主键 | | `student_id` / `course_id` | bigint | 联合唯一 `uk_student_course`,防重复选课 | | `score` | decimal(5,2) | 总评成绩(`NULL` 表示未录入) | | `usual_score` / `midterm_score` / `final_score` | decimal(5,2) | 平时 / 期中 / 期末 | | `remark` | varchar(20) | 正常 / 缓考 / 缺考 / 舞弊 | 总评合成规则:`平时 × 30% + 期中 × 30% + 期末 × 40%`(未填分项按 0 计)。 ### course_grade_audit — 课程成绩审核表 | 字段 | 类型 | 说明 | | --- | --- | --- | | `id` | bigint PK | 自增主键 | | `course_id` | bigint UNIQUE | 一门课程一条审核记录 | | `teacher_id` | bigint | 授课教师 | | `status` | varchar(20) | `DRAFT` \| `SUBMITTED` \| `APPROVED` \| `PUBLISHED` | | `submitted_at` / `approved_at` / `published_at` | datetime | 各阶段时间戳 | | `reject_reason` | varchar(500) | 审核退回原因 | --- ## 十二、设计取舍 以下是本项目**刻意没有使用**的技术,写在这里是为了避免误解: | 未使用 | 原因 / 替代方案 | | --- | --- | | 完整 Spring Security | 本项目鉴权是「拦截器 + ThreadLocal」的轻量方案,只从 Spring Security 引入了 `BCryptPasswordEncoder` 用于密码加密,不引入整套过滤器链 | | Redis | 登录失败计数与 Token 校验都在应用进程内 / 无状态完成,无需外部缓存;引入 Redis 会增加部署成本 | | MyBatis XML 映射文件 | 全部使用 `BaseMapper` + `LambdaQueryWrapper` / `@Select` 注解,SQL 与代码同处一文件,便于维护 | | 物理外键 | 表间仅保留逻辑外键与索引(如 `idx_teacher`),由业务层保证一致性,便于数据迁移与批量导入 | | 逻辑删除 | 学生 / 教职工删除采用物理删除,并前置校验业务关联(有选课、成绩、课程则拒绝删除) | --- ## 十三、常见问题 **Q1:启动报 `Access denied for user 'root'@'localhost' (using password: YES)`** 数据库密码与配置不一致。三种解决方式(任选其一): 1. 修改 `application.properties` 中 `spring.datasource.password` 的默认值; 2. 设置环境变量 `DB_PASSWORD`; 3. 修改 MySQL 中该账号的真实密码使其与配置一致。 **Q2:启动报 `Unknown database 'stu'`** 尚未初始化数据库,先执行 `sql/student.sql`。 **Q3:接口返回 401「未登录或 Token 缺失」** 检查请求头是否携带 `Authorization: Bearer `,以及账号是否已被冻结或删除。 **Q4:接口返回 403** 当前登录角色无权调用该接口,请对照 [接口清单](#九接口清单) 中的角色列。 **Q5:上传的图片 404** 确认 `app.upload.path` 指向的目录真实存在且有写权限(默认 `D:/pic/`),访问地址应为 `/static/<文件名>`。 **Q6:导入 Excel 提示「文件格式不正确」** 系统除校验扩展名外还会校验**文件头魔数**(xlsx 为 `50 4B`,xls 为 `D0 CF 11 E0`),单纯改扩展名的文件会被拒绝。请使用「导出」功能得到的模板填写。 **Q7:修改课程提示「已发布课程永久锁定」** 这是设计如此。发布是不可逆操作,如需修改只能新建课程。 --- *本文档随代码同步维护,若接口或配置有调整请一并更新。*