车辆管理系统这个项目,说实话在Java全栈里已经算是一个很经典的业务系统了。它的定位很简单:把公司内部的车、司机、用车申请、维修保养这些事,从纸质台账和微信群里捞出来,变成一套可以查、可以审批、可以统计的线上流程。我这次用Spring Boot + Vue + MySQL + MyBatis这套组合完整做了一遍,前端负责页面交互,后端提供REST接口,车辆信息、司机档案、申请审批、维保记录这些模块全部打通。不管你是准备拿它做毕业设计,还是刚学完Java和Vue想找一个完整项目练手,这套前后端分离的代码结构都值得参考。文章不会只甩源码,我会把数据表怎么建、接口怎么分层、前端怎么对接、哪些地方容易踩坑,一条条捋清楚。
1. 项目定位与整体设计思路
1.1 车辆管理系统到底要解决什么问题
很多中小公司的车辆管理其实一直处于“半失控”状态。车辆档案可能躺在行政部的Excel里,谁借了车、车什么时候保养、油费花了多少,基本靠问人或者翻聊天记录。这套系统的第一个价值,就是把“人找信息”变成“系统找信息”,所有车辆档案、司机资料、申请记录全部落到数据库里,随时能查。
第二个痛点是流程不透明。以前员工要用车,得先发消息问行政有没有车,行政再去查台账、找人协调,中间很容易漏信息。车辆管理系统用“申请—审批—派车—归还”这条状态线把用车过程固定下来,员工提交申请后能看到当前是待审核还是已经通过,管理员在后台一眼就能看到今天哪些车被借走了,哪些车还空闲,整个人力协调成本会明显降下来。
第三个痛点是数据统计困难。车辆每个月油费多少、维修花了多少、哪台车出险率高,这些如果靠手工登记,月底汇总能让人头大。系统里专门设计了维保记录、加油记录和统计看板,费用和时间都能按车、按月筛出来,管理者做决策也更有依据。所以这个项目虽然叫车辆管理系统,本质上是在解决企业的资产数字化和流程规范化问题。
1.2 技术选型:为什么是Spring Boot + Vue + MyBatis
这套组合在今天已经不算新鲜,但胜在实用。
先说后端。Spring Boot最大的好处是降低了工程搭建成本,不需要像传统SSH那样写一堆XML配置,内置Tomcat,一个main方法就能启动整个Web服务。配合Spring官方生态,权限校验、参数校验、事务管理这些能力都是现成的,适合快速迭代。而且对刚开始学项目的同学来说,Spring Boot的社区资料非常全,遇到问题一搜就能找到解决方案。
MyBatis在数据库操作层很有优势。它不是完全自动化的ORM,SQL是开发自己写的,所以你能精确控制每一条查询语句,遇到多表关联、动态条件、批量更新这类复杂需求时,写法比Hibernate更直观。特别是车辆管理这种业务,列表页经常要按车牌号、状态、车辆类型做组合筛选,用MyBatis的where和if标签写动态SQL,比拼字符串优雅太多。
前端用Vue是因为它的学习曲线相对平缓,组件化开发特别适合后台管理系统。一个车辆列表页,可以拆成搜索栏组件、表格组件、弹窗表单组件,哪里改动就动哪里,不会牵一发而动全身。再配合Element UI这种成熟组件库,表格、分页、日期选择器、表单校验就是几行代码的事,开发效率非常高。
用生活里的话说,选技术栈就像选装修队。Spring Boot是给整个房子搭水电的,把基础环境通好;MyBatis像是具体的泥瓦工,每个砖头怎么放都由你说了算;Vue则是负责软装和界面设计的,让住在里面的人用得舒服。三者各管一段,配合起来条理清晰。
1.3 功能模块总览
整套系统按角色主要分成两类使用端:普通员工和管理员。普通员工能看到车辆信息、提交用车申请、查看自己的申请记录;管理员除此之外还能维护车辆档案、管理司机、审核申请、登记维保和加油记录。
具体功能模块可以这么拆:
- 登录与用户管理:用户名密码登录,简单RBAC角色区分,后台统一管理用户账号。
- 车辆档案管理:新增、编辑、删除车辆信息,维护车牌号、品牌、车型、购买日期、当前里程和车辆状态。
- 司机信息管理:司机姓名、身份证、电话、驾驶证号的增删改查。
- 用车申请与审批:员工创建申请单,选择车辆和时间段,填写事由;管理员审核通过后,车辆状态联动更新。
- 维修保养管理:登记维修项目、费用、日期,可查询每台车的维保历史。
- 加油管理:记录加油量、费用、时间,支持按月统计。
- 数据看板:首页展示车辆总数、在途车辆数、本月用车申请量、本周维保费用等统计信息。
模块之间不是孤立的,比如申请审批通过后不能只是把申请单状态改成“已通过”,还要把对应车辆的状态改为“使用中”。这些跨表联动的业务逻辑,既是项目的难点,也是面试时能拿出来讲的亮点。
2. 数据库设计:表结构拆解
2.1 核心数据表与字段说明
数据库设计是这类管理系统的基础。我建议先画清楚实体关系,再动手建表。这套项目里最重要的几个实体是用户、车辆、司机、申请单、维保记录、加油记录。下面是我实际建表时的核心字段设计。
用户表sys_user:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键,自增 |
| username | varchar(50) | 登录账号,唯一 |
| password | varchar(100) | 加密后的密码 |
| real_name | varchar(50) | 真实姓名 |
| role | tinyint | 角色:1管理员,2普通员工 |
| create_time | datetime | 创建时间 |
| status | tinyint | 账号状态:1启用,0禁用 |
车辆表vehicle:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| plate_no | varchar(20) | 车牌号 |
| vehicle_type | varchar(20) | 车辆类型,如轿车、SUV |
| brand | varchar(50) | 品牌型号 |
| status | tinyint | 状态:1空闲,2使用中,3维修中,4报废 |
| buy_date | date | 购买日期 |
| current_mileage | decimal(10,2) | 当前里程(公里) |
| driver_id | bigint | 当前绑定司机,逻辑外键 |
| remark | varchar(255) | 备注 |
| create_time | datetime | 创建时间 |
司机表driver_info:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| name | varchar(50) | 司机姓名 |
| id_card | varchar(20) | 身份证号 |
| phone | varchar(20) | 联系电话 |
| driver_no | varchar(30) | 驾驶证号 |
| status | tinyint | 1可用,0停用 |
用车申请表apply_record:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| user_id | bigint | 申请人ID |
| vehicle_id | bigint | 车辆ID |
| apply_reason | varchar(255) | 申请事由 |
| start_time | datetime | 预计用车开始时间 |
| end_time | datetime | 预计归还时间 |
| status | tinyint | 状态:1待审核,2已通过,3已驳回,4已结束 |
| auditor_id | bigint | 审核人ID |
| audit_time | datetime | 审核时间 |
| create_time | datetime | 申请时间 |
维保记录表maintenance_record:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| vehicle_id | bigint | 车辆ID |
| maintenance_type | varchar(50) | 维修/保养类型 |
| cost | decimal(10,2) | 费用 |
| maintenance_date | date | 维保日期 |
| remark | varchar(255) | 备注 |
加油记录表fuel_record结构和维保表类似,加上油量油费字段。这里不做过多展开。
id字段统一用bigint自增主键,能保证数据量大了以后不容易撞主键。create_time这类时间字段全部用datetime,不搞时间戳,查数据时一眼能看懂。角色、状态这些枚举值都用tinyint,后端定义好常量或枚举类,数据库里只存数字,不存中文,这样存储小、查询快,也不会出现“空闲”和“空闭”这种手工登记时才会打的错别字。
2.2 索引和外键怎么取舍
数据库设计里有个容易被新手忽略的点:外键到底建不建?我在这类管理系统里倾向于不建物理外键,只保留逻辑外键,也就是在业务表里存关联表的ID,但不声明FOREIGN KEY约束。
原因很简单。物理外键在插入、更新时会触发数据库层面的完整性校验,单机小系统没问题,但一旦以后做读写分离或者微服务拆表,物理外键会成为严重制约。比如申请单里的vehicle_id如果声明了外键,想按月份归档旧申请单就很难操作。所以更常见的做法是:在vehicle_id、user_id、auditor_id这些字段上建普通索引,用代码保证数据的逻辑正确。
索引要建的字段主要有三类:一是查询频繁的状态字段,比如vehicle.status、apply_record.status;二是关联字段,比如apply_record.vehicle_id、maintenance_record.vehicle_id;三是时间范围查询字段,比如apply_record.start_time、fuel_record.fuel_date。列表页最常见的搜索方式是“某段时间内,某状态的车辆申请”,这种场景建一个(status, start_time)的联合索引,效率比两个单列索引更高。
我在本地测试过,数据量在两三万条时,索引不索引区别不太明显,但一旦日志表和申请单积累到几十万条,没有索引的count查询可能从几十毫秒变成一两秒,体验差距非常大。所以建表的SQL里,别忘了把索引一起写进去,比如:
CREATE INDEX idx_apply_vehicle_status ON apply_record (vehicle_id, status); CREATE INDEX idx_apply_user_time ON apply_record (user_id, start_time);2.3 初始化数据和状态枚举
项目能不能跑起来,很大程度看初始化数据全不全。我会准备两个数据脚本:一个是schema.sql,负责建库建表;另一个是data.sql,插入管理员账号、几辆测试车辆、几个司机和几条模拟申请记录。这样项目拉下来以后,不用自己手动造数据,启动完直接就能登录体验。
状态枚举建议在后端建一个常量类统一管理,比如:
public class VehicleStatus { public static final int IDLE = 1; public static final int USED = 2; public static final int REPAIRING = 3; public static final int DISABLED = 4; }这样在写业务逻辑时,代码里不会散落各种魔法数字,别人接手时也能快速看懂状态含义。前端再对应维护一份状态和标签文字、颜色的映射,页面里就能把1渲染成“空闲”绿色标签,2渲染成“使用中”橙色标签。整个数据一致性就靠这套枚举串起来。
3. 后端实现:Spring Boot + MyBatis
3.1 项目包结构与分层
后端代码我习惯按controller -> service -> mapper三层来组织。刚开始写项目时很容易把所有逻辑都塞进Controller,后患无穷,接口一多就乱。推荐结构如下:
com.example.vehicle ├── VehicleApplication.java ├── controller │ ├── VehicleController.java │ ├── ApplyController.java │ └── AuthController.java ├── service │ ├── VehicleService.java │ └── impl │ ├── VehicleServiceImpl.java │ └── ApplyServiceImpl.java ├── mapper │ ├── VehicleMapper.java │ └── ApplyMapper.java ├── entity │ ├── Vehicle.java │ ├── ApplyRecord.java │ └── SysUser.java ├── dto │ ├── ApplyDTO.java │ └── LoginDTO.java ├── vo │ ├── ApplyVO.java │ └── VehicleVO.java ├── common │ ├── Result.java │ └── PageResult.java └── config ├── WebConfig.java └── MybatisPlusConfig.javaentity对应数据库表结构,dto用于接收前端入参,vo用于返回前端展示数据。为什么不直接用entity返回给前端?因为在列表页里,申请单返回时要显示申请人姓名、车辆车牌号,这些需要关联查询后拼装,用单独的ApplyVO正好把这些字段接住,不会污染实体类。
Result是统一返回结构,我常用的是code, message, data三个字段,Success的时候code为200,失败时根据业务返回4xx或5xx。前端axios拦截器只需要关注这个统一格式,处理起来非常省事。
3.2 核心配置和启动类
Spring Boot项目的核心配置全在application.yml里。这里要提醒一句:不要一味追求最新版本。Spring Boot 3.x虽然已经出来很久了,但它要求JDK 17以上,而且很多老版本的MyBatis Starter、PageHelper插件会出现兼容性问题。如果只是做车辆管理系统这种常规业务,Spring Boot 2.7.x配合JDK 8是最稳定的组合,生产环境也大量在用。
一个基础的数据源配置长这样:
server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/vehicle_manage?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.vehicle.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImplmap-underscore-to-camel-case必须开启,这样数据库里的plate_no才能自动映射到实体的plateNo字段,不然你必须在XML里手动给每个字段写as别名,非常麻烦。配置log-impl后控制台会打印SQL和参数,排查问题时能直接看到实际执行语句。
启动类只需要一个@SpringBootApplication注解加main方法。如果你用事务,记得在Service方法或类上加@Transactional。比如申请审核通过后,既要改申请单状态,又要改车辆状态,这两步必须在一个事务里,只要有一步失败就整体回滚。
3.3 车辆管理接口的后端逻辑
车辆的增删改查是这套系统最常规的接口。以列表查询为例,前端会传页码、每页条数、车牌号关键字、车辆状态、车辆类型这几个参数。后端不能写死SQL,得用MyBatis动态SQL拼接条件。
Mapper接口写法:
public interface VehicleMapper { List<VehicleVO> selectVehicleList(@Param("query") VehicleQueryDTO query); }对应的Mapper XML关键片段:
<select id="selectVehicleList" resultType="com.example.vehicle.vo.VehicleVO"> select v.id, v.plate_no, v.vehicle_type, v.brand, v.status, v.current_mileage, d.name as driver_name from vehicle v left join driver_info d on v.driver_id = d.id <where> <if test="query.plateNo != null and query.plateNo != ''"> and v.plate_no like concat('%', #{query.plateNo}, '%') </if> <if test="query.status != null"> and v.status = #{query.status} </if> <if test="query.vehicleType != null and query.vehicleType != ''"> and v.vehicle_type = #{query.vehicleType} </if> </where> order by v.create_time desc </select>这段SQL里left join driver_info是为了把司机的名字一起查出来,VehicleVO里既有车牌又有司机名,前端表格一列接一列直接显示。这里用left join而不是inner join,是因为车辆可能没有绑定司机,用inner join会把这类车辆过滤掉,不符合查询预期。
分页我用PageHelper插件,在Service里写:
PageHelper.startPage(query.getPageNum(), query.getPageSize()); List<VehicleVO> list = vehicleMapper.selectVehicleList(query); PageInfo<VehicleVO> pageInfo = new PageInfo<>(list);调用PageHelper.startPage之后的第一次查询会被自动拦截,拼上limit,业务代码不用手动传起始数,非常方便。但有个坑是startPage只对紧接着的一条查询生效,所以中间千万别夹其他查询语句,否则分页条件会被吞掉。
删除车辆时我建议用逻辑删除。给vehicle表加一个deleted字段,删除时执行update vehicle set deleted = 1 where id = #{id},查询时统一带and deleted = 0。这样用户误删数据还能恢复,面试时讲出来也算一个亮点。
3.4 用车申请状态流转
用车申请是这个系统里最体现业务逻辑的模块。状态流转建议用状态机思维设计,不要用户点了审核通过就随便改字段,整个流程应该是:员工创建申请单,状态变为“待审核”;管理员点击通过,状态变为“已通过”,同时把对应车辆status改为“使用中”;车辆归还后,管理员确认结束申请单,状态变为“已结束”,同时车辆status改回“空闲”。
核心Service逻辑大概是:
@Transactional public void auditApply(Long applyId, Integer auditResult) { ApplyRecord apply = applyMapper.selectById(applyId); if (apply == null) { throw new BusinessException("申请单不存在"); } if (!apply.getStatus().equals(ApplyStatus.WAIT)) { throw new BusinessException("只有待审核的申请才能审核"); } if (auditResult.equals(ApplyStatus.PASS)) { apply.setStatus(ApplyStatus.PASS); applyMapper.updateStatus(applyId, ApplyStatus.PASS); vehicleMapper.updateStatus(apply.getVehicleId(), VehicleStatus.USED); } else { apply.setStatus(ApplyStatus.REJECT); applyMapper.updateStatus(applyId, ApplyStatus.REJECT); } }这里最重要的是先校验当前状态,再执行更新。否则一张已经被审核过的单子再次点审核,状态会越改越乱。这就是为什么我不在Controller里直接写更新语句,而是放到Service层,通过一系列校验后再落库。
驳回申请时还可以让后端生成一条驳回原因,存到申请表的audit_remark字段里,前端审核弹窗提示,员工在申请记录里能看到结果,减少来回沟通。
3.5 MyBatis批量写操作和缓存避坑
车辆管理系统里最常见的批量操作是批量导入车辆,或者月底批量录入加油记录。MyBatis批量插入的标准做法是用foreach标签拼一条多值insert,比循环调用单条插入快一个数量级。
<insert id="batchInsertFuel"> insert into fuel_record (vehicle_id, amount, fuel_cost, fuel_date, create_time) values <foreach collection="list" item="item" separator=","> (#{item.vehicleId}, #{item.amount}, #{item.fuelCost}, #{item.fuelDate}, now()) </foreach> </insert>这里提醒一下,foreach的集合参数如果没加@Param("list")注解,XML里用list可能拿不到值,报错或插入为空数据。所以Mapper接口里写清楚:
int batchInsertFuel(@Param("list") List<FuelRecord> list);另外,批量插入数据量太大时,拼出来的SQL可能超过数据库max_allowed_packet限制,建议每500条一批分批执行,既稳定又不至于把数据库连接卡死。
MyBatis缓存也值得讲。默认一级缓存是SqlSession级别的,同一个SqlSession内多次查询相同SQL会命中缓存。但我们在Spring Boot中每次Mapper操作通常都会从连接池重新拿SqlSession,一级缓存的生命周期很短,基本感知不到。二级缓存是跨SqlSession的,需要显式开启,但车辆管理系统这类实时性要求高的业务不建议开,因为一旦有更新操作,缓存刷新不及时,用户会看到车辆状态还是旧的,产生误导。保持“不配置二级缓存”反而更安全。
还有一个经常让新手卡半天的点:MyBatis里单个数字字符比较。比如在XML里判断车辆状态:
<if test="status == '1'.toString()"> and v.status = 1 </if>直接写status == '1'在MyBatis解析OGNL表达式时会出问题,因为'1'会被当成Character类型,而Java的Character和String比较恒为false。我之前在这种小地方排查了很久,最后发现加.toString()就正常了。如果你使用的是Java 9以上版本,也可以用'1'.equals(status)这种写法,本质都是让类型统一。
4. 前端Vue页面与交互
4.1 前端工程结构和请求封装
前端我用的是Vue加Element UI。工程结构建议这样分:
src ├── api │ ├── vehicle.js │ └── apply.js ├── assets ├── components ├── router │ └── index.js ├── store │ └── user.js ├── views │ ├── login.vue │ ├── vehicle.vue │ ├── apply.vue │ └── dashboard.vue ├── utils │ └── request.js ├── App.vue └── main.js把接口请求统一放在api目录,页面组件不直接写axios,而是引入封装好的函数。比如api/vehicle.js里:
import request from '@/utils/request' export function getVehicleList(params) { return request({ url: '/vehicle/list', method: 'get', params }) }utils/request.js里封装一个带拦截器的axios实例:
import axios from 'axios' const request = axios.create({ baseURL: process.env.VUE_APP_BASE_URL || '/api', timeout: 10000 }) request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = token } return config }) request.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { this.$message.error(res.message) return Promise.reject(new Error(res.message)) } return res }, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token') window.location.href = '/login' } return Promise.reject(error) } )这样页面里不需要关心请求头、错误码、401跳转这些琐碎事,只管拿到数据去渲染。
4.2 登录鉴权与路由守卫
登录页面负责收集用户名密码,调后端登录接口,拿到Token后存进localStorage,再把用户ID和角色信息放进Vuex或sessionStorage。后端我这里用一个简化方案:登录成功返回一个UUID生成的token,存到Redis里,后续请求再校验用户身份。Redis不是必须的,初期项目可以全放内存Map,但上线推荐用Redis,重启不丢登录态,也好做过期时间。
前端路由守卫用来拦截未登录用户的访问:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.path === '/login') { next() return } if (!token) { next('/login') return } if (to.meta && to.meta.role && to.meta.role !== localStorage.getItem('role')) { next('/403') return } next() })登录跳转后,动态生成侧边栏菜单。管理员能看到“车辆管理”“司机管理”“申请审批”“维保记录”“加油记录”等菜单,普通员工只看到“车辆查询”“我的申请”“申请用车”。这种菜单级权限控制不需要太复杂,后端接口加上角色判断,前端配置好meta.role就够用。
4.3 车辆列表和申请用车页面
车辆列表页是整个后台的核心页面。页面结构大致是:顶部搜索栏,中间表格,底部翻页,右侧新增编辑按钮。
表格列设置:车牌号、车辆类型、品牌、当前里程、状态、绑定司机、操作按钮。“状态”列不要直接显示数字,用Element UI的el-tag根据状态渲染不同颜色。比如:
<el-table-column label="状态"> <template slot-scope="scope"> <el-tag :type="statusTypeMap[scope.row.status]"> {{ statusTextMap[scope.row.status] }} </el-tag> </template> </el-table-column>新增车辆用el-dialog弹窗包一个el-form,表单校验规则按需配置,车牌号必填、里程用input-number组件控制数字范围。提交时把所有表单字段组装成对象,调新增接口,成功后刷新列表。
用车申请页面则更关注业务交互。用户选择一个车辆时,最好能实时看到该车当前状态,避免选中一台“使用中”的车还被允许提交。我在这里做了一个小功能:申请单里车辆选择组件下拉数据只返回状态为“空闲”的车,提交到后端后再校验一次状态,双保险。这也是我在实际项目中踩过的坑——前端过滤了,后端没校验,结果两个人同时申请同一台车,管理员审核后状态就乱了。
申请列表页要注意时间条件查询。前端用el-date-picker的daterange,提交给后端时转成startTime和endTime两个参数,SQL里用apply.start_time >= #{startTime} and apply.start_time <= #{endTime}去匹配。这里千万别把时间范围字符串直接拼SQL,一定要用预编译参数,既能防注入又不会因为格式问题查错数据。
4.4 打包部署和布局异常
前端开发时用npm run serve,上线时执行npm run build,默认会生成到dist目录。很多新手把dist里的index.html直接双击打开,看到的是一片空白,控制台报各种加载路径错误。原因很简单:构建资源默认用的是绝对路径/js/xxx.js,本地打开时根目录不对。
解决方式是在根目录的vue.config.js里设置publicPath:
module.exports = { publicPath: './' }重新打包后,资源路径变成相对路径,静态文件就能正常加载。如果后端用Nginx部署,也可以在Nginx里配置location /的根目录指向dist,并做try_files到index.html,这样前端路由的history模式也不会刷新404。
还有一个常见的“打包后布局异常”问题:本地开发一切正常,打包部署后页面的背景图、图标全丢了。这通常是因为CSS里引用的url()使用了绝对路径。规范的做法是把静态资源放到src/assets,在代码中通过require或import引入,Webpack打包时会自动处理资源路径,而不是直接在public下写死引用。
5. 本地部署和问题排查
5.1 环境准备与启动顺序
想要从零把这个项目跑起来,你至少需要准备这些环境:
- JDK 8:推荐使用JDK 8,兼容Spring Boot 2.7.x,不用折腾模块和Java版本问题。
- MySQL 8.x:安装时注意字符集选择utf8mb4,建库用
utf8mb4_general_ci,避免中文乱码。 - Maven 3.6以上:用于后端依赖下载和构建。
- Node.js 14以上:前端Vue项目依赖npm打包。
- IDEA开发工具:后端用IntelliJ IDEA,前端也可以在IDEA里直接打开Vue工程。
启动顺序有讲究。第一步先启动MySQL,把schema.sql和data.sql导入建库;第二步启动Redis(如果用到登录Token缓存);第三步启动后端Spring Boot项目,看到Started VehicleApplication启动完成;第四步在前端目录执行npm install安装依赖,然后npm run serve启动前端开发服务。
如果npm install下载慢或者卡住,可以设置npm国内镜像源,速度会好很多。执行命令npm config set registry https://registry.npmmirror.com。注意安装依赖时尽量使用package-lock.json固定版本,避免团队同学装了不同依赖版本,代码行为不一致。
5.2 常见报错速查表
做项目过程中一定会遇到各种报错,我把高频问题整理成一张速查表,方便你排查时直接对照。
| 现象 | 原因 | 解决办法 |
|---|---|---|
| 后端启动报端口占用 | 8080端口被其他程序占用 | 修改application.yml里的server.port,或杀掉占用进程 |
| 数据库连接失败 | MySQL密码错误或库名不对 | 检查url的数据库名和username/password是否匹配 |
| 中文存入数据库变成问号 | 数据库、连接URL字符集不一致 | 建库时指定utf8mb4,连接URL加characterEncoding=utf8 |
| 前端请求接口CORS报错 | 前后端端口不同,浏览器跨域拦截 | 后端配置CORS过滤器,或前端用Vite代理转发 |
MyBatis提示Invalid bound statement | Mapper接口和XML的namespace或方法名不匹配 | 检查XML文件路径、namespace、方法名是否严格一致 |
| XML文件没被打包进target | Maven默认没有把src/main/java下的XML资源复制 | 在pom.xml里增加resources配置,把mapper/*.xml引入 |
| 前端打包后路由刷新404 | 使用了history模式但Nginx未配置重写 | Nginx配置try_files $uri $uri/ /index.html; |
| 车辆状态查询不到数据 | SQL里状态字段类型问题或前端传了字符串 | 检查MyBatis参数是否加上@Param,状态比较用.toString() |
还记得我前面说的MyBatis单数字字符比较问题吗?如果你遇到“状态明明传了1,查询结果却是空”,优先怀疑是不是OGNL表达式比较类型不一致。这种报错一般不会直接抛异常,只会让条件失效,查出来的数据变多或变少,最难排查。
5.3 调试与日志小技巧
排查后端问题有个非常实用的技巧:把MyBatis的SQL打印开起来。配置了log-impl: org.apache.ibatis.logging.stdout.StdOutImpl后,每次执行SQL,控制台会输出查询语句和所有参数值。我通常不会全项目打开,而是只给需要排查的Mapper目录打开,比如:
logging: level: com.example.vehicle.mapper: debug这样日志量可控,SQL眼不乱。但要注意打印SQL时会暴露参数,生产环境不建议一直开着,最好只在测试环境开启。
另外,Spring Boot启动后如果数据一直不对,可以先用Postman或Apifox单独调接口,不要总是从前端页面点。接口返回的JSON里能看到真实数据,定位问题比看浏览器控制台更直接。后端接口调试都通了,再回到前端查渲染和数据绑定问题,这是一个很高效的排查路线。
6. 项目还能怎么扩展
6.1 加上Redis缓存和异步导出
基础版本跑通以后,功能扩展方向还挺多的。第一个值得加的是Redis缓存。当前车辆状态、最新里程这种热点数据每次刷新列表都要查数据库,字段量不大但访问频率高。可以把车辆状态先缓存到Redis,过期时间设成1分钟,列表页查询直接读缓存,减少数据库压力。做审批操作时再更新缓存,保证状态最终一致。
还有一个方向是异步导入导出。比如管理员要把所有车辆信息导出成Excel,上万条数据同步导出会让接口阻塞很久。可以改成:前端发起导出请求,后端用线程池执行导出任务,导出完成后把文件路径放到Redis,前端轮询查询状态,任务完成后自动下载。这个问题正好也关联到Java里多线程“等待所有子任务完成”的场景,可以用CompletableFuture.allOf来编排导出任务,写起来比future.get方法简洁很多。
6.2 接入车辆定位和视频监控
如果项目想往企业版方向做,可以在车辆信息里加入经纬度字段,接入第三方地图API,在管理后台实时展示车辆位置。车载硬件把GPS坐标上报到后端,后端入库后通过WebSocket推送给前端,车辆位置就能在地图上动起来。
视频监控也是一个实用方向。现在很多行车记录仪或车载摄像头能输出HLS视频流,也就是m3u8格式的地址。前端可以在车辆详情页嵌入vue-video-player,传一个m3u8播放地址,就能在浏览器里直接预览车辆周边画面。当然前提是视频流地址已经做了鉴权,否则直接把流地址暴露出去会被人恶意盗看。做这部分时,后端可以把播放地址请求参数加签名和时间戳,生成临时有效的播放链接。
单个车辆管理系统做完后再加这些功能,既能练习SSE推送、WebSocket、异步任务这些高并发技术,也让项目从“CRUD管理系统”变成更完整的可视化监控平台,后续写进简历的含金量会明显不一样。
做了几轮之后我自己最深的体会是,这类管理系统不怕功能简单,怕的是表结构乱、状态一改就蹦。真正值得花时间打磨的,是那些不起眼的边界处理:审核前的状态校验、车辆状态的联动更新、MyBatis动态SQL的条件判断。把这些细节一个个抠稳,系统哪怕功能不多,用起来也会非常顺手。最后再分享一个小技巧:开发时尽量先用自己的数据库账号建一套测试数据,多模拟几个并发场景,比如两个人同时申请同一台车,只有把异常流程都测过一遍,交出去的系统才不算埋雷。