☰
Servlet+JSP少儿编程网站源码拆解:三层架构与自动判分实现
2026/10/8 8:35:02 网站建设 项目流程

简介:基于Java Web的少儿编程网站源码,采用MVC开发模式且不依赖任何框架,适合正在学习服务端编程与MySQL数据库交互的初学者,也可作为毕业设计或课程设计的完整参考。压缩包共包含七十一个文件,以Java源文件、JSP页面、Servlet类、CSS样式和PNG图片为主,其中十二个JSP页面实现了用户注册登录、笔记管理和反馈管理等核心界面,十四个Java类对应后台业务逻辑与数据操作,七个CSS文件负责页面样式,另有E-R图和流程示意图辅助理解数据库结构,资源包整体仅三点六九MB。目前已有六十七人浏览学习,属于轻量但结构完整的Java Web入门项目。解压之后可获取完整网站源码、数据库设计图以及说明文档,能直观查看用户管理、笔记管理、反馈管理三大模块的前后端实现,对理解传统分层开发流程和完成课程设计都很有帮助。

1. 拆完这份 Java Web 少儿编程网站源码:它能跑通的不只是课程展示

做少儿编程培训最累的往往不是上课,而是作业闭环。我见过太多老师在微信群里收代码截图、用 Excel 登记成绩、再手动判对错,十几个学员还能忍,几十个就彻底失控。这份「基于 Java Web 的少儿编程网站.zip」源码包,我拆完之后判断它最大的价值不是页面多漂亮,而是把“学生看课 → 提交编程练习 → 服务端自动判分 → 教师看统计”这一整条链路跑通了。它适合 Java Web 方向的计算机专业学生做课程设计参考,也适合机构老师拿回来改一改当内部教学后台。登录进来的角色有学生、教师、管理员三种,课程、练习题、提交记录都有对应的表结构和页面。接下来我从数据表开始拆,一直拆到判分进程和常见翻车点。

2. 三层架构与数据表:Servlet + JSP 的经典骨架要先立住

2.1 技术选型:Servlet + JSP 为什么是这套源码的正确起点

一提到 Java Web,很多人的第一反应是 Spring Boot。但这套源码用的偏偏是最传统的 Servlet + JSP,配 JDBC 直连数据库。我的判断是:这种选型在课程设计和教学系统场景里其实是优点。Servlet 的请求入口一眼就能看懂,一个路径对应一个类,接手的时候不用去翻一大堆自动配置。对计算机专业的学生来说,Controller 层是透明的,不会出现“项目能跑但不知道为什么能跑”的情况。

它当然不是生产级方案。高并发撑不住、鉴权模型偏简单、密码加密还停留在 MD5 加盐级别。但作为教学后台或课设源码,这套链路是完整的,改造成 Spring Boot 也不难,我在第 6 章会给出迁移路径。你先别急着替换框架,第一步是把 Servlet + JSP 的请求流转看懂,后面所有坑都好定位。

2.2 包结构与请求入口:拿到源码从哪里看起

拿到 zip 解压后,我一般先不看页面,直接打开src/main/java下的包结构。常见的分层是三段式:controller放 Servlet,service放业务逻辑,dao放 JDBC 操作,再加一个filter包处理登录拦截,util包放 MD5、DBUtil 这类工具。实体类放在entity包,对应数据库表字段。

请求入口有两种写法需要先确认:web.xml里配<servlet-mapping>,或者 Servlet 3.0 之后直接在类上写@WebServlet注解。教学类源码现在大多用注解,省得 web.xml 越写越长。下面这段是登录入口的典型写法:

@WebServlet("/user/login") public class LoginServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 真正逻辑在 service 层,Servlet 只做参数收口和页面跳转 String username = req.getParameter("username"); String password = req.getParameter("password"); User user = userService.login(username, password); if (user == null) { req.setAttribute("error", "用户名或密码错误"); req.getRequestDispatcher("/login.jsp").forward(req, resp); return; } req.getSession().setAttribute("loginUser", user); resp.sendRedirect(req.getContextPath() + "/course/list"); } }

这段代码的逻辑很直白:从表单拿参数,交给userService.login()做数据库校验,成功就把用户对象塞进 Session,失败则带着错误信息转发回登录页。注意resp.sendRedirect()后面跟的是项目上下文路径req.getContextPath(),不是裸路径/course/list,否则部署到带 context 的 Tomcat 下会 404。

我习惯把源码里的请求路径先列成一张路由表,对照着看功能边界。这份源码里常见路径是下面这些:

请求路径功能可访问角色
/user/register学生注册公开
/user/login登录公开
/course/list课程列表登录用户
/exercise/detail练习题详情登录用户
/submission/submit提交代码学生
/teacher/exercise/add添加练习题教师
/admin/user/list用户与班级管理管理员

对照这张表再去看 Filter 的拦截范围,就清楚为什么静态资源会被误伤——这个问题我在第 5 章会单独讲。

2.3 数据库设计:五张核心表与字段的含义

这套源码的数据库设计是典型的教育管理系统结构,核心表就那么几张。先把导入数据库的脚本看清楚,再动手跑项目。下面这段建表 SQL 是我按源码常见结构整理的,你可以直接对照:

CREATE DATABASE IF NOT EXISTS kids_edu DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE kids_edu; -- 班级表:教师开课和学生分班都依赖它 CREATE TABLE tb_class ( class_id INT PRIMARY KEY AUTO_INCREMENT, class_name VARCHAR(50) NOT NULL COMMENT '班级名称,如周六上午班' ); -- 用户表:学生、教师、管理员用 role 字段区分 CREATE TABLE tb_user ( user_id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(128) NOT NULL COMMENT 'MD5加盐后的密文', real_name VARCHAR(50), role TINYINT NOT NULL DEFAULT 1 COMMENT '1学生 2教师 3管理员', class_id INT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (class_id) REFERENCES tb_class(class_id) ); -- 课程表:一门课对应一位教师 CREATE TABLE tb_course ( course_id INT PRIMARY KEY AUTO_INCREMENT, teacher_id INT NOT NULL, title VARCHAR(100) NOT NULL, summary TEXT, cover_path VARCHAR(255) COMMENT '封面图相对路径', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (teacher_id) REFERENCES tb_user(user_id) ); -- 课时表:一门课包含多个课时,课件和视频都挂在课时下 CREATE TABLE tb_lesson ( lesson_id INT PRIMARY KEY AUTO_INCREMENT, course_id INT NOT NULL, title VARCHAR(100) NOT NULL, content_path VARCHAR(255) COMMENT '课件文件相对路径,例如 upload/lesson/xxx.pdf', video_url VARCHAR(255), sort_no INT DEFAULT 0, FOREIGN KEY (course_id) REFERENCES tb_course(course_id) ); -- 练习题表:expected_output 是自动判分的比对基准 CREATE TABLE tb_exercise ( exercise_id INT PRIMARY KEY AUTO_INCREMENT, lesson_id INT NOT NULL, title VARCHAR(100) NOT NULL, description TEXT COMMENT '题目描述,可带积木块示意', starter_code TEXT COMMENT '初始代码,学生在此基础上补全', expected_output TEXT COMMENT '预期输出,判分时逐行比对', time_limit_sec INT DEFAULT 5, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (lesson_id) REFERENCES tb_lesson(lesson_id) ); -- 提交记录表:每次判分的结果都留痕 CREATE TABLE tb_submission ( submission_id INT PRIMARY KEY AUTO_INCREMENT, exercise_id INT NOT NULL, student_id INT NOT NULL, code_content TEXT, status TINYINT DEFAULT 0 COMMENT '0未判 1通过 2失败', judge_output TEXT COMMENT '实际运行输出或编译报错', score INT DEFAULT 0, submit_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (exercise_id) REFERENCES tb_exercise(exercise_id), FOREIGN KEY (student_id) REFERENCES tb_user(user_id) );

注意这里有个关键选择:用户表用tb_user而不是user。因为user在 MySQL 里是保留字,直接建表会触发语法歧义,很多新手导入源码 SQL 时报错,根因就在这。判分核心在tb_exercise.expected_output字段——它存的是这道题预期的控制台输出文本,学生提交的 Java 代码运行后,服务端把实际输出和它做逐行比对,一致就判通过。

2.4 数据库连接配置:db.properties 里的五个参数

源码里数据库连接信息一般集中在src/main/resources/db.properties。拿到源码后第一件事就是改这里,改错任何一项项目都起不来。下面是我整理的对照模板:

# database connection jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/kids_edu?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=123456 jdbc.pool.maxActive=10

jdbc.driver这个值最容易翻车。MySQL 5.x 时代写com.mysql.jdbc.Driver,MySQL 8.x 以后必须写成com.mysql.cj.jdbc.Driver,否则启动直接报ClassNotFoundException。serverTimezone=Asia/Shanghai是 MySQL 8 连接时的必备参数,不加它会报时区错误,这是新手踩得最多的一处配置问题。

改完配置文件,我会先用命令行验证数据库脚本有没有跑干净,再启动 Tomcat:

mysql -u root -p < db_init.sql

如果 SQL 文件里没有USE kids_edu;,导入时会默认进入当前库,表会建错位置,登录时表现为“表不存在”。这里建议打开 Navicat 或命令行确认kids_edu库下确实有tb_user这张表,再做下一步。

3. 登录、权限与课程列表:把请求链路一层层看明白

3.1 注册与登录:从表单到 Session 的一次完整流转

登录功能是这套源码的入口,也是大多数课程设计答辩必问的地方。它的流转路径是:login.jsp表单提交到LoginServlet,Servlet 调UserService.login(),Service 再走 DAO 层执行 PreparedStatement 查询用户,成功就写 Session,失败就回登录页。我平时给同事讲这段代码,会重点强调两个细节。

第一个细节是防 SQL 注入。源码里如果不小心用了字符串拼接 SQL,登录框输入' or '1'='1就能直接绕过密码。正确做法是像下面这样用PreparedStatement的参数占位符:

public User login(String username, String md5Password) { String sql = "SELECT user_id, username, real_name, role, class_id FROM tb_user " + "WHERE username = ? AND password = ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, md5Password); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { User u = new User(); u.setUserId(rs.getInt("user_id")); u.setUsername(rs.getString("username")); u.setRealName(rs.getString("real_name")); u.setRole(rs.getInt("role")); u.setClassId(rs.getInt("class_id")); return u; } } } catch (SQLException e) { e.printStackTrace(); } return null; }

第二个细节是密码不能以明文形式进数据库。这套源码里如果用的是 MD5 加盐,md5Password就是MD5Util.md5(rawPassword + SALT)的结果,SALT可以定义在工具类里。课程设计阶段这样做可以接受,但如果要放到真实机构里跑,我建议把 MD5 替换成 BCrypt,UserService里做BCrypt.checkpw()校验,改动很小但安全等级完全不同。

3.2 登录拦截:Filter 只拦业务路径,别碰静态资源

登录功能写完后,必须有一个 Filter 挡住未登录用户,否则任何同学都能直接访问课程详情和提交链接。这套源码里典型的拦截器写法是对业务路径做前缀匹配。我看过不少翻车案例,问题都出在 Filter 配了/*导致 CSS、JS、图片全被拦住,页面打开是裸 HTML。正确的匹配范围应该是下面这样:

@WebFilter({"/course/*", "/exercise/*", "/submission/*", "/teacher/*"}) public class AuthFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpSession session = req.getSession(false); if (session == null || session.getAttribute("loginUser") == null) { ((HttpServletResponse) response).sendRedirect(req.getContextPath() + "/login.jsp"); return; } chain.doFilter(request, response); } }

注意req.getSession(false)这里必须传false,意思是“拿不到 Session 就返回 null,而不是新建一个”。如果写成req.getSession(),那么每次请求都会强制创建 Session,Filter 的判断就永远失守了,未登录用户也能一路畅通。

这里的chain.doFilter()是关键动作,它表示放行。前面sendRedirect()跳转登录页时,要带上req.getContextPath(),否则部署在/kidsweb上下文下,重定向会变成localhost:8080/login.jsp,照样 404。请求路径匹配、Session 判空、放行,这三个动作缺一个登录拦截就是摆设。

3.3 课程列表与详情:Service 返回数据,JSP 负责拼页面

页面渲染这部分,源码用的是 JSP + JSTL。Service 层把课程列表查出来,塞进 request,转发给 JSP,JSP 再用c:forEach循环输出卡片。理解这个流程的关键,在于区分request.setAttribute()和 JSP 里的取值方式。下面这段是课程列表页里的典型片段:

<%-- courseList.jsp --%> <c:forEach items="${courseList}" var="course"> <div class="course-card"> <h3><a href="${pageContext.request.contextPath}/course/detail?courseId=${course.courseId}">${course.title}</a></h3> <p>${course.summary}</p> <span>讲师:${course.teacherName}</span> </div> </c:forEach>

${courseList}是 EL 表达式,它取值来自request.getAttribute("courseList")。很多同学容易混淆的点是:从detail?courseId=1这种链接拿参数要用param.courseId,而拿到数据库查询结果要用setAttribute + getAttribute这条线,两者不是一个存储位置。页面没数据时,先确认 Service 是否把 list 放进 request,再确认 JSP 里 EL 的变量名拼写一致。

这里还有一个容易被忽略的点:课程封面、课件路径都是相对路径,JSP 里拼接时必须带${pageContext.request.contextPath}。如果不带,页面部署到根路径时没问题,部署到带上下文的路径下就全部指向错了,这是部署类源码最常见的资源 404 问题。

4. 练习题与自动判分:从提交代码到比对输出的完整实现

4.1 练习题如何组织:题目、起始代码与预期输出

少儿编程教学里的常见做法是“积木拖拽 + 代码过渡”。低龄段用 Scratch 拖积木,到小学高年级和初中转入代码训练,这套源码里练习题的定位就是后者:面向图形化到代码的过渡阶段,题目是 Java 控制台小程序,比如打印九九乘法表、计算求和、判断奇偶数之类。

每个练习题挂在课时下,由tb_exercise表承载。description字段放题目要求和示例,starter_code是学生看到的初始代码(通常留一个空方法),expected_output存标准答案的控制台输出。判分逻辑就是“运行学生提交的完整代码 → 拿到 stdout 文本 → 和 expected_output 比对”。这种方案对入门题足够用,但对格式敏感,只要多一个空格、多一个换行就可能误判,所以后面必须做输出归一化。

我一般会让题目描述里附带“预期输出格式”区块,明确告诉学生输出的行数、分隔符和大小写。这一条能少掉一半判分争议。源码里如果已经有类似的提示模板,保留并且统一它。

4.2 自动判分:用 ProcessBuilder 跑用户代码并管好子进程

判分是整个源码里最有技术含量的一环。它的本质是:在服务端启动一个子进程,编译并运行学生提交的 Java 代码,然后捕获输出。这里有个巨大的坑——如果直接Runtime.getRuntime().exec("java Main")而不做超时和目录隔离,一个死循环代码就能拖垮整个 Tomcat。我习惯的判分核心代码长这样:

String judgeDir = "/tmp/kidsweb/judge/u_" + userId + "/e_" + exerciseId; Files.createDirectories(Paths.get(judgeDir)); // 把学生代码写入独立临时目录,避免污染项目路径 Files.write(Paths.get(judgeDir, "Main.java"), codeContent.getBytes(StandardCharsets.UTF_8)); // 编译阶段:编码必须显式指定 UTF-8,否则 Windows 下中文输出全乱码 ProcessBuilder compilePb = new ProcessBuilder("javac", "-encoding", "UTF-8", "Main.java"); compilePb.directory(new File(judgeDir)); compilePb.redirectErrorStream(true); Process compileProc = compilePb.start(); boolean compileDone = compileProc.waitFor(10, TimeUnit.SECONDS); if (!compileDone) { compileProc.destroyForcibly(); submission.setStatus(2); // 编译超时 } else if (compileProc.exitValue() != 0) { String compileErr = new String(compileProc.getInputStream().readAllBytes(), StandardCharsets.UTF_8); submission.setJudgeOutput(compileErr); submission.setStatus(2); } else { // 运行阶段:等待时间受题目的 time_limit_sec 控制 ProcessBuilder runPb = new ProcessBuilder("java", "Main"); runPb.directory(new File(judgeDir)); Process runProc = runPb.start(); boolean finished = runProc.waitFor(exercise.getTimeLimitSec(), TimeUnit.SECONDS); if (!finished) { runProc.destroyForcibly(); submission.setStatus(2); submission.setJudgeOutput("TIME_LIMIT_EXCEEDED"); } else { String output = new String(runProc.getInputStream().readAllBytes(), StandardCharsets.UTF_8); submission.setJudgeOutput(output); boolean ok = normalize(output).equals(normalize(exercise.getExpectedOutput())); submission.setStatus(ok ? 1 : 2); submission.setScore(ok ? 100 : 0); } } // 最后记得清理临时目录,避免磁盘被学生代码塞满

这段代码有几个点需要深入说。第一,compilePb.directory(new File(judgeDir))是必须的,它把子进程的工作目录切到那个独立临时目录,这样javac Main.java才会按预期找到文件。第二,redirectErrorStream(true)把编译错误和标准输出合并到同一根管道,这样读一次就能拿到全部信息,否则要同时读 errorStream 和 inputStream,代码复杂度会成倍上升。第三,waitFor(timeout, TimeUnit.SECONDS)是阻塞等子进程结束,但加了超时上限,一旦超时就destroyForcibly(),否则死循环代码会让子进程越来越多,最后服务器卡死。

这里我见过一个很隐蔽的问题:明明天天跑判分,但服务器每隔几天就 CPU 飙升。原因是子进程虽然结束了,但部分 JDK 内部守护线程没有完全释放;更常见的是从来没有 destroy 编译进程,导致一个学生一次提交留下好几个僵尸 java 进程。解决思路是try (…)资源释放写法,或者在判分结束时统一 destroy。

所有判分结果都回写tb_submission表,包括状态码status(1 通过,2 未通过)和实际运行输出。教师端统计页直接从这个表聚合,按题号、按班级就能拉出正确率报表。

4.3 Scratch 内容嵌入:iframe 与积木代码对照的配合

这套网站的少儿编程特色,体现在课件和练习题展示层。Scratch 3 导出的项目本质是一个 HTML 网页,源码里最常见的做法是把它放在webapp/upload/scratch/目录下,然后用 iframe 嵌入课时页。下面这种写法在课件页里很普遍:

<div class="scratch-player"> <iframe src="upload/scratch/02-小猫走迷宫.html" width="960" height="540" title="小猫走迷宫项目"> </iframe> </div>

iframe 的src是相对路径,部署到 Tomcat 后,浏览器会以当前页面的访问路径为基准去拼。课件放在upload/scratch/下,页面 URL 是/kidsweb/lesson/detail?lessonId=1,这时候相对路径upload/scratch/...会解析成/kidsweb/lesson/upload/scratch/...,而不是/kidsweb/upload/scratch/...,于是就会 404。这是 Scratch 项目嵌不进去最常见的坑,解决方式是用 EL 表达式写全路径:

<iframe src="${pageContext.request.contextPath}/upload/scratch/02-小猫走迷宫.html" ...></iframe>

练习题页面里,我还会建议保留一个“积木与代码对照”的展示区。tb_exercise.description里可以放一段 HTML,用左右两栏结构,左边是积木示意图(图片外链或本地图),右边是 Java 代码。这一步不涉及复杂逻辑,但能显著提升教学体验——学生能看到“今天拖的那些积木,翻译成代码长什么样”,学习动机比直接抛一段代码强得多。

5. 常见问题与避坑:五处会影响你复现的关键配置

5.1 数据库导入后登录报“表不存在”:多半是 user 表名和库名的问题

现象:SQL 脚本导入成功,但一登录就报Table 'kids_edu.user' doesn't exist,或者干脆提示数据库里空表一片。

原因:第一种,源码里用户表直接叫user,而user是 MySQL 保留字,导入时被当成特殊语法处理,表没建成或者建错。第二种,SQL 脚本开头没有USE kids_edu;,表建到了默认库下,连接 URL 却指向kids_edu。

解决:建表语句统一用tb_user这类带前缀的表名;导入前确认连接 URL 里的库名和脚本开头的USE一致。我的习惯是导入完成后先执行SHOW TABLES;,确认五张核心表都在,再启动项目。

5.2 Tomcat 版本不对导致 JSP 一直编译失败

现象:启动 Tomcat 后访问任意 JSP 页面,控制台报The superclass "javax.servlet.http.HttpServlet" was not found on the Java Build Path,一堆类找不到,页面 500。

原因:源码是基于javax.servlet.*写的,但 Tomcat 10 开始把包名改成了jakarta.servlet.*。如果本机装的是 Tomcat 10+,源码里的import javax.servlet.http.HttpServlet自然全部失效。

解决:换回 Tomcat 8.5 或 9.0,配合 JDK 8 使用。这不是源码的问题,是服务器版本匹配的问题。如果你一定要用 Tomcat 10,那要全局替换javax.servlet为jakarta.servlet,工作量不小,不建议为了一个课程设计项目去折腾。

5.3 判分结果是 0 分,但代码在本机运行完全正常

现象:学生在本机 Eclipse 里运行代码输出完全正确,提交到网站后判分永远status=2,打开 judge_output 发现乱码或多余字符。

原因:两个层面。第一,Windows 下javac默认编码是 GBK,代码里如果包含中文,编译后的字符串在子进程输出时是 GBK 字节;而expected_output是从 MySQL 读出来的 UTF-8 文本,两者比对必然失败。第二,Windows 换行符是\r\n,Linux 是\n,比对时多一个\r字符。

解决:编译命令固定加-encoding UTF-8,运行参数也显式指定,同时比对前做字符串归一化。归一化函数我放在判分工具类里,三段代码就能解决:

private static String normalize(String text) { if (text == null) { return ""; } // 统一换行符,再掐头去尾,忽略末尾多余空白 return text.replace("\r\n", "\n").replace("\r", "\n").trim(); }

从那以后我每次判题都强制走一遍normalize,不再信任学生代码和数据库文本的原始格式。

5.4 登录跳转后 CSS 全丢:页面变成裸 HTML

现象:登录成功后跳转到课程列表页,页面结构还在,但所有样式、图片全部消失,控制台一堆 404。

原因:Filter 拦截范围配成了/*,把/css/main.css、/js/main.js这些静态资源也当成业务请求拦掉了,未登录判断触发后直接跳登录页,资源请求拿到的是 HTML 而不是 CSS。另一个常见原因是页面里 CSS 路径用的是相对路径,部署上下文一变就失效。

解决:Filter 注解范围收窄到业务前缀,只匹配/course/*、/exercise/*这类路径,静态资源完全放行。JSP 里静态资源全部改用${pageContext.request.contextPath}/css/main.css拼绝对路径。这两个动作做完,CSS 丢失问题基本绝迹。

5.5 ZIP 解压后中文文件名乱码,课件目录全变成火星文

现象:zip 解压后,upload/lesson/下的附件文件名、scratch/下的 HTML 文件名全部乱码,工程编译后页面引用乱码路径,课件打不开。

原因:压缩包生成时的文件名编码和本地系统不一致。Windows 下如果压缩工具没有按 UTF-8 存储文件名,解压到中文系统时就会按 GBK 反向解码,结果就是一堆乱码文件名。

解决:用 7-Zip 或 Bandizip 打开 zip,解压选项里选择“以 UTF-8 解码文件名”,不要直接用 Windows 自带的资源管理器解压。如果已经解压出乱码,在压缩软件里重新解压一次即可。检查工程时,确认web.xml和 JSP 页面头部都声明了 UTF-8 编码,避免二次乱码。

6. 扩展改造与验证:把这份源码变成你自己的教学后台

6.1 跑通后的验证清单:一张表确认每个角色都没白给

源码拿到手,先别急着改代码,按角色把主链路完整跑一遍,确认项目本身没问题。我整理的验证顺序是下面这张表:

步骤操作预期结果
1导入数据库脚本,改 db.properties项目启动无报错
2访问/kidsweb/register.jsp注册一个学生账号注册成功,跳转登录页
3用学生账号登录进入课程列表页,样式正常
4打开一个课程详情页课件和 iframe 里的 Scratch 项目能加载
5进入练习题,提交一段正确代码判分通过,状态变为 1
6提交一段故意写错的代码判分失败,能看到编译报错或输出比对失败
7用教师账号登录,新增一道练习题学生端刷出题目
8用管理员账号查看用户列表能按班级筛选学生

第 5 步是核心,如果判分链路能跑通,这套源码的主要价值就兑现了。我帮一位做少儿编程教学的老师改过类似项目,比对输出这块花的调试时间最长,多数不是逻辑问题,而是编码和换行符问题。

6.2 改成 Spring Boot 的三个动作

如果不想在 Servlet + JSP 上继续投入,可以按三步迁移到 Spring Boot。第一步,把@WebServlet注解换成@Controller+@PostMapping;第二步,把web.xml里的配置换成配置类或 application.yml;第三步,把 JDBC 模板换成 JdbcTemplate 或 MyBatis。登录逻辑迁移后大概长这样:

@Controller public class LoginController { @PostMapping("/user/login") public String login(String username, String password, HttpSession session) { User user = userService.login(username, MD5Util.md5(password + SALT)); if (user == null) { return "login"; } session.setAttribute("loginUser", user); return "redirect:/course/list"; } }

return "login"对应的是视图名,Spring Boot 会解析到模板目录下的login.html;redirect:/course/list是重定向,语义和 Servlet 里的sendRedirect一致。数据库访问层建议直接换 JdbcTemplate,原来几十行 JDBC 样板代码能压到十行以内。

6.3 一个更实际的扩展:练习截止时间不要只看前端

很多课设项目会把截止时间存在前端表单里,提交时只在前端 JS 判断。这完全不设防——学生改一下本地时间或直接绕过页面就能在截止后继续提交。我的做法是:tb_exercise表加一个deadline字段,判分 Servlet 里用服务端时间校验:

// 前端传的时间不可信,必须用数据库里的截止时间和服务端当前时间比对 if (LocalDateTime.now().isAfter(exercise.getDeadline())) { submission.setStatus(3); // 状态码用于标识逾期 submission.setJudgeOutput("已超过截止时间,无法提交"); // 这里直接 return,不再执行编译和运行 }

这个改动虽然只有几行,但它把判分可靠性提升了一个档次。第一次带着几个学生做课程设计时,因为 Filter 配错把整个页面的静态资源拦死,我折腾了一下午才定位到问题。从那以后我每次拿到 Java Web 源码,都强制走一遍“建库 → 改配置 → 启动 → 用一个测试账号跑通主链路”四个步骤,再往下改代码。这份源码的完整链路不长,值得花一下午把它吃透。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询