1. 企业车辆管理系统到底在管什么:从一个真实需求说起
先聊点实际的。做过企业内部系统的人应该都有感触,车辆管理这类项目看似简单,真做起来却远比想象中琐碎。我最早接触这个需求,是帮一家物流公司做内部系统。他们当时的情况是:公司有三十几辆车、二十多位司机,车辆调度靠微信群里喊,加油记录在Excel里,保养和保险什么时候到期得靠行政翻合同。结果就是——月底对账对不上,年检过期没人提醒,驾驶员谁在开哪辆车全靠记忆。
这个系统的核心价值,其实就是把这几件事从"人管"变成"系统管":车辆档案统一建卡、出车/回厂登记留痕、加油维保费用自动归集、年检保险到期主动预警,再加上基础的驾驶员管理和权限控制。东西不难,但模块之间耦合度高,业务规则多,比如一辆车出车回来必须关联里程数、一个司机不能同时被派两趟任务、费用单据要和车辆档案挂钩,这些逻辑如果不在设计初期理清楚,后面改起来非常痛苦。
我在做这个项目的时候,选型定的是SpringBoot2 + Vue3 + MyBatis-Plus + MySQL8.0这套组合,并且把全过程沉淀成了带完整文档的源码项目。这篇文章不是那种"教你敲一遍hello world"的教程,我会把这套系统从数据库设计、后端通用CRUD的封装思路、前端Vue3页面和接口的对接方式,到部署上线的坑,全部摊开来讲。适合手里有类似需求、想用一套成熟方案快速落地的人参考。
2. 技术栈取舍:为什么是SpringBoot2+Vue3+MyBatis-Plus+MySQL8.0
技术选型这种事,没有最好的,只有最合适的。这套组合拿到2026年的今天来看,不算最新,但绝对是最稳、资料最全、坑最少的搭配,没有之一。
2.1 后端骨架:SpringBoot2的生命力
SpringBoot2确实不算新了,官方维护期也快走到尽头,但企业级项目里它的存量市场实在太大了。我选它有几个现实考量:
第一,稳定性和生态兼容性。很多老牌中间件、报表组件、工作流引擎最初适配的都是SpringBoot2,供应商给的示例代码也大多是2.x版本。你要用SpringBoot3,经常得先去趟兼容性的坑。如果是给甲方做交付项目,SpringBoot2的安全性、稳定性、人才供给都是最稳妥的。
第二,资源占用和启动速度。SpringBoot2.7配上JDK8,内存占用控制在几百兆以内很轻松,对服务器配置要求不高。SpringBoot3强制JDK17,对不少还在用老机器的企业来说,光升级JDK就是一道坎。
第三,社区问答的覆盖率。你说SpringBoot2遇到一个奇葩报错,搜索引擎一搜,大概率十年前就有人踩过并给出了答案。这个优势在项目交付的时候特别宝贵,你不需要自己从零试错。
2.2 前端框架:Vue3的组合式API真香
前端我选Vue3 + Vite + Element Plus。Vue3的Composition API刚出来那会儿,很多人觉得"不就是把data和methods换个写法吗",实际用下来完全不是一回事。
以这个车辆管理系统为例,车辆表单页需要同时处理车牌号校验、所属部门级联选择、司机下拉联动、费用明细动态增行,用Options API写,各种watch和computed堆在一起,后期根本不敢动。换成Composition API,我可以把"车辆基础信息""费用相关逻辑""状态联动"拆成独立的逻辑块,页面级别只有编排,代码清晰度完全不同。
Vite的开发调试体验也是加分项,热更新秒开,后台管理系统的开发效率提升非常明显。
2.3 数据访问层:MyBatis-Plus的实用主义
MyBatis-Plus是我个人非常偏爱的一个框架。它不是把MyBatis玩出花,而是专治各种重复劳动。在这个项目里,我没有手写哪怕一条单表的增删改查SQL,全部通过MyBatis-Plus的BaseMapper和IService接口搞定。配合分页插件,列表查询的Page对象一步到位。
有人可能会问,那复杂报表和联表查询怎么办?答案是照常手写XML里的SQL,MyBatis-Plus完全兼容,并不冲突。它的价值在于把80%的CRUD工作量压缩到10%,剩下的精力全部集中在业务逻辑上。
2.4 数据库:MySQL8.0的版本红利
既然是新项目,数据库就没有理由再守着5.7了——MySQL8.0的窗口函数、CTE(公共表表达式)、JSON函数、更好的索引下推,都是实打实的效率提升。特别是做车辆费用统计报表的时候,窗口函数一条SQL就能搞定月份累计、同环比,换成5.7你得写一段很绕的子查询。
这套系统的初始化SQL也是按MySQL8.0编写的,字符集使用utf8mb4,排序规则用utf8mb4_general_ci。后续迁移到8.0以上版本基本零成本。
3. 数据库设计:车辆管理系统的表结构是这样规划的
先看一张整体的表规划思路,然后逐个说明关键表的设计要点。
| 模块 | 表名 | 核心字段 | 说明 |
|---|---|---|---|
| 系统基础 | sys_user / sys_role / sys_menu | 账号、角色编码、菜单权限 | 基于RBAC模型 |
| 车辆档案 | vehicle_info | 车牌号、品牌型号、购置日期、状态 | 全系统核心主数据 |
| 驾驶员 | driver_info | 姓名、驾驶证号、准驾车型、手机号 | 与车辆建立关联 |
| 出车管理 | vehicle_use_record | 车辆ID、司机ID、出车时间、回厂时间、里程 | 日常业务流水 |
| 费用管理 | vehicle_cost | 费用类型、金额、关联车辆、经办人 | 加油/维修/保险/其他 |
| 到期提醒 | vehicle_remind | 提醒类型、提醒日期、关联车辆 | 年检/保险/保养 |
| 公告通知 | sys_notice | 标题、内容、类型 | 站内消息 |
3.1 车辆档案表:这是所有业务的挂载点
车辆档案表vehicle_info,我给它定义的字段大约二十个左右。除了常规的车牌号、车辆类型(轿车/货车/客车)、品牌型号、发动机号、车架号以外,有两个字段的设计值得单独说一下。
第一个是vehicle_status,用枚举值管理:1-可用、2-出车中、3-维修中、4-停用。这个状态不是靠用户手动填的,而是系统根据业务流水自动联动。比如新增一条出车记录且未回厂,那车辆状态自动切成"出车中";回厂登记之后恢复"可用"。手动改状态只能处理特例,比如临时封存。这样状态永远可信。
第二个是current_mileage(当前里程)。听上去就是普通数字字段,但这里藏了一个业务规则:车辆出车回厂登记时的里程数,不能小于上次回厂时记录的里程,否则系统抛出校验异常。这是防止司机乱填、防止账实不符的关键校验。
3.2 出车记录:流水表是业务量的晴雨表
vehicle_use_record表的每条记录,都对应一次完整的出车-回厂闭环。设计上我不建议分成出车单和回厂单两张表,那样反而增加关联复杂度。一张表里用out_time、back_time两个时间字段来标记状态:back_time为空代表在途。
为了保证数据质量,我在前端表单里做了两项约束:出车里程必须大于车辆档案里的上次里程,回厂里程必须大于出车里程。后端接口层又校验了一遍。两层校验不是浪费,而是防止有人绕过前端直接调接口刷数据。
3.3 费用表:类型字段决定报表的统计口径
vehicle_cost表里最关键的字段是cost_type,我用字典表管理:1-加油、2-维修保养、3-保险、4-路桥停车、5-其他。之所以不直接在代码里写死,是因为不同企业的费用分类差异挺大,写成数据字典后,实施人员可以直接在后台改分类名称,不需要动代码。
3.4 表关系和数据一致性
从表关系图来看非常简单:车辆1对N出车记录、1对N费用记录、1对N提醒记录;驾驶员1对N出车记录。没有复杂的多对多关系,根本原因在于我刻意做了业务简化——一辆车在同一时间段只能出现在一条未回厂的出车记录里。这个约束在应用层实现:新增出车记录时,先查该车辆是否存在back_time is null的记录,有则拒绝新增。为什么不在数据库层做唯一索引?因为"未回厂且未删除"这个条件没法用简单的唯一索引表达,用应用层查询判断更灵活。
提示:这类条件约束如果后续并发量上来了,建议在
vehicle_use_record表中增加status字段(1-在途、2-已完成、3-已取消),然后对(vehicle_id, status)建唯一索引,把并发冲突降到最低。
4. 后端落地:MyBatis-Plus通用CRUD的封装思路与关键实现
项目后端用Maven多模块还是单模块?这套系统规模不大,我采用的是单模块结构,但包划分清晰:
com.company.vehicle ├── common // 通用返回结果、异常处理、工具类 ├── config // 配置类(拦截器、跨域、MyBatis-Plus分页) ├── controller // 接口层 ├── service // 业务接口与实现 ├── mapper // MyBatis-Plus Mapper接口 ├── entity // 数据库实体 ├── dto // 前端交互数据对象 ├── vo // 视图对象 └── utils // 业务工具4.1 通用CRUD Service的封装
常规做法是每个实体类写一个Service接口+ServiceImpl实现,然后在里面写一堆getById、list、save、removeById的透传方法。我在这套系统里换了一种做法:封装一个通用的BaseService<T>接口,继承IService<T>,然后每个业务Service直接继承它。
核心的代码是这样一个接口定义:
public interface BaseService<T> extends IService<T> { /** * 新增或更新 */ boolean saveOrUpdateWithCheck(T entity); /** * 分页查询(带关键字) */ Page<T> pageQuery(Page<T> page, Wrapper<T> queryWrapper); }实现类里最值得展开的是pageQuery。为了满足列表页的搜索条件,我直接在Controller层接收前端传来的Map<String, Object> params,然后通过一个QueryBuilder工具类把Map转换为MyBatis-Plus的QueryWrapper。这样做的收益很明显:新增一个查询条件,前端加一个参数即可,后端不需要新增方法。代价是可读性略差,但对于车辆管理这类字段明确、搜索条件不超过五六个的表,效率远大于可读性损失。
4.2 QueryBuilder工具类的实现细节
简单贴一下这个工具类的核心逻辑:
public class QueryBuilder { public static <T> QueryWrapper<T> build(Map<String, Object> params) { QueryWrapper<T> wrapper = new QueryWrapper<>(); if (params == null || params.isEmpty()) { return wrapper; } params.forEach((key, value) -> { if (value == null || "".equals(value.toString().trim())) { return; } // 约定:以 _eq 结尾表示精确查询,_like 表示模糊查询,_ge/_le 表示范围 if (key.endsWith("_eq")) { wrapper.eq(camelToUnderline(key.replace("_eq", "")), value); } else if (key.endsWith("_like")) { wrapper.like(camelToUnderline(key.replace("_like", "")), value); } else if (key.endsWith("_ge")) { wrapper.ge(camelToUnderline(key.replace("_ge", "")), value); } else if (key.endsWith("_le")) { wrapper.le(camelToUnderline(key.replace("_le", "")), value); } }); return wrapper; } }前端传参示例:
{ "plateNo_like": "京A", "vehicleStatus_eq": 1, "createdAt_ge": "2026-01-01", "createdAt_le": "2026-12-31" }这套约定非常实用,团队新成员看一眼接口文档就能上手,不需要为每个列表页单独写查询接口。
注意:使用Map接收参数虽方便,但一定要在Service层做白名单校验,否则用户传一个未知字段进来,MyBatis-Plus会尝试拼接字段名,直接导致SQL异常。我这里是在QueryBuilder里维护了一张字段白名单HashMap,不在白名单内的key直接忽略。
4.3 通用返回体与全局异常处理
所有接口统一返回Result<T>,结构如下:
{ "code": 200, "message": "success", "data": {} }全局异常处理这块,我在GlobalExceptionHandler里直接继承了ResponseBodyAdvice的方式做统一包装,并且细化了异常类型:
- 业务异常:返回对应module的错误码,前端弹出Message提示
- 参数校验异常:逐条返回字段错误信息
- 数据库异常:捕获
DuplicateKeyException返回"数据重复"的友好提示,而不是把SQL丢给前端
4.4 出车登记接口:一个完整的业务闭环
拿"出车登记"这个核心接口举例,看看后端业务逻辑是怎么串起来的。
@PostMapping("/out") public Result<String> registerOut(@RequestBody @Valid VehicleUseDto dto) { // 1. 校验车辆是否存在且状态可用 VehicleInfo vehicle = vehicleService.getById(dto.getVehicleId()); if (vehicle == null || !"1".equals(vehicle.getVehicleStatus())) { throw new BizException("车辆不可用"); } // 2. 校验该车辆没有未回厂的记录 long runningCount = vehicleUseRecordService.count( new LambdaQueryWrapper<VehicleUseRecord>() .eq(VehicleUseRecord::getVehicleId, dto.getVehicleId()) .isNull(VehicleUseRecord::getBackTime)); if (runningCount > 0) { throw new BizException("该车辆已有在途任务"); } // 3. 校验出车里程 if (dto.getOutMileage() < vehicle.getCurrentMileage()) { throw new BizException("出车里程不能小于上次回厂里程"); } // 4. 写入记录,同时更新车辆状态 VehicleUseRecord record = BeanUtil.copyProperties(dto, VehicleUseRecord.class); record.setOutTime(LocalDateTime.now()); vehicleUseRecordService.save(record); vehicle.setVehicleStatus("2"); vehicle.setCurrentMileage(dto.getOutMileage()); vehicleService.updateById(vehicle); return Result.success("出车登记成功"); }这段代码没有任何高深技巧,好处是逻辑链路一目了然。业务经验上我要强调一点:第2步和第3步之间存在一个极小的并发窗口,但从实际使用场景看,企业内部车辆登记的使用频率不高,并发冲突可能性极低。如果后续要接入车队大规模使用,可以通过数据库锁或Redis分布式锁处理,我在源码注释里也标明了这个升级路径。
5. Vue3前端实战:页面搭建、接口封装与状态管理的取舍
前端部分我用的是Vue3 + Vite + Pinia + Element Plus + Axios。这套组合现在已经是Vue3后台管理系统的标准配餐,网上脚手架一抓一大把,这里不重复讲初始化,重点说几个我在这个项目里真正动过脑子的地方。
5.1 请求封装:Axios拦截器统一处理
所有请求通过/utils/request.js封装,核心逻辑是三个拦截器:
请求拦截器:自动在header里带上Authorization: Bearer token,同时根据用户权限限制接口访问。
响应拦截器:判断res.data.code,200直接返回data,其他code统一弹出ElementPlus的Message提示。针对401,清除本地token并跳转到登录页。
我额外加了一个很小的功能:当接口响应耗时超过2秒时,在控制台打一条warning日志。因为在后台管理系统里,很多卡顿问题不是前端引起的,而是接口慢,提前在开发环境暴露慢接口非常有价值。
5.2 动态表单:费用明细行的增删处理
车辆费用登记页面有一个"费用明细"的表格,允许用户一次填写多条明细,每条明细包含费用类型、金额、备注,并且支持动态新增和删除行。这块如果用传统的el-form数组嵌套,校验会比较麻烦。我的做法是:
- 每一行用一个独立的
reactive对象管理,行数据保存到feeRows数组中 - 金额输入的校验放在自定义
blur事件里,而不是依赖Form的rules - 删除行时判断若只剩一行,则重置该行而不是移除(避免出现空白表单)
为什么不用Form rules?因为el-form对数组下标对象的校验提示在UI层不够友好,行一多会显示得很乱。自定义事件的校验逻辑虽然多写几行代码,但用户体验好得多。
5.3 Pinia状态管理:不要把一切塞进Store
车辆管理系统的页面级状态非常多:左侧菜单折叠状态、用户权限集合、车辆选择器当前选中项、报表查询条件。我看到的很多Vue3项目,恨不得把每个页面的数据都塞进Pinia,这是种错误倾向。
我的原则是:跨页面、跨组件必须共享的状态才放Store;页面内部状态,比如表单数据、列表查询条件,老老实实放在组件自己的ref和reactive里。这套系统里Pinia只存了三样:用户信息、角色权限、全局字典数据(费用类型等)。像车辆列表的搜索条件,刷新后重新填一次就是了,没必要全局缓存。
5.4 车辆选择器:自定义组件的封装
车辆信息在出车、费用、提醒多个页面都要选,而且需求是:下拉框显示车牌号、后端要传递车辆ID、旁边还要展示车辆当前状态。这种场景重复出现,我封装了一个VehicleSelect组件,内部通过Props接收modelValue(车辆ID),外部调用就是这样:
<VehicleSelect v-model="form.vehicleId" :status-filter="1" />组件内部做的事情是:挂载时加载全部车辆列表,根据status-filter过滤掉不可选项,选中后抛出车辆对象。这样一个组件在项目里被五个页面复用,后端不需要为每个页面单独写车辆下拉接口。
5.5 权限控制:按钮级别的细粒度指令
菜单权限用路由守卫做,按钮权限我用了一个自定义指令v-permission,这在高管操作、费用审核这类敏感按钮上特别有用。
<el-button v-permission="['vehicle:cost:audit']" @click="auditCost">审核</el-button>指令的逻辑很简单:从Pinia里取出当前用户的权限码数组,如果按钮需要的权限码不在数组中,直接el.remove()。为什么不用v-if?因为v-if每次渲染都要写一遍判断,而且指令化的方式对业务代码侵入最小,视觉上也干净。
6. 报表统计与到期提醒:两类最容易翻车的模块
车辆管理系统做得好不好,用户感知最明显的其实就是两块:统计数据算得准不准、提醒消息能不能及时出现。这两块我都踩过坑,专门拿一节来讲。
6.1 用MySQL8.0窗口函数做月度费用统计
需求简单说就是:左侧按费用类型分组(加油、维修、保险…),顶部按月份分组(1月到12月),表格里是对应金额,底部算合计。
我最初的实现方式是用GROUP BYcost_type, MONTH(pay_date),然后代码里再重组数据。这种写法在数据量小的时候没问题,但有两个坑:一是如果某个月份没有某种费用,那一格就是没有数据,前端需要补零;二是如果想看"截止到目前的本月累计",SQL会越写越复杂。
换成窗口函数之后,整个统计接口变得非常清爽:
SELECT cost_type, MONTH(pay_date) AS month_no, SUM(amount) AS total_amount, SUM(SUM(amount)) OVER (PARTITION BY cost_type ORDER BY MONTH(pay_date)) AS cumulative_amount FROM vehicle_cost WHERE YEAR(pay_date) = ? AND deleted = 0 GROUP BY cost_type, MONTH(pay_date)外层再套一层查询,用IFNULL补零,前端直接拿到一个完整的二维数组填充表格。窗口函数在报表场景里的价值,确实只有真正写过大SQL的人才能体会。
6.2 到期提醒的两种实现思路
年检到期、保险到期、保养到期的提醒,是这个系统被业务方最直接认可的模块。实现上有两个思路。
第一种是"被动查询":进入后台首页时,调一个接口查询vehicle_remind表,把未来30天内到期的记录列出来。好处是简单,坏处是用户如果隔很久不登录,提醒就形同虚设。
第二种是"主动推送":加一个定时任务,每天早上8点扫表,把当天到期或临近到期的车辆信息,通过站内通知写入sys_notice表。用户在系统登录后,右上角铃铛能看到未读数。
我的选择是两种都做:首页Dashboard展示的是实时查询结果(被动),定时任务负责生成站内通知(主动)。定时任务用@Scheduled(cron = "0 0 8 * * *"),在SpringBoot主类上开启@EnableScheduling就好。
6.3 报表慢查询的优化
这个系统的数据量不大,但有一段时间月度报表接口响应要三秒以上。排查下来原因有两个:一是vehicle_cost表的pay_date字段没建索引,扫全表;二是报表接口里嵌套循环调用了车辆档案表获取车牌号,造成了N+1问题。
解决方案:pay_date加上普通索引,嵌套循环改成先批量查出所有车辆ID,再用IN查询一次拿全量车牌号,内存里做映射。优化后响应时间降到400毫秒以内。这个案例我特意写进项目文档里,提醒后来者:后台管理系统80%的性能问题,都出在索引缺失和循环查库上。
7. 权限系统与安全设计:RBAC模型的落地姿势
企业车辆管理系统虽然规模不大,但有几个角色天然存在:超级管理员、车管专员、普通驾驶员、财务审核人。不同角色看到的菜单不一样,可操作的按钮不一样。这里直接说RBAC模型在本项目里的落地方式。
7.1 数据库三张核心表
sys_user:用户表,包含username、password、real_name、department_id、statussys_role:角色表,包含role_code、role_namesys_menu:菜单表,包含menu_name、parent_id、path、perms(权限码)- 再加两张关联表
sys_user_role、sys_role_menu
菜单表这里有一个关键设计:每个菜单项都挂了一个perms权限码,比如vehicle:cost:add、vehicle:cost:audit。前端v-permission指令用的就是这些权限码,后端接口的@PreAuthorize("hasAuthority('vehicle:cost:audit')")也是基于这些权限码。前后端权限验证是同一套码表,不会出现"前端看不到按钮、但直接调接口能成功"的漏洞。
7.2 登录认证与Token
认证这块用的是JWT。用户登录成功,后端签发一个有效期12小时的JWT令牌,前端存到localStorage。每次请求在Axios拦截器里自动带上。后端用一个JwtInterceptor解析token并存入ThreadLocal,业务代码里随时可以取当前登录人的userId。
有人问我为什么不用Spring Security,我承认Security功能更完整,但配置成本和学习曲线对于这种规模的项目偏高。JWT + 拦截器 + 自研权限注解的组合,代码量少,逻辑透明,排查问题也容易。对于车辆管理系统这种内部工具级别的项目,完全够用。
7.3 密码加密与操作日志
用户的密码存储用的是BCryptPasswordEncoder,也就是Spring Security里那个常用的加密器,单独引入即可。它的好处是每次生成的hash带随机盐,哪怕两个用户密码相同,存储的密文也不一样,安全性比MD5高好几个量级。
另外我加了一张sys_oper_log表,记录每个用户的关键操作:出车登记、回厂确认、费用审核、车辆信息修改。不是为了审计追责,而是为了解决一个很实际的业务纠纷场景——司机说"我那天开的是那辆车",管理员说"系统记录你开的是另一辆",这时候操作日志就是最有效的对账凭证。
8. 部署、文档与常见问题:项目交付最容易被低估的三个环节
源码写完只是项目的一半,另一半是能让别人顺利跑起来、看明白、改得动。这一节聊聊我在文档组织和部署交付上的习惯。
8.1 项目文档应该包含哪些内容
这套系统带的文档,我分成四份,每一份对应不同读者:
| 文档 | 面向对象 | 核心内容 |
|---|---|---|
| README.md | 开发人员 | 项目介绍、技术栈、快速启动步骤、目录结构说明 |
| 数据库设计文档 | 开发/运维 | 表结构说明、字段字典、ER关系说明 |
| 接口文档 | 前后端开发 | 各接口的地址、入参、出参、状态码说明 |
| 部署文档 | 实施/运维 | 环境要求、MySQL初始化、构建打包、Nginx/Java启动配置 |
写文档的教训是:一定要站在"一个完全不了解项目的新人"的视角。常见的文档通病是默认读者已了解开发背景,结果环境配置这一步就直接劝退。我的做法是,把每一步可执行命令都完整写到文档里,哪怕是java -version这种你以为大家都会的操作。
8.2 前端构建与Nginx部署
前端打包使用Vite,产物在dist目录。部署到Nginx时,关键的配置点在try_files和路由重写:
server { listen 80; server_name your-domain.com; root /opt/vehicle-web/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8088/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }location /api/里那个proxy_pass末尾的斜杠非常容易出错。带斜杠表示把/api/前缀去掉再转发到后端地址,不带斜杠则原样转发。比如前端请求/api/vehicle/list,带斜杠时后端收到的是/vehicle/list,不带斜杠收到的是/api/vehicle/list。两边的Controller路径要对应上,我在这上面浪费过至少半小时。
8.3 部署时容易遇到的环境坑
MySQL8.0部署在Linux上,最常见的坑是Authentication plugin 'caching_sha2_password' cannot be loaded。这是MySQL8默认认证插件变了,老版本的客户端工具连不上。
解决办法有两个:一是升级客户端驱动到8.x,推荐;二是创建用户时强制指定mysql_native_password,适合老环境兼容。我在部署文档中建议用第一种方案,因为应用服务器上的JDBC驱动和Navicat这类工具升级到新版都很简单。
后端启动时如果遇到端口被占用、时区报错、字符集乱码,大概率是启动参数问题,我在文档里给出了完整的JVM启动参数模板:
nohup java -Xms512m -Xmx1024m \ -Dserver.port=8088 \ -Dspring.profiles.active=prod \ -Duser.timezone=Asia/Shanghai \ -jar vehicle-system.jar \ > /opt/vehicle/logs/system.log 2>&1 &8.4 本地启动检查清单
我把本地启动的常见失败场景和排查顺序总结成了一份清单,这也是文档里被评价最高的部分之一:
- 确认MySQL8.0已启动,且已执行
init.sql创建数据库和表 - 确认后端
application-dev.yml里的数据库账号密码、URL中的时区参数serverTimezone=Asia/Shanghai正确 - 确认Redis(如果使用)已启动,本项目未用Redis,纯粹为提醒留的扩展位
- 先启动后端,再启动前端,Vite开发服务器配好
proxy代理指向localhost:8088 - 登录页输入管理员账号,如果报验证码错误,多半是缓存问题,清一下浏览器缓存再看
9. 后期扩展:这套架构还能往哪些方向走
最后聊点代码之外的东西,也是我在项目总结时经常会跟团队讲的内容。
这个系统上线运行之后,业务方几乎一定会提新的需求。我总结下来的高频扩展方向有三个。
第一个方向是GPS定位和轨迹回放。车辆管理系统最容易增加的价值功能就是实时定位。接入思路也不复杂:每辆车安装一个GPS终端,设备通过MQTT协议上报经纬度到后端,后端用Netty或EMQ X接收并存储位置记录,前端用高德地图或百度地图的JavaScript API渲染轨迹。后端表只需增加vehicle_gps_record表,字段包含车辆ID、经纬度、速度、方向、上报时间。原来的出车记录表可以和GPS记录做关联,出车单自动绘制轨迹。
第二个方向是移动端适配。司机不可能随时坐在电脑前做出车登记,实际场景中司机用手机操作是刚需。这套系统的前端是Vue3,可以顺势做一套Vue3 + Vant的移动端页面,或者直接用uni-app做小程序。后端接口完全不用改,天然支持多端调用。
第三个方向是审批流和消息推送。现在是"费用录入后财务人工审核",如果业务量上去了,可以引入简单的审批流引擎,比如Flowable或自研状态机,把"司机提交出车申请→车管审批→出车→回厂→费用确认"串成一条标准流程。同时把站内通知升级为短信或企业微信推送,解决用户不登录就看不到提醒的问题。
这些扩展方向我都写在了项目的README末尾,不是画饼,而是确实在业务上被反复验证过的需求。做交付项目,最忌讳把系统做成死水一潭,留好扩展位,后面加需求时才不会动大手术。
这套车辆管理系统源码项目,从数据库建模、后端通用CRUD封装、Vue3前端联调、报表统计到部署上线,整个过程我尽量把关键决策背后的理由说透了。如果你正好要做一个类似的内部管理系统,照着这个思路走,能省掉不少摸索的时间。