
简介这是一套基于PHPAJAX的学生成绩管理系统源码面向大中小学及培训机构也适用于部队等需要成绩统计的单位。系统提供管理员、校长室、班主任、任课老师、学生及家长六种权限可在线录入、查询成绩自动排名并生成平均分、最高分、最低分支持多次考试对比与柱状图展示还具备Excel导入导出、留言互动及公告发布功能。资源共1166个文件压缩包18.99MB包含229个php源码、277个so动态库、81个dll组件及gif/html/css等辅助文件另有50组MySQL数据文件frm/myd/myi便于本地部署测试。已有3197人学习下载适合需要快速搭建成绩管理系统的开发者或学校信息化人员包内自带虚拟测试数据删除全部年级信息即可一键清空。1. 学生成绩管理系统源码从“能跑”到“能演示”的关键不在代码量学生成绩管理系统可能是国内高校里被写烂但依然写不完的一个项目。每个学 Java 的人都改过一版每次答辩现场都能看到几个界面相似、功能残缺的“全新”系统。问题不在需求不明确而在于大多数源码把重点放在页面堆砌上数据库设计得像是一张 Excel 转储表统计班级排名要么全表循环要么查一个数要跑三条 SQL。这篇文章要给的不是某个现成的完整源码包而是讲清楚一套可维护、可演示的成绩管理系统是如何拆解的。从表结构、权限边界、接口实现到本地部署每个环节都给出具体的代码与参数你可以拿着它对照自己的项目逐行修改。适合正在做课程设计的学生也适合需要快速交付内部教学工具的开发者。2. 技术选型与权限模型先定边界再写第一行代码2.1 三种技术栈对应的三种交付场景先回答最常见的问题成绩管理系统到底用哪套框架。答案取决于交付场景。传统课设最稳妥的组合是 Spring MVC MyBatis JSP理由很简单部署时只需要一个 Tomcat不用处理跨域JSP 在服务端渲染页面逻辑全在本地答辩时网络断了也能演示。缺点是前后端耦合严重界面改动需要重启服务后续想加个图表功能要把整个渲染链路翻一遍。前后端分离的做法近年来成了主流Spring Boot 提供 REST 接口Vue 负责页面展示。这套方案读起来更接近企业开发流程源码结构也更好讲答辩时可以重点说接口设计。代价是需要同时维护两个服务构建和部署环节都要多配置一步。如果只是做课设第一次写前后端分离的项目光是把前端 dev server 和后端接口调通就要耗掉不少时间。如果目标是快速交付给某老师或某部门内部使用我一般会用 Spring Boot Thymeleaf服务端渲染单包部署既不用处理跨域又保留了模板渲染的灵活性。三套方案没有绝对的好坏只有匹配度的差异。判断标准是三个问题这个系统要维护多久演示环境能不能联网接手的人会不会写前端成绩管理系统的核心难点在权限设计和统计 SQL 上只要这两个点做扎实前端是 Vue 还是 JSP 影响并不大。2.2 按角色拆分权限边界成绩管理系统最容易被忽略的需求是权限边界。很多源码里登录进去只有一个页面管理员能做的事全部堆在同一张表上学生点开还能看到修改成绩的按钮这种设计在答辩时很难解释清楚。按使用角色划分系统至少要有三类用户管理员、教师、学生另外可能还有教务处这类特殊角色。不同角色的操作边界必须在一开始就定死。管理员负责维护用户账号、课程信息和班级字典能看全校的统计汇总但不直接录入成绩教师只能查看和录入自己授课课程的成绩能导出所授课程的成绩表学生只能查看自己的成绩、学期课程排名和学分统计。这张边界表建议直接写进需求文档的第一页后面所有接口的权限校验都围绕它展开。这个模型的落点是权限校验必须在后端完成前端隐藏按钮只是体验优化不是安全措施。实现方式有两种。传统方案是登录时把角色写进 session每个接口用拦截器判断角色字符串。前后端分离的方案则是登录成功后返回 JWTJWT 里带上角色字段后端接口通过拦截器或 AOP 校验角色。具体的代码在第四章给出这里先明确一点权限校验的粒度应该到接口级别而不是页面级别。2.3 目录结构直接决定后期维护成本很多课设源码只有一个 Controller 包、一个 Mapper 包所有逻辑全写在 Controller 里一个类动辄上千行。成绩系统的业务量不算大但查询场景多、统计口径多如果 Service 层不做隔离后期加一个“按专业汇总平均分”的需求会牵连十几个方法。推荐的模块划分代码如下com.example.score ├── controller # 只做参数接收与统一响应封装 ├── service # 业务校验、事务边界、统计计算 ├── mapper # MyBatis / JPA 数据访问层 ├── entity # 与表结构对应的实体 ├── dto # 接口出入参对象不与实体混用 ├── security # JWT 生成、拦截器、角色校验 ├── config # 跨域、拦截器注册、全局异常处理 └── common # 统一返回体、分页对象、常量Service 层的核心职责是把 Controller 和 Mapper 之间的业务逻辑收拢。比如成绩录入时判断学生是否存在、该课程是否由当前教师授课这些判断不属于 SQL也不属于参数接收放在 Service 是最合适的。目录定了后续的编码过程会省去大量来回定位的时间。Controller 层应该瘦到只有十行左右接收参数、调用 Service、返回统一响应对象。3. 数据库设计与统计SQL五张表撑起整个成绩系统的查询骨架3.1 五张核心表的字段设计成绩系统的数据模型绕不开五张表学生表、教师表、课程表、成绩表、用户表。很多源码会把用户表和角色表拆到四张表对权限极度复杂的业务有道理但成绩系统只有三种角色拆太细反而让联表查询变繁琐。我习惯把账号信息单独放一张 user 表student 和 teacher 表只存业务属性通过外键关联 user 表的主键。建表脚本MySQL 8.0CREATE TABLE user ( id BIGINT NOT NULL AUTO_INCREMENT, username VARCHAR(32) NOT NULL COMMENT 登录账号, password VARCHAR(128) NOT NULL COMMENT BCrypt加密后的密码, role TINYINT NOT NULL COMMENT 1管理员 2教师 3学生, status TINYINT NOT NULL DEFAULT 1 COMMENT 1启用 0禁用, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT登录账号表; CREATE TABLE student ( id BIGINT NOT NULL AUTO_INCREMENT, user_id BIGINT NOT NULL COMMENT 关联user.id, student_no VARCHAR(20) NOT NULL COMMENT 学号, name VARCHAR(32) NOT NULL, class_name VARCHAR(64) NOT NULL COMMENT 班级名称如软件2302, major VARCHAR(64) NOT NULL DEFAULT COMMENT 专业, enroll_year CHAR(4) NOT NULL COMMENT 入学年份, PRIMARY KEY (id), UNIQUE KEY uk_student_no (student_no), KEY idx_class (class_name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT学生信息表;这里有两处设计值得说明。password 字段用 VARCHAR(128) 而不是 VARCHAR(64)是因为 BCrypt 加密后长度约 60 字符但不同版本的 BCrypt 实现可能产生不同长度留足余量可以避免后续升级框架时被迫改表结构。student 表的班级信息直接存 class_name 字符串而不是班级表 ID是为了查询时少一次联表成绩统计中按班级分组是最高频的操作省一次 JOIN 在大数据量下收益明显。如果班级要改名单独 UPDATE 一次即可不影响历史成绩。成绩表是整套系统的命脉设计时要把“课程快照”概念落实CREATE TABLE score ( id BIGINT NOT NULL AUTO_INCREMENT, student_id BIGINT NOT NULL COMMENT 关联student.id, course_id BIGINT NOT NULL COMMENT 关联course.id, course_name VARCHAR(64) NOT NULL COMMENT 冗余课程名课程改名不影响历史成绩, teacher_name VARCHAR(32) NOT NULL COMMENT 冗余授课教师名, score DECIMAL(5,2) NOT NULL COMMENT 成绩满分100, semester VARCHAR(16) NOT NULL COMMENT 如 2024-2025-1, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_student_course (student_id, course_id, semester), KEY idx_semester (semester), KEY idx_course (course_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT学生成绩表;评分字段用 DECIMAL(5,2) 而不是 INT原因是很多学校的成绩有 0.5 的小数档INT 会把 88.5 截断成 88这个错误隐藏得很深往往到期末统计平均分时才发现分数对不上。唯一索引放到 (student_id, course_id, semester) 三个字段上用来防止同一门课在同一学期被重复录入两次这是成绩录入接口幂等性的最后一道数据库防线。3.2 为什么成绩表必须冗余课程名和教师名这里有一个容易被忽视的设计决策score 表为什么要冗余 course_name 和 teacher_name而不是只存 course_id 然后查询时 JOIN 课程表。原因有两个。第一是历史追溯某学院把“高等数学A”改名为“高等数学(上)”之后如果成绩表只有 course_id所有往年成绩的显示名称都会跟着课程表走演示时老师问“为什么 2023 年的课程名单里显示 2024 年的课程名”解释成本极高。冗余字段让成绩变得不可变课程表怎么改都影响不到历史记录。第二是查询性能。学生查成绩列表、班级查平均分这两类高频 SQL 都只需要 score 一张表的数据。如果不冗余每次查询都要 JOIN course 和 teacher 两张表数据量到了十万级之后JOIN 的成本会明显影响接口响应时间。冗余字段占用的存储空间极少但换来的查询简化是真实的。这是典型的“读多写少”场景里的空间换时间做法对于成绩系统这种极低频写入的业务冗余几乎是零成本。3.3 成绩统计 SQL平均分、排名与及格率成绩统计是整个系统里最值得写的部分。先看按班级计算平均分和及格率的 SQLSELECT s.class_name, COUNT(*) AS total_cnt, ROUND(AVG(sc.score), 2) AS avg_score, ROUND(SUM(CASE WHEN sc.score 60 THEN 1 ELSE 0 END) / COUNT(*) * 100, 2) AS pass_rate FROM score sc JOIN student s ON sc.student_id s.id WHERE sc.semester 2024-2025-1 GROUP BY s.class_name ORDER BY avg_score DESC;这个查询的要点集中在 CASE WHEN 那一行。及格率分母用的是 COUNT() 而不是 SUM(CASE…)因为如果某学生缺考score 字段为 NULLAVG 会跳过 NULL但 COUNT 不会直接用 SUM(CASE WHEN score 60 THEN 1 ELSE 0 END) 除以 COUNT() 是最稳妥的口径。如果要把缺考算作不及格把 CASE 条件改成 score IS NOT NULL AND score 60语义才准确。这里的分母口径是整个统计模块最容易出错的地方写的时候一定要想清楚缺考算不算分母。排名查询在 MySQL 8.0 里用窗口函数SELECT student_id, course_name, score, RANK() OVER (PARTITION BY course_id ORDER BY score DESC) AS course_rank FROM score WHERE semester 2024-2025-1 ORDER BY course_id, course_rank;RANK 与 DENSE_RANK 的区别要留意两个学生并列第二时RANK 会让下一个名次变成第四DENSE_RANK 则还是第三。成绩单上通常希望展示 DENSE_RANK 的效果如果源码里直接用 RANK演示时两个人同分就会出现名次断层容易被当成 bug。这个细节是统计 SQL 里最常见的真实翻车点。4. 后端接口与登录鉴权Spring Boot 最精简的可运行实现4.1 登录接口JWT 生成与拦截器注册登录是成绩系统第一个要实现的接口。用 JWT 替代 Session 的好处是无状态、前后端分离时跨域友好缺点是服务端无法主动注销但这不影响成绩系统的场景。JWT 生成代码Component public class JwtUtil { private SecretKey key Keys.hmacShaKeyFor(score-system-secret-key-please-change.getBytes()); private long expireMs 12 * 60 * 60 * 1000L; public String createToken(Long userId, String role) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim(role, role) .setExpiration(new Date(System.currentTimeMillis() expireMs)) .signWith(key) .compact(); } }参数说明secret 不能写死在源码里实际项目应该放到配置文件并用环境变量注入这里写死是为了课设演示方便交付前一定要改。过期时间设置成 12 小时适合课设场景如果系统是给某高校内部长期使用建议改成 30 分钟并配合刷新接口否则学生登录一次能挂一整天安全性差一些。登录接口的完整链路是接收用户名密码用 BCrypt 校验密码加载用户角色生成 JWT 返回前端。拦截器是权限校验的落点。Spring Boot 中注册 WebMvcConfigurer 并重写 addInterceptors 方法拦截所有 /api/** 路径放行 /api/auth/login。拦截器里从请求头取 Authorization解析 JWT把 userId 和 role 存入 ThreadLocal后续接口直接从 ThreadLocal 读取当前用户。这里有个容易踩的坑拦截器拦截了 OPTIONS 预检请求导致前端跨域请求被 401解决方法是在拦截器里优先处理 OPTIONS 请求并直接返回成功。4.2 成绩录入接口事务边界与幂等控制成绩录入接口的业务规则是教师只能录自己教的课程一个学生同一学期同一课程只能有一条成绩录入后同步更新学期统计缓存。Controller 层只做参数接收PostMapping(/api/score) public Result? add(RequestBody ScoreAddDTO dto, RequestAttribute(userId) Long userId) { scoreService.addScore(dto, userId); return Result.success(); }Service 层的实现是核心事务边界必须放在这里而不是 ControllerService public class ScoreServiceImpl implements ScoreService { Autowired private ScoreMapper scoreMapper; Autowired private CourseMapper courseMapper; Override Transactional(rollbackFor Exception.class) public void addScore(ScoreAddDTO dto, Long teacherUserId) { // 1. 校验课程是否由当前教师授课 Course course courseMapper.selectById(dto.getCourseId()); if (course null || !course.getTeacherUserId().equals(teacherUserId)) { throw new BizException(无权录入该课程成绩); } // 2. 校验学生存在 Student student studentMapper.selectById(dto.getStudentId()); if (student null) { throw new BizException(学生不存在); } // 3. 幂等控制先查再插数据库唯一索引兜底 Score exists scoreMapper.selectByUnique(dto.getStudentId(), dto.getCourseId(), dto.getSemester()); if (exists ! null) { throw new BizException(该学生本学期已录入成绩如需修改请走更新接口); } scoreMapper.insert(buildEntity(dto)); } }参数说明三点。第一Transactional 必须显式写 rollbackFor Exception.classSpring 默认只对 RuntimeException 回滚而业务中常抛的自定义异常 BizException 如果继承的是 Exception默认不会被回滚成绩插了一半的脏数据就产生了。第二幂等判断放到数据库唯一索引层面Service 里的先查再插只能挡住常规请求挡不住并发场景但成绩录入基本是单人在操作先查再插加唯一索引兜底已经足够。第三教师权限的校验是拿当前登录教师的 user_id 和课程表的 teacher_user_id 比对而不是比对角色字符串因为管理员也登录系统但管理员不属于任何课程。这是接口级权限和角色级权限的差别。4.3 成绩分页查询手写 SQL 比自动分页更可控成绩列表是前端用得最多的接口条件包括学期、班级、课程、分数区间四类。很多源码用 PageHelper 做分页在小数据量课设里跑得通但和 GROUP BY 同用时 PageHelper 的自动 count 会出错它默认在包一层 count 查询时不会保留 GROUP BY 的语义导致总数统计错乱。我一般直接手写分页 SQLpublic PageResultScoreVO pageQuery(ScoreQueryDTO query, PageParam page) { // 1. 手工查询总数 Long total scoreMapper.countByCondition(query); // 2. 手工 limit 查询列表 ListScoreVO list scoreMapper.selectPage(query, page); return PageResult.of(total, list); }Mapper 里的 SQL 用 XML 或注解都行核心是 limit 参数从 PageParam 取select idselectPage resultTypeScoreVO SELECT sc.id, s.student_no, s.name, s.class_name, sc.course_name, sc.score, sc.semester FROM score sc JOIN student s ON sc.student_id s.id where if testquery.semester ! null and query.semester ! AND sc.semester #{query.semester} /if if testquery.className ! null and query.className ! AND s.class_name #{query.className} /if if testquery.courseName ! null and query.courseName ! AND sc.course_name LIKE CONCAT(%, #{query.courseName}, %) /if /where ORDER BY sc.semester DESC, s.class_name, sc.score DESC LIMIT #{page.offset}, #{page.size} /select分页参数说明offset 的计算方式是 (pageNum - 1) * pageSize注意前端传的参数一般是 pageNum 而不是 offset后端要自行转换。ORDER BY 的列名不能直接拼接前端传参否则会产生 SQL 注入风险固定的排序字段用 switch 映射。JOIN 之后的 COUNT 查询需要根据场景判断是否要 JOIN student 表按班级筛选时才需要按课程筛选时只查 score 表即可一个小优化却能减少大表场景下的扫描行数。5. 学生成绩系统的常见踩坑编码、分页、事务与安全的四次翻车5.1 中文乱码三处编码不一致导致的经典问题现象学生姓名存入数据库后显示为问号或者接口返回 JSON 里中文变成 \uXXXX。原因多数情况下是 MySQL 连接串缺了 characterEncodingutf8也有可能是前端页面没声明 UTF-8还有可能是响应头 Content-Type 没有指定 UTF-8。三处必须保持一致。解决方法是逐层排查先改 JDBC URL 加 ?characterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai再看后端响应统一设置 Content-Type: application/json;charsetUTF-8最后确认前端静态页面的 meta charset 和 axios 请求头。如果三处都正确依然乱码检查 MySQL 建库时用的 charset 是否 utf8mb4utf8 在 MySQL 里存的不是完整四字节遇到特殊符号时也会出乱码。5.2 分页总数与列表数据不一致聚合查询的统计口径问题现象列表页显示“共 38 条”但翻到最后一页只剩 3 条。原因总数查询和列表查询的 WHERE 条件不一致。多发生在加了条件筛选之后后补筛选条件时只改了列表 SQL忘记同步改 count SQL。解决把 count 查询和 list 查询放在同一个 SQL 文件里相邻位置每次修改条件必查两个方法。另一个高发场景是 PageHelper 和 GROUP BY 同用导致 count 查出来的数字是分组后的行数而不是记录行数此时手写 count 子查询或者干脆像第四章那样完全手写分页。5.3 批量导入 Excel 时事务回滚失效现象导入 200 条成绩第 150 条数据非法前 149 条已经插入整体接口却报错退出。原因使用了 EasyExcel 的监听器模式监听器的 invoke 方法不在 Spring 管理的 Service 事务上下文里事务注解没有生效。解决不要在整个导入流程上套 Transactional改为在监听器的每个批量块上手动调用事务方法或者先把 Excel 全部解析成 List 再做一次性批量插入。注意批量插入时用 batch 模式循环单条 insert 在几百条数据时可能感觉不到到了几千条就会慢到难以接受。事务注解失效的检查方式是看日志里有没有 “Rolling back” 字样没有就说明事务根本没参与。5.4 密码明文存储课设系统最常见的风险点现象数据库 user 表里的 password 字段直接存了明文打开数据库就能看到所有人的登录密码。原因教学阶段很多源码为了简化演示省略了加密步骤但作为交付给学校使用的系统这是不可接受的。解决用 BCrypt 加密不自己写 MD5 加盐逻辑。引入 spring-security-crypto 包单独使用 BCryptPasswordEncoder 即可不依赖完整 Spring Security 就能解决加密问题。BCrypt 每次加密生成的哈希值不同校验时用 matches 方法而不是等号比较。已有的明文数据迁移时写一个启动时校验的逻辑检测到密码字段长度小于 60 就触发强制重置流程不要试图把明文逆向转换回去。5.5 前端刷新 404 与跨域配置冲突现象Vue 项目用 history 路由部署后页面内跳转正常手动刷新 URL 直接报 404。原因前端路由是前端处理的路径后端没有对应映射刷新时后端直接 404。解决是部署时用 Nginx 的 try_files $uri $uri/ /index.html 做兜底或者在 Spring Boot 后端加一个转发到 index.html 的 fallback 配置。跨域配置冲突是另一个高频问题具体表现是后端同时配置了 allowCredentials(true) 和 allowedOrigins()运行时报错。Spring Boot 2.4 之后要求 allowedOrigins 和 allowCredentials 不能同时使用通配符改用 allowedOriginPatterns() 即可。6. 演示前的最后一步用接口自测脚本和种子数据锁定交付质量源码写完后真正拉开差距的是交付前的自测流程。我习惯在每个成绩系统的 controller 层补一个基于 MockMvc 的自测方法用 JUnit 5 跑一遍核心链路管理员登录、创建课程、分配教师、教师录入成绩、学生查询列表、统计接口返回正确排名。这套自测不追求覆盖率只覆盖主链路目的是在项目启动之前把接口层面的错误全部暴露出来避免到演示现场手动点错一步才发现接口 500。种子数据也要单独维护不能依赖开发时随手插入的测试数据。一个 seed.sql 至少包含三部分三个角色的登录账号、两个班级共十名学生、四门课程和对应的成绩。演示时只需要执行一个 SQL 脚本再启动后端系统就是完整的可讲解状态。某次我在演示前临时改了课程表结构忘了重新导入种子数据结果排名接口查出来全是 NULL只能现场改代码从那以后我把 seed.sql 放进了项目的 scripts 目录每次修改表结构都同步更新脚本并跑一遍自测。成绩系统这类项目的交付标准不是功能堆得够不够多而是主链路能不能在十分钟内被完整走通。建议你把自己项目的核心流程画一遍对照着补自测脚本和种子数据用这套流程替换掉手工点页面的测试习惯。希望帮到你。本文还有配套的精品资源点击获取