
简介数据库课程设计完整报告聚焦班级事务管理系统适合数据库原理、信息系统开发课程设计的学生与教师参考。报告按数据库设计流程展开先明确登录、管理员管理学生成绩课程、学生查询、生活委员班费管理等功能给出Windows XP SQL Server MyEclipse Tomcat的运行环境再从数据需求、事务需求、E-R图、关系模式到SQL建表与完整性约束逐层分析并提供学生信息、课程、成绩、班费等表结构定义。资源为单份PDF压缩包共1个文件大小961KB已有120人学习浏览。通过这份报告可快速梳理数据库需求分析、概念结构设计、逻辑结构设计和权限管理设计的完整思路读懂典型教务场景下的关系模式拆分与外键约束写法对完成同类课程报告或期末项目设计有直接参考价值。1. 这份课程设计报告哪些部分值得直接抄拿到数据库课程设计报告先别急着嫌它老。这份《班级事务管理系统》运行环境写着 Windows XP、MyEclipse 6.0、Tomcat 5.5.28连 SQL Server 5.0 这个不存在的版本号都出现了但它的建表 SQL 里全是 auto_increment实际按 MySQL 方言写的。值得抄的不是 JSP 页面和后端代码而是两条骨架六张表的关系模型学生、课程、成绩、详细信息、班费、个人事务各归其位外键把成绩表做成标准桥表userrole 字段驱动的三角色模型管理员、生活委员、普通个人共用一张 user 表。这套设计放到今天的教务系统、班级 SaaS 里照样能用。对正在做 CS 课程设计、或要接手老式 JSP 项目的人来说这是一份可直接复现的蓝本。2. 六张表的关系建模从 E-R 图到可执行的 MySQL DDL2.1 六张表分别承担什么职责报告的需求分析部分其实来回写了两遍数据需求一段、事务需求一段但透过这些文字能直接看到最终的表清单。这里先把六张表的归属理清。表名核心字段服务角色一句话职责userusername、userpass、userjob、userroleadmin / shwy / qita登录账号 基本信息是全局主表coursecname、ccredit、cteacheradmin课程字典Scuid、cid、sgradeadmin / 学生学生与课程的多对多桥表stuinfostuid、stubirth、stuidentity、stuaddr、studorm、stucardadmin学生的扩展详细信息shwytime、addr、stunum、startmoney、expense、endmoneyshwy班费收入支出流水qitaqtime、qcontent、qresultqita个人待办事务这张表就是后面所有 SQL 和页面功能的地基。原报告里生活委员事务表和其它管理页面表都带上了 id 主键和 VARCHAR 类型的时间字段这种所有表先给自增主键的做法在课程设计里问题不大但时间字段用 VARCHAR(20) 存到了按月份统计班费时就只能写字符串截取后面的实际项目中建议直接换成 DATETIME。2.2 学生基本信息和详细信息为什么要拆两张表user 表里已经存了姓名、职务、角色stuinfo 表里再存出生日期、身份证号、家庭住址、宿舍号、银行卡号。第一次做设计的人很容易问为什么不能合并成一张大表拆表的理由有三个。第一是敏感数据隔离身份证号和银行卡号属于隐私字段单独成表后可以只在管理员页面里按需查询普通查询接口默认不带出这些列。第二是更新频率不同基本信息里的职务会随换届变化而详细住址、银行卡号很少变拆开之后 update 语句不会互相影响锁范围。第三是数据库设计里一对一的拆表原则看字段的敏感级别、访问频率和变更频率而不只是实体边界。2.3 一键建表带注释的 MySQL 8 DDL原报告的建表语句分散在 1.4.2 里我按 MySQL 8.0 整理成一份可以直接执行的脚本字段名保持与报告一致只是把截断的ame还原成cnameCREATE DATABASE IF NOT EXISTS class_manage DEFAULT CHARSET utf8mb4; USE class_manage; -- 学生用户表一个账号对应一个学生 CREATE TABLE user ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(10) NOT NULL COMMENT 姓名, userpass VARCHAR(20) NOT NULL COMMENT 密码, userjob VARCHAR(10) COMMENT 担任职务, userrole VARCHAR(10) NOT NULL DEFAULT qita COMMENT 角色admin/shwy/qita ) ENGINEInnoDB; -- 课程表课程字典 CREATE TABLE course ( id INT AUTO_INCREMENT PRIMARY KEY, cname VARCHAR(20) NOT NULL COMMENT 课程名, ccredit INT COMMENT 学分, cteacher VARCHAR(20) COMMENT 任课老师 ) ENGINEInnoDB; -- 成绩表Sc 是标准桥表连接学生与课程 CREATE TABLE Sc ( id INT AUTO_INCREMENT PRIMARY KEY, uid INT NOT NULL, cid INT NOT NULL, sgrade VARCHAR(5) COMMENT 分数, CONSTRAINT fk_Sc_uid FOREIGN KEY (uid) REFERENCES user(id), CONSTRAINT fk_Sc_cid FOREIGN KEY (cid) REFERENCES course(id) ) ENGINEInnoDB; -- 学生详细信息表 CREATE TABLE stuinfo ( id INT AUTO_INCREMENT PRIMARY KEY, stuid INT NOT NULL, stubirth VARCHAR(20), stuidentity VARCHAR(30), stuaddr VARCHAR(200), studorm VARCHAR(20), stucard VARCHAR(20), CONSTRAINT fk_stuinfo_stuid FOREIGN KEY (stuid) REFERENCES user(id) ) ENGINEInnoDB; -- 生活委员班费账目表 CREATE TABLE shwy ( id INT AUTO_INCREMENT PRIMARY KEY, time VARCHAR(20), addr VARCHAR(100), stunum INT, startmoney DECIMAL(8,2), expense DECIMAL(8,2), endmoney DECIMAL(8,2), actmeaning TEXT, actresult VARCHAR(10) ) ENGINEInnoDB; -- 其他个人事务表 CREATE TABLE qita ( id INT AUTO_INCREMENT PRIMARY KEY, qtime VARCHAR(20), qcontent TEXT, qresult VARCHAR(20) ) ENGINEInnoDB;代码里有几个点需要解释。第一user 在 MySQL 里是系统有特殊含义的名字所以建表时必须加反引号实际项目里更推荐直接把表名改成 student否则每一条关联 SQL 都要写反引号反引号漏一次就是一条线上事故。第二外键约束名沿用报告里的 fk_Sc_uid、fk_Sc_cid、fk_stuinfo_stuid在 MySQL 里外键名在库内必须唯一改成你自己的前缀也可以。第三原报告里 DECIMAL 没写精度MySQL 会按 DECIMAL(10,0) 处理也就是只能存整数所以这里显式改成 DECIMAL(8,2)班费收支带角分的场景才落得住。2.4 外键约束在课程设计里的处理策略外键在这里有两层作用。一层是数据完整性Sc 表里不允许出现不存在的 uid 或 cidstuinfo 必须挂在一个已有的 user 上这满足了报告中数据库设计及完整性约束这一节的硬要求。另一层是删除时的连锁反应默认的 FOREIGN KEY 约束是 RESTRICT 语义想删一个已经有成绩记录的学生直接 DELETE 会被数据库拒绝报外键约束错误。课程设计里遇到这种情况常见做法是调整删除策略。要么把成绩表的 fk 改成 ON DELETE CASCADE删学生时成绩一起删要么像真实系统那样先删子表再删主表在 DAO 里按固定顺序执行。我一般建议课程设计保留 RESTRICT然后在编程时把删除顺序写对这样既能让外键起作用又能在答辩时讲清楚删除学生需要连带清理成绩和详细信息的理由。提示MySQL 定义外键时会自动在被引用列上建索引。Sc 表的 uid、cid 因此在查询时不会走全表扫描但只覆盖单列场景。后面要按班级统计学生成绩时还是需要自己补复合索引。3. 三角色权限模型userrole 字段如何撑起不同的登录后界面3.1 应用层角色 vs 数据库账号报告里有一节标题是数据库用户权限管理列了三种用户管理员、生活委员、其他个人。严格说这三类角色是应用层的权限模型跟 MySQL 里的 CREATE USER、GRANT 不是一回事。课程设计里把这两者混在一起写很常见但遇到较真的答辩老师需要能说清楚这里解决的是登录后能点哪些菜单而不是数据库层面谁能连、能执行哪些 DDL。落在表结构上就是一个 userrole 字段取值 admin、shwy、qita。登录时把 userrole 写进 session后续每个页面、每个 DAO 方法都按这个字段做判断。这是最简单的 RBAC 雏形一张用户表、一个角色字段、没有单独的权限表。班级事务管理系统这种规模角色数固定三个、功能边界清晰用字段就够如果以后出现副班长也能管部分班费这类交叉权限再拆 role 表和 permission 表不迟。角色和功能之间的对应关系如下表功能模块adminshwyqita学生基本/详细信息增删改查可否否课程增删可否否成绩增删改查可可查可查本人班费收支管理可查可增删改查否个人事务管理可可可本人这个矩阵同时也是写菜单和写 DAO 时的权限检查清单。生活委员进班费页面数据层只能访问 shwy 表普通学生登录后看不到任何管理入口。3.2 登录后按角色拦截页面请求原报告用的是 JSP Servlet没有提到权限框架所以这里给一个最贴近原始技术栈的 Filter 实现。它的作用是未登录用户直接重定向到登录页已登录但角色和 URL 前缀不匹配的用户同样打回登录页。public class AuthFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; HttpServletResponse resp (HttpServletResponse) response; HttpSession session req.getSession(false); String role session null ? null : (String) session.getAttribute(userrole); String uri req.getRequestURI(); if (role null) { resp.sendRedirect(req.getContextPath() /login.jsp); return; } if (uri.startsWith(/admin) !admin.equals(role)) { resp.sendRedirect(req.getContextPath() /403.jsp); return; } if (uri.startsWith(/shwy) !shwy.equals(role) !admin.equals(role)) { resp.sendRedirect(req.getContextPath() /403.jsp); return; } chain.doFilter(request, response); } }这段代码里有三个值得注意的点。第一登录成功后要往 session 里放 id、username、userrole 三个属性但绝不放 userpass减少密码在会话层的暴露面。第二URL 前缀的规划要配合工程目录/admin/* 放管理员页面/shwy/* 放班费页面/student/* 放成绩查询这样 Filter 的匹配逻辑一目了然。第三admin 被单独放行访问 /shwy 前缀因为管理员要查班费流水反过来生活委员不能进 /admin这种角色包含关系用 if 顺序写好比把所有条件塞进一个正则里易读。3.3 数据权限的边界在 DAO 还是 SQL菜单拦住了但直接调 DAO 还是能绕过界面拿到数据所以权限还得在数据访问层收口。课程设计里常见做法是给 DAO 方法显式传当前登录用户的 uid管理员调用 queryAllStudent普通学生调用同样的方法时就在方法里强制拼上自己的 uid 条件。这种角色越大查询范围越大的写法也叫行级数据权限和上面的 URL 拦截属于两个层面。真实项目里更稳妥的做法是拆两个方法queryAllStudent 给管理员用queryMyResult(uid) 给自己用而不是在一个方法里靠 if 分支切换 SQL。原因是把权限判断和业务查询混在一起后每加一种角色就要改一遍老方法回归成本会越来越高。对于这份课程设计报告我建议在原有 DAO 基础上保留一个通用查询再给普通用户单独写一个带 uid 条件的查询方法这样代码量不大答辩时也能讲出这里做了一层数据权限隔离。4. 成绩 join 与班费账本外键约束和账务一致性的实战边界4.1 成绩表为什么不直接存学生姓名Sc 表只有 uid、cid、sgrade 三个业务字段这是典型的学生-课程多对多桥表。很多课程设计会在这里直接加上姓名字段觉得查询省事但这是需要纠正的用法。姓名属于派生属性存进成绩表后如果学生改名成绩表里的冗余数据就错了还会出现同一学生在不同成绩记录里姓名不一致的问题。正确做法是查询时通过外键 join 回 user 表和 course 表SELECT u.username AS 姓名, c.cname AS 课程, s.sgrade AS 成绩 FROM Sc s JOIN user u ON s.uid u.id JOIN course c ON s.cid c.id WHERE u.id ? ORDER BY c.cname;这条 SQL 的参数是登录用户的 id从 session 里取不参与字符串拼接。查询逻辑分两步理解先由 Sc 表定位这个学生选了哪些课程再由 uid 和 cid 两个外键把 user 表和 course 表带进来所以 Sc 表不需要冗余任何名字字段。这也回答了 MySQL 里 join 到底在图什么桥表只存关系实体属性各回各家。还要注意 sgrade 字段的类型。报告里成绩字段用的是 VARCHAR(5)这会导致 ORDER BY sgrade 按字典序排序出现9 分排在10 分后面的情况。最直接的修复是建表时用 DECIMAL(5,2)如果老库已经上线查询侧可以临时用 CAST 兜底SELECT uid, cid, CAST(sgrade AS DECIMAL(5,2)) AS score FROM Sc ORDER BY score DESC;CAST 会触发隐式转换sgrade 列上的索引会失效但成绩表在班级场景下就几十行可以接受。两种处理方式的取舍如下表处理方式是否改表结构查询侧成本长期风险改 DECIMAL(5,2)是一次 ALTER无查询时 CAST否每次查询都转换sgrade 索引失效4.2 班费表的三步余额字段shwy 表的设计里有一个很容易被忽略但很值钱的点同时存了 startmoney、expense、endmoney 三个字段。这是账本式记账的思路。消费前余额、本次消费金额、消费后余额三条都落库之后任何一笔记录都可以独立验证不需要依赖前一条记录计算。-- 找出余额算不平的流水 SELECT id, time, addr, startmoney, expense, endmoney, (startmoney - expense) AS calc_end FROM shwy WHERE startmoney - expense endmoney;如果这条 SQL 返回非空结果说明要么录入时把金额写错要么后一位操作人直接改动了 startmoney。账本式设计的意义就在这里它不是靠当前余额这一个值来兜底而是每条流水自己闭环。新表上加约束的话MySQL 8.0.16 之后的版本可以加 CHECKALTER TABLE shwy ADD CONSTRAINT chk_balance CHECK (startmoney - expense endmoney);注意 MySQL 8.0.16 之前的版本只解析 CHECK 不生效如果是 5.7 库这种校验只能在应用层做靠生活委员提交数据时的后端判断。4.3 并发记账与死锁规避班费管理在课程设计里通常是单人操作但在真实应用中生活委员和其他管理员可能同时记账。两个会话同时更新 shwy 表时会出现典型的余额覆盖问题事务 A 读出 endmoney500事务 B 也读出 500A 记一笔 100 写回 400B 记一笔 200 写回 300最终余额应该是 200数据库里却是 300。这类账务更新的常用做法是悲观锁先把目标行锁住再计算START TRANSACTION; -- 锁住这一行直到事务结束 SELECT endmoney FROM shwy WHERE id 1 FOR UPDATE; -- 应用层算出新的 startmoney/expense/endmoney 后回写 UPDATE shwy SET startmoney ?, expense ?, endmoney ? WHERE id 1; COMMIT;FOR UPDATE 在 InnoDB 默认的 REPEATABLE READ 隔离级别下拿到的是行锁另一事务再对同一行 FOR UPDATE 会阻塞等待。代价是并发低但班费记账本来就是低频操作完全够用。如果担心长时间持锁可以在连接串上配置 innodb_lock_wait_timeout让等待超时后快速失败。多行更新的死锁场景也值得提一句。如果以后要把批量流水统一记账两条事务按不同顺序更新同一组记录就可能互相等待形成死锁。规避方式很简单所有事务按 id 升序更新固定访问顺序。这也是数据库面试题里死锁章节的标准答法。5. StudentDao 的重构PreparedStatement、连接管理与被写坏的 finally5.1 原报告 DAO 代码的问题出在哪原报告里 StudentDao 的代码能跑但写得很粗糙。以 deleteInfo 为例核心逻辑只有几行但连接关闭和返回值的处理存在明显问题。文档里出现的写法大致是这样的结构public int deleteInfo(int id) { Connection conn DBConnection.getConnection(); int flag 0; try { PreparedStatement ps conn.prepareStatement(delete from user where id?); ps.setInt(1, id); flag ps.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); } finally { return flag; } }这段代码的毛病有三层。第一finally 块里放 return会让 catch 里的异常信息被吞掉调用方拿到的永远是 0出错时根本不知道是数据库连不上还是 SQL 写错。第二PreparedStatement 没有被关闭连接 close 的逻辑写在方法末尾一旦 executeQuery 抛异常conn 就不会关连接池迟早被耗尽。第三异常处理只打了堆栈没有把错误传递给上层页面端看到的结果永远是操作完成。当时这么写能过的原因是 DBConnection 每次 new 一个连接用完直接 close泄漏一两次感觉不出来。一旦换成连接池连接不归还的后果会立刻放大。5.2 重构后的写法try-with-resources 加连接池用现代的写法重构逻辑不变但每一处资源都有关闭保证。这里用 HikariCP 作为连接池初始化一次全局复用public class StudentDao { private final DataSource dataSource; public StudentDao(DataSource dataSource) { this.dataSource dataSource; } public int deleteInfo(int id) { String sql DELETE FROM user WHERE id ?; try (Connection conn dataSource.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, id); return ps.executeUpdate(); } catch (SQLException e) { throw new RuntimeException(删除学生失败, e); } } }理解这段代码要看三个地方。第一try 后面的括号里声明了 Connection 和 PreparedStatement方法结束或抛异常时 Java 会自动按逆序调用 close这就是 try-with-resources 的作用。第二delete 用 PreparedStatement 的 ? 占位符setInt 把 id 作为参数传入SQL 模板和参数值分离注入的字符串无法改变 SQL 结构这是防注入的标准做法。第三RuntimeException 包装了 SQLException异常信息会保留原始堆栈同时让调用方能感知失败而不是静默吞掉。查询方法同理注意 SELECT 里不应该带 userpass 字段public ListUserBean queryAllStudent() { String sql SELECT id, username, userjob, userrole FROM user; ListUserBean list new LinkedList(); try (Connection conn dataSource.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { while (rs.next()) { UserBean student new UserBean(); student.setId(rs.getInt(id)); student.setUsername(rs.getString(username)); student.setUserjob(rs.getString(userjob)); student.setUserrole(rs.getString(userrole)); list.add(student); } } catch (SQLException e) { throw new RuntimeException(查询学生列表失败, e); } return list; }这里故意不查 userpass避免密码在日志和结果集里出现。班级事务管理系统的密码不需要做多复杂的脱敏但查询不返回密码字段是一个成本极低、收益明显的习惯。5.3 数据访问层的选型怎么权衡课程设计以 JDBC 为主因为要展示对数据库操作的理解。但真实项目里一般会在三种方案里选。这直接决定了新增一个班费流水功能时要写多少个方法。方案适合场景代价裸 JDBC HikariCP课程设计、几十行业务每个表都要写增删改查样板代码MyBatis / MyBatis-Plus业务 SQL 多、团队熟悉 XML增加映射配置和拦截器学习成本Spring Data JPA后端管理系统、实体关系密集复杂查询需要 JPQL 或原生 SQL 兜底班级事务管理系统这种规模裸 JDBC 完全够用但要配上连接池。如果以后要在这个课程设计的基础上扩展比如加一个按学期统计成绩的报表页我会选择 MyBatis让 SQL 独立在 XML 里便于直接复制第 4 章里的 join 查询语句调试。6. 重建环境迁移到 MySQL 8 的配置清单与上线前验证6.1 迁移清单原报告运行环境里的 Tomcat 5.5.28 和 JDK 1.2 现在已经很难找到可用的安装包也没有必要装。重建这套系统时推荐直接落在 Tomcat 8.5 加 JDK 8 上数据库用 MySQL 8.0。JDK 8 是 javax.servlet 命名空间最后能舒适运行的版本再往上 JDK 11 或 17 就要面对 jakarta 命名空间迁移的问题课程设计没有必要折腾。组件原报告配置重建建议JDKJDK 1.2JDK 8应用服务器Tomcat 5.5.28Tomcat 8.5 / 9.0数据库报告中写的 SQL Server 5.0实际是 MySQL 方言MySQL 8.0JDBC 驱动无明确版本mysql-connector-java 8.0.33数据库账号应用层区分角色class_app 单账号最小权限MySQL 8 的默认认证插件是 caching_sha2_password老项目的 JDBC 驱动不升级会报 Public Key Retrieval 错误。pom 里加驱动dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency应用账号按最小权限建只给业务库的增删改查权限不给 DDL 权限CREATE USER class_applocalhost IDENTIFIED BY Class2025; GRANT SELECT, INSERT, UPDATE, DELETE ON class_manage.* TO class_applocalhost; FLUSH PRIVILEGES;JDBC URL 写成jdbc:mysql://localhost:3306/class_manage?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/ShanghaiuseSSLfalse。这里重点检查 characterEncodingutf8mb4。MySQL 8 下 utf8 是 utf8mb3 的别名如果学生姓名里有 emoji 或者生僻字会插入失败建库用 utf8mb4连接串也用 utf8mb4 才对齐。6.2 上线前跑一遍完整性验证应用账号建好后先把第 2 章的六张表导进去再导入老数据。导入顺序有讲究先 user 和 course再 Sc 和 stuinfo最后 shwy 与 qita顺序反了外键直接报错。导入完成后跑一遍孤儿数据检查SELECT s.id, s.uid, s.cid FROM Sc s LEFT JOIN user u ON s.uid u.id LEFT JOIN course c ON s.cid c.id WHERE u.id IS NULL OR c.id IS NULL;返回空结果集说明 Sc 的引用关系完整。最后复用第 4 章的班费对账 SQL确认 startmoney - expense endmoney。两条检查都通过迁移后的班级事务管理系统就具备上线条件了。本文还有配套的精品资源点击获取