简介:这是一套基于Java、JavaScript、CSS、HTML、PHP等多种语言开发的驾照考试系统完整源码,面向需要学习全栈Web开发或直接部署驾考平台的开发者、学生及项目实践者,也可作为二次开发的基础。资源共300个文件,其中含78个Java源文件、32个JSP页面、32个JavaScript脚本、22个CSS样式表,以及HTML、PHP、XML、图片、字体等素材,压缩包仅7.13MB;Java负责后端业务逻辑,JSP动态生成页面,JavaScript实现交互,CSS/LESS/SCSS控制界面样式。目前已有313人学习下载。该系统覆盖驾考理论学习、模拟考试与正式考试完整流程,涉及前后端交互、数据库操作、响应式页面设计等典型技术栈;读者可通过源码理解多语言协作方式,掌握JSP动态渲染、PHP数据接口、样式预处理器等实战技巧,并借助完整目录与部署说明快速搭建同类考试系统,适合作为毕业设计、课程项目或全栈入门实操范本。
1. 驾照考试系统源码:298 个文件里五种语言如何分工协作
这不是一套把静态网页打包起来的课程设计,而是一个典型的全栈考试业务系统。源码里 78 个 Java 文件负责核心业务逻辑,32 个 PHP 文件承担表单处理与数据库读写,剩余是 22 个 CSS、13 个 LESS、13 个 SCSS、11 个 HTML,以及一批 JS、JSP、XML 配置文件,总量 298 个。最大的看点不在某个算法多精巧,而在同一套系统里 Java 和 PHP 各自守住哪条边界,前端样式为什么同时保留 css、less、scss 三种形态。对正在做驾考类课程设计、想理解多语言项目文件组织、或者准备 Java 面试时被问到"全栈项目怎么分层"的开发者,这份源码足够当一份真实参考。本文直接按后端分工、前端样式体系、考试核心流程、部署排错四个方向拆开讲。
2. Java 与 PHP 双后端:业务边界、Servlet 与 PDO 的协作方式
2.1 为什么考试系统要同时保留 Java 和 PHP 两套后端
很多人在导入项目后会先困惑:既有.java又有.php,还有.jsp,容器到底该配 Tomcat 还是 Apache?实际上这在早期驾考类 Web 项目里很常见,属于"渐进式全栈"的典型形态。Java 侧 78 个源文件承担的是重业务:试卷组卷、判分规则、用户状态流转,这些逻辑对强类型和异常控制要求高,适合用 Servlet + JSP 体系落地。PHP 侧 32 个文件则聚焦轻量操作:表单提交、验证码校验、考试记录写入、后台管理接口,PHP 在这种短平快场景下开发效率明显更高,直连 MySQL 也不需要像 JDBC 那样写大量样板代码。
这种分工在后端架构上有一个明确的边界原则:涉及"一次请求要修改多条数据、且需要事务保证"的操作交给 Java,涉及"单表查询、简单写入、页面渲染辅助数据"的操作交给 PHP。例如用户答题完成后的成绩汇总需要跨表计算,放在 Java 侧;而"查询某分类下题目数量"这种单条 SQL 就能完成的接口,放在 PHP 侧。理解这个边界,比纠结"哪种语言更好"更有价值,因为真实项目中经常就是多套技术并存,关键是定好契约。
2.2 Java 侧核心代码:试卷生成与 JSP 数据回填
Java 侧最值得读的是试卷生成逻辑。常见做法是ExamPaperServlet接收前端传入的考试类型 ID,调用PaperService从题库读取题目并按规则组卷。下面这段代码是这类系统的核心骨架:
@WebServlet("/generatePaper") public class ExamPaperServlet extends HttpServlet { private QuestionDao questionDao = new QuestionDao(); @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); int examType = Integer.parseInt(req.getParameter("examType")); int totalCount = Integer.parseInt(req.getParameter("totalCount")); // 按题型分配题目:单选50%、判断30%、多选20% int singleCount = (int) Math.floor(totalCount * 0.5); int judgeCount = (int) Math.floor(totalCount * 0.3); int multiCount = totalCount - singleCount - judgeCount; List<Question> paper = new ArrayList<>(); paper.addAll(questionDao.findRandomByType(examType, 1, singleCount)); paper.addAll(questionDao.findRandomByType(examType, 2, judgeCount)); paper.addAll(questionDao.findRandomByType(examType, 3, multiCount)); Collections.shuffle(paper); req.getSession().setAttribute("paper", paper); req.getSession().setAttribute("paperStartTime", System.currentTimeMillis()); req.getRequestDispatcher("/exam.jsp").forward(req, resp); } }这段逻辑的关键在于把"组卷规则"和"题目存储"解耦。findRandomByType使用ORDER BY RAND() LIMIT ?实现随机抽题,虽然大数据量下性能一般,但驾考题库通常只有几百到几千题,完全够用。组卷完成后把试卷对象放入 Session,后续判分时直接从 Session 取,避免前端把答案和题目一起传回来被人为篡改。这里用Collections.shuffle(paper)对整卷题目再次乱序,是很多课程设计里容易漏掉的细节——它能防止相邻考生拿到的题目顺序完全一致。
2.3 PHP 侧数据操作:PDO 预处理与考试记录写入
PHP 32 个文件里,出现频率最高的是两类:一类是接收页面表单的submit_score.php这类入口,另一类是对应后台管理的列表页。驾考系统涉及用户成绩和考试次数,SQL 注入是不可忽略的风险点,所以 PHP 侧所有数据库操作都应走 PDO 预处理。代码写法如下:
<?php $pdo = new PDO( 'mysql:host=localhost;dbname=driving_exam;charset=utf8mb4', 'root', 'password', [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION] ); $stmt = $pdo->prepare( 'INSERT INTO exam_record (user_id, score, exam_type, start_time, submit_time) VALUES (:uid, :score, :type, :start, :submit)' ); $stmt->execute([ ':uid' => $_SESSION['user_id'], ':score' => $totalScore, ':type' => $examType, ':start' => $startTime, ':submit' => date('Y-m-d H:i:s'), ]);prepare加execute的好处是参数和 SQL 语句分离,MySQL 服务端会单独编译 SQL 结构,用户输入只能作为数据值存在,无法拼接成新语句。另外注意这里显式声明了charset=utf8mb4,避免中文用户名和备注字段在写入时出现乱码。如果项目里用的是老式mysqli_query("SELECT ... WHERE id=$id")拼串写法,在导入后务必统一改成预处理,这是代码评审里必然会被问到的一点。
2.4 Java 与 PHP 在同一个项目里的接口约定
两套后端共存时,最容易出现的是请求路径混乱和参数命名不一致。从源码里能观察到一个实用约定:Java Servlet 统一使用驼峰参数名(examType、totalCount),PHP 入口统一使用下划线参数名(user_id、exam_type),两种语言之间通过前端的 JS 脚本做一层适配。实际部署时,常见做法是用 Nginx 或 Apache 做反向代理,/api/java/*转发给 Tomcat,/api/php/*转发给 PHP-FPM,前端只面向统一域名请求资源。
| 维度 | Java(Servlet + JSP) | PHP(PDO + MySQL) |
|---|---|---|
| 典型职责 | 组卷、判分、状态流转 | 表单提交、成绩写入、管理接口 |
| 事务控制 | 强类型 + 手动事务 | 简单写入依赖单条 SQL |
| 部署载体 | Tomcat | Apache/Nginx + PHP-FPM |
| 源码数量 | 78 个 Java 文件 | 32 个 PHP 文件 |
| 请求风格 | /generatePaper | /submit_score.php |
提示:双后端项目最容易踩的坑不是语言本身,而是 Session 不互通。Java 和 PHP 各自维护 Session,如果考试流程中两个后端都要读取登录态,需要把用户标识通过参数或统一 Token 传递,不能默认两边 Session 同步。
3. 前端三层拆解:HTML 结构、CSS 样式体系与 JavaScript 交互
3.1 样式文件清单与 LESS/SCSS 源文件的编译关系
前端文件分布能直接看出这个项目的工程化程度:bootstrap.css和bootstrap.min.css是框架本体,bootstrap-theme.css是官方主题样式,font-awesome.css提供图标字体,datepicker.css负责日期选择控件,最后还有一个style.css作为业务自定义层。同时存在 13 个 LESS 和 13 个 SCSS 源文件,说明项目经历过从预处理语言到纯 CSS 的编译产物沉淀。
这里有一个容易误读的点:浏览器只会加载.css文件,.less和.scss是不能直接引用的。13 个 LESS 和 13 个 SCSS 是开发期的源文件,通过lessc或node-sass编译后输出到css目录。如果导入项目后发现页面样式错乱,第一步不是去改 HTML,而是检查style.css是否存在、是否由最新源码编译生成。常见构建命令是:
# LESS 编译到 css 目录 lessc less/bootstrap.less > css/bootstrap.css # SCSS 压缩输出 sass scss/custom.scss:css/custom.css --style compressed编译命令的核心是入口文件组织。LESS 通常以bootstrap.less为总入口,内部用@import按 variables、mixins、components 的顺序引入;SCSS 则常用_variables.scss这类下划线前缀片段文件,通过@use或@import组合。改样式时优先改源文件再编译,而不是直接改bootstrap.min.css,否则下一个人拉取代码后重新编译,你的修改会全部丢失。
3.2 style.css 如何覆盖 Bootstrap 主题并实现响应式
驾考页面里有大量重复出现的组件:左侧答题导航、右侧题目卡片、倒计时条、交卷按钮。这些组件如果全部用 Bootstrap 类名堆在 HTML 里,页面会变得不可维护。style.css在源码里承担的就是"约定优于配置"这一层,比如对答题导航的选中态、题目卡片的间距、交卷弹窗的层级做统一收敛。
/* style.css 中覆盖 Bootstrap 默认主题的关键片段 */ .exam-nav .list-group-item.active { background-color: #1a73e8; border-color: #1a73e8; color: #fff; } .question-card { padding: 24px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); } @media (max-width: 768px) { .question-card { padding: 12px; border-radius: 4px; } .exam-timer { position: fixed; top: 0; right: 0; left: 0; z-index: 1000; background: #fff; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1); } }这段样式的设计要点有两个。第一,覆盖 Bootstrap 类时必须保证选择器优先级不低:.exam-nav .list-group-item.active比 Bootstrap 本身的.list-group-item.active多了父级限定,能稳定生效。第二,用@media (max-width: 768px)把倒计时条在移动端改成吸顶定位,这是考试页在手机上的刚需——倒计时必须时刻可见。这类响应式处理在源码里不止一处,datepicker.css配合的日期控件样式也做了类似的小屏适配。
3.3 JavaScript 在考试页面的三个关键职责
前端 JS 在本系统里承担三类任务:倒计时管理、答题状态同步、表单校验。其中最容易被忽略的是"答题状态同步",考试系统常见的做法是每答一题就把当前题号与选项写入隐藏域或 JS 对象,同时更新左侧导航的状态色。下面这段代码完整展示了倒计时和答题状态的最小实现:
// exam.js —— 考试页面核心交互 const state = { answers: {}, // 题号 -> 选项ID remainingSeconds: 2700, // 45 分钟 timer: null }; // 每题作答时同步更新导航状态 function selectAnswer(questionId, optionId) { state.answers[questionId] = optionId; const navItem = document.querySelector(`#q-nav-${questionId}`); navItem.classList.remove('unanswered'); navItem.classList.add('answered'); // 更新底部进度文案 const answeredCount = Object.keys(state.answers).length; document.getElementById('progress-text').textContent = `已作答 ${answeredCount} / 100`; } // 倒计时:每秒递减,最后 5 分钟切换颜色 function startTimer() { state.timer = setInterval(() => { state.remainingSeconds--; if (state.remainingSeconds <= 0) { clearInterval(state.timer); submitPaper('timeout'); // 超时自动交卷 return; } const m = Math.floor(state.remainingSeconds / 60); const s = state.remainingSeconds % 60; const display = `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`; document.getElementById('timer').textContent = display; if (state.remainingSeconds <= 300) { document.getElementById('timer').classList.add('danger-color'); } }, 1000); }这段 JS 里有三个细节值得注意。String(m).padStart(2, '0')是补零的现代写法,兼容性已经足够;超时自动交卷调用submitPaper('timeout')而不是直接弹窗,是为了在接口层面区分"主动交卷"和"超时交卷",教室场景下这个区分直接影响成绩有效性判定;最后 5 分钟切换.danger-color类,比在 CSS 里写死时间更灵活,换考试时长时不需要改样式。JavaScript 运行时报错排查在这个文件里最常见的问题就是querySelector返回null,所以选择器里的#q-nav-${questionId}必须和 JSP 渲染生成的 id 完全对应,改 JSP 时很容易漏改这里。
提示:源码中的 JS 没有使用任何框架,原生实现正好方便逐行调试。如果你打算把这段逻辑迁移到 Vue 或 React,推荐把
state.answers换成响应式对象,其余倒计时逻辑可以直接复用。
4. 考试核心流程:题库加载、模拟判分与 Session 会话保持
4.1 XML 题库结构与 Java 解析方式
题库数据在这个项目里不是纯 MySQL,而是 XML 与数据库并存。XML 文件通常用于存放静态的配置型题库或公告数据,数据库表question存放完整题库。XML 结构一般是这样的:
<?xml version="1.0" encoding="UTF-8"?> <question-bank> <question id="1001" type="single" score="1"> <stem>驾驶机动车在道路上违反道路交通安全法的行为,属于什么行为?</stem> <options> <option id="A">违章行为</option> <option id="B">违法行为</option> <option id="C">违规行为</option> <option id="D">犯罪行为</option> </options> <answer>B</answer> <analysis>依据《道路交通安全法》,违反道路交通安全法律法规的行为属于违法行为。</analysis> </question> </question-bank>Java 侧解析用 DOM 或 SAX 都可以,驾考项目文件不大,推荐 DOM 直接加载,代码更直观:
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = factory.newDocumentBuilder(); Document doc = builder.parse(new File("/WEB-INF/questions.xml")); NodeList questionNodes = doc.getElementsByTagName("question"); for (int i = 0; i < questionNodes.getLength(); i++) { Element q = (Element) questionNodes.item(i); String qid = q.getAttribute("id"); String type = q.getAttribute("type"); String stem = q.getElementsByTagName("stem").item(0).getTextContent(); String answer = q.getElementsByTagName("answer").item(0).getTextContent(); // 组装 Question 对象并写入内存题库 }注意DocumentBuilderFactory在解析时默认不限制外部实体引用,虽然本例 XML 是内部文件,但如果是接收外部上传的 XML,一定要关闭setFeature("http://apache.org/xml/features/disallow-doctype-decl", true),否则存在 XXE 注入风险。这道题在 Java 面试八股文里出现的频率很高,源码里正好有现成例子可以对照。
4.2 判分逻辑:单选、判断、多选的分值累计
判分是考试系统的核心正确性所在。多选和单选不能共用一套判断,多选必须满足"全选对才得分",这是源码里最值得逐行读的部分:
// score_calculator.php —— 判分核心逻辑 function calculateScore($paper, $submittedAnswers) { $total = 0; $detail = []; foreach ($paper as $question) { $qid = $question['id']; $type = $question['type']; $correct = $question['answer']; // 如 "B" 或 "A,C" $userAnswer = isset($submittedAnswers[$qid]) ? $submittedAnswers[$qid] : ''; // 未作答直接记 0 分 if ($userAnswer === '') { $detail[$qid] = ['status' => 'unanswered', 'score' => 0]; continue; } switch ($type) { case 'single': // 单选:完全匹配 $isCorrect = ($userAnswer === $correct); break; case 'judge': // 判断:A为正确 B为错误 $isCorrect = ($userAnswer === $correct); break; case 'multi': // 多选:严格比对,选错、漏选都不得分 $userSet = explode(',', $userAnswer); sort($userSet); $correctSet = explode(',', $correct); sort($correctSet); $isCorrect = ($userSet == $correctSet); break; default: $isCorrect = false; } $score = $isCorrect ? (int)$question['score'] : 0; $total += $score; $detail[$qid] = [ 'status' => $isCorrect ? 'correct' : 'wrong', 'score' => $score ]; } return ['total' => $total, 'detail' => $detail]; }这个函数的三个要点。第一,多选比对必须先排序再比较,因为用户提交的选项顺序可能和答案不一致,PHP 数组比较==对顺序敏感,排完序后两个数组内容一致才算对。第二,explode(',', $correct)假设答案以逗号分隔存储,如果数据库里答案是AC而不是A,C,需要在读取时先转换,否则永远判不对。第三,函数返回total和detail两个值,detail用于考试结束后的逐题解析页面——这部分数据来自真实作答记录,而不是重新生成的随机答案。
4.3 Session 会话保持:防重复提交与考试状态恢复
考试系统的 Session 管理比普通网站严格,因为要防止三种情况:刷新页面重复提交成绩、开多个标签页进行多次考试、超时后状态丢失。源码里的做法是给一次考试分配一个唯一的examId,写入 Session 并同时存到数据库记录里。
session_start(); // 考试开始时生成唯一会话标识 if (!isset($_SESSION['exam_id'])) { $_SESSION['exam_id'] = uniqid('exam_', true); $_SESSION['submit_flag'] = false; } // 交卷入口检查:已交卷则拒绝重复提交 if (isset($_POST['submitPaper']) && $_SESSION['submit_flag'] === true) { die('请勿重复交卷,你的成绩已提交。'); } // 校验通过后,先写入成绩再置位标志 $recordId = insertExamRecord($_SESSION['user_id'], $_POST['examType'], $score); $_SESSION['submit_flag'] = true; $_SESSION['final_record_id'] = $recordId;这个方案的细节在于submit_flag必须先置位再跳转。如果先跳转后置位,用户快速双击交卷按钮,两个请求都可能在标志位更新前到达,从而写入两条成绩。更稳妥的做法是在数据库端对(user_id, exam_id)加唯一索引,程序层面和数据库层面双重兜底。涉及成绩校验时,Session 值永远只做参考,最终以数据库记录为准。
提示:如果考试过程中浏览器崩溃,Session 丢失后考生无法恢复未交卷的考试,这是该方案的已知边界。改进方向是把答题进度实时写入 localStorage,并在重新登录时提示"检测到未完成考试,是否恢复"。
5. Tomcat 混跑部署、报错排查与考试场景性能优化
5.1 容器选型与资源文件版本控制
系统同时含.jsp和.php,部署时必须解决混跑问题。最常见也最简单的方案是:Tomcat 处理 JSP/Java Servlet,Apache 或 Nginx 处理 PHP,前端静态资源(CSS、JS、图片)直接由 Nginx 托管。Nginx 反代配置里只需要两个核心 location 块:
# 静态资源与 PHP 请求 server { listen 80; server_name exam.example.com; root /var/www/driving-exam; # PHP 请求转发 PHP-FPM location ~ \.php$ { fastcgi_pass 127.0.0.1:9000; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } # Java Servlet 路径转发 Tomcat location /api/java/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; } }注意proxy_pass http://127.0.0.1:8080/末尾带了斜杠,意味着把/api/java/generatePaper重写为 Tomcat 里的/generatePaper,省去在 Java 侧处理统一前缀。另外,部署目录里保留.gitignore是有工程意义的,通常它排除了target/、node_modules/、*.log、本地数据库配置等。检查.gitignore时会发现WEB-INF/classes也被排除,确保编译产物不进入版本库,别人拉取代码后必须重新编译。
5.2 高频报错与排查方向
| 报错现象 | 大概率原因 | 排查与修复 |
|---|---|---|
| JSP 页面显示源码而非渲染结果 | Tomcat 未编译 JSP,或文件后缀大小写不一致 | 确认web.xml中 servlet-mapping 覆盖*.jsp,清理work/Catalina缓存 |
| PHP 页面返回 500 | PHP 文件权限或 PDO 连接异常 | 查看 PHP-FPM 日志,检查$pdo构造参数中数据库密码是否与本机一致 |
| 中文乱码 | JSPpageEncoding与 MySQLcharset不一致 | 统一为 UTF-8,JSP 顶部<%@ page contentType="text/html;charset=UTF-8" %> |
| 样式全部错乱 | bootstrap.min.css与style.css加载顺序颠倒 | 自定义样式必须在框架样式之后引入,否则被覆盖 |
| 交卷后成绩为 0 | score_calculator.php中题目score字段读不到 | 打印$question['score']确认字段名是score还是point |
| 倒计时不更新 | JS 文件报错,多为querySelector目标不存在 | 打开 DevTools Console 查看具体行号,核对 JSP 渲染后的元素 ID |
5.3 抽题 SQL 与静态资源缓存改造
最后一个值得动手的优化点是抽题 SQL。我一般建议将随机抽题语句改为WHERE id >= (SELECT FLOOR(RAND() * (SELECT MAX(id) FROM question))) LIMIT 20的索引扫描方式,它在题库量到十万级时也不会出现ORDER BY RAND()那样的全表排序和临时文件。另外考试页的 CSS、JS 可以启用长缓存版本号:style.css?v=20250110,每次发布样式变更时统一改版本号即可。版本号要写进 Bootstrap 引用的公共头文件,避免每个页面维护一处。
本文还有配套的精品资源,点击获取