
简介这份资源是《软件工程》课程设计报告文档主题为学生选课系统适合高校软件工程、计算机相关专业学生及需要完成课程设计或撰写课程报告的读者参考。文档围绕高校选课场景覆盖问题定义、可行性研究、项目开发计划等标准软件工程阶段可作为课程设计选题与写作框架的参考范本。压缩包内共1个doc文件约574KB即完整版课程设计文档内容含目录页码与分章节排版便于直接查阅各阶段要点。目前已有1930人学习下载。文档从选课系统的安全性与可靠性、效率与响应速度、用户体验、师生需求等角度切入梳理快速查询选课、课程容量控制、选课行为跟踪分析等待解决问题可行性研究部分则展开背景功能、前提要求、目标与条件假定限制、所建议系统的处理流程与数据流程、工作负荷、技术条件及法律、使用、经济层面的可行性分析并给出结论随后进入项目开发计划。读者可借此理解软件工程课程设计从问题定义到可行性论证的完整组织思路也能参考其章节结构与论述方式用于自身报告的撰写与修改。1. 从一份课程设计文档拆出能跑的学生选课系统很多人做软件工程课设的真实状态是文档写了四十页代码散在五个文件夹里答辩前一天才发现需求分析里写的查看选课人数在代码里根本没有对应实现。这份《学生选课系统》课程设计资源把两边都留全了——前半部分是问题定义、可行性研究、项目开发计划、需求分析、概要设计、数据库设计、详细设计、测试报告、用户手册这一整条文档链后半部分附录里直接给出 DB.java、Login.java、Student.java、Query.java、Modify.java、Teacher.java、Administrator.java 七个类每个类对应一个界面或一层职责。技术栈是 Java 加关系数据库用 JDBC 直连业务逻辑写在界面事件里属于典型的课程设计体量。它适合三类人正在赶软件工程课设、需要一份能对照着改的模板的本科生刚学完 JDBC、想找一个完整小系统练手的 Java 初学者以及想看看文档驱动开发在真实作业里长什么样、哪些章节是凑字数、哪些章节真能指导编码的人。接下来我会按数据建模、持久层封装、事务选课、三种角色查询、测试验证的顺序把它拆开每一步都给出可抄的 SQL 和 Java 片段。2. 需求分析到建表学生、教师、课程、选课四张表怎么定2.1 问题定义里能直接抽出的实体原始文档 1.2 节把角色写得很清楚用户学号、姓名、年龄、性别、系别、班级、出生年月能注册、选课、查已选课程教师姓名、职位、所教课程能查某门课的选课人数和上课时间管理员能查所有学生选课情况、各门课任课教师、各门课选课人数、注册用户信息。把这段话里的名词圈出来实体只有四个学生、教师、课程、选课记录管理员单独一张表或者直接写死在配置里。关键在于选课不能当成学生表的一个字段。一个学生选多门课、一门课被多个学生选是多对多关系必须用中间表拆开。课程容量ccapacity和已选人数cselected放在课程表上属于冗余计数好处是查询快代价是每次选课/退课都要同步更新这一点在事务里必须成对处理。常见的做法是中间表 sc 上加唯一约束unique(sno, cno)从数据库层面挡住重复选课而不是只靠 Java 里 if 判断。2.2 五张表的字段、主外键与约束表名关键字段约束说明studentsno(PK), sname, sage, ssex, sdept, sclass, sbirth, spwdsno 学号做主键spwd 存密码teachertno(PK), tname, ttitle, tdept, tpwdtno 工号主键ttitle 是职位coursecno(PK), cname, tno(FK), ctime, ccredit, ccapacity, cselectedtno 外键指向 teacherscid(PK), sno(FK), cno(FK), scoreunique(sno,cno) 防重复选课adminano(PK), apwd超级管理员一般只有一条记录一个容易踩的坑是外键指向。如果 course.tno 建了外键约束那么删除教师前必须先处理他开的课否则报 1451 错误。课程设计阶段数据量小不少人干脆不建外键靠代码保证一致性但答辩时老师往往会问你怎么保证引用的完整性这时候有外键反而是加分项。我一般会建外键同时在删除逻辑里先做一次引用检查。2.3 建表 DDL 与测试数据脚本-- 建库时显式指定字符集避免中文课程名乱码 CREATE DATABASE IF NOT EXISTS course_select DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE course_select; CREATE TABLE student ( sno VARCHAR(12) PRIMARY KEY, sname VARCHAR(20) NOT NULL, sage INT, ssex CHAR(2), sdept VARCHAR(30), sclass VARCHAR(20), sbirth DATE, spwd VARCHAR(32) NOT NULL ); CREATE TABLE teacher ( tno VARCHAR(12) PRIMARY KEY, tname VARCHAR(20) NOT NULL, ttitle VARCHAR(20), tdept VARCHAR(30), tpwd VARCHAR(32) NOT NULL ); CREATE TABLE course ( cno VARCHAR(10) PRIMARY KEY, cname VARCHAR(40) NOT NULL, tno VARCHAR(12), ctime VARCHAR(40), -- 上课时间如周三 3-4 节 ccredit DECIMAL(3,1) DEFAULT 2.0, ccapacity INT DEFAULT 50, -- 容量上限 cselected INT DEFAULT 0, -- 已选人数冗余计数 CONSTRAINT fk_course_teacher FOREIGN KEY (tno) REFERENCES teacher(tno) ); CREATE TABLE sc ( id INT AUTO_INCREMENT PRIMARY KEY, sno VARCHAR(12) NOT NULL, cno VARCHAR(10) NOT NULL, score DECIMAL(4,1), UNIQUE KEY uk_sno_cno (sno, cno), CONSTRAINT fk_sc_student FOREIGN KEY (sno) REFERENCES student(sno), CONSTRAINT fk_sc_course FOREIGN KEY (cno) REFERENCES course(cno) );这段 DDL 的两处设计值得说明ccapacity给默认值 50是为了让新增课程不必强制填容量uk_sno_cno这个唯一键是并发安全的最后一道防线——即使应用层判断失效同一学生重复提交也会被数据库拦下并抛 Duplicate entry。灌测试数据时建议至少造 3 个教师、10 门课、20 个学生否则后面测容量边界根本测不出来。2.4 数据字典和可行性报告怎么对齐文档 6.4.1 节要求写数据字典很多人把它写成字段列表就交差。合格的做法是每条字段都写清类型、取值范围、来源和约束例如ccreditDECIMAL(3,1)取值范围 0.5~10.0由教务录入非空。这样后面详细设计里的界面校验规则可以直接从数据字典抄不用另想一套。可行性研究里信息维护做到简单易用、不需要对用户进行培训这句要求落到实现上就是所有输入框都要有默认值和格式提示这是个可以量化的验收点。3. DB.java 封装 JDBCLogin.java 做三角色鉴权3.1 为什么要把 JDBC 收进一个 DB 工具类如果每个界面都写一遍DriverManager.getConnection会出现三个问题连接参数改一次要改七个文件忘记关连接导致连接数耗尽异常处理各写各的报错信息五花八门。附录里单独放一个 DB.java本质就是把这些横切关注点收拢。我一般把它设计成三类方法取连接、执行更新返回影响行数、执行查询返回 List 而不是 ResultSet。为什么不直接返回 ResultSet因为 ResultSet 的生命周期绑定在 Connection 上一旦方法内部把连接关了外面拿到的结果集立刻失效这是初学者最常见的结果集已关闭报错来源。3.2 连接串、驱动与三个基础方法package db; import java.sql.*; import java.util.*; public class DB { // useUnicode characterEncoding 解决中文乱码serverTimezone 解决时区报错 private static final String URL jdbc:mysql://127.0.0.1:3306/course_select ?useUnicodetruecharacterEncodingutf8 useSSLfalseserverTimezoneAsia/Shanghai; private static final String USER root; private static final String PWD 123456; static { try { Class.forName(com.mysql.cj.jdbc.Driver); // 驱动只加载一次 } catch (ClassNotFoundException e) { throw new RuntimeException(MySQL 驱动未引入, e); } } public static Connection getConn() throws SQLException { return DriverManager.getConnection(URL, USER, PWD); } /** 通用更新insert / update / delete 都走这里返回影响行数 */ public static int update(String sql, Object... args) { try (Connection conn getConn(); PreparedStatement ps conn.prepareStatement(sql)) { fill(ps, args); return ps.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); return 0; } } /** 通用查询把 ResultSet 转成 ListMap方法返回后连接即可关闭 */ public static ListMapString, Object query(String sql, Object... args) { ListMapString, Object list new ArrayList(); try (Connection conn getConn(); PreparedStatement ps conn.prepareStatement(sql)) { fill(ps, args); try (ResultSet rs ps.executeQuery()) { ResultSetMetaData md rs.getMetaData(); int cols md.getColumnCount(); while (rs.next()) { MapString, Object row new LinkedHashMap(); for (int i 1; i cols; i) { row.put(md.getColumnLabel(i), rs.getObject(i)); } list.add(row); } } } catch (SQLException e) { e.printStackTrace(); } return list; } private static void fill(PreparedStatement ps, Object... args) throws SQLException { for (int i 0; i args.length; i) { ps.setObject(i 1, args[i]); // 占位符下标从 1 开始 } } }这段代码里try-with-resources保证连接和语句一定会关闭fill方法把可变参数依次塞进占位符调用方只需要关心 SQL 和参数顺序。LinkedHashMap而不是HashMap是为了让列顺序和 SELECT 顺序一致界面上按列名取值时不容易错位。使用md.getColumnLabel(i)而不是getColumnName(i)因为带别名的字段比如COUNT(*) AS num只有 label 才拿得到正确名字。3.3 Login.java 的角色分流与密码找回登录界面通常有一个角色下拉框和两个输入框。判断逻辑不要去写三段几乎一样的 SQL而是先按角色决定查哪张表和哪个字段// 根据角色返回登录后的用户信息失败返回 null public static MapString, Object login(String role, String account, String pwd) { String sql; if (学生.equals(role)) { sql SELECT sno AS id, sname AS name FROM student WHERE sno? AND spwd?; } else if (教师.equals(role)) { sql SELECT tno AS id, tname AS name FROM teacher WHERE tno? AND tpwd?; } else { sql SELECT ano AS id, 管理员 AS name FROM admin WHERE ano? AND apwd?; } ListMapString, Object rows DB.query(sql, account, pwd); return rows.isEmpty() ? null : rows.get(0); }选项说明把主键统一别名成id、姓名统一别名成name上层界面就不需要为三种角色写三套渲染逻辑新加一种角色也只要加一个分支。查找忘记的密码这个功能在原始需求里有实现上有两种做法一种是直接按学号查回明文另一种是验证出生年月等辅助信息后再允许重置。课程设计用前者没问题但如果要写进用户手册的安全保密一节至少要在文档里说明这是演示级别的做法。3.4 预编译、字符集与连接泄漏的排查连不上或读出乱码时按这个顺序查驱动 jar 有没有加到 classpath报No suitable driverURL 里的库名、端口对不对报Unknown database时区参数是否缺失报The server time zone value ... is unrecognized中文乱码优先看建库时的字符集和 URL 的characterEncoding是否一致。连接泄漏的典型症状是跑一会儿就报Too many connections用SHOW PROCESSLIST;看一眼有没有大量 Sleep 状态的连接就能确认根因基本都是某处getConn()之后没进 try-with-resources。4. Student.java 的选课事务容量、冲突与重复选课4.1 选课动作的三步校验、占用、落库一次选课在数据库层面是三个动作检查课程是否还有余量、往 sc 表插一条记录、把 course.cselected 加一。这三步必须要么全成功要么全回滚否则会出现记录插进去了但计数没加或者计数加了但记录没插的脏数据后续统计全部失真。更要紧的是并发两个学生同时点选最后一个名额如果先查后插中间没有锁两边都会查到还有 1 个名额然后都插成功容量就超了。4.2 用事务和行锁堵住超选public static String selectCourse(String sno, String cno) { Connection conn null; try { conn DB.getConn(); conn.setAutoCommit(false); // 关闭自动提交开启事务 // FOR UPDATE 锁住这一行第二个并发请求会在这里等待 PreparedStatement ps1 conn.prepareStatement( SELECT cselected, ccapacity FROM course WHERE cno? FOR UPDATE); ps1.setString(1, cno); ResultSet rs ps1.executeQuery(); if (!rs.next()) { conn.rollback(); return 课程不存在; } int selected rs.getInt(cselected); int capacity rs.getInt(ccapacity); if (selected capacity) { conn.rollback(); return 名额已满; } // 唯一键兜底重复选课会抛异常被 catch 捕获后回滚 PreparedStatement ps2 conn.prepareStatement( INSERT INTO sc(sno, cno) VALUES(?, ?)); ps2.setString(1, sno); ps2.setString(2, cno); ps2.executeUpdate(); PreparedStatement ps3 conn.prepareStatement( UPDATE course SET cselected cselected 1 WHERE cno?); ps3.setString(1, cno); ps3.executeUpdate(); conn.commit(); return 选课成功; } catch (SQLIntegrityConstraintViolationException dup) { rollbackQuietly(conn); return 你已经选过这门课; } catch (SQLException e) { rollbackQuietly(conn); e.printStackTrace(); return 选课失败 e.getMessage(); } finally { closeQuietly(conn); } }关键参数说明setAutoCommit(false)是事务的开关忘记写这句后面的 rollback 全部无效FOR UPDATE是行级排他锁加在 SELECT 上会让并发事务串行执行到这一行代价是并发度下降但选课这种场景完全够用把SQLIntegrityConstraintViolationException单独 catch是因为唯一键冲突是预期内的业务分支不该和真正的 SQL 错误混在一起提示。cselected cselected 1而不是先读出来再加一是为了让加法在数据库端原子完成。4.3 退课与重复选课的幂等处理退课是选课的逆操作先删 sc 记录判断影响行数为 1 才把 cselected 减一并加WHERE cselected 0防止计数被减成负数。public static String dropCourse(String sno, String cno) { Connection conn null; try { conn DB.getConn(); conn.setAutoCommit(false); PreparedStatement del conn.prepareStatement( DELETE FROM sc WHERE sno? AND cno?); del.setString(1, sno); del.setString(2, cno); if (del.executeUpdate() 0) { // 没删到说明本来就没选 conn.rollback(); return 你未选修这门课; } PreparedStatement dec conn.prepareStatement( UPDATE course SET cselected cselected - 1 WHERE cno? AND cselected 0); dec.setString(1, cno); dec.executeUpdate(); conn.commit(); return 退课成功; } catch (SQLException e) { rollbackQuietly(conn); return 退课失败; } finally { closeQuietly(conn); } }executeUpdate()的返回值在这里被当成业务信号用删到 0 行说明用户压根没选过直接回滚并给明确提示而不是傻乎乎地继续减计数。4.4 选课失败的常见返回与提示返回信息触发条件排查方向名额已满cselected ccapacity检查容量是否被人为改小你已经选过这门课唯一键冲突sc 表是否残留历史数据课程不存在cno 查不到界面传来的课程编号是否带空格选课失败Lock wait timeout行锁等待超时是否有未提交的事务卡住5. Query/Teacher/Administrator三种视角的联表查询与详细设计落地5.1 四种角色查询的 SQL 差异学生查的是我选了什么教师查的是我教的课有多少人选管理员查的是全局面貌。三者的表连接路径完全不同但都和 sc 表有关。学生视角以 sc 为起点关联 course 和 teacher教师视角以 teacher 为起点关联 course 再左连 sc 做计数管理员视角直接在 sc 上做全表分组统计。理解这个差别Query.java 和 Teacher.java 就不用写两遍几乎相同的代码了。5.2 Query.java 的分页与条件拼装// 学生查询已选课程含课程名、任课教师、上课时间、学分 public static ListMapString, Object myCourses(String sno) { String sql SELECT c.cno, c.cname, t.tname, c.ctime, c.ccredit, s.score FROM sc s JOIN course c ON s.cno c.cno LEFT JOIN teacher t ON c.tno t.tno WHERE s.sno ? ORDER BY c.cno; return DB.query(sql, sno); }这里用 LEFT JOIN 连教师表而不是 INNER JOIN是因为课程可能暂时还没分配任课教师如果用内连接这类课程会直接从学生的已选列表里消失学生看到的现象是我明明选了课但列表是空的排查起来很费时间。加上score字段是为了同一份查询同时支撑查已选课程和查成绩两个需求避免再写一个方法。5.3 教师端与管理员端的统计查询-- 教师端某教师所开课程的选课人数与上课时间 SELECT c.cno, c.cname, c.ctime, c.ccapacity, c.cselected, (c.ccapacity - c.cselected) AS remain FROM course c WHERE c.tno ? ORDER BY c.cselected DESC; -- 管理员端各门课程选课人数排行含授课教师 SELECT c.cno, c.cname, t.tname, c.cselected, c.ccapacity, ROUND(c.cselected / c.ccapacity * 100, 1) AS rate FROM course c LEFT JOIN teacher t ON c.tno t.tno ORDER BY c.cselected DESC;remain和rate这两个计算列放到 SQL 里算比拿到 Java 里循环算省事也不容易漏掉除零保护——不过ccapacity如果允许为 0rate会返回 NULL写 DDL 时给它个默认值就规避了。管理员端那把LEFT JOIN同样是为了让未分配教师的课程也能出现在统计里。5.4 详细设计文档与界面代码的对应关系文档第七章叫详细设计很多模板写得非常空通篇是本模块负责处理相关业务。真正有用的详细设计应该能一一对上代码模块名对应类名输入输出对应方法签名和返回的 Map 键名处理流程对应方法里的步骤编号。比如 Query.java 的详细设计可以写成输入学号 sno输出List每项含 cno/cname/tname/ctime/ccredit/score异常数据库不可达时返回空列表并在控制台打印堆栈。这样答辩时随手翻开文档和代码能直接对上比背概念有用得多。6. 测试报告的可复现写法与密码修改校验6.1 把测试概要写成可执行用例原始文档 8.3 节要求写测试结果及发现实际写出来常常是经测试系统运行正常。可复现的写法是每条用例都带上前置条件、操作步骤、预期结果、实际结果四栏下面这张表可以直接照抄格式。编号前置条件操作步骤预期结果TC-01数据库已灌 20 门课用不存在的学号登录提示账号或密码错误不进入主界面TC-02学生 S001 已选 C001再次选 C001提示已选过sc 表仍只有一条记录TC-03课程 C002 容量 30、已选 30选 C002提示名额已满cselected 保持 30TC-04学生 S001 已选 C001退课后再查列表列表中不再出现 C001cselected 减 1TC-05教师 T001 开 2 门课教师端查选课人数两门课人数与管理员端统计一致判断用例是否真的执行过最直接的方法是对着数据库看SELECT * FROM sc WHERE cnoC002;的行数和cselected是否自洽。计数和实际记录条数对不上就是事务没写对这个自检比看界面提示可靠得多。6.2 并发抢课的边界验证容量边界单线程测不出来要造并发。把某门课容量改成 1然后开两个命令行同时提交选课请求观察最终结果正常情况下一个成功一个提示名额已满cselected等于 1sc 表该课程只有一条记录。如果两个都成功说明少了FOR UPDATE或者事务没生效如果两个都失败检查是不是两个请求用了同一个学号撞了唯一键。想更稳一点可以在测试前把SELECT autocommit;和SELECT transaction_isolation;打印出来确认会话隔离级别是 REPEATABLE-READ避免因为环境差异导致结论不成立。6.3 密码修改的校验清单Modify.java 看着简单但它是测试报告里最容易暴露问题的模块。一个够用的校验清单是旧密码必须与库中一致否则拒绝新密码长度不少于 6 位且不能与旧密码相同两次输入必须一致这一步在界面层做不要提交到数据库再比更新时用UPDATE student SET spwd? WHERE sno? AND spwd?把旧密码也放进 WHERE 条件这样即使前端校验被绕过也不会误改。public static boolean changePwd(String table, String idCol, String pwdCol, String id, String oldPwd, String newPwd) { // 表名和列名由调用方从白名单传入不能用用户输入拼接 String sql UPDATE table SET pwdCol ? WHERE idCol ? AND pwdCol ?; return DB.update(sql, newPwd, id, oldPwd) 1; }返回影响行数等于 1 才算成功等于 0 就意味着旧密码不对或账号不存在这两种情况不必区分提示统一说原密码错误更稳妥。表名和列名虽然做成了参数但调用处必须写死字符串常量绝不能拼接界面上的输入否则就是自己给自己开了个注入口子。真正把这一条落实到代码里比在用户手册的安全章节写十行本系统具有良好的安全性要有说服力。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。