驾照考试系统源码解析:Java与PHP双后端协作实战

发布时间:2026/9/15 21:22:17
驾照考试系统源码解析:Java与PHP双后端协作实战
简介这是一套基于Java、JavaScript、CSS、HTML、PHP等多种语言开发的驾照考试系统完整源码面向需要学习全栈Web开发或直接部署驾考平台的开发者、学生及项目实践者也可作为二次开发的基础。资源共300个文件其中含78个Java源文件、32个JSP页面、32个JavaScript脚本、22个CSS样式表以及HTML、PHP、XML、图片、字体等素材压缩包仅7.13MBJava负责后端业务逻辑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; ListQuestion 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:hostlocalhost;dbnamedriving_exam;charsetutf8mb4, 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 结构用户输入只能作为数据值存在无法拼接成新语句。另外注意这里显式声明了charsetutf8mb4避免中文用户名和备注字段在写入时出现乱码。如果项目里用的是老式mysqli_query(SELECT ... WHERE id$id)拼串写法在导入后务必统一改成预处理这是代码评审里必然会被问到的一点。2.4 Java 与 PHP 在同一个项目里的接口约定两套后端共存时最容易出现的是请求路径混乱和参数命名不一致。从源码里能观察到一个实用约定Java Servlet 统一使用驼峰参数名examType、totalCountPHP 入口统一使用下划线参数名user_id、exam_type两种语言之间通过前端的 JS 脚本做一层适配。实际部署时常见做法是用 Nginx 或 Apache 做反向代理/api/java/*转发给 Tomcat/api/php/*转发给 PHP-FPM前端只面向统一域名请求资源。维度JavaServlet JSPPHPPDO MySQL典型职责组卷、判分、状态流转表单提交、成绩写入、管理接口事务控制强类型 手动事务简单写入依赖单条 SQL部署载体TomcatApache/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 version1.0 encodingUTF-8? question-bank question id1001 typesingle score1 stem驾驶机动车在道路上违反道路交通安全法的行为属于什么行为/stem options option idA违章行为/option option idB违法行为/option option idC违规行为/option option idD犯罪行为/option /options answerB/answer analysis依据《道路交通安全法》违反道路交通安全法律法规的行为属于违法行为。/analysis /question /question-bankJava 侧解析用 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 ServletApache 或 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 页面返回 500PHP 文件权限或 PDO 连接异常查看 PHP-FPM 日志检查$pdo构造参数中数据库密码是否与本机一致中文乱码JSPpageEncoding与 MySQLcharset不一致统一为 UTF-8JSP 顶部% page contentTypetext/html;charsetUTF-8 %样式全部错乱bootstrap.min.css与style.css加载顺序颠倒自定义样式必须在框架样式之后引入否则被覆盖交卷后成绩为 0score_calculator.php中题目score字段读不到打印$question[score]确认字段名是score还是point倒计时不更新JS 文件报错多为querySelector目标不存在打开 DevTools Console 查看具体行号核对 JSP 渲染后的元素 ID5.3 抽题 SQL 与静态资源缓存改造最后一个值得动手的优化点是抽题 SQL。我一般建议将随机抽题语句改为WHERE id (SELECT FLOOR(RAND() * (SELECT MAX(id) FROM question))) LIMIT 20的索引扫描方式它在题库量到十万级时也不会出现ORDER BY RAND()那样的全表排序和临时文件。另外考试页的 CSS、JS 可以启用长缓存版本号style.css?v20250110每次发布样式变更时统一改版本号即可。版本号要写进 Bootstrap 引用的公共头文件避免每个页面维护一处。本文还有配套的精品资源点击获取