☰
JSP课表查询系统实战:从表结构设计到部署调试全解析
2026/10/22 8:20:18 网站建设 项目流程

选课设题目的时候,很多人看到“JSP课表查询系统”这几个字,第一反应就是:这不就是一张二维表格的增删改查嘛,能有多难。我当初也是这样想的,结果真正动手之后才发现,一个能正常交给老师检查的课表系统,要处理的东西远比想象中多:周次和节次怎么建模、单双周怎么处理、按第几周查询的边界条件怎么定、登录权限怎么拦、中文乱码从哪冒出来的——每一环都有坑。这篇就把我完整实现这个题目的思路和踩坑过程从头到尾写一遍,重点讲清楚表结构设计、查询链路、页面渲染、权限拦截和部署调试这几块,给正在做JSP课设、或者想快速上手Servlet/JSP的同学一个能直接参考的路线。

说实话,这个题目在JSP课设里属于经典中的经典:难度适中,知识点覆盖全面,从页面展示到数据库操作到会话管理全都能练到。但正因为是经典题,老师的要求往往也不低,有的还会现场改需求,比如“你给我按第8周查一下课表”“同一个教室冲突了怎么处理”。所以这篇文章不打算只讲“怎么写一个课表”,而是尽量把实现过程中每个关键决策背后的原因也交代清楚,你照着做,起码在演示和答辩环节不会慌。

1. 选题先想清楚:课表系统的功能范围与角色权限怎么定

很多同学拿到题目就直接开写,建一个数据库,写几个页面,能查出数据就收工。这样不是不行,但如果想让项目看起来完整、答辩时有点东西讲,建议先花半小时想清楚三件事:系统给谁用、有哪些页面、权限怎么控制。

1.1 为什么课表查询系统适合做JSP课设

这个题目的妙处在于,它把JSP阶段要学的核心知识点都串起来了。你至少要用到下面的技术:

  • JSP基础语法、指令、内置对象(request/session/application)
  • EL表达式和JSTL标签库,替代繁杂的Java脚本片段
  • JDBC连接MySQL,执行增删改查
  • Servlet接收请求、跳转页面,配合JSP形成MVC结构
  • Filter过滤器做登录拦截和字符编码处理
  • Session管理用户登录状态

覆盖面广,但每一块都不算难,刚好控制在一个人能独立完成的范围里。所以我经常跟同学说,选这个题不是因为它简单,而是因为它能让你把整个JSP阶段的知识串成一条线。

1.2 功能范围:不要一上来就想着做得多

我最后落地的功能是这样的:

  • 登录与注销:学生和管理员都能登录,不同角色看到不同菜单
  • 课程信息管理:管理员登录后可以新增、编辑、删课程
  • 按班级查询课表:选择班级,显示一周课表
  • 按周次查询课表:默认显示当前周,也可以输入第几周查看
  • 课程详情:点击某个时间格的课程,弹出课程信息

没有做选课、没有做调课、没有做教师自主排课,这些不是不能做,而是对课设来说范围太宽。做系统最忌讳的就是什么都想做,结果每块都只写了一半。把上面五个功能做扎实,已经能拿一个不错的分数。

1.3 用户角色怎么划分

我用了两个角色:admin和student。

  • admin:可以维护课程数据,新增课表,管理用户
  • student:只能查询,不能改数据

角色字段直接放在用户表里,一个字符串搞定。登录时把用户对象放进Session,Filter里判断角色后决定是否放行。这样做的好处是简单直接,代码量少,演示效果也清楚。

2. 数据表设计:把一张二维课表变成关系模型的思考过程

课表这个东西,肉眼看起来就是“横轴星期、纵轴节次”的二维表。但让它变成数据库里的记录,就得先想明白一个问题:一门课在一个时间格里,还要区分它是第几周到第几周有效。这一层维度很多人一开始会漏掉。

2.1 课表数据模型背后的隐藏需求

先看一个日常场景:某门课程“高等数学”每周三第1到第2节在101教室上课,从第1周持续到第16周。这个描述翻译成字段就是:

  • 星期三(day_of_week = 3)
  • 第1节到第2节(start_section = 1, end_section = 2)
  • 第1周到第16周(start_week = 1, end_week = 16)

如果再细致一点,有的课程是单周上、双周不上,或者前8周上、后8周换教室,这种周次组合也要能表达。数据库表如果只存“星期几+第几节”两个字段,那“第1周和单双周”这些查询条件全都实现不了。所以课表系统的第一步,不是写页面,而是把课程这个实体的全部维度想全。

2.2 课程表字段设计与建表SQL

我最终设计的course表结构如下:

CREATE TABLE course ( id INT PRIMARY KEY AUTO_INCREMENT, course_name VARCHAR(100) NOT NULL COMMENT '课程名', teacher_name VARCHAR(50) COMMENT '教师', classroom VARCHAR(50) COMMENT '上课教室', class_name VARCHAR(50) COMMENT '班级', day_of_week TINYINT NOT NULL COMMENT '星期几:1=周一,7=周日', start_section TINYINT NOT NULL COMMENT '开始节次', end_section TINYINT NOT NULL COMMENT '结束节次', start_week TINYINT NOT NULL COMMENT '起始周', end_week TINYINT NOT NULL COMMENT '结束周', week_type TINYINT DEFAULT 0 COMMENT '0=每周都上,1=仅单周,2=仅双周' );

这个表把一门课的“什么时候上、在哪上、什么范围有效”全部表达清楚了。class_name是后来补的,最初我的设计里没有班级字段,只想着按教师查,结果演示时老师问“如果一个年级五个班,怎么能看到自己班的课表”就答不上来。加上之后,业务解释就通顺了。

用户表就简单一些:

CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(64) NOT NULL, real_name VARCHAR(50), role VARCHAR(20) DEFAULT 'student' );

密码这块我建议至少做一次MD5加密再入库,不要存明文。虽然很多课设样例里都是明文,但答辩时你主动说一句“密码是加密存储的”,在印象分上很有用。

2.3 初始化几条测试数据

插入数据时要把“连上两节”“单双周”“跨周段”这些情况都覆盖到,否则后面查询测试会不够充分。

INSERT INTO course (course_name, teacher_name, classroom, class_name, day_of_week, start_section, end_section, start_week, end_week, week_type) VALUES ('高等数学', '王老师', 'A101', '计科2401', 1, 1, 2, 1, 16, 0), ('大学英语', '李老师', 'B302', '计科2401', 1, 3, 4, 1, 16, 0), ('数据结构', '张老师', 'C205', '计科2401', 3, 1, 2, 1, 12, 1), ('体育', '刘老师', '操场', '计科2401', 5, 5, 6, 1, 16, 2);

这里“数据结构”配了week_type=1,表示只在单周上课;“体育”配了week_type=2,表示只在双周上课。后面查询时需要根据不同周数判断到底这节课出不出来。

2.4 教训:周次字段不能省,也最好不要用字符串存

我第一次做这个表的时候偷懒,想用一个字段存“1-8周,10-12周”这种字符串,查询时用逗号切分再逐个判断。后来发现完全是自己给自己挖坑:字符串匹配复杂、容易出边界错误、也没法高效地查范围。用start_week和end_week两个数字字段,一切判断都变成大小比较,清晰又可靠。“单双周”用week_type一个字段足够,没必要额外设表。

3. 核心查询链路:按周次查出数据并渲染成课表

表结构定好之后,接下来就是整个系统最核心的部分:用户输入一个周次,系统把所有在该周有效的课程找出来,按“星期几-第几节”摆到对应的格子里。

3.1 当前周次的计算方式

查询课表时,默认要显示“当前周”。这里需要一个辅助方法:根据开学日期算出今天是第几周。

public static int getCurrentWeek(Date startDate) { Date now = new Date(); long diff = now.getTime() - startDate.getTime(); int days = (int) (diff / (1000 * 60 * 60 * 24)); return days / 7 + 1; }

计算逻辑不复杂,但要注意两点:一是startDate参数得是某高校开学第一周周一凌晨零点;二是如果日期差不是整7倍数,比如周五算出来的 days 在某个区段,用整除结果加1能得到当前周。比如今天是开学第33天,33除以7等于4,加1就是第5周。前提是开学第一天记作第1周,这个跟实际算一致。

3.2 Servlet里组织查询逻辑

用户请求queryCourse?week=5&className=计科2401时,Servlet处理流程大致为:

  1. 接收参数week和className
  2. 调用 CourseDao 查询符合条件的课程列表
  3. 按“星期-节次”维度整理成 Map
  4. 存到 request 里,转发到 courseTable.jsp

Dao 层的关键SQL如下:

SELECT * FROM course WHERE class_name = ? AND day_of_week = ? AND start_week <= ? AND end_week >= ? AND ( week_type = 0 OR (week_type = 1 AND ? % 2 = 1) OR (week_type = 2 AND ? % 2 = 0) ) ORDER BY start_section;

这里?依次是className、day_of_week、week、week、week。判断单双周的条件是当前周次对2取余:单周的余数是1,双周的余数是0。

注意这个SQL不是一次性把七天的数据全部查出来,而是循环星期一到星期天每天查一次。你也可以用一条SQL不加day_of_week,查出所有课程后再按天分组,两种都行。我选择循环查,是因为课表数据量不大,循环7次的开销完全可以忽略,而单条SQL的IN或多条件组合会让语句难读,调试时容易晕。

3.3 组装数据:Map的方式比二维数组更灵活

查询得到的是课程列表,要渲染成课表,需要按“横坐标星期、纵坐标节次”定位。我用的结构是:

Map<String, List<Course>> courseMap = new HashMap<>(); for (Course c : list) { String key = c.getDayOfWeek() + "-" + c.getStartSection(); courseMap.computeIfAbsent(key, k -> new ArrayList<>()).add(c); } request.setAttribute("courseMap", courseMap);

key 用"3-1"这种格式,表示“周三第1节”。如果同一时间有冲突排了两门课,List 里会有多条数据,JSP 渲染时可以把它们并排显示,这样冲突情况也能一眼看出来。

有人可能会想,直接用二维数组Course[8][8]不就行了。我实测后发现 Map 好在两点:一是可以任意扩展,比如不需要提前规定节次上限;二是配合 JSP 的map[key]取值非常自然。二维数组的行列索引一旦写死,遇到第9节、第10节这种学校扩展节次就得动整个渲染逻辑,Map 就没有这个问题。

3.4 JSP页面渲染课表的写法

页面渲染最忌讳在 JSP 里写一大堆<% %>Java代码。用 JSTL 的<c:forEach>就能把表格结构写干净:

<table border="1"> <tr> <th>节次</th> <c:forEach var="d" begin="1" end="7"> <th>星期${d}</th> </c:forEach> </tr> <c:forEach var="s" begin="1" end="10"> <tr> <td>第${s}节</td> <c:forEach var="d" begin="1" end="7"> <td> <c:forEach var="course" items="${courseMap['${d}-${s}']}"> <div class="course-item">${course.courseName}</div> <div class="course-info">${course.teacherName}</div> <div class="course-info">${course.classroom}</div> </c:forEach> </td> </c:forEach> </tr> </c:forEach> </table>

还有更简洁的表达方式:先把 key 拼好再取值。比如:

<c:forEach var="s" begin="1" end="10"> <tr> <td>第${s}节</td> <c:forEach var="d" begin="1" end="7"> <c:set var="key" value="${d}-${s}" /> <td> <c:forEach var="course" items="${courseMap[key]}"> ... </c:forEach> </td> </c:forEach> </tr> </c:forEach>

关键是注意 JSTL 里${}的嵌套问题,直接拼${courseMap['${d}-${s}']}这种写法并不安全,应当先计算 key 再用。

连上两节课的课程,我这里选择在对应的开始节次格子里显示,并不做rowspan合并。合并美观,但实现成本高,还要处理跨节次冲突,对课设来说性价比不高。如果你想加分,可以只做固定节次跨度的合并,比如所有课程都是2节连上,那rowspan=2就不难写。我最后为了稳定性选择了不合并,演示效果也够清楚。

4. 登录、权限拦截与页面跳转:最容易丢分的三个细节

课表查询本身是只读功能,但系统里既然有管理员维护课程,就一定要有登录和权限控制。很多项目功能都实现了,最后却因为权限漏洞或者页面跳转混乱被扣分,这部分值得单独说。

4.1 用Filter做登录拦截,而不是每个页面手动判断

正确做法是写一个LoginFilter,统一拦住所有请求。它做两件事:

  • 判断当前是否已登录
  • 判断访问的资源是否在放行名单里
@WebFilter("/*") public class LoginFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; String uri = request.getRequestURI(); // 放行登录相关和静态资源 if (uri.endsWith("login.jsp") || uri.endsWith("/login") || uri.contains("/css/") || uri.contains("/js/") || uri.contains("/images/")) { chain.doFilter(req, resp); return; } HttpSession session = request.getSession(false); if (session == null || session.getAttribute("loginUser") == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return; } chain.doFilter(req, resp); } }

登录成功的 Servlet 里把用户对象放进 Session:

User user = userDao.findByUsernameAndPassword(username, password); if (user != null) { session.setAttribute("loginUser", user); response.sendRedirect(request.getContextPath() + "/index.jsp"); } else { request.setAttribute("msg", "用户名或密码错误"); request.getRequestDispatcher("login.jsp").forward(request, response); }

这里用sendRedirect而不是forward,是为了避免刷新页面时反复提交登录表单。这个细节答辩时如果老师问起来,你能解释清楚,加分。

4.2 角色权限:Filter里判断还是页面里判断

管理端的页面,比如新增课程addCourse.jsp,一定要做角色控制。最省事的做法是在 Filter 里加一段白名单:

String uri = request.getRequestURI(); if (uri.endsWith("addCourse.jsp") || uri.endsWith("/addCourse")) { User loginUser = (User) session.getAttribute("loginUser"); if (!"admin".equals(loginUser.getRole())) { response.sendRedirect(request.getContextPath() + "/index.jsp"); return; } }

这样做完,普通学生即使用户手输 URL 也进不去管理页面。安全性上一个简单的权限检查胜过大门口一堵高墙。

4.3 路径问题:一定要用绝对路径

JSP页面里的超链接、表单action、静态资源引用,建议统一用request.getContextPath()拼绝对路径。比如:

<link rel="stylesheet" href="${pageContext.request.contextPath}/css/style.css"> <form action="${pageContext.request.contextPath}/queryCourse" method="get">

这样做的原因是,如果当前页面位于WEB-INF子目录,或者被转发到深层URL,相对路径就会算错,常见表现就是CSS样式丢失、图片不加载、表单提交到404。用${pageContext.request.contextPath}能确保从项目根目录开始定位,省掉一大半路径相关的奇怪问题。

5. 乱码、404、数据库连接:Tomcat环境下的问题排查记录

这部分是实战环节。我在调试项目时踩过几个比较典型的坑,单独拎出来写,你可以当检查清单用。

5.1 中文乱码要从三个环节同时防护

乱码是JSP课设里出现频率最高的问题,没有之一。它的根源是字符编码在“浏览器-服务器-数据库”之间不一致。我最后的配置是这样的:

第一,JSP文件头部统一写:

<%@ page contentType="text/html; charset=utf-8" pageEncoding="utf-8" %>

对应生成的 HTML 里也要有<meta charset="utf-8">。

第二,Servlet处理POST请求时,在读取任何参数之前设置编码:

request.setCharacterEncoding("utf-8"); response.setContentType("text/html; charset=utf-8");

注意setCharacterEncoding必须在第一次读取参数之前调用,否则失效。对于GET请求,Tomcat 8及以上默认按UTF-8解码,通常没问题;如果你用老版本Tomcat,还得去server.xml改URIEncoding="UTF-8"。

第三,JDBC连接URL带上编码参数:

jdbc:mysql://localhost:3306/course_db?useUnicode=true&characterEncoding=utf8&useSSL=false

这三层都设好,中文基本不会出问题。如果还有乱码,去检查MySQL表本身的字符集是不是utf8。

5.2 404和500的常见来源

404多由路径写错引起,集中在:

  • web.xml里的<url-pattern>和 Servlet 类上的@WebServlet路径不一致
  • JSP页面放在WEB-INF里面,外部无法直接访问
  • 项目名大小写不一致,Linux服务器对大小写敏感

500则是运行时错误,最常见的是数据库驱动找不到、SQL语法错误、空指针。排查500我一般先看 Tomcat 的 logs 目录下 localhost 或 catalina 日志,错误堆栈会直接告诉你是哪一行代码出了事。

一个小建议:开发时用 IDEA 或 Eclipse 集成的 Tomcat,带控制台输出,错误信息直接看,效率远高于手工复制 war 包到 Tomcat 再启动。

5.3 课设做完了,如何打包部署到独立Tomcat

简单流程如下:

  1. 项目里确保没有写死本地路径,数据库配置写在db.properties里
  2. 在 IDEA 里 Build Artifact,打成 war 包
  3. 把 war 包复制到 Tomcat 安装目录的webapps下
  4. 启动bin/startup.bat
  5. 浏览器访问http://localhost:8080/项目名/index.jsp

如果访问报404,先确认下 webapps 下面是否自动解压出了同名目录。Tomcat 的启动日志里会有部署结果,观察一下。

数据库连不上的排查顺序是:驱动jar是否放在WEB-INF/lib-> MySQL服务是否启动 -> 3306端口是否开放 -> 用户名密码是否有权限 -> 库是否存在。

6. 从“能跑”到“好看”:三个加分项值得花时间

如果上面的基本功能已经稳定,代码也还干净,剩下的是怎么让这个课设从“会做”变成“做得不错”。

6.1 排课冲突检测:一个很有说服力的功能点

课表系统如果只做查询,本质上是静态展示。真正体现“系统思维能力”的地方在于:新增课程时能不能发现时间和教室冲突。我实现了两层:

  • 新增课程时,如果同一个教室在同一时间已经排了课,提示冲突并拒绝添加
  • 同一个班级在同一时间排了两门课,也要提示

冲突检测的核心SQL可以复用上面的查询逻辑,只不过把课程参数换成待添加的课程:

SELECT COUNT(*) FROM course WHERE classroom = ? AND day_of_week = ? AND start_week <= ? AND end_week >= ? AND ( week_type = 0 OR (week_type = 1 AND ? % 2 = 1) OR (week_type = 2 AND ? % 2 = 0) )

注意要判断时间的重叠,而不是只判断某个节次相等。两节课只要存在交集就算冲突,交集判定的标准是:待添加的开始节次 <= 已有课程的结束节次 且 待添加的结束节次 >= 已有课程的开始节次。自己写个isOverlap方法封装一下,代码会很清楚。

6.2 课表打印与导出:实用且演示效果好

做一个“打印课表”按钮,用CSS控制打印样式,把导航、操作按钮隐藏,只保留课表区域:

@media print { .no-print { display: none; } .course-table { width: 100%; } }

按钮触发window.print(),浏览器就会弹出打印界面。在演示时这个功能很显眼,因为它是一个“实际可用”的功能,而不是单纯的数据展示。

导出Excel如果时间充裕可以接Apache POI,把courseMap遍历写进XSSFWorkbook。不接也没问题,课设主体框架稳步落地,已经足够完整。

6.3 如果以后想往 Spring Boot 迁移,哪些能直接搬

做课设的过程中,你其实已经把很多核心逻辑写对了:实体类、DAO层的SQL、Filter的思路、JSP页面的渲染。这些代码在不同框架下是通用的,尤其是DAO层的SQL几乎可以原封不动带过去。区别主要在:

  • web.xml 和@WebServlet换成 Spring MVC 的@Controller
  • JDBC 工具类可以换成 MyBatis 或 Spring Data JPA,SQL逻辑不变
  • JSP 可以直接被 Spring Boot 支持,只需要把页面挪到src/main/resources/templates并按规则调整

我自己的体会是,做完这个课设再去学框架,很多概念不会觉得陌生,因为你已经知道“原来它是替代这里的什么”。这大概就是这个经典题目最大的价值——它不是让你炫技,而是让你把最基础的那些东西彻彻底底搞明白。

最后分享一个我在演示时的小技巧:把默认查询周次做成“当前周”,并在页面上显示“今天是第X周”。这个细节做出来以后,系统立刻显得像一个真实在用的工具,而不是课程作业,老师对整体完成度的感受会好很多。以上这些,就是我从这个项目踩出来的全部心得,希望能帮你少走点弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询