ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

学生信息管理系统课程设计:SSM分页、事务与批量录入实战

学生信息管理系统课程设计:SSM分页、事务与批量录入实战 简介这是一份面向高校软件工程课程设计与期末大作业的学生信息管理系统完整报告文档适合正在准备课程设计、需要参照标准开发流程与文档规范的学生及指导教师。文档以软件工程传统方法学为主线依次展开可行性分析、需求分析、总体设计、详细设计四大阶段涵盖问题定义、功能与界面及性能需求、分析建模、各模块功能描述、数据库设计、功能模块详细设计等内容并配有摘要、目录、绪论与结论的完整章节编排。系统采用 Visual Studio 6.0 与 SQL Server 2000 实现设学生信息管理、班级管理、课程管理、成绩管理、信息查询与成绩查询等模块可完成添加、修改、删除、查询、打印等操作。压缩包内仅 1 个 doc 文件约 304KB结构紧凑可直接套用为报告模板或答辩参考。目前已有 4133 人学习下载。1. 软件工程课程设计里学生信息管理系统真正要交的是什么答辩现场最容易翻车的往往不是界面丑或者功能少而是文档和代码对不上需求分析里写了成绩审核流程数据库里却没有审核状态字段用例图画了管理员、教师、学生三个角色登录表里只有孤零零一张 user。老师追问一句成绩录入的事务边界在哪、两个老师同时改一条成绩怎么办场面就很难收拾。所以学生信息管理系统这个题目的重点从来不是能增删改查而是能不能用一条可追溯的软件工程链路把需求、设计、实现、测试串起来让每一行代码都能指回某张图、某个用例。它适合两类人一类是需要在两三周内交出可演示系统和配套文档的在校生另一类是想把 SSM、JSP、MyBatis 这套传统技术栈重新捡起来、顺带补齐建模能力的从业者。下面按建模、后端、联调、验收四段推进从空目录做到能上台演示。2. 需求建模到表结构学生信息管理系统的用例图、流程图与 E-R 设计课程设计扣分最集中的地方就在这一层。很多人直接打开 IDE 建库建表等到写文档时再补需求结果发现学生可以自己改成绩这种漏洞已经写进代码了。正确的顺序是先定角色与用例再定状态流转最后才落到字段和索引。这一层做扎实后面的后端代码几乎是照着搬文档也能直接由模型生成而不是回头硬编。2.1 角色与用例先钉死再列功能清单三个角色的权限边界必须在第一版就写进表里不然后期加权限会牵动所有接口。角色核心用例数据可见范围典型误判管理员学生/班级/课程维护、账号重置、数据导入导出全量数据让管理员直接录成绩与教师职责重叠教师成绩录入与修改、成绩单导出、任课班级查询仅本人任课课程只按班级过滤忽略任课关系学生个人信息查看与有限修改、成绩查询、密码修改仅本人记录允许改姓名学号导致主键语义漂移清单里每一条用例后面都要挂一个验收条件比如学生只能查到自己的成绩对应的验收条件就是把 URL 里的 studentId 手动改成别人的接口必须返回 403 而不是数据。这一条写进文档答辩时几乎必被问到提前答上就是加分项。2.2 软件工程流程图怎么画才经得起追问成绩从录入到发布是一条状态机不要画成一张把所有人框进去的大流程图。常见做法是拆成教师提交—教务审核—发布—学生可见四个状态每个状态明确谁能触发、触发后写哪些字段。当前状态触发动作触发角色目标状态落库字段变化DRAFT提交教师SUBMITTEDstatus1, submit_timenow()SUBMITTED审核通过管理员PUBLISHEDstatus2, audit_id, audit_timeSUBMITTED驳回管理员DRAFTstatus0, reject_reasonPUBLISHED申请修改教师SUBMITTEDstatus1, versionversion1画到这一层老师再问已发布的成绩被改了怎么追溯你可以直接指向 version 字段和一条操作日志表。那种只有一张登录—查询—修改—退出的流程图是毕设里最容易被一眼看穿凑数的部分。2.3 学生信息管理系统核心表结构 DDL字段设计要预留两类东西一类是软删除和审计字段一类是能把并发问题关掉的唯一约束。-- 班级表一个班级对应一名辅导员冗余字段避免高频联表 CREATE TABLE t_class ( id BIGINT PRIMARY KEY AUTO_INCREMENT, class_code VARCHAR(20) NOT NULL COMMENT 班级编号如 CS202101, class_name VARCHAR(50) NOT NULL, counselor VARCHAR(30) DEFAULT NULL, UNIQUE KEY uk_class_code (class_code) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 学生表学号唯一deleted 做软删除避免物理删除破坏成绩外键 CREATE TABLE t_student ( id BIGINT PRIMARY KEY AUTO_INCREMENT, sno VARCHAR(20) NOT NULL COMMENT 学号登录账号, name VARCHAR(30) NOT NULL, gender TINYINT DEFAULT 0 COMMENT 0未知 1男 2女, class_id BIGINT NOT NULL, phone VARCHAR(20) DEFAULT NULL, deleted TINYINT NOT NULL DEFAULT 0, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_sno (sno), KEY idx_class_id (class_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 成绩表同一学生同一课程只允许一条有效记录用唯一键兜住并发重复插入 CREATE TABLE t_score ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_id BIGINT NOT NULL, course_id BIGINT NOT NULL, teacher_id BIGINT NOT NULL, score DECIMAL(5,2) DEFAULT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0草稿 1待审核 2已发布, version INT NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, audit_time DATETIME DEFAULT NULL, UNIQUE KEY uk_stu_course (student_id, course_id), KEY idx_course_status (course_id, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;逻辑说明uk_stu_course这一个唯一索引比在 Service 里写先查再插要可靠得多因为两次请求同时通过查重检查时数据库会替你把第二条挡掉代码只需捕获唯一键冲突异常即可。idx_course_status是为某门课已发布的成绩单这个高频查询准备的它让过滤在索引层完成而不是把整门课的数据拉回内存再筛。参数说明DECIMAL(5,2)支持到 999.99够放百分制也够放绩点TINYINT表示布尔和枚举比 VARCHAR 省空间且便于统计utf8mb4是必须的学生姓名和外文课程名都可能出现超出 utf8 的字符时间字段用DEFAULT CURRENT_TIMESTAMP而不是应用层赋值能避免不同服务器时钟不一致带来的排序错乱。2.4 把头歌软件工程实训的阶段产出映射到自己的仓库很多人在头歌上做过需求分析、概要设计、详细设计那一串实验交完就丢了。其实那套阶段划分正好可以当课程设计的交付骨架直接映射成目录student-ms/ ├── docs/ │ ├── 01-requirement/ # 用例图、用例规约、非功能需求 │ ├── 02-design/ # 类图、E-R 图、接口定义、状态机 │ ├── 03-detail/ # 表结构说明、接口字段表、异常码表 │ └── 04-test/ # 测试用例表与执行记录 ├── sql/schema.sql # 建表脚本与 03-detail 一一对应 ├── src/main/java/... # 分层controller/service/mapper/entity └── src/main/webapp/WEB-INF/jsp/每个阶段目录下只放一份能对上的产物不要堆截图。老师翻文档时看的是详细设计里的字段表能不能和 schema.sql 对齐对齐了文档分和实现分就一起拿到。3. SSM 后端实现学生信息管理系统的分页、事务与批量录入后端是工作量最集中的一段。选型、分层、分页、事务这四件事定下来剩下的接口大多是重复劳动两三天就能推完。3.1 技术选型SSMJSP、Spring Boot、Flask/Django 怎么选课程设计的选型不用追新要看三件事环境是否容易在答辩机上装好、分层是否清晰便于讲、文档是否好写。方案上手成本分层清晰度部署复杂度适合情况SSM JSP中XML 配置多高controller/service/mapper 天然分三层需装 Tomcat老师明确要求 JavaWeb 或课时就是 SSMSpring Boot MyBatis低注解为主高内嵌容器一条命令启动想省时间做功能答辩机不想装环境Flask / Django低中Django 自带 admin 省不少页面需装 Python 依赖选题偏 Python 或要写数据分析脚本三个都做得出成绩单和分页区别在于 SSM 的 XML 映射更适合拿来讲详细设计Spring Boot 更快Python 方案便于顺手写数据校验和统计脚本。如果课时里已经铺过 SSM就别换换栈的时间成本远高于它带来的收益。3.2 MyBatis 分页查询的最小可运行实现分页最容易写错的是总数和列表用两套条件导致页数对不上。把条件收敛到一个查询对象里两处复用同一个 where 片段。public class ScoreQuery { private Long courseId; private Integer status; private String keyword; // 学号或姓名模糊匹配 private Integer pageNum 1; private Integer pageSize 10; // getter / setter 省略 }sql idscoreWhere where s.deleted 0 if testcourseId ! nullAND sc.course_id #{courseId}/if if teststatus ! nullAND sc.status #{status}/if if testkeyword ! null and keyword ! AND (st.sno LIKE CONCAT(%, #{keyword}, %) OR st.name LIKE CONCAT(%, #{keyword}, %)) /if /where /sql select idcountByQuery resultTypeint SELECT COUNT(1) FROM t_score sc JOIN t_student st ON st.id sc.student_id WHERE 11 include refidscoreWhere/ /select select idpageByQuery resultTypeScoreVO SELECT sc.id, st.sno, st.name, c.course_name, sc.score, sc.status FROM t_score sc JOIN t_student st ON st.id sc.student_id JOIN t_course c ON c.id sc.course_id include refidscoreWhere/ ORDER BY st.sno ASC LIMIT #{offset}, #{pageSize} /select逻辑说明countByQuery和pageByQuery共用scoreWhere条件永远不会漂移这是分页总数对不上的最常见根因。Service 层负责把pageNum换算成offset (pageNum - 1) * pageSizeMapper 只认 offset 和 pageSize职责不混。参数说明pageSize建议在 Controller 里强制上限比如超过 100 就重置为 100否则有人传pageSize999999会直接把内存打满ORDER BY必须带唯一列兜底这里用学号只按可重复的字段排序会导致翻页时记录重复或漏出LIKE CONCAT(%, ?, %)里参数必须走占位符拼接字符串就是注入。3.3 成绩批量录入事务边界与幂等控制批量导入最怕两件事一半成功一半失败以及重复点提交插两遍。前者靠事务后者靠唯一键加幂等判断。Service public class ScoreServiceImpl implements ScoreService { Transactional(rollbackFor Exception.class) Override public ImportResult importScores(Long courseId, Long teacherId, ListScoreRow rows) { ImportResult result new ImportResult(); for (ScoreRow row : rows) { Long studentId studentMapper.findIdBySno(row.getSno()); if (studentId null) { result.addError(row.getLineNo(), 学号不存在: row.getSno()); continue; // 单条数据问题不拖垮整批 } if (row.getScore() null || row.getScore().compareTo(BigDecimal.ZERO) 0 || row.getScore().compareTo(new BigDecimal(100)) 0) { result.addError(row.getLineNo(), 分数越界); continue; } Score exist scoreMapper.findByStudentAndCourse(studentId, courseId); if (exist ! null exist.getStatus() 2) { result.addError(row.getLineNo(), 成绩已发布需先申请修改); continue; } scoreMapper.upsert(studentId, courseId, teacherId, row.getScore()); } if (result.getErrorCount() 0 result.getSuccessCount() 0) { throw new BizException(全部数据校验失败已回滚); } return result; } }逻辑说明事务用rollbackFor Exception.class因为 Spring 默认只对运行时异常回滚抛受检异常时数据会留在库里。校验失败的行只记录错误行号并继续这样一份两百行的表里有两行学号写错其余 198 行照样入库学生体验和老师印象都好得多。upsert在 SQL 层写成INSERT ... ON DUPLICATE KEY UPDATE配合uk_stu_course唯一键把先查再插的竞态窗口彻底关掉。参数说明courseId和teacherId必须在方法入口校验是否为当前登录教师的任课课程否则越权写入错误收集器用行号而不是下标因为 Excel 有表头行号才和用户看到的对得上单批建议控制在 500 行以内一次性提交上万行会让事务持锁时间过长。3.4 必调参数表参数建议值作用改错的后果maxActive连接池20并发连接上限过大压垮数据库过小请求排队超时maxWait3000ms取连接最长等待设为 -1 会永久阻塞线程堆满pageSize 上限100单页最大记录数不设上限易被大页码拖慢事务超时 timeout10s单事务最长执行时间不设则批量导入可能长时间持锁验证码/登录失败次数5 次防止暴力猜密码不限制则账号可被穷举4. JSP 与接口联调学生信息管理系统的检索、导入导出与数据校验后端接口通了不等于系统能用。联调阶段暴露的问题大多集中在参数约定、空值处理和文件编码上占掉的时间往往比写业务代码还多。4.1 接口约定与统一响应体先定返回结构避免每个接口各写一套前端每页都要写不同解析逻辑。public class RT { private int code; // 0 成功非 0 为错误码 private String msg; private T data; public static T RT ok(T data) { ... } public static T RT fail(int code, String msg) { ... } }逻辑说明错误码分三段1xxx 参数类、2xxx 权限类、3xxx 业务类前端只按段位决定是弹提示还是跳登录页。msg面向开发者用户看到的是前端按 code 映射的文案避免把 SQL 异常原文甩到页面上。参数说明分页接口统一返回{ list, total, pageNum, pageSize }total必须是符合当前过滤条件的总数而不是全表行数这是最常见的联调返工点。4.2 JSP 页面里的条件检索与分页form idqueryForm action${pageContext.request.contextPath}/score/page methodget 课程input namecourseId value${query.courseId}/ 关键字input namekeyword value${query.keyword}/ input typehidden namepageNum idpageNum value${query.pageNum}/ button typesubmit查询/button /form table c:forEach items${page.list} varrow tr td${row.sno}/tdtd${row.name}/td tdfmt:formatNumber value${row.score} pattern0.00//td td${row.status 2 ? 已发布 : 待审核}/td /tr /c:forEach /table逻辑说明查询表单用 GET 提交翻页时只需要改pageNum再提交条件自动带上刷新页面也不会重复提交。fmt:formatNumber处理分数显示避免89.0和89.00混着出现。翻页按钮的 JS 只做一件事设置 hidden 的pageNum然后提交表单不重新拼 URL。参数说明${query.courseId}回填是为了翻页后条件不丢分页控件的上一页/下一页要在边界处禁用而不是靠服务端返回空列表让用户自己发现。4.3 批量导入导出 Excel 的实现要点导入用 POI 的流式读取SXSSFWorkbook用于导出导出时不要一次把全表塞进内存。常见坑有三个一是.xls和.xlsx要用不同 Workbook 实现不能只写一种二是导出文件名要 URL 编码否则中文名在部分浏览器里变乱码三是导入必须限制文件大小和行数比如 5MB、2000 行超了直接拒绝。导出接口的响应头写法固定response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); response.setHeader(Content-Disposition, attachment; filename URLEncoder.encode(成绩单.xlsx, UTF-8));逻辑说明Content-Disposition里的文件名必须编码且等号后不要加引号包裹原始中文否则不同浏览器表现不一致。导出走响应流直接写出不在服务器落临时文件省去清理逻辑。参数说明导出前先按当前查询条件统计行数超过阈值比如 1 万行就提示用户缩小范围或改为异步导出避免一次请求长时间占线程。4.4 用 Python 脚本对拍 Excel 与数据库交件前最值得花半小时做的事是拿原始 Excel 和库里的数据对一遍。手写的对拍脚本比人眼快得多。import pymysql, openpyxl conn pymysql.connect(host127.0.0.1, userroot, passwordroot, databasestudent_ms, charsetutf8mb4) cur conn.cursor() cur.execute(SELECT st.sno, sc.score FROM t_score sc JOIN t_student st ON st.id sc.student_id WHERE sc.course_id %s, (1001,)) db {row[0]: float(row[1]) for row in cur.fetchall()} wb openpyxl.load_workbook(grades.xlsx) ws wb.active bad [] for r in ws.iter_rows(min_row2, values_onlyTrue): sno, score str(r[0]).strip(), r[1] if sno not in db: bad.append((sno, 库中缺失)) elif abs(db[sno] - float(score)) 0.001: bad.append((sno, f不一致 库{db[sno]} 表{score})) print(f总行数{ws.max_row - 1} 异常{len(bad)}) for item in bad[:20]: print(item)逻辑说明脚本只做读和对拍不写库避免把错误数据再灌一遍。差异阈值取 0.001 是为了避开浮点误差DECIMAL取出来的值转 float 后严格相等通常没问题但留一点余量更省心。参数说明min_row2跳过表头str(r[0]).strip()处理 Excel 里学号被识别成数字导致的前后空格打印只取前 20 条避免刷屏淹没真正的错误统计。5. 答辩前的验收慢查询定位与一键复现部署到这一步功能已经齐了剩下的是让它在老师的机器上也能跑起来并且被问到时能当场给出证据。先做索引体检。成绩单页面通常是最慢的用EXPLAIN看执行计划重点看type和rows两列EXPLAIN SELECT sc.id, st.sno, st.name FROM t_score sc JOIN t_student st ON st.id sc.student_id WHERE sc.course_id 1001 AND sc.status 2 ORDER BY st.sno ASC LIMIT 0, 10;如果t_score那行显示typeALL或rows接近全表行数说明idx_course_status没被用上常见原因是查询里对索引列做了函数运算比如DATE(sc.audit_time) 2024-06-01改成范围条件sc.audit_time 2024-06-01 AND sc.audit_time 2024-06-02就能走索引。另一个高频问题是ORDER BY的列和过滤列不同表MySQL 只能先取交集再排序数据量大时加一个覆盖索引(course_id, status, student_id)会明显改善。部署环节建议写一个脚本把建库、导数据、启动三步固定下来答辩时直接演示#!/usr/bin/env bash set -e DBstudent_ms mysql -uroot -proot -e DROP DATABASE IF EXISTS $DB; CREATE DATABASE $DB DEFAULT CHARSET utf8mb4; mysql -uroot -proot $DB sql/schema.sql mysql -uroot -proot $DB sql/seed.sql mvn -q clean package -DskipTests java -jar target/student-ms.jar --spring.profiles.activedemo逻辑说明set -e让任一步失败立即中止避免带着半截数据库继续启动。先删库再建库保证每次演示的数据完全一致不会因为上一次演示留下的脏数据影响结果。--spring.profiles.activedemo指向演示用的配置文件把数据库连接、端口、日志级别都固定下来。验收项检查方式通过标准越权访问手动改 URL 里的 studentId返回 403不返回他人数据重复导入同一份 Excel 连续导入两次第二次不产生新记录只更新分页总数检索条件下翻到最后一页最后一页记录数与 total 一致已发布成绩保护教师尝试修改已发布成绩提示需申请修改不直接落库冷启动在干净机器执行部署脚本三条命令内可访问首页最后补一个演示顺序先用管理员登录展示班级与课程维护再切教师录一批带错误行的成绩现场展示错误行号和成功行数最后切学生查成绩并尝试访问他人记录被拒。这条路径把需求分析里的三个角色、详细设计里的状态机、异常处理里的错误码全串了一遍比逐个页面点过去有说服力得多。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进