Spring Boot实现的公司用车管理系统:从数据库设计到答辩避坑全流程
简介基于Java的南昌航空大学软件学院21级Web大作业公司用车管理系统源码定位于高校Web课程设计与中小型公司车辆调度场景覆盖用车申请、司机信息、审批派车、后台管理等常见业务模块适合Java Web学习者对照完整项目结构进行综合实训。压缩包共175个文件容量约914KB包含66个Java源码、30个HTML页面、19个JavaScript脚本、16个CSS样式表以及XML配置、JSP页面、SQL脚本、数据库视图代码和Markdown文档等从后端逻辑到前端展示与数据库设计均有体现。目前已有343人学习下载。资源附带了接口设计、SQL语句、数据库视图及新增表等资料能帮助读者理解车辆管理系统的表关系与核心流程也可作为课程设计或大作业的参考蓝本。1. 公司用车管理系统这个 Java Web 大作业到底在考察什么公司用车管理系统是 Java Web 课程设计里的一个经典选题对软件学院的学生来说不用为选题发愁对老师来说它恰好覆盖了 Java、Web 前端、数据库设计和业务流程四个考察点。这类系统的正反馈很快——只要把申请、审批、派车、归还四个动作在页面上串成一条线再配上几张核心表演示效果就能立得住。它适合学期中间布置的一轮 web 大作业也能延续成一个毕业设计加费用结算、加司机排班、加地图轨迹都不会动到骨架。接下来我就按一个能跑通全流程的方案来写从工程初始化讲到答辩前的最后检查新手能跟着动手老手直接翻边界和踩坑记录。2. Spring Boot 还是 SSM先把可运行的工程骨架立起来2.1 选型理由演示那天谁更不容易出岔子就选谁公司用车管理系统本质上就是几张表的增删改查加一条审批流。用 Java 做这类管理系统最主流的两个选择是 SSM 和 Spring Boot。SSM 是 Spring Spring MVC MyBatis 的组合教材老、知识点多面试和答辩时能讲的东西也多Spring Boot 做的事是把配置自动化内置 Tomcat一个命令就能启动。我的判断标准很简单这是你第一次完整做 Web 项目优先 Spring Boot。大作业的评分重点不是框架有多深而是功能演示能不能流畅跑完、代码能不能在老师电脑上启动Spring Boot 把最容易出错的容器配置和包版本冲突压缩到了最低。对比维度SSMSpring Boot启动方式需要外部 Tomcat 并手动部署 war 包内置 Tomcatmain 方法直接跑配置量XML 注解混用配置项多application.yml 一个文件管大部分配置学习曲线偏陡适合对原理有强烈兴趣平缓先跑起来再回看原理答辩风险容易在“为什么这么配”上被追问默认配置多原理更容易被追问数据访问层我建议直接选 MyBatis 或 MyBatis-Plus。MyBatis 的 SQL 是手写的运行日志里能直接看到查了哪张表、传了什么参数答辩被问“这条数据怎么查出来的”时好解释MyBatis-Plus 再省掉单表 CRUD 的重复代码写起来更快。JDK 建议用 8 或 11不要图新鲜上太高版本否则可能出现依赖兼容问题而这个系统和 JDK 版本没什么关系。2.2 初始化最小工程pom 依赖、配置文件和启动类打开 IDEA 新建 Spring Initializr 工程先把核心依赖贴进 pom.xml。版本以你自己能拉到的 Spring Boot 版本为准下面这个组合是我常用的基线parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency /dependencies逻辑说明spring-boot-starter-web提供 Spring MVC 和内嵌 Tomcat是整个 Web 能力的基础mybatis-plus-boot-starter在 MyBatis 之上封装了单表 CRUD省去大部分 mapper XMLmysql-connector-j是 MySQL 的 JDBC 驱动。因为父工程统管了依赖版本所以 web 和 mysql 依赖不需要写版本号。数据库连接信息写在src/main/resources/application.yml里server: port: 8081 spring: datasource: url: jdbc:mysql://localhost:3306/car_ms?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl参数说明server.port我直接改成 8081因为 8080 太容易被本机其他程序占用serverTimezoneAsia/Shanghai是 MySQL 8 的必填项不写会在启动时看到一个让人摸不着头脑的 timezone 报错map-underscore-to-camel-case让数据库的plate_no自动映射成 Java 里的plateNo这个配置后面还会救你一次。启动类放在com.example.carmanage包下位置不要乱放package com.example.carmanage; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; SpringBootApplication public class CarManageApplication { public static void main(String[] args) { SpringApplication.run(CarManageApplication.class, args); } }SpringBootApplication是一个组合注解包含了组件扫描、自动配置和配置属性绑定它所在包的层级决定了 Controller、Service 能被扫描到。如果启动类放错包后面会出现“接口存在但 404”的怪现象。2.3 源码目录结构静态资源路径不是玄学一套能交的源码目录至少应该是这样carmanage/ ├─ src/main/java/com/example/carmanage/ │ ├─ controller/ │ ├─ service/ │ ├─ mapper/ │ ├─ entity/ │ └─ config/ ├─ src/main/resources/ │ ├─ mapper/ │ ├─ static/ │ └─ application.yml这里有个大作业里出现频率极高的坑mapper XML 文件放到了resources/mapper下但是没在配置里告诉 MyBatis-Plus 去哪个目录找于是启动时报Invalid bound statement。解决方式是加一行配置mybatis-plus: mapper-locations: classpath:mapper/*.xml前端静态资源放在static或templates下。用 JSP 的话需要额外引入 Tomcat JSP 依赖用 Thymeleaf 或直接把 HTML 丢在static里更省事。页面打不开先看控制台是不是有 404再检查 Controller 的RequestMapping是不是和前端请求路径差了一个字母。资源路径这类问题大多不是玄学而是路径拼写不一致。3. 数据库与审批流程建模用五张表和状态机把业务立住3.1 五张核心表谁在开哪辆车去干什么公司用车管理系统最少需要五张表用户表、车辆表、申请单表、审批记录表、还车记录表。用户表和车辆表是基础数据申请单表是业务核心审批记录和还车记录各撑住一条流程链路。建表 SQL 我习惯直接把字符集定为 utf8mb4避免后面中文乱码折腾一轮。CREATE TABLE user ( id int NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL, password varchar(200) NOT NULL, real_name varchar(20) NOT NULL, department varchar(50) DEFAULT NULL, role varchar(20) NOT NULL DEFAULT USER, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE vehicle ( id int NOT NULL AUTO_INCREMENT, plate_no varchar(20) NOT NULL, model varchar(50) DEFAULT NULL, status varchar(20) NOT NULL DEFAULT FREE, PRIMARY KEY (id), UNIQUE KEY uk_plate_no (plate_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE car_order ( id int NOT NULL AUTO_INCREMENT, order_no varchar(30) NOT NULL, applicant_id int NOT NULL, vehicle_id int DEFAULT NULL, use_date date NOT NULL, start_time datetime NOT NULL, end_time datetime NOT NULL, purpose varchar(200) DEFAULT NULL, passengers int DEFAULT 1, status varchar(20) NOT NULL DEFAULT PENDING, remark varchar(200) DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;参数说明role字段用字符串存可读性好取值控制在USER、APPROVER、ADMIN三个值车辆status用FREE、IN_USE、MAINTENANCE区分订单status是整条流程的核心取值规则在下一节展开。注意order是 SQL 关键字直接当表名会把 MySQL 惹恼所以叫car_order这是我能想到最稳妥的命名方式。审批记录表和还车记录表字段少但必须独立建表审批记录至少要有id、order_id、approver_id、opinion、create_time还车记录至少要有id、order_id、return_time、end_mileage、operator_id。独立建表的理由是保留历史轨迹否则审批人想改意见时直接把原记录盖掉答辩时老师问“这条审批是谁做的”你拿不出证据。3.2 审批流程用状态机建模别用两个布尔列很多大作业喜欢在申请单上放approved和dispatched两个布尔字段这是最容易把业务写乱的做法。两个布尔字段会出现“既通过又未派车”“未通过却派车”这类非法组合而且后面加一个“归还”状态时就只能继续加字段。正确做法是把订单状态定义成一个枚举public enum OrderStatus { PENDING(待审批), APPROVED(审批通过), DISPATCHED(已派车), COMPLETED(已归还), REJECTED(已拒绝), CANCELLED(已取消); private final String desc; OrderStatus(String desc) { this.desc desc; } public String getDesc() { return desc; } }状态机的好处是每个时刻只有一个状态流转关系清晰待审批可以变成审批通过或已拒绝审批通过可以变成已派车已派车只能变成已归还。代码里更新状态时要带上旧状态条件防止并发下重复审批int rows carOrderMapper.updateStatus(orderId, OrderStatus.PENDING, OrderStatus.APPROVED); if (rows 0) { throw new BizException(当前状态不允许审批请刷新后重试); }对应的 SQL 是UPDATE car_order SET status #{newStatus} WHERE id #{id} AND status #{oldStatus}逻辑说明update返回的rows是影响行数。如果订单已经不是待审批状态WHERE条件不成立影响行数是 0说明有人抢先改过状态这时必须让用户刷新页面而不是继续操作。这是状态机加乐观锁最简单的实现大作业写到这一步已经比“直接 set status”高明一个档次。3.3 车辆占用冲突检查边界条件决定演示质量用车申请最容易被老师问到的场景是同一辆车同一天被两个人申请第二个人的申请应该提示“该时段车辆已被占用”。实现这个判断只需要一条带重叠区间判断的 SQLSELECT COUNT(*) FROM car_order WHERE vehicle_id #{vehicleId} AND status IN (APPROVED, DISPATCHED) AND start_time #{endTime} AND end_time #{startTime}逻辑说明两条时间段[start1, end1]和[start2, end2]重叠的条件是start1 end2 AND end1 start2。把它翻译到 SQL 里就是传入的新申请的startTime和endTime与库里已通过订单比较。COUNT(*)大于 0 就说明有重叠直接拒绝。参数说明比较用的是严格小于和严格大于。这样处理的好处是前一条申请到 18:00 结束新申请 18:00 开始两边不冲突可以顺利提交。如果写成和这种边界接续的申请会被误杀演示时反而显得逻辑不对。另外这个查询别漏掉status IN条件否则已拒绝的订单也会占着车辆那才是真的踩坑。4. 角色权限与页面联调把员工、审批人、管理员三个入口串成完整链路4.1 三种角色与最小权限模型公司用车管理系统至少要分三种角色普通员工提交申请审批人处理申请管理员维护车辆和用户。权限控制用 Spring MVC 的拦截器就够不要为了一个大作业引入 Spring Security配置复杂度会吃掉你大量时间。一个登录拦截器加一个角色判断能覆盖 80% 的权限场景。Configuration public class WebConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns(/**) .excludePathPatterns(/login, /api/login, /css/**, /js/**, /images/**); } }public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException { Object userId request.getSession().getAttribute(userId); if (userId null) { response.sendRedirect(/login); return false; } return true; } }逻辑说明拦截器在请求进入 Controller 之前执行检查 Session 里有没有userId。没有就跳回登录页有就放行。excludePathPatterns里放行的/login是登录接口/css/**和/js/**是静态资源漏掉这两个前缀会导致页面样式全挂。角色级别的控制我一般放在需要管理员权限的接口里手动从 Session 取出role判断一次。写一个RequireRole注解加 AOP 虽然更“高级”但功能就三个角色时拦截器加一次判断的代码反而更好读、更好讲。真正动手时把角色判断抽到一个工具方法里别在每个 Controller 里复制粘贴三段同样的逻辑。4.2 页面清单最小可用方案至少需要七个页面前端技术选型上如果不想碰前后端分离的跨域问题直接用 Thymeleaf 或 JSP 由后端渲染如果已经掌握 Vue单独起一个前端项目也行但大作业时间紧我建议优先后端模板渲染。至少需要的页面如下页面路径角色功能登录页/login所有人用户名密码登录车辆列表/vehicle/list所有人只读查看车辆空闲状态提交申请/order/add员工选择车辆、填写时间段和事由我的申请/order/my员工查看自己提交的申请与状态审批中心/order/approve审批人审批或驳回待处理申请还车登记/order/return管理员登记归还时间与里程用户管理/user/list管理员维护用户和角色页面不用多能把这条链路走完就是完整大作业。很多同学喜欢加一堆花哨页面结果核心流程没打通答辩时反而暴露短板。先把这七个页面做扎实有余力再扩展统计报表。4.3 前后端联调最容易踩的三个坑404、时间格式、响应结构页面提交申请时前端拿到的时间格式和后端LocalDateTime对不上是出现频率最高的联调问题。用fetch提交 JSON 时时间字段要传成字符串并且格式要能被后端解析const res await fetch(/api/order/add, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ vehicleId: vehicleId, purpose: purpose, startTime: startTime :00, endTime: endTime :00 }) }); const result await res.json(); if (result.code 200) { location.href /order/my; } else { alert(result.message); }逻辑说明startTime来自input typedatetime-local值是2026-03-20T14:30这种格式没有秒。后端用LocalDateTime解析时要求至少到秒所以提交前拼上:00这是最省事的处理方式。后端 Controller 参数需要加RequestBody接收 JSON 对象。后端返回结构我建议统一成一个 Result 对象包含code、message、data三个字段。作用是前端只判断code是不是 200不用每个接口单独处理异常字符串联调时心智负担小很多。404 的排查顺序是先看控制台有没有报错再看请求 URL 和RequestMapping是否完全一致最后看启动类包路径是不是包住了 Controller。500 则先看日志里 SQL 语句和异常栈九成是字段名拼错或参数没传进来。5. 大作业翻车最多的地方运行与答辩避坑记录做完一轮大作业真正花时间的往往不是功能开发而是处理一些让人看不懂的报错。这类问题的共同点是本地能跑换个环境或者重启一次就崩甚至代码没动过也出问题。我把课程设计中重复率最高的五个问题列出来按现象、原因、解决的顺序写。这些都是别人交过学费换来的经验照着排查能省下半天。5.1 端口被占用导致启动失败现象点启动后控制台红字报错核心信息是Port 8080 was already in use页面完全打不开。原因本机有其他程序占用了 8080或者之前跑过的项目进程没有真正停止。很多同学电脑上同时装了多个中间件8080 是它们的默认端口冲突概率很高。解决我一般直接改端口把server.port改成 8081副作用最小。如果想保留 8080就得找到占用进程结束掉。Windows 上先查再杀netstat -ano | findstr :8080 taskkill /pid 进程号 /f参数说明第一条命令列出占用 8080 的进程 PID第二条按 PID 强制结束进程。macOS 或 Linux 用lsof -i:8080查占用再用kill -9 PID结束。改端口是一劳永逸的办法至少能保证现场演示时不受本机其他服务干扰。5.2 数据库连不上驱动类、时区、密码三个雷现象启动时一直报Communications link failure或者Access denied for user检查数据源配置感觉一点问题都没有。原因三个高频原因叠在一起。MySQL 5 和 MySQL 8 的驱动类名不一样8 还得在连接串里指定时区密码里带#或的特殊字符会被配置文件解析错yml 缩进错了导致配置项根本没生效。解决驱动类统一用com.mysql.cj.jdbc.Driver连接串拼上serverTimezoneAsia/Shanghai和useSSLfalse。密码带特殊字符时先在 IDEA 数据库面板里用同一套账号密码连一次能连上就说明账号没问题问题出在 yml 解析上。注意连不上时先打开 MySQL 客户端工具试一遍能快速排除“密码错误”和“服务没启动”这两个最基础的原因别盯着配置看半天。5.3 中文乱码从页面到数据库全线崩溃现象表单里输入的中文提交后列表页显示成问号控制台打印的 SQL 里中文也是乱码。原因建表时字符集不是 utf8mb4连接串没加characterEncodingutf8IDEA 文件编码不是 UTF-8三个环节任何一个断了都会乱码。更隐蔽的是MySQL 连接串里没指定编码时驱动会拿服务器默认字符集去读结果就是各种问号。解决三个地方同时改。建表 SQL 统一写成CHARSETutf8mb4连接串加characterEncodingutf8IDEA 右下角把文件编码切成 UTF-8。改完之后重启服务再测三个环节缺一个都可能复发。5.4 查询出来对象全是 null驼峰映射没开现象车辆列表显示出来了id有值但plateNo、model这些字段全是 null。页面代码看着没问题数据库里也有值就是显示不出来。原因数据库列名是plate_noJava 属性是plateNoMyBatis 默认不会自动把下划线转成驼峰。这种问题特别隐蔽因为 SQL 执行没报错只是结果集映射不上。解决在application.yml里开启驼峰映射mybatis-plus: configuration: map-underscore-to-camel-case: true如果已经开启还是 null就看 XML 里是不是写了resultMapresultMap里的column属性要和数据库列名完全一致。改完配置必须重启Mapper 映射是在启动阶段构建的。5.5 演示现场静态资源打不开测试数据一锅粥现象答辩时页面样式全丢表格里有几十条乱糟糟的测试数据。老师点开一条问“这条记录怎么回事”你答不上来整个演示就垮在这了。原因写页面时为了赶时间样式引用的是绝对路径Controller 又拦截了/css/**这两种情况叠加后样式必然丢失。测试数据没清理就直接进了演示环境越紧张越解释不清脏数据。解决检查addInterceptors的excludePathPatterns有没有放行/css/**、/js/**页面里样式引用改成相对路径或加模板前缀。数据方面单独建一套演示账号只保留三辆车和两条干净的申请单其余数据全部删掉。演示前把演示账号密码写在一张纸上比现场回忆靠谱得多。6. 答辩前一晚的最后检查演示脚本比功能更重要功能都做完后花半小时把演示脚本过一遍比继续加功能更能提高答辩分数。我的演示顺序是固定的先用员工账号登录提交一条明天出行的申请切到审批人账号在审批中心看到这条申请通过它切回员工账号看到状态变成审批通过再看车辆列表那辆车状态从空闲变成占用最后用管理员账号还车登记回填里程车辆恢复空闲。整个链路五分钟跑完正好覆盖系统所有核心功能。为了确保现场不翻车我习惯在答辩前一晚把数据库重新导入一次mysql -uroot -p car_ms car_ms_backup.sql逻辑说明这条命令从备份文件重建数据库能清掉开发期间产生的垃圾测试数据。备份文件放在桌面命名带日期现场万一数据被弄乱了一分钟就能还原。重新导入后用演示账号把脚本完整走一遍确认每个按钮都还能点。答辩时老师喜欢问的不是“你写了多少行代码”而是“为什么这么设计”。提前把三个问题想明白为什么用状态机而不是布尔字段为什么车辆冲突要查重叠时间段为什么拦截器要放行静态资源。这三个回答好了比多写一个页面有用得多。Spring Boot 和 Spring MVC 的关系这类“八股”也抽十分钟背一遍被问到的时候别一个字都说不出来。我一般会克制住答辩前一晚继续改代码的冲动只重启一次服务不再动任何功能。最后把演示数据保持在一个干净的状态把日志窗口清空关掉不相关的软件。这些细节看起来很琐碎但它们决定了演示那几分钟你是否从容。希望帮到你。本文还有配套的精品资源点击获取