每年到毕业季,“XX管理系统”这一挂的题目永远是最热门的存在。你拿到的这个“基于JAVA的机床厂车辆管理系统的设计与实现”,光看标题就知道是典型的SSM框架毕设实战项目。这个题目听起来带“机床厂”这种工业背景,但实际上它的核心就是一个非常经典的车辆管理业务系统:车辆档案、驾驶员管理、出车归还登记、维修保养、里程和油耗统计,再加登录权限和后台管理。你要是能把这套东西完整做下来并且真正讲明白,毕业答辩基本上就稳了一大半。
我带过的学生里,有不少人选的就是类似题目。这里面的门道我太清楚了:题目难度适中、网上参考材料多、演示效果直观,而且老师熟悉这套技术栈,答辩时稍微讲几句技术细节就能加分。这篇文章我就把这个项目从选题分析、系统设计到核心功能实现、源码结构、论文撰写、答辩准备,再到实际部署中踩过的坑,一次全给你拆开讲清楚,希望对准备做同类毕设的同学有实际帮助。
1. 选题分析与系统定位
1.1 为什么“机床厂车辆管理”是SSM毕设的好题目
先说说选题的事。很多同学看到“机床厂”三个字就容易慌,觉得是不是还要懂什么工厂业务流程、车间排产、刀具管理之类的东西。其实完全不用,这个名字只是给系统一个业务场景的壳,真正要做的就是通用型的车辆信息管理。
我之所以说这个题目好,原因有三点:
第一,难度可控。整个业务核心是增删改查加统计,没有复杂算法,没有高并发,没有分布式,这对本科毕设来说是非常合适的体量。你花三四周时间能把主体功能撸出来,剩下时间用来写论文和准备答辩。
第二,技术栈完整。SSM(Spring + SpringMVC + MyBatis)三个框架各自承担什么职责、请求是怎么从页面走到数据库再返回的、事务是怎么控制的,这些全是高频考点。做完这个项目,你对Java Web开发的理解会非常系统。
第三,扩展空间大。你觉得功能太简单?可以往里面加用车申请审批流、Excel导入导出、ECharts统计图表、RBAC细粒度权限,随便加两个模块,论文的“系统实现”章节就厚实很多。
1.2 需求拆解与角色边界
这个系统要管什么,我习惯先列角色再列功能。一套标准的车辆管理系统通常有三类角色:
管理员:管车辆档案、管驾驶员、管所有登记记录、管系统用户、查看统计数据。 司机/车队人员:登记出车、登记归还、查看自己名下的出车记录。 普通部门员工(可选,看扩展设计):提交用车申请,等待审批。
核心业务模块可以拆成下面这些:
- 用户登录与权限控制
- 车辆信息管理(新增、修改、删除、分页查询、条件搜索)
- 驾驶员信息管理
- 出车登记(记录出车时间、事由、目的地、预计返回时间、出车里程)
- 归还登记(记录实际返回时间、归还里程、车况备注)
- 维修保养记录(保养项目、费用、维修厂、经办人)
- 数据统计(出车次数、里程汇总、维保费用)
这些模块全部做完,系统就有了一个非常完整的业务闭环,论文的需求分析章节也好写,因为每个模块都能对应上业务痛点。
2. 系统架构与数据库设计
2.1 分层架构与请求流转
SSM项目的经典分层方式,拿这个项目来说,大致是这样:
- View层:JSP页面 + Bootstrap + jQuery + ECharts
- Controller层:接收请求、参数校验、调用Service、返回视图或JSON
- Service层:业务逻辑处理、事务控制
- Mapper层(DAO层):MyBatis操作数据库
- 数据库:MySQL
一个最常见的请求流转就是“打开车辆列表页面”:
浏览器发起 /vehicle/list 请求 -> DispatcherServlet 根据 HandlerMapping 找到 VehicleController 的 list 方法 -> Controller 调用 VehicleService 的 selectPage 方法 -> Service 调用 VehicleMapper 的 selectList 方法 -> MyBatis 执行 XML 里的 SQL -> 返回 List -> 逐层返回 -> Controller 把数据放到 Model -> 渲染 vehicle/list.jsp。
这套流程你在论文里需要画成时序图,答辩时也要能口头描述清楚。如果连这个都讲不明白,那基本等于告诉老师你没有真正跑通过项目。
2.2 核心数据表设计
数据库设计是论文里占篇幅很大的一块,也是系统的根基。我直接给出一套经过验证的核心表结构。
用户表 tb_user:
CREATE TABLE tb_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '用户ID', username VARCHAR(50) NOT NULL UNIQUE COMMENT '登录名', password VARCHAR(64) NOT NULL COMMENT '密码(MD5加密)', real_name VARCHAR(50) COMMENT '真实姓名', role INT DEFAULT 1 COMMENT '角色: 1管理员 2普通用户', phone VARCHAR(20), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) COMMENT '系统用户表';车辆信息表 tb_vehicle:
CREATE TABLE tb_vehicle ( id BIGINT PRIMARY KEY AUTO_INCREMENT, vehicle_no VARCHAR(50) NOT NULL COMMENT '车辆编号', plate_number VARCHAR(20) NOT NULL COMMENT '车牌号', brand VARCHAR(50) COMMENT '品牌', model VARCHAR(50) COMMENT '型号', engine_number VARCHAR(50) COMMENT '发动机号', chassis_number VARCHAR(50) COMMENT '车架号', purchase_date DATE COMMENT '购置日期', status TINYINT DEFAULT 0 COMMENT '状态: 0在场 1出车中 2维修中 3已报废', remark VARCHAR(255), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) COMMENT '车辆信息表';驾驶员表 tb_driver:
CREATE TABLE tb_driver ( id BIGINT PRIMARY KEY AUTO_INCREMENT, driver_no VARCHAR(50) NOT NULL COMMENT '驾驶证编号', name VARCHAR(50) NOT NULL, license_type VARCHAR(10) COMMENT '准驾车型', phone VARCHAR(20), department VARCHAR(100) COMMENT '所属部门', hire_date DATE COMMENT '入职日期', status TINYINT DEFAULT 0 COMMENT '状态: 0在职 1离职' ) COMMENT '驾驶员表';出入登记表 tb_vehicle_log:
CREATE TABLE tb_vehicle_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, vehicle_id BIGINT NOT NULL COMMENT '车辆ID', driver_id BIGINT COMMENT '驾驶员ID', log_type TINYINT NOT NULL COMMENT '类型: 1出车 2归还', reason VARCHAR(255) COMMENT '出车事由', destination VARCHAR(100) COMMENT '目的地', expected_return_time DATETIME COMMENT '预计返回时间', actual_return_time DATETIME COMMENT '实际归还时间', start_mileage DECIMAL(10,2) COMMENT '出车里程', end_mileage DECIMAL(10,2) COMMENT '归还里程', operator_id BIGINT COMMENT '登记人ID', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) COMMENT '车辆出入登记表';维修保养表 tb_maintenance:
CREATE TABLE tb_maintenance ( id BIGINT PRIMARY KEY AUTO_INCREMENT, vehicle_id BIGINT NOT NULL, maintain_type VARCHAR(20) COMMENT '保养/维修/年检', maintain_date DATE, service_shop VARCHAR(100) COMMENT '维修厂/保养点', cost DECIMAL(10,2) COMMENT '费用', detail VARCHAR(500) COMMENT '项目明细', operator_id BIGINT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) COMMENT '维修保养记录表';这套表结构足够支撑起整个系统,而且每一张表都能在论文里画成ER图。注意所有外键关联我都用的逻辑外键,也就是说没有真正在数据库层面加 FOREIGN KEY,而是通过 Java 代码进行关联查询。原因也很简单:毕设项目经常要手动改数据来模拟各种业务情况,物理外键太死板,删除车辆时会一直报错影响演示。逻辑外键配合删除前校验,效果完全一样,还不容易出现数据操作被卡死的情况。
2.3 表关系与设计细节
表之间的关系其实很好梳理:
一个用户(管理员)可以登记多条出车归还记录; 一辆车可以有多条出入记录和维修记录; 一个驾驶员名下也会有多条出车记录。
所以 tb_user 和 tb_vehicle_log 是 1对多,tb_vehicle 和 tb_vehicle_log 是 1对多,tb_vehicle 和 tb_maintenance 也是 1对多,tb_driver 和 tb_vehicle_log 是 1对多。整个系统的核心就围着 tb_vehicle_log 这张“流水表”转。
设计时有几个细节我可以强调一下,论文里写出来会显得你很专业:
状态字段都设计成 TINYINT 而不是 VARCHAR。例如车辆状态 0 表示在场、1 表示出车中、2 表示维修中、3 表示已报废。这样在代码里写 switch 或者 if 判断时特别干净,前端下拉框也能直接映射成中文显示。
金额字段用 DECIMAL(10,2),不是 FLOAT/DOUBLE。浮动类型在计算费用时会出精度问题,答辩时如果被问到为什么这么设计,这本身就是个加分回答。
时间字段出车和归还分开存。很多仿毕设项目只存一个登记时间,导致“这个车在外面跑了几天”“本次跑了多少公里”这类问题根本没法回答。你这套结构里,一条出车记录和一条归还记录配合 start_mileage 和 end_mileage,就能算里程,也能算出车时长。
3. 核心功能模块实操解析
3.1 登录认证与权限拦截实现
登录模块看起来简单,但它是整个系统的门面,也是老师第一个会看的功能。用 SpringMVC 的拦截器来实现登录状态验证,是最稳妥的做法。
先定义一个权限拦截器:
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); Object user = session.getAttribute("loginUser"); // 放行登录请求,其他请求必须带登录状态 if (request.getRequestURI().contains("/login") || request.getRequestURI().contains("/static")) { return true; } if (user == null) { response.sendRedirect(request.getContextPath() + "/login"); return false; } return true; } }然后在 spring-mvc.xml 里注册这个拦截器:
<mvc:interceptors> <mvc:interceptor> <mvc:mapping path="/**"/> <mvc:exclude-mapping path="/login"/> <mvc:exclude-mapping path="/static/**"/> <bean class="com.example.interceptor.LoginInterceptor"/> </mvc:interceptor> </mvc:interceptors>这里有个设计取舍要说清楚:登录密码在数据库里不能存明文。我自己是在注册用户的时候就用 MD5 加盐处理密码。MD5 虽然现在不算强力加密,但放在毕设项目里完全够用,答辩时还能顺势说出“如果生产环境应该用BCrypt”这种话,至少说明你有安全意识和知识延伸。
3.2 车辆档案管理:分页与条件搜索
车辆档案是整个系统最基本的管理模块,涉及到的就是“列表 + 搜索 + 新增 + 修改 + 删除”。这里比较能体现功底的细节有两个:分页插件用 PageHelper,条件查询用 MyBatis 动态 SQL。
Controller 层这样写:
@Controller @RequestMapping("/vehicle") public class VehicleController { @Autowired private VehicleService vehicleService; @RequestMapping("/list") public String list(@RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize, Vehicle condition, Model model) { PageHelper.startPage(pageNum, pageSize); List<Vehicle> vehicles = vehicleService.selectByCondition(condition); PageInfo<Vehicle> pageInfo = new PageInfo<>(vehicles); model.addAttribute("pageInfo", pageInfo); model.addAttribute("condition", condition); return "vehicle/list"; } }这里最关键的就是 PageHelper.startPage 一定要写在查询语句的第一行,它基于 MyBatis 拦截器机制,会对紧接着的下一条查询 SQL 自动拼接 LIMIT 子句。很多人用错是因为它和业务代码中间又隔了一层调用,结果页码永远不对。
Service 层不需要写太多逻辑,直接转调 Mapper。重头戏在 Mapper XML 的动态 SQL:
<select id="selectByCondition" resultType="com.example.entity.Vehicle" parameterType="com.example.entity.Vehicle"> select * from tb_vehicle <where> <if test="plateNumber != null and plateNumber != ''"> and plate_number like concat('%', #{plateNumber}, '%') </if> <if test="brand != null and brand != ''"> and brand like concat('%', #{brand}, '%') </if> <if test="vehicleNo != null and vehicleNo != ''"> and vehicle_no like concat('%', #{vehicleNo}, '%') </if> <if test="status != null"> and status = #{status} </if> </where> order by create_time desc </select>标签会自动处理 AND 前缀,条件为空时不加 WHERE,这点写论文和答辩的时候都可以拿出来说一下。而且注意,这里用的是 #{} 而不是 ${},这是防 SQL 注入的关键。你可以在论文里专门写一段“MyBatis的#{}预编译机制如何避免SQL注入”,这个点在答辩里基本是必问的。
3.3 出车与归还登记的业务流
这个模块是整个系统的业务核心,它不能做成单纯的增删改查——状态流转才是核心逻辑。
出车时要做的事:
- 校验车辆状态是否为“在场”(status=0),只有在场车辆才能出车。
- 登记出车记录(log_type=1,填入出车事由、目的地、驾驶员、出车里程、预计返回时间)。
- 更新车辆状态为“出车中”(status=1)。
归还时要做的事:
- 找到对应的在场出车记录,校验当前确实有“出车中”的车辆。
- 登记归还记录(log_type=2,填入实际返回时间、归还里程)。
- 可选:计算本次行驶里程 = end_mileage - start_mileage。
- 更新车辆状态回“在场”(status=0)。
这三个操作必须放在同一个事务里。如果只插了出车记录但没有改车辆状态,系统里的车就会“明明不在场还能再出一次”,这就是数据不一致。Spring 的事务只需要在 Service 实现类上加注解:
@Transactional(rollbackFor = Exception.class) public void addOutLog(VehicleLog log) { vehicleLogMapper.insert(log); vehicleMapper.updateStatus(log.getVehicleId(), 1); }归还也是一样的写法。这里你可以在论文里好好写一段“为什么用事务保证状态一致性”,然后配合一个状态流转图,这部分内容在答辩技术追问环节相当能打。
还有一个小细节:出车和归还我认为不建议做成一张表里两条记录,而是可以让“归还操作”基于某条出车记录来登记,也就是说出车时生成一条 log_type=1 的记录,归还时再往同一张表插入一条 log_type=2 的记录,并通过一个 business_id 字段把两次记录关联起来。当然如果你觉得两张表更清晰也可以,但不建议在答辩时说“这是我的不足之处”这种话,一定要把你的设计理由讲圆。
3.4 统计报表实现
统计报表是给系统“加分”用的模块。使用 ECharts 是绝大多数毕设的标准选择,因为 JSP 页面直接引入 JS 文件就能用,不用搞复杂的构建打包。
我常用的方案是后端返回 JSON 字符串,前端通过 Thymeleaf/JSP 输出到一个隐藏字段或者直接 model 传值。比如统计“本月每天出车次数”,后端 SQL 这样写:
SELECT DATE_FORMAT(create_time, '%Y-%m-%d') AS day, COUNT(*) AS count FROM tb_vehicle_log WHERE log_type = 1 AND create_time >= DATE_FORMAT(CURDATE(), '%Y-%m-01') GROUP BY DATE_FORMAT(create_time, '%Y-%m-%d') ORDER BY day;Controller 返回这个 List<Map<String, Object>> 之后,转成 JSON 传到页面:
[{"day": "2025-05-01", "count": 3}, {"day": "2025-05-02", "count": 5}]前端用 ECharts 的折线图或者柱状图展示,效果立刻就有了。类似的统计还能做:“车辆出车次数Top5”、“维保费用月度趋势”、“车辆里程汇总”。这些图表一放上去,系统演示的观赏性直接提升一个档次,而且论文里也能截图,一张图抵三百字。
4. 源码结构与开发环境搭建
4.1 项目包结构规划
拿到完整源码之后,第一步不是急着跑起来,而是先弄清楚项目结构。规范的包名结构能体现开发习惯。这个项目我的推荐分包方式如下:
com.example.vehicle ├── controller // 控制层 │ ├── UserController.java │ ├── VehicleController.java │ ├── DriverController.java │ ├── VehicleLogController.java │ └── MaintenanceController.java ├── service // 业务层 │ ├── UserService.java │ ├── VehicleService.java │ └── ...Impl.java ├── dao // 数据访问层 Mapper接口 │ ├── UserMapper.java │ └── VehicleMapper.java ├── entity // 实体类 │ ├── User.java │ ├── Vehicle.java │ └── VehicleLog.java ├── interceptor // 拦截器 │ └── LoginInterceptor.java ├── common // 通用工具类、常量类、统一返回结果 │ ├── Result.java │ └── MD5Util.java └── config // 配置类注意包名从项目名 vehicle 开始,实体类放在 entity,而不是 model,这个命名习惯在答辩时如果老师问“你的项目为什么这样分包”,回答“按照职责划分控制层、业务层、持久层”就够了。
还有一个特别容易被忽略的点:Mapper 接口与 XML 文件必须同名且同包(或在 resources 下同路径)。XML 里定义 namespace 必须写接口的全限定名。无数人启动项目时报 Invalid bound statement (not found),基本都是这三个地方没对应上。
4.2 SSM整合配置细节
SSM 整合最怕的就是配置文件写得不对,下面给你一份极简且能跑通的配置框架。
applicationContext.xml(Spring 核心配置):
<context:component-scan base-package="com.example.vehicle"> <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/> </context:component-scan> <context:property-placeholder location="classpath:db.properties"/> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="mapperLocations" value="classpath:mapper/*.xml"/> <!-- 驼峰映射 --> <property name="configuration"> <bean class="org.apache.ibatis.session.Configuration"> <property name="mapUnderscoreToCamelCase" value="true"/> </bean> </property> </bean> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.example.vehicle.dao"/> </bean> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <tx:annotation-driven transaction-manager="transactionManager"/>spring-mvc.xml:
<context:component-scan base-package="com.example.vehicle.controller"/> <mvc:annotation-driven/> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/views/"/> <property name="suffix" value=".jsp"/> </bean>这里有两个容易踩坑的细节:
mapUnderscoreToCamelCase 必须开启。数据库字段是 plate_number,Java 实体属性是 plateNumber,如果不开启驼峰映射,查询结果里这个字段就是 null,而且程序不会报错,属于隐藏 BUG。
component-scan 要去除 Controller,否则 Spring 容器和 SpringMVC 容器重复管理 Controller 会引发奇怪的代理问题。这句话你在论文或答辩时能顺口说出来,老练程度一眼就能看出来。
4.3 环境版本与部署步骤
SSM 项目对版本兼容性比较敏感,我用过非常稳的一套组合:
| 组件 | 推荐版本 |
|---|---|
| JDK | 1.8 |
| Maven | 3.6.x |
| MySQL | 5.7.x 或 8.0.x |
| Tomcat | 8.5.x 或 9.0.x |
| Spring | 5.2.x |
| MyBatis | 3.5.x |
| MyBatis-Spring | 2.0.x |
| Druid | 1.2.x |
| PageHelper | 5.3.x |
| JSTL | 1.2 |
如果你是 MySQL 8.0,连接驱动必须是 mysql-connector-java 8.0.x,同时 jdbc.url 要加 serverTimezone=Asia/Shanghai,否则会报时区错误。如果是 MySQL 5.7,用 5.1.49 驱动就挺稳的,不用折腾。
部署步骤整体梳理如下:
- 用 IDEA 打开项目,配置好本地 Maven 仓库设置,等待依赖下载完成。
- 修改 src/main/resources/db.properties 里的数据库连接信息,包括 URL、用户名、密码。
- 用 Navicat 或者命令行创建数据库,比如 vehicle_db,字符集选 utf8mb4。
- 执行项目里带有的 init.sql 脚本,初始化表结构和测试数据。
- 配置 Tomcat:如果用的是 Tomcat 8.5 以上版本,注意 Deployment 里要把 Application context 配置成 /vehicle 或者直接留 /。
- 启动 Tomcat,浏览器访问类似 http://localhost:8080/vehicle/login 这样的地址。
- 用管理员账号登录,过一遍车辆 -> 驾驶员 -> 出车 -> 归还 -> 维保 -> 统计的完整流程。
- 最后一定要自己走一遍删除重来:删库、重建、重新导入数据、重新启动。这个流程走通了,答辩现场你就是最稳的那一个。
5. 论文撰写与答辩准备
5.1 论文核心章节结构
很多同学代码跑起来了,但论文写不出来。其实这篇论文的套路化结构是非常明确的,我按顺序给你列出来:
摘要 + 关键词 第1章 绪论:背景、意义、国内外研究现状、论文结构 第2章 相关技术介绍:Spring、SpringMVC、MyBatis、MySQL、ECharts 第3章 需求分析:可行性分析、功能需求、用例分析、非功能需求 第4章 系统设计:总体架构、功能模块设计、流程设计、数据库设计 第5章 系统实现:环境说明、各功能模块实现截图 + 关键代码说明 第6章 系统测试:测试环境、功能测试用例、结论
每章必须要写够内容,不能只贴代码截图。我写论文的经验是:需求分析章节重点描述系统中的角色和业务流,把用例图画清楚;系统设计章节重点放 ER 图和数据库设计说明;系统实现章节不要满屏贴代码,每段截图配合几句业务说明和核心实现思路,让老师看得出你理解这个功能的逻辑。
5.2 用例图、ER图与流程图的绘制
画图工具我不推荐还去专门学什么复杂画图软件,直接用 ProcessOn 或者 Draw.io 就够了。系统中要准备的图包括:
- 系统总体用例图,把管理员、驾驶员两类角色各自的用例画出来。
- 车辆出车归还流程图,用简单的矩形和箭头表示,从“选择车辆”开始到“更新车辆状态”结束。
- ER 图,把 tb_user、tb_vehicle、tb_driver、tb_vehicle_log、tb_maintenance 这五张表的关系画出来,标注上主键外键。
- 系统架构图,画三层架构的分层模型。
这些图本身就是论文里的大分项,图的质量上去了,论文的观感完全不一样。
5.3 答辩高频问题整理
答辩前我建议把下面这些问题准备一遍,不只是背答案,而是要清楚原理:
- 项目用的什么架构?为什么用 SSM 而不是 Spring Boot?你只要回答“为了更清晰地体现分层架构和Spring两大核心思想”或者“毕设选型基于课程所学”都合理,不要贬低其中任何一个。
- MyBatis 中 #{} 和 ${} 的区别?这个基本必问,直接回答预编译和字符串拼接的区别,再说出 SQL 注入风险。
- 事务是如何控制的?回答用 @Transactional 标注 Service 层方法,由 Spring 的声明式事务管理,举例说明出车登记需要同时插入流水和更新状态。
- 车辆状态不同步怎么处理?如果你把事务写好,这个问题你就可以说“我已经通过事务保证了两步操作的一致性”。
- 系统如何防止表单重复提交?可以利用 Session 令牌机制,在进入页面时生成 token、提交后删除 token。
- 项目中遇到过什么困难?挑一个你真实解决过的问题讲,比如 MySQL 时区报错、Mapper XML 没扫描到、中文乱码,都会显得有说服力。
答辩时千万不要照着 PPT 念代码,重点讲清楚“我做了什么”“我为什么这么做”“遇到问题怎么排查的”。技术深度不用多,但逻辑链必须完整。
6. 常见坑位与排错实录
6.1 环境与部署类问题
项目跑不起来的坑,十有八九出在环境上。最大的坑就是 Tomcat 和 Spring 版本兼容性。Spring 5.x 最低要求 JDK 8 以上,但你如果用 Tomcat 10 跑,会遇到 javax.servlet 包名不存在的报错——因为 Tomcat 10 改了包名。所以更推荐用 Tomcat 8.5 或者 9.0,Spring 5.2.x 在它们上面跑得非常稳。
还有一类经典问题是中文乱码。现象是数据库插入中文正常,但页面显示问号或者乱码。排查路线就是:数据库连接 URL 加 characterEncoding=utf8 -> MySQL 表字符集设置 utf8mb4 -> 项目文件编码全部 UTF-8 -> JSP 页面头加 pageEncoding="UTF-8" -> 必要时在 Tomcat 的 server.xml 添加 URIEncoding="UTF-8"。一层层排查,总会解决。
启动时如果遇到“端口被占用”,在 IDEA 控制台能看到明确的 8080 端口提示。在命令行执行 netstat -ano | findstr 8080 查看占用进程,然后结束掉它,或者直接把项目端口改掉也可以。
6.2 数据与SQL类问题
这一类里我见得最多的就是“实体类字段查询出来为 null,但数据库有值”。前面已经说了,就是没开驼峰映射,或者是 Mapper XML 的 resultType 写成了 HashMap。打开 mybatis configuration 设置 mapUnderscoreToCamelCase=true 基本能解决。
再一个是外键删除受限问题。因为车辆被出入记录、维修记录引用了,直接删除车辆会失败或者产生孤儿数据。我的处理方式是:有业务记录的车辆不做物理删除,而是加一个逻辑删除状态(is_deleted 或 status=3 已报废),这样既保留了数据完整性,又不会让删除功能报错。答辩被问到“删除车辆时为什么提示不可删除”,你正好可以引出“数据完整性约束”的设计点。
还有 MyBatis 里时间段查询的问题。如果要做日期范围搜索,参数建议用 String 类型加上前端 yyyy-MM-dd 格式,在 SQL 里用 >= 和 < 结合 DATE_ADD 处理,尽量不要用 BETWEEN,因为边界判断容易出误差。举个例子:
AND create_time >= #{startTime} AND create_time < DATE_ADD(#{endTime}, INTERVAL 1 DAY)这样就能把结束日期当天的时间全部包含进去。
6.3 业务逻辑与操作类问题
这个项目的业务核心是状态流转,“车辆当前状态”这个数据极其容易被人为改乱。比如在数据库直接改了 status,但出入记录没删,就会出现一辆车“没有归还记录却显示在场”的矛盾情况。所以我自己做的时候,把所有状态的变化都集中在 Service 层处理,禁止在 Controller 里直接改状态字段,这样状态变更的入口是唯一的,排查问题也好查。
表单重复提交也是一个隐蔽问题。出车登记如果用户快速点了两次提交按钮,可能出现车辆被连续出车两次的情况,程序还不会报错。解决办法有两个层面:第一是前端把按钮在提交成功后置灰,第二是后端用 Session 令牌机制做防重。毕设做到第二种防御手段基本就属于亮点功能了。
还有一个操作层面的坑,就是 PageHelper 的线程安全问题。PageHelper.startPage() 调用后必须紧跟一条 Mapper 查询,如果中间又执行了别的查询,分页会作用到错误的 SQL 上。我自己习惯的写法是把 startPage 放在 Controller 方法里、紧挨着调用 Service 查询那一行,这样最安全。
最后再分享几条实操经验
这个项目我做过几个版本之后,最大的感受是:这个题目真正拉开差距的地方,在于数据设计是否合理、状态流转是否严谨、事务控制是否到位,而不在于功能数量多不多。有的同学堆了一堆没用到的模块,一被追问就露馅;有的同学虽然只做了五个表六个页面,但把业务逻辑讲得清清楚楚,反而拿了高分。所以我建议你动手之前,先把表结构和状态流转彻底想明白,再去看源码,最后自己动手把它重新“默写”一遍。
另外,拿到一份完整源码之后,不要急着直接拿去交。自己从头到尾部署一次,然后把几个核心表的数据手动改一改,插入几条异常数据看看系统的反应。这样做的好处太多了:你会在排错过程中理解代码里的数据流,你会发现很多细节问题,你还能在答辩时非常自然地说出“这个问题我遇到过,当时是怎么排查的”。这些经验,老师是能听出来的。
千万不要只停留在“能跑”这个层面,一定要让自己能讲清楚。项目是你的,论文是你的,答辩是你自己面对的。把这几天的时间花在吃透项目上,远远比找什么捷径要靠谱得多。如果你正在准备类似题目,希望这篇内容能帮你少走点弯路。