简介:一份基于Java的南昌航空大学软件学院21级web大作业——公司用车管理系统完整设计源码,面向高校学生、Java Web初学者及课程设计或毕业设计人群,适合用来理解企业车辆调度、用车申请、审批与驾驶员管理的基本流程。资源共175个文件,压缩包仅914KB,以66个Java源码、30个HTML页面、19个JavaScript脚本和16个CSS样式表为主,同时包含XML配置、JSP页面、SQL语句、数据库视图代码、Markdown说明以及少量PNG图片和字体资源,目录划分清楚,便于按功能定位。项目围绕公司用车业务,覆盖申请用车、司机请假、调度派车、驾驶员与员工信息管理等核心模块,前端页面与后端逻辑分层明确。包内还提供接口设计、properties配置和数据库脚本,可帮助理解表结构、视图设计与前后端交互流程,并快速搭建运行环境。已有343人学习下载,适合拿来快速搭建原型并在此基础上做二次开发。
1. 先聊两句:这个 Java Web 大作业值不值得下
如果你正在找 Java web 课程设计的参考源码,或者马上要交大作业但还没理清「审批流程 + 角色权限 + 页面跳转」这套东西怎么串起来,这份南昌航空大学软件学院 21 级的公司用车管理系统源码,可以拿来当脚手架。它不是那种只有几个静态页面的演示项目,而是把员工申请、调度派车、司机出车、归还登记这条完整链路做进了 172 个文件里,Java 后端、JSP 页面、CSS 样式、JS 交互都有,结构上是标准的 Servlet + JSP + MySQL 课程设计套路。
我拆完第一感受是:这项目的代码量不算大,但胜在「五脏俱全」——从一开始就在处理多角色视图、状态流转和表单交互这些真实业务里躲不开的问题。对正在做毕设或课程设计的人来说,比起从零搭框架,拿着这套源码对照着改,效率高得多。接下来我从模块设计、核心流程、数据库表结构到踩坑点逐层拆给你看,不吹不黑,把能用的部分和容易翻车的地方都说清楚。
2. 从 CSS 文件名反推系统设计:角色权限如何落地
2.1 先看文件命名,业务模块一目了然
我拆这份源码时有个习惯:不急着看 Java,先扫一遍静态资源的命名。这套项目里的 CSS 文件名暴露了它的业务分层——index_drive.css是司机首页的样式,index_staff.css是员工首页的,backStage-templet.css是后台管理模板,再加上dispatchBS.css(调度)、driver_rest.css(司机休息)、diver_infor.css(司机信息)、login.css(登录页),整个系统的角色边界在文件名里就已经画清楚了。
也就是说,这套系统至少有三个端:员工端(提交申请、查看自己订单)、司机端(接单、填行驶记录、报休息)、管理员端(车辆管理、司机审批、派车调度)。这种「按角色拆页面」的做法在课程设计里很讨巧,因为不用做复杂的动态权限标签,直接通过登录后的 Session 角色字段控制跳转目标就行。
从 Java Web 大作业的角度看,这种设计非常务实。相比把所有功能堆在同一个 JSP 里,按角色拆分让每个页面的职责单一,答辩时你能讲清楚「为什么司机看不到员工提交申请的按钮」——因为登录后 Servlet 只 forward 到对应当前角色的页面,这是最朴素的权限控制方案,但够用。
2.2 后端分层与一个关键配置:web.xml 里的路由
拆完静态资源后,我翻了下 Java 源码的包结构,是经典的servlet / dao / entity / util四层。entity 对应数据库表字段,dao 负责 JDBC 查询,servlet 处理请求并跳转页面。这套结构最大的好处是:大作业答辩时老师问「业务逻辑在哪一层」,你能明确指出来——servlet 只做参数接收和页面路由,具体 SQL 全部在 dao 里。
看web.xml能看到项目的路由设计。常见的课程设计会在这里给每个 Servlet 做 URL 映射,比如/login、/applyUseCar、/dispatch。当你把项目部署到 Tomcat 后,所有请求都经过这些映射进入对应的 Servlet 类,再通过request.getRequestDispatcher().forward()把结果带回 JSP 页面。有一点要提醒你:这类项目里如果用了 Servlet 3.0 的@WebServlet注解,web.xml里就没有大量映射了。我建议你拿到源码后先确认项目用的是哪种方式,这会直接影响你后续改路由的切入点。
<servlet> <servlet-name>LoginServlet</servlet-name> <servlet-class>com.nchu.web.servlet.LoginServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>LoginServlet</servlet-name> <url-pattern>/login</url-pattern> </servlet-mapping>这段配置的意思是把/login这个 URL 交给LoginServlet处理。如果后续你想把登录入口改成/user/login,只需要改<url-pattern>里的值,同时保证 JSP 表单的action属性指向新路径。改完以后一定要重启 Tomcat,因为web.xml的修改不会热加载。很多新手在这里出现「改了没反应」的困惑,其实不是代码问题,是忘记重启容器了。
2.3 登录与 Session:角色字段如何串起整个系统
登录逻辑是这个项目的枢纽。用户提交用户名密码后,LoginServlet调用UserDao查询数据库,比对成功后把用户 ID、姓名、角色存入 Session,然后根据角色跳转到不同首页。我拆了下登录这块的代码,能看出里面已经把「车辆管理」「司机管理」「调度管理」等入口做了区分,管理员登录后能看到后台管理导航,员工和司机登录后看到的是对应的功能面板。
这里有个常见的做法值得注意:密码存储。课程设计级别项目大多直接明文存数据库,这份源码我没看到加密处理的痕迹。如果是自己拿去扩展,建议至少用 MD5 加盐或者 BCrypt 做一层哈希。虽然这是大作业不是生产系统,但答辩时能主动说出「密码应该加密存储,目前为了演示方便采用明文」这种话,反而说明你考虑过安全问题,印象分不会低。
// LoginServlet 核心逻辑 String username = request.getParameter("username"); String password = request.getParameter("password"); User user = new UserDao().findByUsernameAndPassword(username, password); if (user != null) { HttpSession session = request.getSession(); session.setAttribute("userId", user.getId()); session.setAttribute("userRole", user.getRole()); // 按角色分流跳转 if ("admin".equals(user.getRole())) { response.sendRedirect("admin/index.jsp"); } else if ("driver".equals(user.getRole())) { response.sendRedirect("driver/index.jsp"); } else { response.sendRedirect("staff/index.jsp"); } } else { request.setAttribute("errorMsg", "用户名或密码错误"); request.getRequestDispatcher("login.jsp").forward(request, response); }这段代码的关键在角色分流逻辑:通过user.getRole()拿到的角色字符串决定用户落地页面。三个角色分别对应三个目录下的首页 JSP,页面结构被 CSS 文件名里的index_drive和index_staff印证了。维护这个逻辑时要注意 Session 超时的问题,Tomcat 默认 30 分钟无操作会销毁 Session,超时后再点页面的跳转请求会拿到空值,所以每个页面在取值前都应该做一次空判断。源码里有些页面做了,有些没做,你自己扩展的时候把没做的补上就行。
3. 用车申请到费用结算:核心流程实现拆解
3.1 申请页面:JSP 表单到 Servlet 的参数传递链路
公司用车管理系统的核心业务从「用车申请」开始。员工在staff目录下打开申请页面,填写用车时间、目的地、事由等信息,表单提交后被送到ApplyServlet处理。这个链路虽然简单,但它是整个系统最基础的数据入口,后面所有调度、出车、归还操作都依赖这一条申请记录。
看源码里对应的 JSP 页面,表单字段大致有:申请人、联系电话、出发地、目的地、用车时间、乘车人数、用车事由。这些字段提交后,ApplyServlet通过request.getParameter("fieldName")逐个取出,封装成Apply实体对象,再调用ApplyDao.insert(apply)写入数据库。我建议你在读这段代码时对照数据库表结构看,字段名对不齐是最常见的报错来源——JSP 里name="carId",Servlet 里取request.getParameter("car_id"),一跑就空指针,这类问题在课程设计项目里出现频率极高。
// 用车申请提交处理 request.setCharacterEncoding("UTF-8"); String applicant = request.getParameter("applicant"); String phone = request.getParameter("phone"); String startPlace = request.getParameter("startPlace"); String endPlace = request.getParameter("endPlace"); String useTime = request.getParameter("useTime"); int passengerNum = Integer.parseInt(request.getParameter("passengerNum")); String reason = request.getParameter("reason"); Apply apply = new Apply(); apply.setApplicant(applicant); apply.setPhone(phone); apply.setStartPlace(startPlace); apply.setEndPlace(endPlace); apply.setUseTime(useTime); apply.setPassengerNum(passengerNum); apply.setReason(reason); apply.setStatus("待审批"); // 新申请默认状态 new ApplyDao().insert(apply); response.sendRedirect("staff/myApply.jsp");这段代码里最容易出问题的是Integer.parseInt那行。如果表单里某个输入框没有填内容,浏览器提交空字符串,parseInt("")会直接抛NumberFormatException。我一般会先用StringUtils.isBlank做空值校验,或者把parseInt包进 try-catch 再给用户返回友好提示。还有一处细节:request.setCharacterEncoding("UTF-8")必须放在取参数之前。中文表单出现乱码时,十有八九是这句写晚了或者漏了,它只对 POST 请求生效,GET 请求的中文乱码还得去改 Tomcat 的server.xml里的 URIEncoding 配置。
3.2 审批与调度:状态机模型如何驱动流程
审批和调度是这个系统里最有「业务感」的部分。员工提交申请后,记录的状态是「待审批」。管理员登录后台看到待审批列表,点击通过,状态变成「已通过待派车」;随后进入调度环节,管理员选择可用司机和车辆,状态变成「已派车」。这份源码基本按这个状态流程走,用字符串字段表示状态,每次操作就是一个 UPDATE 语句。
这种字符串状态机的实现方式非常直接,也适合大作业的复杂度:不用引入工作流引擎,一个UPDATE apply SET status = ? WHERE id = ?就是状态流转。但它的短板很明显——状态边界靠代码自觉,如果你在多个 Servlet 里都能改状态,很容易把某条记录改到非法状态。以这套系统的规模,其实够用了;如果想让答辩更有亮点,可以在ApplyDao里用 switch 校验「当前状态是否可以迁移到目标状态」,把状态机的规则显式写出来。
// 调度派车的核心处理 String applyId = request.getParameter("applyId"); String driverId = request.getParameter("driverId"); String carId = request.getParameter("carId"); ApplyDao applyDao = new ApplyDao(); Apply apply = applyDao.findById(Integer.parseInt(applyId)); // 校验当前状态:只有待派车的申请才能进入调度 if (!"已通过待派车".equals(apply.getStatus())) { response.sendRedirect("admin/dispatch.jsp?error=statusError"); return; } apply.setDriverId(Integer.parseInt(driverId)); apply.setCarId(Integer.parseInt(carId)); apply.setStatus("已派车"); applyDao.update(apply); CarDao carDao = new CarDao(); carDao.updateStatus(Integer.parseInt(carId), "出车中"); response.sendRedirect("admin/dispatchList.jsp");这里有个容易被忽略的联动操作:派车成功后,除了申请单状态要变,车辆表里的车辆状态也要同步更新成「出车中」。否则车已经被派走了,列表里还显示「空闲」,下一个调度员就会把这台车重复分配。这种跨表状态同步是课程设计项目里最容易漏的逻辑,但也是答辩时的加分点——主动提「我在派车时同步更新了车辆状态,避免重复派车」,老师会觉得你想到了业务流程的深层约束。
3.3 司机端出车与归还:流程闭环中的两个关键更新
司机端的操作是整个流程闭环的收尾部分。司机登录后看到已派给自己的任务,出车前可以更新行驶状态,归还时填写实际里程数、用油量、归还时间,提交后系统把申请单状态改为「已完成」,同时把车辆状态复位成「空闲」。这一步做完,一辆车的完整生命周期才算走完。
看diver_infor.css能推断司机信息维护在这个模块里,司机可以维护自己的驾驶证号、驾龄、联系电话等信息。出车归还的代码逻辑上不复杂,就是几个 UPDATE,但要注意事务问题:更新申请单状态和更新车辆状态这两条 SQL 必须放在同一个事务里,否则第一条成功、第二条失败时,会出现「申请单已完成但车辆还在出车中」的数据不一致。常见做法是在Connection上关闭自动提交,两条 SQL 都成功后commit(),任何一条失败就rollback()并跳回页面提示用户重试。
// 车辆归还登记(事务版本示意) Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交 String sql1 = "UPDATE apply SET status='已完成', actual_mileage=? WHERE id=?"; PreparedStatement ps1 = conn.prepareStatement(sql1); ps1.setInt(1, Integer.parseInt(request.getParameter("mileage"))); ps1.setInt(2, Integer.parseInt(request.getParameter("applyId"))); ps1.executeUpdate(); String sql2 = "UPDATE car SET status='空闲' WHERE id=?"; PreparedStatement ps2 = conn.prepareStatement(sql2); ps2.setInt(1, Integer.parseInt(request.getParameter("carId"))); ps2.executeUpdate(); conn.commit(); } catch (Exception e) { if (conn != null) conn.rollback(); e.printStackTrace(); } finally { if (conn != null) conn.close(); }这段代码是典型的手写 JDBC 事务控制,setAutoCommit(false)之后所有 SQL 都不会立即生效,必须等commit()才落库。很多新手在这块踩坑是因为DBUtil.getConnection()每次调用都开启了新连接,两条 SQL 用了两个连接,事务自然控制不住。课程设计里通常只有一个DBUtil工具类,每次返回新连接,你要是想这么做事务,就得保证拿到的conn是同一个对象。另一个玄学点是conn.close()之后下面的代码还在用这个连接操作数据库,会莫名报错,排查时优先看是不是连接被提前关了。
4. 数据库设计拆解:六张核心表如何支撑业务
4.1 表结构与关系:先看 ER 再看 SQL
这个系统的数据库设计走的是课程设计标准路线。根据源码里的实体类和 SQL 文件,核心表大概有:用户表(员工/司机/管理员统一存放)、车辆信息表、用车申请表、司机休息表、调度记录表。用户表通过role字段区分角色,车辆表通过status字段表示空闲/出车中/维修,申请表则通过status字段承载整个审批流程的状态流转。
表之间的关系也直接:申请表和用户表通过申请人和司机 ID 关联,和车辆表通过车辆 ID 关联。这种设计的好处是查询简单——SELECT * FROM apply WHERE applicant_id = ?就能拿到某个人所有的用车记录。坏处是车辆信息、司机信息在申请表里以 ID 形式存在,展示列表页面时需要 JOIN 拿到名字,如果写 SQL 时忘了 JOIN,页面上显示的就会是一串数字 ID,翻车现场。
从大作业答辩的角度看,能讲清楚「为什么把用户表设计成单表 + 角色字段,而不是拆成员工表、司机表、管理员表三张表」,就是一个很好的加分点。核心理由是:三类用户的公共字段(用户名、密码、手机号、姓名)高度重合,拆表会导致大量冗余,而且登录查询也要 UNION 三次才能定位用户,单表 + 角色字段用一个 WHERE 就解决了。
4.2 建表 SQL 与字段设计要点
我整理了一份这套系统典型的建表 SQL,你可以直接拿去看结构,字段名可能和源码里略有出入,但骨架是准确的。我要特别强调的是字段类型选择:status用VARCHAR(20)存中文状态描述,虽然直观但浪费空间且容易写错,更规范的做法是用TINYINT存数字状态码,查询时再映射成文字。不过课程设计里中文状态串更直观,答辩讲起来省事,这也是很多模板项目这么写的原因。
CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '用户ID', username VARCHAR(50) NOT NULL UNIQUE COMMENT '登录名', password VARCHAR(50) NOT NULL COMMENT '密码', real_name VARCHAR(50) COMMENT '真实姓名', phone VARCHAR(20) COMMENT '联系电话', role VARCHAR(20) COMMENT '角色:admin/driver/staff', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) COMMENT '用户表'; CREATE TABLE t_car ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '车辆ID', car_no VARCHAR(20) NOT NULL COMMENT '车牌号', brand VARCHAR(50) COMMENT '品牌型号', seat_num INT COMMENT '座位数', status VARCHAR(20) DEFAULT '空闲' COMMENT '空闲/出车中/维修', driver_id INT COMMENT '常驻司机ID' ) COMMENT '车辆信息表'; CREATE TABLE t_apply ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '申请ID', applicant_id INT NOT NULL COMMENT '申请人ID', use_time DATETIME COMMENT '计划用车时间', start_place VARCHAR(100) COMMENT '出发地', end_place VARCHAR(100) COMMENT '目的地', passenger_num INT COMMENT '乘车人数', reason VARCHAR(255) COMMENT '用车事由', status VARCHAR(20) DEFAULT '待审批' COMMENT '状态流转', driver_id INT COMMENT '调度的司机ID', car_id INT COMMENT '调度的车辆ID', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_apply_user FOREIGN KEY (applicant_id) REFERENCES t_user(id) ) COMMENT '用车申请表';这份 SQL 里外键只做了一处示范(申请表关联用户表),车辆 ID 和司机 ID 在申请表里没有建外键,这是有意的省事。外键会影响写入性能,而且删除数据时约束多,课程设计里很多老师也不强制要求。但你要是在答辩时说「为了查询效率,对非核心关联不建外键」,这反而能体现你有工程权衡意识。字段命名统一用下划线风格,Java 实体类用驼峰风格,中间在 DAO 层做映射,这是 Java Web 的传统做法。
4.3 常见查询:列表分页与统计报表
这种管理系统里出现频率最高的查询有两类:一是条件筛选列表,比如「查询状态为待审批的申请单」,直接WHERE status = '待审批' ORDER BY create_time DESC;二是统计报表,比如「统计本月各部门的用车次数」,需要GROUP BY加时间范围过滤。源码里大概率这两个查询都写在ApplyDao里,拿到结果后封装成List<Apply>传到 JSP 用 forEach 循环渲染表格。
分页是一个绕不开的点。课程设计常见做法是「假分页」——一次性把全表查出来,然后在 JSP 里切片显示。数据量小的时候看不出来问题,一旦申请表累积到几千条,页面响应会明显变慢。更规范的做法是 SQL 层分页:LIMIT ?, ?,第一参数是起始偏移量,第二参数是每页条数,页面上通过pageNo和pageSize两个参数控制。你在源码里如果是全表查 + 循环截断,建议改成 LIMIT 分页,这个改进在答辩时很好讲,「我做了真分页,避免了全表扫描」。
// 分页查询申请列表 int pageNo = Integer.parseInt(request.getParameter("pageNo") == null ? "1" : request.getParameter("pageNo")); int pageSize = 10; // 每页10条 int offset = (pageNo - 1) * pageSize; String sql = "SELECT * FROM t_apply WHERE status=? ORDER BY create_time DESC LIMIT ?, ?"; PreparedStatement ps = conn.prepareStatement(sql); ps.setString(1, "待审批"); ps.setInt(2, offset); ps.setInt(3, pageSize); ResultSet rs = ps.executeQuery();这里有个页码计算的经典坑:LIMIT的偏移量是从 0 开始的,第一页是(1-1)*10=0,第二页是(2-1)*10=10,如果你直接用pageNo * pageSize,每页都会丢前十条数据。我见过很多新手在这个公式上翻车,页面第一页和第二页数据重叠,第三页开始又跳数据。另外一个注意点是request.getParameter("pageNo")拿到的永远是字符串,不转成 int 直接拼接进 SQL 会报错,而且存在 SQL 注入风险。习惯做法是先判空、再转 int、再参与计算,任何一步失败都回退到第一页。
5. 部署避坑手册:从 404 到中文乱码的六个现场
5.1 CSS 全挂:访问页面裸奔无样式
现象:启动 Tomcat 后打开登录页,页面能显示但完全没有样式,HTML 结构裸奔,控制台报一堆 404,全是login.css、index_staff.css这些文件找不到。
原因:这是课程设计里最常见的问题。项目部署到 Tomcat 后,应用有一个上下文路径,比如http://localhost:8080/car_management/。如果 JSP 里写死了<link href="css/login.css">这种相对路径,浏览器会从当前 URL 的目录去拼,当你在login.jsp页面时拼出来是http://localhost:8080/car_management/css/login.css没问题,但从 Servlet forward 过来后相对路径可能就乱了。更深层的原因是项目里可能用了href="css/login.css"而不是href="${pageContext.request.contextPath}/css/login.css"。
解决:把每个 JSP 页面里的 CSS、JS、图片引用全部改成绝对上下文路径,第一步在 JSP 顶部加一行String path = request.getContextPath(); String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path + "/";,第二步在<head>标签里写<base href="<%=basePath%>">,第三步把所有href="css/xxx.css"改成href="css/xxx.css"并用<base>标签强制浏览器从根路径解析。这样不管页面从哪个 Servlet 跳转过来,资源都能找到。
5.2 MySQL 驱动找不到:连接数据库直接 ClassNotFound
现象:启动项目后一登录就报java.lang.ClassNotFoundException: com.mysql.jdbc.Driver或com.mysql.cj.jdbc.Driver,数据库连接建立不起来。
原因:项目里写的驱动类名和导入的 JAR 包版本不匹配。MySQL Connector/J 5.x 用的是com.mysql.jdbc.Driver,8.x 用的是com.mysql.cj.jdbc.Driver。很多课程设计模板代码写在 MySQL 5 时代,但现在新装的 MySQL 8.x 需要新版驱动。如果你用的 MySQL 8 但代码里还写着老驱动类名,注定翻车。另一个原因是 JAR 包没有拷贝到项目的WEB-INF/lib目录下,IDE 里不报错是因为编译时引用了本地库,部署到 Tomcat 时 JAR 没跟着走,运行时自然会 ClassNotFound。
解决:先确认你的 MySQL 版本,再决定用哪个驱动。MySQL 8 就去 Maven 仓库下载mysql-connector-java-8.0.x.jar,放到WEB-INF/lib下,代码里写com.mysql.cj.jdbc.Driver,JDBC URL 要加时区参数:jdbc:mysql://localhost:3306/car_db?useSSL=false&serverTimezone=Asia/Shanghai。MySQL 5.7 及以下用com.mysql.jdbc.Driver,URL 不需要时区参数。我在折腾这套源码时专门把两种组合都试了一遍,核心判断标准是你的 MySQL 大版本号——SELECT VERSION()看一下就明了。
5.3 表单提交中文乱码:页面显示问号或乱码
现象:员工提交用车申请时,申请人和事由字段写入数据库后变成「???」或者乱码,但管理员后台登录的用户名如果是中文也乱。
原因:三层乱码叠加。第一层是 JSP 页面本身的编码,pageEncoding没设 UTF-8;第二层是 Servlet 接收参数的编码,request.setCharacterEncoding("UTF-8")没写或写在取参数之后;第三层是数据库连接 URL 的字符集参数没指定,MySQL 连接时默认用了latin1。这三层任何一层漏了,中文就会在传输中丢失。
解决:三层逐个检查。JSP 顶部加<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>;每个接收参数的 Servlet 里把request.setCharacterEncoding("UTF-8")放在取参数的代码之前,确保它是 you de 第一个处理请求的方法;JDBC URL 最后追加&characterEncoding=utf8。改完以后重启 Tomcat,重启 MySQL 客户端重新看数据。如果之前已经写入了乱码数据,DELETE 掉重新插入测试数据,别指望已经歪掉的数据能自己正过来。
5.4 Session 空指针:每页都报错但首页能打开
现象:管理员登录后跳转到后台首页正常,但点击「待审批列表」或「车辆管理」菜单时跳转到一个空页面或报 500 错误。
原因:页面里取 Session 值的时候没有判空。当用户直接通过 URL 访问某个页面(比如admin/dispatchList.jsp),Tomcat 会直接渲染 JSP,此时 Session 里的属性还没有被设置(或者已过期),取出来是 null,NULL 调用方法就炸了。这类问题在课程设计里极其常见,因为正常操作流程是先登录再跳转,但浏览器地址栏里直接敲 URL 是绕过了登录的。
解决:两个层面处理。第一,在每个需要登录才能访问的页面顶部加 Session 判空,取不到就response.sendRedirect("login.jsp")回去重新登录。第二,在 JSP 里用 EL 表达式${sessionScope.userId}而不是 scriptlet<%= session.getAttribute("userId") %>,前者能友好地处理 null 值。如果不打算做拦截器这种高级玩意,至少每个页面第一行加一个判断,保底不会白屏。
5.5 时间字段提交报错:日期格式解析失败
现象:表单里填了2024-06-01 08:30的用车时间,提交后 Servlet 里解析日期报错,后台异常信息指向SimpleDateFormat.parse或Date.valueOf。
原因:JSP 表单里的时间输入框type="datetime-local"提交的格式是2024-06-01T08:30,里面有字母 T,而 Servlet 里解析用的模板是yyyy-MM-dd HH:mm:ss,解析时遇到 T 直接抛ParseException。这是 HTML5 日期控件和 Java 日期格式之间的经典格式冲突。
解决:在 Servlet 接收参数后,先把字符串里的 T 替换成空格,再按yyyy-MM-dd HH:mm:ss解析。更稳妥的做法是前端把时间转成标准格式再提交,用一个隐藏域存格式化后的值。还有一个思路是接收时直接用LocalDateTime.parse,它原生支持 ISO 格式,能少写不少转换代码。不过课程设计里大部分项目还在用SimpleDateFormat,你就按「先替换 T 再解析」处理,这个坑在你自己写项目时大概率还会遇到。
5.6 Tomcat 热部署失效:改了代码不生效
现象:在 IDEA 里改了 Servlet 代码,重新访问页面,发现还是老逻辑,甚至报错说方法不存在,但代码里已经改过了。
原因:Tomcat 对 classes 目录的热加载需要开启reloadable="true",或者 IDE 配置了「Update resources」但没触发 classes 重新编译。更常见的情况是你只按了Ctrl+S保存了源码,但没执行Build Project,编译后的 class 文件根本没更新。课程设计里经常有同学改了 JSP 发现不生效,改了 Java 还是不生效,最后发现是 IDE 的编译开关被关了或者 Tomcat 部署的是 exploded 目录但没触发更新。
解决:在 IDEA 的 Tomcat 配置里,On Update Action 选Redeploy,On frame deactivation 选Update classes and resources,这样切窗时自动更新资源和编译后的类。Java 源码改动后,手动Ctrl+F9强行走一次 Build 再切到浏览器刷新。如果还不行,直接在 Tomcat 控制台点Redeploy按钮,全量重新部署。这套源码的路径结构天然支持这种部署方式,你只要别手滑把WEB-INF里的东西改错就行。
6. 部署到验证一气呵成:让这套源码当天跑起来
拿到这套源码后,最值得做的事就是把整个生命周期跑通。我习惯用的流程是把环境先固定下来:JDK 8、Tomcat 8.5、MySQL 5.7 或 8.0、IDEA 2023 起。版本不必完全一致,但 JDK 别上 17,很多老课程设计的代码在 JDK 17 下会因为模块化限制出现反射报错,犯不着在这上面浪费时间。
数据库导入用 Navicat 或命令行都行,先创建库CREATE DATABASE car_db DEFAULT CHARACTER SET utf8mb4;,再把项目里自带的 SQL 文件导进去。导入后重点检查三张核心表的数据是否初始化了——管理员账号有没有、车辆有没有初始状态为「空闲」的记录、司机账号能不能登录。很多项目跑不起来不是代码问题,是数据库里缺初始数据,登录时查不到人就一直报错。
验证路径是这样的:第一步用管理员账号登录,进后台看车辆列表和待审批列表是否正常加载;第二步创建一个员工账号(或使用初始账号)提交一条用车申请;第三步切回管理员账号审批通过,然后调度一台空闲车和一位司机;第四步用司机账号登录,看到已派给自己的任务,填写出车和归还记录;第五步回到管理员后台确认申请单状态变成了「已完成」,车辆状态恢复「空闲」。这五步走通,这套系统的核心业务就全部验证完毕了。如果哪一步卡住,基本就是我在前面避坑章节里写的那些问题,对照着排查很快能找到原因。
最后落一个实际的建议:拿到这套源码先别急着改功能,把运行环境搭好跑通一遍再动手。你后面想扩展什么——比如加一个部门维度、加一个审批驳回操作、把明文密码换成 MD5——都在这条跑通的主干上做加法。我自己在拆这类课程设计项目时,每次都会先跑通再读代码,因为跑通之后看代码的思路会清晰很多,你不知道某个字段是干嘛的,看一遍它在数据库里的变化就全明白了。先跑起来再谈改造,这套流程真的能帮你省掉不少无头苍蝇式的时间,希望帮到你。
本文还有配套的精品资源,点击获取